# Shared Structured Logging Module

**Task**: AZ-266_log_module
**Name**: Shared Logging Module
**Description**: Provide the `get_logger(component_id)` entrypoint, a stable JSON formatter that emits records matching the log_record_schema contract, and the stdout / journald handlers used by Tier-1 and Tier-2 deployments.
**Complexity**: 3 points
**Dependencies**: AZ-263_initial_structure
**Component**: shared.logging (cross-cutting; epic AZ-245 / E-CC-LOG)
**Tracker**: AZ-266
**Epic**: AZ-245 (E-CC-LOG)

## Problem

Every onboard component must emit structured JSON logs at DEBUG / INFO / WARN / ERROR with a stable, machine-parseable shape so post-flight analysis (FDR tooling, blackbox scenario checks, traceability matrix verification) can correlate events across components. Without one shared logger, format drift is guaranteed within a few weeks of parallel component development.

## Outcome

- A single `get_logger(component_id)` call is the only logging entrypoint any onboard module ever uses.
- Every emitted record is a single-line JSON object whose key set, key order, and value types match the `log_record_schema` contract version 1.0.0.
- Tier-1 deployments capture logs via Docker stdout; Tier-2 deployments capture logs via journald — switched by config, not by code.

## Scope

### Included

- `get_logger(component_id: str) -> Logger` factory backed by Python stdlib `logging`.
- A JSON formatter that emits the schema's 8 fields in the contract-mandated order, regardless of construction order. Implementation may use `python-json-logger` or `orjson`-backed formatter — whichever is already pinned in the project's lockfile from AZ-263.
- A stdout handler for Tier-1 (Docker) and a journald handler for Tier-2 (Jetson). Selection is config-driven via the structured-logging entry of the cross-cutting config epic (AZ-246 / E-CC-CONF).
- Per-frame structured-logging helpers for the documented per-component shapes referenced in epic AZ-245 (`vio.tick`, `vpr.query`, etc.) so component code can emit one-liner logs without rebuilding the kv dict.
- Public interface contract published at `_docs/02_document/contracts/shared_logging/log_record_schema.md`.

### Excluded

- The FDR bridge that forwards ERROR + WARN records into the Flight Data Recorder — owned by the next task (`03_fdr_log_bridge`, parented to the same epic).
- Per-component log call sites (each component epic owns its own logging call sites).
- Log schema versioning beyond 1.0.0 — handled by future change-log entries on the contract file.

## Acceptance Criteria

**AC-1: Single logger entrypoint**
Given any onboard Python module that imports the shared logging package
When the module calls `get_logger("c2_vpr")`
Then it receives a `Logger` whose every record passes the schema contract test (no other logger configuration is required by the caller)

**AC-2: Field order is stable**
Given a logger configured with the JSON formatter
When a component calls `logger.info(msg, extra={"frame_id": 42, "kind": "vpr.query", "kv": {...}})`
Then the emitted bytes parse as a single-line JSON object whose keys appear in the order `ts, level, component, frame_id, kind, msg, kv, exc`, regardless of the order the caller passed the fields

**AC-3: Level normalisation**
Given a logger receiving a record at level `WARNING` (Python stdlib name)
When the formatter emits the JSON record
Then the `level` field reads `WARN` (per contract), not `WARNING`

**AC-4: Handler topology selection**
Given the structured-logging config block selects `tier=1` (or `tier=2`)
When `runtime_root.py` initialises logging
Then exactly one stdout handler (or journald handler) is attached, with no duplicate handlers and no handler from the wrong tier

**AC-5: Non-frame records omit frame_id**
Given a startup or shutdown log call that does not pass a `frame_id`
When the record is emitted
Then `frame_id` appears as JSON `null` (never as a synthesised value, never absent from the key list)

## Non-Functional Requirements

**Performance**
- Per-record formatter latency p99 ≤ 0.2 ms on Tier-2 (Jetson Orin Nano Super) for a record with `len(kv) ≤ 8` scalar entries. Validated by a microbenchmark in unit tests.
- DEBUG records on the steady-state hot path allocate at most one new string (the formatted JSON line); no transient dict copies of `kv` are permitted.

**Reliability**
- Formatter never raises into the caller. A serialisation failure logs an internal `WARN` with `kind="log.format_error"` and drops the offending record's `kv` payload (replaces with `{"_format_error": "<reason>"}`); the rest of the record is still emitted.
- No global mutable state outside the standard `logging` module's own logger registry; multiple `get_logger("c2_vpr")` calls return the same cached `Logger` instance.

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|-------------|-----------------|
| AC-1 | `get_logger("c2_vpr")` returns a Logger with the JSON formatter attached | Logger instance present; formatter produces valid contract record |
| AC-2 | Emit a record with kwargs in shuffled order | Parsed JSON keys appear in the contract's mandated order |
| AC-3 | Log at `logging.WARNING` level | Emitted JSON `level` field equals `"WARN"` |
| AC-4 | Initialise logging twice with the same tier-1 config | Exactly one stdout handler attached; no duplicates |
| AC-5 | Log a startup INFO without `frame_id` | Emitted JSON contains `"frame_id": null` |
| NFR-perf | Microbenchmark formatter on a record with 8 scalar kv entries | p99 ≤ 0.2 ms over 10k iterations |
| NFR-reliability | Pass a non-JSON-serialisable object in `kv` (e.g. a class instance) | Formatter emits the record with `kv={"_format_error": "..."}`; caller does not see an exception |

## Constraints

- Public interface frozen by `_docs/02_document/contracts/shared_logging/log_record_schema.md` v1.0.0 — any change requires a contract version bump.
- Stdlib `logging` is the only allowed underlying logging mechanism (per epic AZ-245 architecture note: "no third-party log aggregator").
- No new dependency beyond what AZ-263 / E-BOOT already pinned in `pyproject.toml`.

## Risks & Mitigation

**Risk 1: Formatter performance regression**
- *Risk*: Naïve `json.dumps` on each record exceeds the 0.2 ms p99 budget on Jetson.
- *Mitigation*: Bench against `orjson`-backed formatter as a fallback if stdlib `json` misses budget; choice is reversible because the contract is the public surface, not the formatter implementation.

**Risk 2: Handler duplication on hot-reload**
- *Risk*: Re-initialising logging during integration tests stacks duplicate handlers, multiplying every emitted record.
- *Mitigation*: `get_logger` checks for existing handlers on the named logger before adding new ones; integration test fixture asserts handler count after teardown.

## Contract

This task produces the contract at `_docs/02_document/contracts/shared_logging/log_record_schema.md`.
Consumers MUST read that file — not this task spec — to discover the interface.
