# Story 6.8: Unify Logger Facade Implementations

## Status: Complete

## Summary

Extract the ~80% shared implementation from `SyncLoggerFacade` and `AsyncLoggerFacade` into a common mixin, eliminating ~500+ lines of duplicated code.

## Motivation

The complexity audit identified this as a key over-engineering issue: two logger facade classes (~1400 lines total) that share the vast majority of their code.

**Duplicated Elements:**

- `__init__` parameters (21 identical parameters)
- Worker management (`start`, `_worker_main`, `_make_worker`)
- `_stop_enrichers_and_redactors` wrapper (~30 lines each, delegates to shared `stop_plugins()`)
- Core `_enqueue` logic (sampling, deduplication, context binding, exception serialization)
- Context binding API (`bind`, `unbind`, `clear_context`)
- Enricher toggles (`enable_enricher`, `disable_enricher`)
- Health checking (`check_health`, `self_test`)
- Metrics scheduling

**Actual Differences:**

- `SyncLoggerFacade._enqueue`: Uses `asyncio.run_coroutine_threadsafe` for cross-thread submission
- `AsyncLoggerFacade._enqueue`: Uses native `await`
- Method signatures: `def` vs `async def` for public methods

This duplication means:

- Bugs fixed in one facade may not be fixed in other
- New features must be implemented twice
- Risk of behavioral drift between facades
- Inflated codebase complexity

## Acceptance Criteria

- [ ] Single source of truth for shared logic via `_LoggerMixin`
- [ ] `SyncLoggerFacade` public API unchanged
- [ ] `AsyncLoggerFacade` public API unchanged
- [ ] ~500+ lines of duplication eliminated
- [ ] All existing tests pass
- [ ] No performance regression (benchmark comparison)
- [ ] Both facades remain independently usable

## Technical Approach

### Option A: Mixin Class (Recommended)

Create `_LoggerMixin` with all shared methods, then have both facades inherit from it.

```python
class _LoggerMixin(_WorkerCountersMixin):
    """Shared implementation for logger facades."""

    _name: str
    _queue: NonBlockingRingQueue
    _enrichers: list
    _redactors: list
    _processors: list
    _filters: list
    _metrics: MetricsCollector | None
    _counters: dict[str, int]
    # ... other shared attributes

    def _common_init(
        self,
        *,
        name: str | None,
        queue_capacity: int,
        batch_max_size: int,
        batch_timeout_seconds: float,
        backpressure_wait_ms: int,
        drop_on_full: bool,
        sink_write: Any,
        sink_write_serialized: Any | None = None,
        enrichers: list[BaseEnricher] | None = None,
        processors: list[BaseProcessor] | None = None,
        filters: list[Any] | None = None,
        metrics: MetricsCollector | None = None,
        exceptions_enabled: bool = True,
        exceptions_max_frames: int = 50,
        exceptions_max_stack_chars: int = 20000,
        serialize_in_flush: bool = False,
        num_workers: int = 1,
        level_gate: int | None = None,
    ) -> None:
        """Common initialization logic."""
        self._name = name or "root"
        self._queue = NonBlockingRingQueue(capacity=queue_capacity)
        # ... rest of init (currently ~70 lines duplicated)

    def bind(self, **context: Any) -> Self:
        """Return self with additional bound context."""
        # ... shared implementation
        return self

    def unbind(self, *keys: str) -> Self:
        # ... shared implementation
        return self

    def clear_context(self) -> None:
        # ... shared implementation

    def enable_enricher(self, enricher: BaseEnricher) -> None:
        # ... shared implementation

    def disable_enricher(self, name: str) -> None:
        # ... shared implementation

    def _prepare_event(
        self,
        level: str,
        message: str,
        *,
        exc: BaseException | None = None,
        exc_info: Any | None = None,
        **metadata: Any,
    ) -> dict[str, Any] | None:
        """Prepare event for enqueueing. Returns None if filtered."""
        # Level gate check
        # Sampling check
        # Error deduplication
        # Context binding
        # Exception serialization
        # LogEvent creation
        return payload

    def _make_worker(self) -> LoggerWorker:
        # ... shared implementation

    async def _stop_enrichers_and_redactors(self) -> None:
        # ... shared implementation (calls stop_plugins from worker.py)

    async def check_health(self) -> Any:
        # ... shared implementation

    async def self_test(self) -> dict[str, Any]:
        # ... shared implementation


class SyncLoggerFacade(_LoggerMixin):
    """Sync facade with thread-crossing for enqueue."""

    def __init__(self, *, name: str | None, ...) -> None:
        self._common_init(name=name, ...)

    def _enqueue(self, level: str, message: str, **kw) -> None:
        payload = self._prepare_event(level, message, **kw)
        if payload is None:
            return
        # Thread-crossing logic here
        self._submit_to_worker(payload)

    def info(self, message: str, **metadata) -> None:
        self._enqueue("INFO", message, **metadata)

    # ... other sync methods


class AsyncLoggerFacade(_LoggerMixin):
    """Async facade with native await."""

    def __init__(self, *, name: str | None, ...) -> None:
        self._common_init(name=name, ...)

    async def _enqueue(self, level: str, message: str, **kw) -> None:
        payload = self._prepare_event(level, message, **kw)
        if payload is None:
            return
        # Native async enqueue
        await self._async_enqueue(payload, timeout=...)

    async def info(self, message: str, **metadata) -> None:
        await self._enqueue("INFO", message, **metadata)

    # ... other async methods
```

