# Story 5.28: Unify Sync/Async Logger Factory Functions

**Status:** Complete
**Priority:** High
**Depends on:** Story 5.25 (Extract Config Builders)

---

## Context / Background

The `src/fapilog/__init__.py` contains two nearly identical logger factory functions:

- `get_logger()` (lines 958-1089) - ~132 lines
- `get_async_logger()` (lines 1092-1216) - ~125 lines

These functions are **~95% identical**. The only difference:
- `get_logger()` calls `_start_plugins_sync()` (synchronous plugin startup)
- `get_async_logger()` calls `await _start_plugins()` (async plugin startup)

This duplication creates problems:
- **Bug propagation risk:** Fixes must be applied twice
- **Feature drift:** Parameters may be added to one but not the other
- **Maintenance burden:** ~250 lines of duplicated logic

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

Reference: `src/fapilog/__init__.py:958-1216`

---

## Scope (In / Out)

### In Scope

- Create unified `_prepare_logger()` function with shared logic
- Refactor `get_logger()` to use the common function
- Refactor `get_async_logger()` to use the common function
- Reduce duplication to <20 lines per public function

### Out of Scope

- Unifying `SyncLoggerFacade` and `AsyncLoggerFacade` classes
- Changing public API signatures
- Adding new logger factory parameters

---

## Acceptance Criteria

### AC1: Common Function Created

**Description:** Shared logic extracted to a single function.

**Validation:**
```python
def _prepare_logger(
    name: str | None,
    *,
    preset: str | None,
    format: Literal["json", "pretty", "auto"] | None,
    settings: Settings | None,
    # ... other params
) -> tuple[_LoggerSetup, Settings]:
    """Prepare logger configuration without starting plugins."""
    # All shared validation, preset handling, format resolution
    ...
```

### AC2: Factory Functions Simplified

**Description:** Each public function reduced to <30 lines.

**Validation:**
```python
def get_logger(...) -> SyncLoggerFacade:
    setup, cfg = _prepare_logger(name, preset=preset, ...)
    enrichers, redactors, processors, filters = _start_plugins_sync(...)
    logger = _create_facade(setup, SyncLoggerFacade, ...)
    logger.start()
    return logger
```

### AC3: Behavior Unchanged

**Description:** All existing tests pass without modification.

**Validation:**
```bash
pytest tests/ -k "get_logger or get_async_logger" -x
```

---

## Implementation Notes

### File Changes

```
src/fapilog/__init__.py (MODIFIED - refactor factories)
tests/unit/test_logger_factory.py (NEW - test _prepare_logger)
```

### Key Code

```python
def _prepare_logger(
    name: str | None,
    *,
    preset: str | None,
    format: Literal["json", "pretty", "auto"] | None,
    settings: Settings | None,
    sinks: list[object] | None,
    auto_detect: bool,
    environment: str | None,
) -> tuple[_LoggerSetup, Settings]:
    """Prepare logger config. Caller must start plugins and create facade."""

    # Mutual exclusivity validation
    if format is not None and settings is not None:
        raise ValueError("Cannot specify both 'format' and 'settings'.")
    # ... more validation

    # Preset handling
    if preset is not None:
        from .core.presets import get_preset
        preset_config = get_preset(preset)
        settings = Settings(**preset_config)

    # Environment detection, format resolution, etc.
    ...

    setup = _configure_logger_common(cfg_source, sinks)
    return setup, cfg_source
```

---

## Tasks

### Phase 1: Analysis

- [ ] Map exactly which lines are shared vs different
- [ ] Identify the minimal divergence point
- [ ] Design return type for `_prepare_logger()`

### Phase 2: Extraction

- [ ] Create `_prepare_logger()` function
- [ ] Move shared validation logic
- [ ] Move preset/environment handling
- [ ] Move format resolution

### Phase 3: Refactoring

- [ ] Update `get_logger()` to use `_prepare_logger()`
- [ ] Update `get_async_logger()` to use `_prepare_logger()`
- [ ] Run tests after each change

### Phase 4: Cleanup

- [ ] Remove remaining duplication
- [ ] Add docstrings
- [ ] Verify line count reduction

---

## Tests

### Unit Tests

- `tests/unit/test_logger_factory.py`
  - `_prepare_logger()` validation (mutual exclusivity errors)
  - `_prepare_logger()` preset resolution
  - `_prepare_logger()` environment auto-detection

### Integration Tests

- All existing `get_logger()` tests pass
- All existing `get_async_logger()` tests pass
- Preset-based creation works for both

---

## Definition of Done

### Code Complete

- [x] All acceptance criteria implemented
- [x] `get_logger()` < 30 lines
- [x] `get_async_logger()` < 30 lines
- [x] No new linting errors

### Quality Assurance

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

### Documentation

- [x] Code has docstrings where needed

---

## Risks / Rollback

### Risks

1. **Risk:** Subtle behavior difference between sync and async paths
   - **Mitigation:** Line-by-line comparison; run both test suites

2. **Risk:** Plugin startup timing changes
   - **Mitigation:** Keep startup logic in same relative position

### Rollback Plan

If issues occur:
1. Revert the commit

---

## Related Stories

- **Depends on:** Story 5.25 - Extract Config Builders
- **Related:** Story 5.29 - Refactor Sink Writers

---

## Change Log

| Date | Change | Author |
|------|--------|--------|
| 2026-01-14 | Initial draft | Claude |
