# Story 1.31: Lazy Envelope Building

**Status:** Draft (Placeholder - Pending Profiling Results)
**Priority:** High
**Depends on:** Story 1.30 (Hot Path Performance Profiling)

---

## Context / Background

Fapilog's hot path currently builds the complete log envelope synchronously before enqueueing:

```
logger.info("message")
  → _enqueue()
  → _prepare_payload()
  → build_envelope()     ← ~250 μs (90% of hot path)
  → queue.enqueue()
  → return to caller
```

**Proposed change:** Defer envelope construction to the background worker, reducing hot path to minimal capture:

```
logger.info("message")
  → capture (level, message, metadata, timestamp, context)  ← ~10-20 μs (projected)
  → queue.enqueue(raw_tuple)
  → return to caller

Worker thread (async):
  → dequeue raw_tuple
  → build_envelope()     ← Moved here
  → enrichers/filters/redactors
  → sink.write()
```

**Projected impact (pending Story 1.30 validation):**

| Metric | Current | Projected | Improvement |
|--------|---------|-----------|-------------|
| Hot path latency | 279 μs | 15-60 μs | 5-18x |
| Throughput | 3,295/sec | 15,000-50,000/sec | 5-15x |
| vs stdlib ratio | 0.04x | 0.2-0.6x | Competitive |

**⚠️ This story is a placeholder.** Implementation details and go/no-go decision depend on Story 1.30 profiling results. If profiling reveals the overhead is inherent to Python dict operations rather than envelope building logic, this optimization may not deliver expected gains.

---

## Scope (In / Out)

### In Scope

- Defer `build_envelope()` from hot path to worker
- Maintain all existing functionality (enrichers, filters, redactors, sinks)
- Handle exception capture correctly (must be eager)
- Handle bound context capture correctly
- Defensive copying strategy for mutable metadata
- Update benchmarks to measure improvement

### Out of Scope

- Sync mode (direct sink writes bypassing queue) - separate story
- Worker performance optimization - separate concern
- Memory optimization - existing benchmarks sufficient
- Exception log optimization (likely must remain eager)

---

## Acceptance Criteria

### AC1: Hot Path Latency Reduction

**Description:** Hot path latency is reduced by at least 3x (conservative) to 10x (optimistic).

**Validation:**
```bash
python scripts/benchmarking.py --latency-iterations 5000
# Current: avg 279 μs
# Target:  avg <90 μs (3x) to <30 μs (10x)
```

### AC2: Throughput Improvement

**Description:** Throughput improves proportionally to latency reduction.

**Validation:**
```bash
python scripts/benchmarking.py --iterations 20000
# Current: 3,295 logs/sec
# Target:  >10,000 logs/sec (3x) to >30,000 logs/sec (10x)
```

### AC3: Exception Logs Preserve Stack Traces

**Description:** Exception logging still captures accurate stack traces.

**Validation:**
```python
try:
    raise ValueError("test error")
except Exception:
    logger.exception("Failed")

# Output must contain:
# - Correct exception type: ValueError
# - Correct message: "test error"
# - Accurate stack trace pointing to the raise line
# - Frame information (file, line, function)
```

### AC4: Bound Context Captured Correctly

**Description:** Bound context at time of log call is preserved, not context at time of worker processing.

**Validation:**
```python
logger.bind(request_id="abc")
logger.info("event1")  # Should have request_id="abc"
logger.unbind("request_id")
logger.info("event2")  # Should NOT have request_id

# After drain, verify:
# - event1 has context.request_id = "abc"
# - event2 does not have context.request_id
```

### AC5: Metadata Mutation Safety

**Description:** Caller mutations after log call don't affect logged data.

**Validation:**
```python
data = {"user_id": 123}
logger.info("event", extra=data)
data["user_id"] = 456  # Mutation after log call

# After drain, verify logged event has user_id=123, not 456
```

### AC6: Existing Tests Pass

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

**Validation:**
```bash
pytest tests/ -x
# All tests pass
```

### AC7: Slow Sink Latency Improves

**Description:** Slow sink scenario latency also improves (hot path overhead reduced).

**Validation:**
```bash
python scripts/benchmarking.py --slow-sink-ms 2.0
# Current: avg 286 μs
# Target:  avg <100 μs
```

---

## Implementation Notes

### Pending Story 1.30 Results

The implementation approach depends on profiling findings:

**If overhead is in `build_envelope()` logic:**
- Proceed with lazy envelope building
- Expected gains: 5-15x

**If overhead is in dict operations/memory allocation:**
- Lazy building may not help significantly
- Consider alternative: object pooling, __slots__, or accept current performance

**If overhead is in thread coordination:**
- Focus on reducing sync overhead instead
- Consider lock-free queue or batch capture

### Key Design Decisions (TBD)

1. **Queue item format:**
   ```python
   # Option A: Named tuple (clearer, slight overhead)
   RawLogEntry = namedtuple('RawLogEntry', ['level', 'message', 'metadata', 'timestamp', 'context', 'exc_info'])

   # Option B: Plain tuple (faster, less clear)
   raw = (level, message, metadata, timestamp, context, exc_info)

   # Option C: Dataclass with __slots__ (balance)
   @dataclass(slots=True)
   class RawLogEntry: ...
   ```

