# Story 5.26: DRY Environment Variable Alias Validators

**Status:** Complete
**Priority:** Critical
**Depends on:** None

---

## Context / Background

The `src/fapilog/core/settings.py` file contains five nearly identical environment variable alias validators (lines 971-1228):

1. `_apply_size_guard_env_aliases()` - ~25 lines
2. `_apply_cloudwatch_env_aliases()` - ~52 lines
3. `_apply_loki_env_aliases()` - ~65 lines
4. `_apply_postgres_env_aliases()` - ~70 lines
5. `_apply_sink_routing_env_aliases()` - ~32 lines

Each follows the same pattern:
1. Build an `env_map` dict mapping field names to `os.getenv()` calls
2. Iterate and call `setattr()` with type conversion
3. Handle special cases (booleans, durations, lists)

This creates problems:
- **Maintenance debt:** ~244 lines of similar-but-different code
- **Bug propagation risk:** Fixes in one may not be applied to others
- **Extension cost:** Adding a new sink config requires copying ~50 lines

The Human Understandability Audit identified this as a **P0 issue**.

Reference: `src/fapilog/core/settings.py:971-1228`

---

## Scope (In / Out)

### In Scope

- Create generic `_apply_env_aliases()` function
- Refactor all five validators to use it
- Support type conversions: `str`, `int`, `bool`, `float`, `duration`, `list`, `size`, `dict`, `enum`, `routing_rules`
- Reduce total validator code by at least 60%

### Out of Scope

- Changing environment variable names
- Adding new environment variable support
- Modifying Pydantic settings behavior

---

## Acceptance Criteria

### AC1: Generic Validator Created

**Description:** A single function handles all env alias patterns.

**Validation:**
```python
# Declarative field mapping
CLOUDWATCH_ENV_FIELDS = {
    "log_group_name": ("FAPILOG_CLOUDWATCH__LOG_GROUP_NAME", "str"),
    "batch_size": ("FAPILOG_CLOUDWATCH__BATCH_SIZE", "int"),
    "create_log_group": ("FAPILOG_CLOUDWATCH__CREATE_LOG_GROUP", "bool"),
}

# Single call replaces 50+ lines
_apply_env_aliases(self.sink_config.cloudwatch, CLOUDWATCH_ENV_FIELDS)
```

### AC2: Line Count Reduced

**Description:** Total validator code reduced by at least 60%.

**Validation:**
```bash
# Before: ~244 lines across 5 validators
# After: <100 lines (generic function + 5 mappings)
```

### AC3: All Env Vars Still Work

**Description:** All existing environment variables function identically.

**Validation:**
```bash
FAPILOG_CLOUDWATCH__LOG_GROUP_NAME=/test pytest tests/unit/core/test_settings.py -k env
```

---

## Implementation Notes

### File Changes

```
src/fapilog/core/settings.py (MODIFIED - refactor validators)
tests/unit/test_settings_env_aliases.py (MODIFIED - extend existing tests)
```

### Key Code

```python
class EnvFieldType(Enum):
    STRING = "string"
    INT = "int"
    BOOL = "bool"
    FLOAT = "float"
    DURATION = "duration"
    LIST = "list"
    SIZE = "size"  # Human-readable bytes ("1 MB", "500 KB")
    DICT = "dict"  # JSON object parsing (e.g., Loki labels)
    ENUM = "enum"  # Constrained choices (e.g., "truncate"|"drop"|"warn")
    ROUTING_RULES = "routing_rules"  # Complex RoutingRule.model_validate()

def _apply_env_aliases(
    target: BaseModel,
    field_map: dict[str, tuple[str, EnvFieldType]],
) -> None:
    """Apply environment variable overrides to a Pydantic model."""
    for field_name, (env_var, field_type) in field_map.items():
        value = os.getenv(env_var)
        if value is None:
            continue
        converted = _convert_env_value(value, field_type)
        if converted is not None:
            setattr(target, field_name, converted)
```

---

## Tasks

### Phase 1: Foundation

- [ ] Create `EnvFieldType` enum
- [ ] Implement type converter functions
- [ ] Implement generic `_apply_env_aliases()` function
- [ ] Add unit tests for generic function

### Phase 2: Migration

- [ ] Define `SIZE_GUARD_ENV_FIELDS` mapping
- [ ] Refactor `_apply_size_guard_env_aliases()`
- [ ] Define `CLOUDWATCH_ENV_FIELDS` mapping
- [ ] Refactor `_apply_cloudwatch_env_aliases()`
- [ ] Repeat for Loki, Postgres, SinkRouting
- [ ] Remove old implementation code

### Phase 3: Documentation

- [ ] Add docstrings with usage examples
- [ ] Document pattern for adding new sinks

---

## Tests

### Unit Tests

- `tests/unit/test_settings_env_aliases.py` (extend existing)
  - `_apply_env_aliases()` with string fields
  - `_apply_env_aliases()` with int fields
  - `_apply_env_aliases()` with bool fields (true/false/1/0/yes/no)
  - `_apply_env_aliases()` with duration fields ("5s", "1m", 30.0)
  - `_apply_env_aliases()` with size fields ("1 MB", "500 KB")
  - `_apply_env_aliases()` with dict fields (JSON parsing)
  - `_apply_env_aliases()` with enum fields (constrained choices)
  - Missing env vars are skipped
  - Invalid values don't crash

### Integration Tests

- All existing settings tests pass unchanged
- Each sink's env vars still work

---

## Definition of Done

### Code Complete

- [ ] All acceptance criteria implemented
- [ ] Code follows project patterns
- [ ] No new linting errors

### Quality Assurance

- [ ] Unit tests written and passing
- [ ] Integration tests written and passing
- [ ] `ruff check` passes
- [ ] `mypy` passes
- [ ] No regression in existing tests

### Documentation

- [ ] Code has docstrings where needed
- [ ] Pattern documented for future sinks

---

## Risks / Rollback

### Risks

1. **Risk:** Subtle type conversion differences break existing behavior
   - **Mitigation:** Exhaustive tests for each env var; compare old vs new

2. **Risk:** Edge cases in duration/list parsing differ
   - **Mitigation:** Extract existing parsing logic; don't rewrite

### Rollback Plan

If issues occur:
1. Revert the commit

---

## Related Stories

- **Related:** Story 5.25 - Extract Config Builders
- **Related:** Story 5.19 - Standardize Plugin Config Parsing

---

## Change Log

| Date | Change | Author |
|------|--------|--------|
| 2026-01-14 | Initial draft | Claude |
| 2026-01-15 | Added missing size_guard validator, fixed line counts/ranges, expanded EnvFieldType enum, referenced existing test file, marked Ready | Claude |
