# Story 6.11: Diagnostics Output Separation

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

---

## Context / Background

The GPT-5.2 audit identified an operability gap: internal diagnostics write to stdout, mixing with application logs and complicating log pipelines.

**Current state in `src/fapilog/core/diagnostics.py:20-22`:**

```python
def _emit_to_stdout(
    payload: dict[str, Any],
) -> None:
    print(json.dumps(payload, separators=(",", ":")), flush=True)
```

**Problem:**

- Diagnostics (internal warnings, errors) go to stdout
- Application JSON logs also go to stdout (via stdout_json sink)
- Log aggregators (Datadog, Splunk, ELK) receive mixed streams
- Can't filter diagnostics vs application logs at the OS level
- Violates Unix convention: stdout for data, stderr for diagnostics

---

## Scope (In / Out)

### In Scope

- Change default diagnostics output to stderr
- Add configuration option to customize output destination
- Maintain backward compatibility via opt-in stdout mode

### Out of Scope

- Structured diagnostics format changes
- Diagnostics filtering/levels (separate concern)

---

## Acceptance Criteria

### AC1: Diagnostics Default to stderr

**Description:** Internal diagnostics write to stderr by default.

**Validation:**

```python
# core/diagnostics.py
import sys

def _emit_to_stderr(payload: dict[str, Any]) -> None:
    print(json.dumps(payload, separators=(",", ":")), file=sys.stderr, flush=True)
```

### AC2: Configurable Output

**Description:** Diagnostics destination can be configured.

**Validation:**

```python
from fapilog.core.settings import Settings

# Default: stderr
s = Settings()
assert s.core.diagnostics_output == "stderr"

# Opt-in stdout for backward compat
s = Settings(core={"diagnostics_output": "stdout"})
```

### AC3: Log Pipelines Can Separate Streams

**Description:** Users can route diagnostics separately from application logs.

**Validation:**

```bash
# Application logs to file, diagnostics visible in terminal
python app.py > app.log 2>&1  # Both to file (unchanged)
python app.py > app.log       # App to file, diagnostics to terminal
python app.py 2>/dev/null     # Suppress diagnostics only
```

---

## Implementation Notes

### File Changes

```text
src/fapilog/core/diagnostics.py (MODIFIED)
src/fapilog/core/settings.py (MODIFIED - add diagnostics_output)
```

### Key Changes

```python
# diagnostics.py
import sys
from typing import TextIO

_output_stream: TextIO = sys.stderr  # Default to stderr

def set_output_stream(stream: TextIO) -> None:
    global _output_stream
    _output_stream = stream

def _emit(payload: dict[str, Any]) -> None:
    print(json.dumps(payload, separators=(",", ":")), file=_output_stream, flush=True)
```

---

## Tasks

- [x] Change default output from stdout to stderr
- [x] Add `diagnostics_output` setting
- [x] Initialize output stream from settings
- [x] Update CHANGELOG

---

## Definition of Done

- [x] Diagnostics write to stderr by default
- [x] Backward-compatible stdout option exists
- [x] All tests pass
- [x] CHANGELOG updated

---

## Risks / Rollback

### Risks

1. **Risk:** Users relying on stdout diagnostics
   - **Mitigation:** Document the change; provide opt-in for stdout

### Rollback Plan

Revert to stdout default if issues reported.

---

## Code Review

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

### Summary

Changed internal diagnostics to write to stderr by default (Unix convention) with configurable `diagnostics_output` setting for backward compatibility.

### AC Verification

| Criterion | Evidence |
|-----------|----------|
| AC1: Diagnostics Default to stderr | `diagnostics.py:23` - `print(..., file=sys.stderr, flush=True)` |
| AC2: Configurable Output | `settings.py:629-635` - `diagnostics_output: Literal["stderr", "stdout"]` with default "stderr" |
| AC3: Log Pipelines Can Separate | Verified by AC1 - stderr output enables `> app.log` and `2>/dev/null` separation |

### Quality Gates

- [x] ruff check passed
- [x] ruff format passed
- [x] mypy passed
- [x] diff-cover 100% (2 lines changed, 0 missing)
- [x] No weak assertions
- [x] No dead code
- [x] No Pydantic v1 syntax
- [x] Settings descriptions >= 15 chars

---

## Change Log

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