# Story 1.24: Fix Strict Mode Drop Accounting

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

---

## Context / Background

The GPT-5.2 audit identified a bug: events dropped in strict serialization mode are not counted in the `dropped` counter, making drop accounting inaccurate.

**Current state in `src/fapilog/core/worker.py:274-276`:**

```python
async def _flush_batch(self, batch: list[dict[str, Any]]) -> None:
    ...
    for entry in batch:
        ...
        if self._serialize_in_flush and self._sink_write_serialized is not None:
            view, drop_entry = await self._try_serialize(entry)
            if drop_entry:
                continue  # ← BUG: Entry dropped but counter NOT incremented!
            ...
```

**The `_try_serialize` function (lines 418-444):**

```python
async def _try_serialize(self, entry: dict[str, Any]) -> tuple[SerializedView | None, bool]:
    try:
        return serialize_envelope(entry), False
    except Exception as exc:
        ...
        if strict_mode:
            return None, True  # drop_entry=True → event is dropped
        ...
```

**Problem:**

- When `strict_envelope_mode=True` and serialization fails, the event is silently dropped
- The `dropped` counter is NOT incremented (line 292 only handles batch-level failures)
- Metrics show fewer drops than actually occurred
- Users can't detect data loss in strict mode

**Evidence:**

The `dropped` counter is only incremented in the exception handler at line 292:
```python
except Exception as exc:
    self._counters["dropped"] += len(batch)  # Only on batch failure
```

---

## Scope (In / Out)

### In Scope

- Increment `dropped` counter when strict mode drops an event
- Record metric for strict-mode drops
- Add test asserting dropped count in strict mode

### Out of Scope

- Changing strict mode behavior itself
- Adding per-event drop reasons/logging

---

## Acceptance Criteria

### AC1: Dropped Counter Incremented

**Description:** When strict mode drops an event, `dropped` counter is incremented.

**Validation:**

```python
# In _flush_batch, after strict mode drop:
if drop_entry:
    self._counters["dropped"] += 1
    continue
```

### AC2: Metrics Recorded

**Description:** Strict mode drops are recorded in metrics if collector is present.

**Validation:**

```python
if drop_entry:
    self._counters["dropped"] += 1
    if self._metrics is not None:
        await self._metrics.record_events_dropped(1)
    continue
```

### AC3: Test Asserts Dropped Count

**Description:** Test verifies dropped counter reflects strict mode failures.

**Validation:**

```python
async def test_strict_mode_drop_accounting():
    """Dropped counter includes strict mode serialization failures."""
    logger = SyncLoggerFacade(
        ...,
        serialize_in_flush=True,
        strict_envelope_mode=True,
    )

    # Log event that will fail serialization (e.g., contains non-serializable object)
    logger.info("test", extra={"bad": object()})
    await logger.drain()

    assert logger._dropped == 1  # Must be counted
```

---

## Implementation Notes

### File Changes

```text
src/fapilog/core/worker.py (MODIFIED - fix drop accounting)
tests/unit/test_worker_drop_accounting.py (NEW)
```

### Key Fix

```python
# worker.py _flush_batch() around line 274

if self._serialize_in_flush and self._sink_write_serialized is not None:
    view, drop_entry = await self._try_serialize(entry)
    if drop_entry:
        # FIX: Count the drop
        self._counters["dropped"] += 1
        if self._metrics is not None:
            await self._metrics.record_events_dropped(1)
        continue
```

---

## Tasks

- [ ] Add `self._counters["dropped"] += 1` before `continue` in strict mode drop path
- [ ] Add metrics recording for strict mode drops
- [ ] Add unit test asserting dropped count in strict mode
- [ ] Update CHANGELOG

---

## Tests

### Existing Test to Fix

**`tests/unit/test_pipeline_processing.py:496-527`** - This test currently asserts the buggy behavior:

```python
async def test_flush_serialization_strict_drops(self, monkeypatch) -> None:
    ...
    await logger._flush_batch(batch)

    assert logger._processed == 0
    assert logger._dropped == 0  # ← BUG: Should be 1!
```

**Fix:** Change line 527 to `assert logger._dropped == 1`

### Additional Tests

- `tests/unit/test_worker_drop_accounting.py` (optional)
  - `test_strict_mode_drop_recorded_in_metrics`

### Existing Tests

Verify no regression in:
- `tests/unit/test_worker*.py`
- `tests/integration/test_worker_lifecycle.py`

---

## Definition of Done

- [x] Drop counter incremented on strict mode failure
- [x] Metrics recorded
- [x] Test asserts dropped count
- [x] All tests pass
- [x] CHANGELOG updated

---

## Risks / Rollback

### Risks

1. **Risk:** Existing tests assert specific dropped counts
   - **Mitigation:** Review and update affected tests

### Rollback Plan

Revert the single-line fix if issues arise.

---

## Related Stories

- **Related:** Story 1.19 - Backpressure semantics (drop behavior documentation)

---

## Change Log

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