2. **Exception handling strategy:**
   ```python
   # Must capture exc_info eagerly - can't defer
   exc_info = sys.exc_info() if capture_exception else None
   # Question: Serialize eagerly or store tuple?
   ```

3. **Defensive copy strategy:**
   ```python
   # Option A: Always deep copy (safest, ~20-50 μs overhead)
   # Option B: Shallow copy (faster, nested mutations leak)
   # Option C: No copy, document "don't mutate" (fastest, risky)
   # Option D: Copy-on-write wrapper (complex)
   ```

### File Changes (Estimated)

```
src/fapilog/core/logger.py (MODIFIED - new _enqueue_lazy path)
src/fapilog/core/worker.py (MODIFIED - build envelope in worker)
src/fapilog/core/envelope.py (MODIFIED - accept raw tuple input)
tests/unit/test_lazy_envelope.py (NEW)
tests/integration/test_lazy_envelope_integration.py (NEW)
docs/user-guide/benchmarks.md (MODIFIED - updated results)
```

---

## Tasks

### Phase 0: Go/No-Go Decision

- [ ] Review Story 1.30 profiling results
- [ ] Determine if lazy envelope building is viable
- [ ] Update projected gains based on actual measurements
- [ ] Decide on implementation approach

### Phase 1: Core Implementation

- [ ] Define raw log entry format (tuple/namedtuple/dataclass)
- [ ] Implement `_enqueue_lazy()` in SyncLoggerFacade
- [ ] Implement `_enqueue_lazy()` in AsyncLoggerFacade
- [ ] Modify worker to call `build_envelope()` on dequeue
- [ ] Handle exception capture (eager path)
- [ ] Handle bound context snapshot
- [ ] Implement defensive copy strategy

### Phase 2: Testing

- [ ] Add unit tests for lazy capture
- [ ] Add unit tests for exception handling
- [ ] Add unit tests for context capture
- [ ] Add unit tests for mutation safety
- [ ] Add integration tests for end-to-end flow
- [ ] Verify all existing tests pass

### Phase 3: Benchmarking & Documentation

- [ ] Run benchmarks, document results
- [ ] Update `docs/user-guide/benchmarks.md`
- [ ] Update architecture documentation
- [ ] CHANGELOG entry

---

## Tests

### Unit Tests

- `tests/unit/test_lazy_envelope.py`
  - test_minimal_capture_creates_raw_entry
  - test_exception_captured_eagerly
  - test_bound_context_snapshot_at_call_time
  - test_metadata_defensive_copy
  - test_worker_builds_envelope_from_raw

### Integration Tests

- `tests/integration/test_lazy_envelope_integration.py`
  - test_end_to_end_lazy_envelope_flow
  - test_exception_stack_trace_accuracy
  - test_context_not_affected_by_later_changes
  - test_concurrent_logging_with_lazy_envelope

### Contract Tests

- `tests/contract/test_lazy_envelope_contract.py`
  - test_raw_entry_produces_valid_envelope
  - test_lazy_envelope_matches_eager_envelope_output

---

## 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
- [ ] Contract tests verify lazy == eager output
- [ ] `ruff check` passes
- [ ] `mypy` passes
- [ ] No regression in existing tests
- [ ] Benchmark shows measurable improvement

### Documentation

- [ ] Code has docstrings where needed
- [ ] Benchmarks documentation updated
- [ ] CHANGELOG updated
- [ ] Architecture docs updated if needed

---

## Risks / Rollback

### Risks

1. **Risk:** Profiling shows overhead is not in envelope building
   - **Mitigation:** Story 1.30 will identify this; adjust or abandon approach
   - **Likelihood:** Medium (40%)

2. **Risk:** Exception handling adds complexity that negates gains
   - **Mitigation:** Exception logs can use eager path; measure separately
   - **Likelihood:** Medium (30%)

3. **Risk:** Defensive copying costs eat into gains
   - **Mitigation:** Measure copy costs in Story 1.30; choose appropriate strategy
   - **Likelihood:** Medium (35%)

4. **Risk:** Worker envelope building errors are hard to debug
   - **Mitigation:** Add detailed error context, consider fallback to raw data logging
   - **Likelihood:** Low (15%)

5. **Risk:** Subtle behavioral differences between lazy and eager paths
   - **Mitigation:** Contract tests verify output equivalence
   - **Likelihood:** Low (20%)

### Rollback Plan

1. Feature flag: `FAPILOG_CORE__USE_LAZY_ENVELOPE=false` disables optimization
2. If issues in production: set flag to false, no code revert needed
3. If fundamental issues: revert PR, document learnings

---

## Related Stories

- **Depends on:** Story 1.30 - Hot Path Performance Profiling (must complete first)
- **Related:** Story 1.25 - Eliminate Settings Hot Path Calls (prior optimization)
- **Future:** Sync mode story (direct writes for fast sinks) - if lazy envelope insufficient

---

## Confidence Assessment (Pre-Profiling)

| Outcome | Confidence |
|---------|------------|
| Achieves 3x improvement | 55-60% |
| Achieves 5x improvement | 40-50% |
| Achieves 10x improvement | 25-35% |
| Must abandon approach | 20-30% |

**These estimates will be updated after Story 1.30 completes.**

---

## Change Log

| Date | Change | Author |
|------|--------|--------|
| 2026-01-19 | Initial placeholder draft | Claude |