### Option B: Composition (LoggerCore)

Create a `LoggerCore` that handles all internal logic; facades become thin wrappers.

```python
class LoggerCore:
    """Core logger implementation."""

    def __init__(self, *, name: str | None, ...) -> None:
        # All initialization

    def prepare_event(self, level: str, message: str, **kw) -> dict | None:
        # Event preparation logic

    async def stop_plugins(self) -> None:
        # Plugin lifecycle

    async def check_health(self) -> Any:
        # Health aggregation


class SyncLoggerFacade:
    def __init__(self, **kwargs) -> None:
        self._core = LoggerCore(**kwargs)

    def info(self, message: str, **metadata) -> None:
        payload = self._core.prepare_event("INFO", message, **metadata)
        if payload:
            self._submit_sync(payload)


class AsyncLoggerFacade:
    def __init__(self, **kwargs) -> None:
        self._core = LoggerCore(**kwargs)

    async def info(self, message: str, **metadata) -> None:
        payload = self._core.prepare_event("INFO", message, **metadata)
        if payload:
            await self._submit_async(payload)
```

### Recommended Approach

**Option A (Mixin)** is preferred because:

- Maintains direct attribute access (no `self._core.x`)
- Type checkers handle mixins well
- Easier migration path from current code
- Less indirection in stack traces

## Migration Steps

1. **Create `_LoggerMixin`** with extracted methods (don't modify facades yet)
2. **Write tests** verifying mixin behavior matches current facades
3. **Refactor `SyncLoggerFacade`** to use mixin
4. **Run full test suite** — fix any issues
5. **Refactor `AsyncLoggerFacade`** to use mixin
6. **Run full test suite** — fix any issues
7. **Benchmark comparison** to verify no performance regression
8. **Delete duplicated code** from facades

## Files Changed

| File                              | Action               | Lines                |
| --------------------------------- | -------------------- | -------------------- |
| `src/fapilog/core/logger.py`      | Major refactor       | -500, +150 for mixin |
| `tests/unit/test_logger.py`       | May need adjustments | ~0                   |
| `tests/unit/test_async_logger.py` | May need adjustments | ~0                   |

**Net reduction:** ~350 lines

## Estimated Effort

6-8 hours

## Risk Assessment

**Risk Level: Medium**

- Core component modification
- Large refactor
- Well-tested code, so regressions detectable
- Behavioral preservation is key

## Mitigation

- Feature branch with thorough review
- Run full test suite at each step
- Benchmark before/after
- Keep old implementation available for comparison

## Rollback Plan

Revert commit; facades remain functional with duplication.

## Performance Validation

```python
# Benchmark script
import time
from fapilog import get_logger, get_async_logger
import asyncio

def benchmark_sync():
    logger = get_logger()
    start = time.perf_counter()
    for _ in range(100_000):
        logger.info("benchmark message", key="value")
    logger.stop_and_drain()
    return time.perf_counter() - start

async def benchmark_async():
    logger = await get_async_logger()
    start = time.perf_counter()
    for _ in range(100_000):
        await logger.info("benchmark message", key="value")
    await logger.drain()
    return time.perf_counter() - start

# Compare before/after refactor
```

## Dependencies

- Story 6.3 (Extract Shared Plugin Stop Logic): **COMPLETE** — `stop_plugins()` already extracted to `worker.py`

## Success Metrics

- [ ] ~500+ lines removed from `logger.py`
- [ ] Zero behavioral changes (all tests pass)
- [ ] No performance regression (within 5% of baseline)
- [ ] Both facades work correctly in isolation
- [ ] Future features only need single implementation
