# Story 6.10: Unify get_logger/get_async_logger Setup

## Status: Complete

## Summary

Extract the shared setup logic from `get_logger()` and `get_async_logger()` into a common helper, eliminating duplicated pipeline construction and reducing feature drift risk.

## Motivation

The complexity audit identified that `get_logger()` (lines 607-750) and `get_async_logger()` (lines 752-849) perform nearly identical setup:

1. Load settings and apply plugin settings
2. Build pipeline (`_build_pipeline`)
3. Start plugins (enrichers, redactors, processors, filters)
4. Configure circuit breaker
5. Create routing/fanout writer
6. Calculate level gate
7. Create facade with 15+ parameters
8. Apply bound context
9. Warn on sensitive fields
10. Capture unhandled exceptions
11. Start logger and assign internal attributes

**Differences:**

- `get_logger()`: Uses `ThreadPoolExecutor` for async plugin start in sync context
- `get_async_logger()`: Uses native `await` for plugin start

This duplication means:

- Features added to one may be missed in the other
- Configuration behavior could drift
- Testing burden doubled

## Acceptance Criteria

- [ ] Single source of truth for logger configuration logic
- [ ] `get_logger()` and `get_async_logger()` use shared helper
- [ ] Both functions remain public API with same signatures
- [ ] ~100-120 lines of duplication removed
- [ ] All existing tests pass
- [ ] Behavior identical before/after
- [ ] `_start_plugins_sync` helper encapsulates ThreadPoolExecutor logic

## Technical Approach

### Create `LoggerSetup` Container

Use a dataclass with slots for efficient storage of configuration results:

```python
@dataclass(slots=True)
class LoggerSetup:
    """Container for logger configuration results."""

    settings: Settings
    sinks: list[object]
    enrichers: list[object]
    redactors: list[object]
    processors: list[object]
    filters: list[object]
    metrics: MetricsCollector | None
    sink_write: Callable
    sink_write_serialized: Callable | None
    circuit_config: SinkCircuitBreakerConfig | None
    level_gate: int | None
```

### Create `_configure_logger_common`

Extract all shared logic into a helper that returns everything needed to create a facade:

```python
def _configure_logger_common(
    settings: Settings | None,
    sinks: list[object] | None,
) -> LoggerSetup:
    """
    Configure logger components without creating facade.

    This is the shared setup logic for both sync and async loggers.
    Returns a LoggerSetup with unstarted plugins - caller must start them.
    """
    cfg_source = settings or Settings()
    _apply_plugin_settings(cfg_source)

    built_sinks, enrichers, redactors, processors, filters, metrics = _build_pipeline(
        cfg_source
    )

    if sinks is not None:
        built_sinks = list(sinks)

    # Build circuit breaker config if enabled
    circuit_config = None
    if cfg_source.core.sink_circuit_breaker_enabled:
        from .core.circuit_breaker import SinkCircuitBreakerConfig

        circuit_config = SinkCircuitBreakerConfig(
            enabled=True,
            failure_threshold=cfg_source.core.sink_circuit_breaker_failure_threshold,
            recovery_timeout_seconds=cfg_source.core.sink_circuit_breaker_recovery_timeout_seconds,
        )

    sink_write, sink_write_serialized = _routing_or_fanout_writer(
        built_sinks,
        cfg_source,
        circuit_config,
    )

    level_gate = None
    if not cfg_source.core.filters:
        lvl = cfg_source.core.log_level.upper()
        if lvl != "DEBUG":
            level_gate = LEVEL_PRIORITY.get(lvl, None)

    return LoggerSetup(
        settings=cfg_source,
        sinks=built_sinks,
        enrichers=enrichers,
        redactors=redactors,
        processors=processors,
        filters=filters,
        metrics=metrics,
        sink_write=sink_write,
        sink_write_serialized=sink_write_serialized,
        circuit_config=circuit_config,
        level_gate=level_gate,
    )
```

### Create `_start_plugins_sync` Helper

Encapsulate the ThreadPoolExecutor logic for starting plugins in a sync context:

```python
def _start_plugins_sync(
    enrichers: list[object],
    redactors: list[object],
    processors: list[object],
    filters: list[object],
) -> tuple[list[object], list[object], list[object], list[object]]:
    """
    Start plugins in sync context.

    If inside an event loop, uses ThreadPoolExecutor to avoid blocking.
    Otherwise, uses asyncio.run() directly.

    Returns:
        Tuple of (started_enrichers, started_redactors, started_processors, started_filters)
    """
    async def _do_start() -> tuple[list[object], list[object], list[object], list[object]]:
        return (
            await _start_plugins(enrichers, "enricher"),
            await _start_plugins(redactors, "redactor"),
            await _start_plugins(processors, "processor"),
            await _start_plugins(filters, "filter"),
        )

    try:
        _asyncio.get_running_loop()
        # Inside event loop - run in thread to avoid blocking
        import concurrent.futures

        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
            future = executor.submit(_asyncio.run, _do_start())
            return future.result(timeout=5.0)
    except RuntimeError:
        # No running loop - safe to use asyncio.run
        return _asyncio.run(_do_start())
    except Exception:
        # If startup fails, return unstarted plugins (they start lazily)
        return enrichers, redactors, processors, filters
```

### Create `_apply_logger_extras` Helper

Apply post-creation configuration to the logger:

