# Unified Logging + Centralized Collection — design (Wave 0)

> **Status:** DRAFT for review (2026-05-28). Author: mgf-common supervisor.
> **Owner / decision-maker:** Bassam.
> **This is the contract** the implementation waves build to. Refine
> here first; the review pass (Wave 8) checks the build against it.
> **Tracking:** task list waves #19–26. **Memory:**
> `project_unified_logging_initiative`.

---

## §1 Goal

A **reliable, unified way to see inside any Magogi app/lib through its
logs — at runtime and afterward.** Logging is the priority surface: it
must be rich + clear. Two layers:

- **Layer A — uniform introspection + access.** For *any* app, one
  consistent way to answer: *where do the logs live · what logging
  config is in effect · what components/loggers exist and how each logs
  · watch them live (CLI + web) · read them after the fact.*
- **Layer B — centralized collection.** A library that ships every
  running app's logs + logging info/stats to a central server that
  **stores now**; analysis + presentation come **later** (out of scope
  for this program).

## §2 Decision log (2026-05-28)

| # | Decision | Consequence |
|---|---|---|
| D1 | **All runtimes from the start** — Python, Zig (`hrb-*`), SvelteKit/TS frontend | Schema + wire protocol are language-neutral; emit conformance for all three is an EARLY wave (1.5), not deferred. |
| D2 | **Introspection + at-rest access first**; real-time follows | Wave order: schema → introspection → CLI (locate + read) → then live tail/web. |
| D3 | **Per-app embedded web endpoint** (not a central UI now) | Each app exposes its own auth-gated live-tail endpoint (mgf-fastapi + mgf-livepush SSE). |
| D4 | **Shipper + protocol now; stub the server** | Build the ship library + wire protocol + a minimal store-now ingest stub; full collector/analysis is a later program. |

## §3 Current foundation (what we build ON, not replace)

`mgf.common.observability` already ships: structured JSON logging + text
formatter, **redaction** (`Redactor`), OTel spans, crash reporter, async
handler, excepthooks; **pluggable seams** (`register_log_handler` /
`register_redaction_pattern_group` / `register_span_processor`); path
helpers (`default_log_path` = `<state>/<app>/logs/<app>.log`, crash/state
dirs). `LoggingSettings` has `level`, **`level_overrides`** (per
component), `format`, `file`, `journald`, `syslog_address`, **`http_sink`**.
Standards: LOGGING (LG-01..17) + OBSERVABILITY (OB-01..08).

**The gap is the access/introspection/central layer — not basic
logging.** We extend the seams; we do not rebuild the pipeline.

## §4 The linchpin — canonical `LogRecord` schema

One language-neutral JSON schema every runtime emits and every consumer
(CLI tail, web stream, stats, central ship) reads — the role `TestRecord`
plays for tests. Source of truth: `mgf.common` (cornerstone), mirrored
to Zig/TS. **Draft fields** (v1, refine in Wave 1):

| Field | Type | Notes |
|---|---|---|
| `schema_version` | str | `"1.0.0"` — evolve additively |
| `ts` | str (RFC3339 UTC) | event time |
| `level` | str | DEBUG/INFO/WARNING/ERROR/CRITICAL |
| `app` | str | `app_name()` |
| `app_version` | str | |
| `component` | str | logger name (the per-component axis) |
| `message` | str | redacted |
| `fields` | object | structured context (redacted) |
| `trace_id` / `span_id` | str? | OTel correlation |
| `request_id` | str? | request correlation |
| `host` / `pid` | str / int | origin |
| `lang` | str | `python`/`zig`/`ts` (emit runtime) |
| `error` | object? | type/message/stack on errors |

This is essentially today's `JsonFormatter` output, formalized + versioned
+ given a cross-language home.

**Evolution — extend without redesign or breakage (OB-09).** The schema
is built to grow; four rules make that safe:
1. **Need a field now?** Put it in `fields` (structured context) or
   `labels` (string tags) — no schema change, round-trips losslessly.
2. **Promote to a typed top-level key later** → add an *optional*,
   defaulted field + bump `LOG_SCHEMA_VERSION`. Old readers ignore it;
   new readers default it for old records. Non-breaking both ways.
3. **Forward-compatible + lossless** → unknown keys from a newer schema
   are parsed without error AND preserved through a round-trip (kept in
   `LogRecord.extra`), so an older relay never strips newer data.
4. **Renaming / repurposing / removing / retyping an existing key** is
   the *only* breaking move → requires a MAJOR `schema_version` bump.

## §5 Layer A — introspection + access

**§5.1 Introspection API** (`mgf.common.observability`, Python). A
queryable `LogInfo`/`describe_logging()` that returns: effective config
(level + overrides + format), **sink locations** (file path, journald,
syslog, http_sink — surfaced from the path helpers), **components**
(known loggers + effective level each), active handlers, and live stats
(§5.4). This is "where/what/how" as data — most of it already exists;
we expose it as one surface.

**§5.2 CLI** — `mgf-fed logs`:
- `logs info` — print the §5.1 introspection (where + settings + components + sinks).
- `logs tail` / `logs follow` — read the rotating-file sink (or journald) and stream it; `--level`, `--component` filters; `--json`/pretty.
- At-rest read is the D2 priority; `follow` is the local real-time half.

**§5.3 Per-app web endpoint** (mgf-fastapi + mgf-livepush). An
auth-gated `GET /logs/stream` SSE endpoint streaming `LogRecord` JSON
live, plus `GET /logs/info` (the §5.1 introspection). Redaction enforced;
reuses the just-shipped livepush SSE/broker. D3: per-app, no central UI.

