# Story 1.21: Extract Logger Envelope Building

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

---

## Context / Background

The GPT-5.2 audit identified `src/fapilog/core/logger.py` as a complexity hotspot. While worker logic has already been extracted to `worker.py` (494 lines), `logger.py` remains at 1,102 lines.

**Current state (post-worker extraction):**

```
src/fapilog/core/worker.py (494 lines) - ALREADY EXISTS
├── LoggerWorker class
├── enqueue_with_backpressure()
├── stop_plugins()
└── Pipeline stages (filters, enrichers, redactors, processors)

src/fapilog/core/logger.py (1,102 lines)
├── AsyncLogger (dataclass)
├── DrainResult (dataclass)
├── _WorkerCountersMixin
├── _LoggerMixin (~540 lines)
│   ├── Envelope construction (embedded in log methods)
│   ├── Thread/loop orchestration (start, bound-loop vs thread-mode)
│   ├── Enqueue coordination (_enqueue)
│   ├── Drain coordination (_drain_on_loop, _drain_thread_mode)
│   └── Context binding (bind, unbind)
├── SyncLoggerFacade
└── AsyncLoggerFacade
```

**Remaining opportunity:**

Envelope building logic (timestamp, level mapping, exception serialization, context merging) is still embedded in the log methods. Extracting this could further reduce complexity.

**Note:** This story is lower priority since worker extraction is complete. The remaining 1,102 lines may be acceptable given the complexity of thread/loop orchestration.

---

## Scope (In / Out)

### In Scope

- Extract envelope building into `src/fapilog/core/envelope.py`
- Keep thread/loop orchestration in `logger.py` (complex, tightly coupled)
- Maintain 100% backward compatibility

### Out of Scope

- Worker extraction (already done in `worker.py`)
- Thread/loop mode refactoring (too risky for marginal benefit)
- Changing public API signatures

---

## Acceptance Criteria

### AC1: Envelope Module Extracted

**Description:** Log envelope construction is in dedicated module.

**Validation:**

```python
from fapilog.core.envelope import build_envelope

envelope = build_envelope(
    level="INFO",
    message="test",
    extra={"key": "value"},
    exception=None,
)
assert "timestamp" in envelope
assert envelope["level"] == "INFO"
```

### AC2: Logger.py Uses Envelope Module

**Description:** Log methods delegate to `build_envelope()`.

**Validation:**

```python
# In logger.py log methods:
from .envelope import build_envelope

def info(self, msg, **kwargs):
    envelope = build_envelope("INFO", msg, **kwargs)
    self._enqueue(envelope)
```

### AC3: Public API Unchanged

**Description:** All existing imports and usage patterns work without modification.

**Validation:**

```python
from fapilog import get_logger

logger = get_logger()
logger.info("test", extra={"key": "value"})
```

### AC4: All Tests Pass

**Description:** Existing test suite passes without modification.

**Validation:**

```bash
pytest tests/ -v
```

---

## Implementation Notes

### File Structure

```text
src/fapilog/core/
├── logger.py      # Facades + orchestration (~900 lines target)
├── worker.py      # EXISTING: Worker lifecycle (494 lines)
├── envelope.py    # NEW: Envelope building (~150-200 lines)
└── __init__.py    # No changes needed
```

### Envelope Module Contents

```python
# envelope.py
from datetime import datetime, timezone
from typing import Any

def build_envelope(
    level: str,
    message: str,
    *,
    extra: dict[str, Any] | None = None,
    bound_context: dict[str, Any] | None = None,
    exception: BaseException | None = None,
    exceptions_enabled: bool = True,
    exceptions_max_frames: int = 50,
    exceptions_max_stack_chars: int = 20000,
    logger_name: str = "root",
) -> dict[str, Any]:
    """Construct a log envelope with all metadata."""
    envelope = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "level": level,
        "message": message,
        "logger": logger_name,
    }
    if extra:
        envelope.update(extra)
    if bound_context:
        envelope.update(bound_context)
    if exception and exceptions_enabled:
        envelope["exception"] = serialize_exception(
            exception,
            max_frames=exceptions_max_frames,
            max_chars=exceptions_max_stack_chars,
        )
    return envelope
```

---

## Tasks

### Phase 1: Envelope Extraction

- [ ] Create `src/fapilog/core/envelope.py`
- [ ] Extract envelope building logic from log methods
- [ ] Extract exception serialization helper
- [ ] Update log methods to use `build_envelope()`

### Phase 2: Testing

- [ ] Run existing tests
- [ ] Add unit tests for `envelope.py`

### Phase 3: Documentation

- [ ] Update CHANGELOG

---

## Tests

### Existing Tests

All existing tests must pass unchanged:

- `tests/unit/test_logger*.py` (10 test files)
- `tests/integration/test_worker_lifecycle.py`

### New Tests

- `tests/unit/test_envelope.py` - unit tests for envelope building

---

## Definition of Done

### Code Complete

- [x] `envelope.py` created and working
- [x] Log methods delegate to `build_envelope()`
- [x] All imports backward compatible

### Quality Assurance

- [x] All existing tests pass
- [x] `ruff check` passes
- [x] `mypy` passes

### Documentation

- [ ] CHANGELOG updated (skipped - internal refactor, no user-facing changes)

---

## Risks / Rollback

### Risks

1. **Risk:** Circular imports with envelope module
   - **Mitigation:** Keep envelope.py dependency-free

2. **Risk:** Breaking internal test mocks
   - **Mitigation:** Mocks target facade methods, not internal helpers

### Rollback Plan

If issues occur:

1. Revert envelope.py
2. Keep envelope building inline

---

## Related Stories

- **Related:** Story 1.19 - Backpressure semantics
- **Note:** Worker extraction already complete in `worker.py`

---

## Code Review

**Date:** 2026-01-18
**Reviewer:** Claude
**Verdict:** OK to PR

### Summary

Reviewed extraction of envelope building logic from `logger.py` into dedicated `envelope.py` module. All acceptance criteria met with 100% diff coverage.

### AC Verification

| Criterion | Evidence |
|-----------|----------|
| AC1: Envelope Module Extracted | `envelope.py:17-89` - `build_envelope()` with full type annotations |
| AC2: Logger.py Uses Envelope Module | `logger.py:24` import, `logger.py:335-347` delegation |
| AC3: Public API Unchanged | 157 existing tests pass unchanged |
| AC4: All Tests Pass | pytest: 157 passed in 6.71s |

### Quality Gates

- [x] ruff check passed
- [x] ruff format passed
- [x] mypy passed
- [x] diff-cover >= 90% (100%)
- [x] No weak assertions
- [x] No dead code

---

## Change Log

| Date       | Change                                      | Author |
|------------|---------------------------------------------|--------|
| 2026-01-16 | Initial draft                               | Claude |
| 2026-01-16 | Revised: worker.py already exists; re-scoped to envelope only | Claude |