```python
def _apply_logger_extras(
    logger: SyncLoggerFacade | AsyncLoggerFacade,
    setup: LoggerSetup,
    *,
    started_enrichers: list[object],
    started_redactors: list[object],
    started_processors: list[object],
    started_filters: list[object],
) -> None:
    """Apply post-creation configuration to logger."""
    cfg = setup.settings

    # Bind default context if configured
    try:
        if cfg.core.context_binding_enabled and cfg.core.default_bound_context:
            logger.bind(**cfg.core.default_bound_context)
    except Exception:
        pass

    # Warn about sensitive fields policy
    try:
        if cfg.core.sensitive_fields_policy:
            from .core.diagnostics import warn as _warn

            _warn(
                "redactor",
                "sensitive fields policy present",
                fields=len(cfg.core.sensitive_fields_policy),
                _rate_limit_key="policy",
            )
    except Exception:
        pass

    # Capture unhandled exceptions if enabled
    try:
        if cfg.core.capture_unhandled_enabled:
            from .core.errors import capture_unhandled_exceptions as _cap

            _cap(logger)
    except Exception:
        pass

    # Assign internal attributes (use started plugins, not setup's unstarted ones)
    logger._redactors = cast(list[BaseRedactor], started_redactors)  # noqa: SLF001
    logger._processors = cast(list[BaseProcessor], started_processors)  # noqa: SLF001
    logger._filters = started_filters  # noqa: SLF001
    logger._sinks = setup.sinks  # noqa: SLF001
```

### Refactored `get_logger`

```python
def get_logger(
    name: str | None = None,
    *,
    settings: Settings | None = None,
    sinks: list[object] | None = None,
) -> SyncLoggerFacade:
    setup = _configure_logger_common(settings, sinks)

    # Start plugins (sync context - use thread if needed)
    enrichers, redactors, processors, filters = _start_plugins_sync(
        setup.enrichers,
        setup.redactors,
        setup.processors,
        setup.filters,
    )

    logger = SyncLoggerFacade(
        name=name,
        queue_capacity=setup.settings.core.max_queue_size,
        batch_max_size=setup.settings.core.batch_max_size,
        batch_timeout_seconds=setup.settings.core.batch_timeout_seconds,
        backpressure_wait_ms=setup.settings.core.backpressure_wait_ms,
        drop_on_full=setup.settings.core.drop_on_full,
        sink_write=setup.sink_write,
        sink_write_serialized=setup.sink_write_serialized,
        enrichers=cast(list[BaseEnricher], enrichers),
        processors=cast(list[BaseProcessor], processors),
        filters=filters,
        metrics=setup.metrics,
        exceptions_enabled=setup.settings.core.exceptions_enabled,
        exceptions_max_frames=setup.settings.core.exceptions_max_frames,
        exceptions_max_stack_chars=setup.settings.core.exceptions_max_stack_chars,
        serialize_in_flush=setup.settings.core.serialize_in_flush,
        num_workers=setup.settings.core.worker_count,
        level_gate=setup.level_gate,
    )

    _apply_logger_extras(
        logger,
        setup,
        started_enrichers=enrichers,
        started_redactors=redactors,
        started_processors=processors,
        started_filters=filters,
    )
    logger.start()
    return logger
```

### Refactored `get_async_logger`

```python
async def get_async_logger(
    name: str | None = None,
    *,
    settings: Settings | None = None,
    sinks: list[object] | None = None,
) -> AsyncLoggerFacade:
    setup = _configure_logger_common(settings, sinks)

    # Start plugins (async context - use await directly)
    enrichers = await _start_plugins(setup.enrichers, "enricher")
    redactors = await _start_plugins(setup.redactors, "redactor")
    processors = await _start_plugins(setup.processors, "processor")
    filters = await _start_plugins(setup.filters, "filter")

    logger = AsyncLoggerFacade(
        name=name,
        queue_capacity=setup.settings.core.max_queue_size,
        batch_max_size=setup.settings.core.batch_max_size,
        batch_timeout_seconds=setup.settings.core.batch_timeout_seconds,
        backpressure_wait_ms=setup.settings.core.backpressure_wait_ms,
        drop_on_full=setup.settings.core.drop_on_full,
        sink_write=setup.sink_write,
        sink_write_serialized=setup.sink_write_serialized,
        enrichers=cast(list[BaseEnricher], enrichers),
        processors=cast(list[BaseProcessor], processors),
        filters=filters,
        metrics=setup.metrics,
        exceptions_enabled=setup.settings.core.exceptions_enabled,
        exceptions_max_frames=setup.settings.core.exceptions_max_frames,
        exceptions_max_stack_chars=setup.settings.core.exceptions_max_stack_chars,
        serialize_in_flush=setup.settings.core.serialize_in_flush,
        num_workers=setup.settings.core.worker_count,
        level_gate=setup.level_gate,
    )

    _apply_logger_extras(
        logger,
        setup,
        started_enrichers=enrichers,
        started_redactors=redactors,
        started_processors=processors,
        started_filters=filters,
    )
    logger.start()
    return logger
```

## Files Changed

| File                      | Action   | Lines     |
| ------------------------- | -------- | --------- |
| `src/fapilog/__init__.py` | Refactor | ~-100 net |

## Estimated Effort

3-4 hours

## Risk Assessment

**Risk Level: Low**

- Logic extraction only
- Both functions remain identical externally
- Well-covered by existing tests
- Easy to verify behavior matches

## Dependencies

- Can proceed independently
- Complements Story 6.8 (Unify Facades) — now complete

## Validation

- [ ] `get_logger()` returns identical logger before/after
- [ ] `get_async_logger()` returns identical logger before/after
- [ ] All integration tests pass
- [ ] Settings properly propagated
- [ ] Plugin lifecycle correctly managed
- [ ] ThreadPoolExecutor fallback tested (sync context inside event loop)

## Rollback Plan

Revert commit; duplication is functional.

## Success Metrics

- [ ] ~100-120 lines of duplication removed
- [ ] Single point of change for configuration logic
- [ ] No behavioral changes
- [ ] `_start_plugins_sync` properly encapsulates sync-context complexity