**§5.4 Statistics.** Per-app counters: log rate, error/warning counts,
per-component volume, async-handler drop count. Exposed via §5.1/§5.2 and
shipped to central (§6).

## §6 Layer B — centralized ship + stub collector

**§6.1 Shipper** — new mgf-* sibling (working name `mgf-logship`). A
handler registered via `register_log_handler` that batches `LogRecord`
JSON + stats and POSTs them to a collector over a **language-neutral wire
protocol** (HTTP/JSON, NDJSON batch). **Redaction runs before ship**
(hard gate — off-box logs are a PII/secret surface). Buffering +
backpressure + fail-open (never block or crash the app on a slow/down
collector).

**§6.2 Wire protocol** — documented, versioned, language-neutral so Zig
+ TS ship without a Python client. `POST /ingest` ← NDJSON of
`LogRecord`s + a periodic stats frame.

**§6.3 Stub collector** — minimal store-now ingest (mgf-fastapi +
mgf-sqlalchemy): accept the protocol, persist to a table, no
analysis/UI. D4: full server is later.

## §7 Cross-language emit (D1 — all runtimes)

- **Python:** native — `JsonFormatter` already emits the shape; formalize to the schema.
- **Zig (`hrb-*`):** a small emit helper (likely in `mgf-hrb-kabi`'s sink layer or a tiny `mgf-tele`-style shared module) that writes `LogRecord` JSON lines + can POST to the per-app endpoint / shipper.
- **TS/SvelteKit:** a thin logger that emits `LogRecord` JSON (server-side) and/or POSTs to the app's `/ingest`.
Each is conformance-tested against the schema (a cross-language fixture, like the TestRecord alignment pins).

## §8 Security / redaction

- Redaction is mandatory **before** any sink that leaves the process (file is local; web stream + central ship leave the box).
- Web `/logs/stream` + `/logs/info` are auth-gated (reuse `mgf-auth`).
- Ship protocol authenticated (token); transport TLS in real deployments.

## §9 Wave sequencing + ownership

| Wave | Deliverable | Lives in |
|---|---|---|
| 0 | This design doc + decisions | mgf-common docs |
| 1 | `LogRecord` schema + introspection API | mgf-common |
| 1.5 | Cross-language emit conformance (Py/Zig/TS) | mgf-common + hrb-*/frontend |
| 2 | `mgf-fed logs` CLI (info/tail/follow) | mgf-common |
| 3 | Per-app web endpoint (SSE stream + info) | mgf-fastapi + mgf-livepush |
| 4 | Logging statistics | mgf-common |
| 5 | Shipper sibling + wire protocol + stub collector | new `mgf-logship` + mgf-fastapi/sqlalchemy |
| 8 | End-to-end review pass | — |

Each wave goes through the federation gates (tests, REL-13 consistency,
signing). Standards updates land alongside (LOGGING/OBSERVABILITY rules
for the schema + introspection contract).

## §10 Open questions / risks

1. **Schema fields** — is the §4 draft complete for your needs? (e.g. do you want a free `labels`/`tags` map, severity-numeric, deployment/env tag?)
2. **Sibling naming** — `mgf-logship` for the shipper? collector name?
3. **Zig/TS emit home** — a shared Zig logging module vs per-repo; the TS logger as a frontend util vs a tiny published package.
4. **At-rest scope** — just the rotating-file sink, or also journald/syslog ingestion for `logs tail`?
5. **Stats granularity** — counters only, or windowed rates?
6. **Stub collector persistence** — sqlite vs Postgres for "store now"?

## §11 Relationship to `mgf-test-supervisor` (parallel cousins, not coupled)

`LogRecord` is deliberately **the logging analog of `mgf.common.testing.TestRecord`**
(the structured wire shape the federation test supervisor emits for *test
outcomes*). The two share a design lineage but are otherwise independent:

- **Same pattern, different plane.** `TestRecord` = versioned JSONL for *test
  results* (CI/build-time, produced by `mgf-test-supervisor`); `LogRecord` =
  versioned JSONL for *runtime log events* (produced by an app's loggers). Both
  expose the same API shape (`to_dict` / `to_jsonl_line` / `from_dict` /
  `from_jsonl_line` + a `SCHEMA_VERSION`); the cross-language conformance here
  (§7, golden fixture + alignment pins) was itself modeled on `TestRecord`'s
  alignment-pin discipline.
- **No code coupling, no conflict.** `mgf-test-supervisor` imports none of the
  logging surfaces (`LogRecord` / `LogStats` / `LogShipper` / `JsonFormatter`);
  the logging stack never imports `TestRecord` (one docstring mention only). The
  two `SCHEMA_VERSION = "1.0.0"` constants are separate names in separate modules
  (`LOG_SCHEMA_VERSION` vs `TestRecord.SCHEMA_VERSION`) and version independently.
- **The supervisor is a potential *producer*, never a *consumer*.** Like any
  Python tool it can emit `LogRecord`s through the logging stack, but it does not
  read the logging pipeline. Test outcomes flow through the supervisor's own JSONL
  output, **not** through `mgf-logship` — whose `/ingest` carries `LogRecord`
  lines only.
- **Possible future convergence (out of scope).** Both being versioned JSONL, a
  central collector *could* eventually ingest both streams; today they are
  deliberately separate pipelines.

---

*Wave-0 contract. Build status (2026-05-28): Waves 1 / 1.5 / 2 / 3 / 4 / 5 + the
Wave 8 review pass are complete; mgf-common 0.42.0 + the sibling cohort are on
PyPI. This doc remains the place to push back on scope, shape, and sequencing.*
