# Events — `mgf-common` log event catalogue

> **Audience:** Operators / SREs configuring alerts, dashboards,
> SIEM rules against `mgf-common`-bearing log streams. AI coding
> agents looking up what an `event=` value means.

> **Scope.** The canonical list of stable `event` field values
> emitted by `mgf-common` core (rule **LG-15** in
> [`LOGGING.md`](https://codeberg.org/magogi-admin/mgf-standard/src/branch/main/docs/standards/LOGGING.md)). Consumers
> ship their own `docs/reference/EVENTS.md` for their own event
> vocabulary. The standard is the format; this file is the
> mgf-common-specific catalogue.

---

## How to read this

Each event has a fixed shape:

| Field | Description |
|---|---|
| **Event** | The `event` field value. Stable; rename only via deprecation cycle. |
| **Level** | The minimum severity this event is ever emitted at. |
| **Audit?** | Whether the record also carries `audit=true` (SIEM filter, long-retention sink — per **SC-17**). |
| **Required fields** | `extra={}` keys that MUST be present on every emission. Operators can rely on them for alert routing. |
| **Optional fields** | Additional context that MAY be present. |
| **Emitted by** | Source `file:line` reference (jump-to-code). |
| **What it means** | One-sentence operator-facing description. |
| **What to do** | If anything; the runbook anchor or "no action — informational". |

---

## §1 Lifecycle events

### `bootstrap.replace`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `prior_app`, `new_app` |
| **Optional fields** | `prior_version`, `new_version` |
| **Emitted by** | [`_lifecycle.py:660`](../../src/mgf/common/_lifecycle.py#L660) |
| **What it means** | A second `bootstrap()` call has fired while a prior `AppContext` was active; the prior context is being torn down and replaced. |
| **What to do** | One firing in a process lifetime is normal (test fixtures, hot-reload). Multiple firings per minute in production is a red flag — investigate the call site. Alert on rate, not on a single occurrence. |

### `bootstrap.runtime_snapshot_failed`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `error_type`, `error_message` |
| **Optional fields** | — |
| **Emitted by** | [`_lifecycle.py:926`](../../src/mgf/common/_lifecycle.py#L926) |
| **What it means** | `bootstrap()` successfully wired the `AppContext` but the optional runtime-snapshot emission (`MGF_DOCTOR_RUNTIME_PATH`) failed. The process is healthy; only the snapshot is missing. |
| **What to do** | Check disk space / permissions on `MGF_DOCTOR_RUNTIME_PATH`. If recurrent, unset the env var to silence the warning (snapshot is opt-in). See `docs/inprogress/RELIABILITY_AND_ACCURACY.md` RA-22. |

### `reload.unrecoverable`

| | |
|---|---|
| **Level** | ERROR |
| **Audit?** | yes |
| **Required fields** | `error_type`, `prior_app` |
| **Optional fields** | `attempted_settings_diff` |
| **Emitted by** | [`_lifecycle.py:395`](../../src/mgf/common/_lifecycle.py#L395) |
| **What it means** | `AppContext.reload()` failed AND the rollback-to-prior-state failed too. The process is in a half-wired state and a restart is the safe path forward. |
| **What to do** | **Page.** The process should be restarted. Cross-ref RA-21 (open) — the contract of `reload()` failure semantics is still being finalized. |

---

## §2 Security events (rule SC-17 audit set)

### `vault.read`

| | |
|---|---|
| **Level** | INFO |
| **Audit?** | yes |
| **Required fields** | `vault_key`, `vault_backend`, `found` (bool) |
| **Optional fields** | — |
| **Emitted by** | [`_env_vault.py:70`](../../src/mgf/common/vault/_env_vault.py#L70), [`_file_vault.py:180`](../../src/mgf/common/vault/_file_vault.py#L180), [`_keyring_vault.py:136`](../../src/mgf/common/vault/_keyring_vault.py#L136) |
| **What it means** | A credential was read from the vault. Routine; one record per access. |
| **What to do** | Stream to SIEM for audit-trail retention. Alert on UNEXPECTED reads (e.g., `vault_key` matching a key that should not be accessed from this service). |

### `vault.write`

| | |
|---|---|
| **Level** | INFO |
| **Audit?** | yes |
| **Required fields** | `vault_key`, `vault_backend` |
| **Optional fields** | `created` (bool — true if new, false if overwrite) |
| **Emitted by** | [`_env_vault.py:91`](../../src/mgf/common/vault/_env_vault.py#L91), [`_file_vault.py:201`](../../src/mgf/common/vault/_file_vault.py#L201), [`_keyring_vault.py:164`](../../src/mgf/common/vault/_keyring_vault.py#L164) |
| **What it means** | A credential was written to the vault — rotation, initial setup, or operator action. |
| **What to do** | Stream to SIEM. Alert on writes to keys that should be read-only at runtime (e.g., a service writing to a key it shouldn't have provisioned). |

### `vault.delete`

| | |
|---|---|
| **Level** | INFO |
| **Audit?** | yes |
| **Required fields** | `vault_key`, `vault_backend` |
| **Optional fields** | — |
| **Emitted by** | [`_env_vault.py:106`](../../src/mgf/common/vault/_env_vault.py#L106), [`_file_vault.py:218`](../../src/mgf/common/vault/_file_vault.py#L218), [`_keyring_vault.py:202`](../../src/mgf/common/vault/_keyring_vault.py#L202) |
| **What it means** | A credential was deleted from the vault — rotation cleanup, decommission, or operator action. |
| **What to do** | Stream to SIEM. Always pair with a `vault.write` for the replacement key in the same incident window — a delete with no corresponding write is suspicious. |

---

## §3 Defense-in-depth events

### `crash.build_failed`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `error_type`, `error_message` |
| **Optional fields** | — |
| **Emitted by** | [`crash_reporter.py:83`](../../src/mgf/common/observability/crash_reporter.py#L83) |
| **What it means** | `_build_report()` raised while assembling the crash payload (before redaction, before write). The crash itself is unobservable from this process beyond this warning — `report_now` returns `_NULL_REPORT_PATH`. |
| **What to do** | Investigate the exception type — usually a typed-field accessor on a custom exception that doesn't follow the AP-08 shape. Fix the upstream typed exception so `_build_report` can introspect it. |

### `crash.write_failed`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `error_type`, `error_message` |
| **Optional fields** | — |
| **Emitted by** | [`crash_reporter.py:95`](../../src/mgf/common/observability/crash_reporter.py#L95) |
| **What it means** | The local atomic write to `<state>/crashes/` failed. Most common causes: disk full, read-only filesystem, permission denied on the state dir. The crash is still logged to stderr but the file form is missing. |
| **What to do** | Check `<state>/<app>/crashes/` permissions and free space. The remote-shipping path may still succeed — check Sentry/OTLP/HTTP destination for the report. |

### `crash.sink_unknown`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `sink_name` |
| **Optional fields** | `env_var`, `hint` |
| **Emitted by** | [`crash_reporter.py:415`](../../src/mgf/common/observability/crash_reporter.py#L415) (settings path), [`crash_reporter.py:444`](../../src/mgf/common/observability/crash_reporter.py#L444) (env-var path) |
| **What it means** | A sink name listed in `MgfSettings.crash.sinks` (or in `<APP>_CRASH_SINK`) doesn't match any registered handler. The crash still ships via the OTHER configured sinks; this one is silently skipped. |
| **What to do** | Either register the sink via `@register_crash_sink("<name>")` OR remove the name from configuration. The `hint` field carries the specific instruction. |

### `crash.sink_failed`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `sink_name`, `error_type`, `error_message` |
| **Optional fields** | — |
| **Emitted by** | [`crash_reporter.py:427`](../../src/mgf/common/observability/crash_reporter.py#L427) |
| **What it means** | A registered crash sink raised when called. The OTHER sinks still fire (EH-10 isolation); just this one failed. |
| **What to do** | Investigate `error_type` — usually a network failure (Sentry / OTLP collector down) or a bug in a consumer-registered sink. Recurring `sink_failed` for the same sink_name should be paged. |

### `crash.ship_failed`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `error_type`, `error_message` |
| **Optional fields** | `sink_name` (for named-sink path), `sink_kind` (for raw-URL HTTP path) |
| **Emitted by** | [`crash_reporter.py:440`](../../src/mgf/common/observability/crash_reporter.py#L440) (HTTP-URL path), [`crash_reporter.py:449`](../../src/mgf/common/observability/crash_reporter.py#L449) (env-var named-sink path) |
| **What it means** | The v0.1 env-var fallback path attempted shipping and the underlying call raised. Like `crash.sink_failed` but specific to the env-var-driven legacy flow. |
| **What to do** | Same triage as `crash.sink_failed`. Consider migrating off `<APP>_CRASH_SINK` to typed `MgfSettings.crash.sinks` (per SH-08 trust documentation). |

### `crash.sink_unavailable`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `sink_name`, `missing_dependency` |
| **Optional fields** | `hint` |
| **Emitted by** | [`crash_reporter.py:479`](../../src/mgf/common/observability/crash_reporter.py#L479) (sentry), [`crash_reporter.py:502`](../../src/mgf/common/observability/crash_reporter.py#L502) (otlp) |
| **What it means** | A built-in remote sink (`sentry` or `otlp`) is configured but its optional dependency isn't installed. The local file write still succeeded; only the remote-ship failed. |
| **What to do** | `pip install 'mgf-common[observability]'` to install the optional deps; OR remove the sink from `MgfSettings.crash.sinks`. The `hint` field carries the specific instruction. |

### `crash.redaction_failed`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `redaction_error_type` |
| **Optional fields** | — |
| **Emitted by** | [`crash_reporter.py:145`](../../src/mgf/common/observability/crash_reporter.py#L145) |
| **What it means** | The crash reporter's redaction pass (rule **SH-01**) raised on a payload; a **minimal fail-safe report** is being shipped in place of the full crash. The operator still sees a crash happened; secret-bearing fields are dropped. |
| **What to do** | Investigate the redactor's failure (the `redaction_error_type` is the exception class — usually a regex backtrack or a malformed payload). The crash itself is still observable from the local crash file + the minimal payload; the missed redaction may indicate a new pattern the redactor doesn't yet handle. |

### `process.reap_timeout`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `child_pid`, `command_id` |
| **Optional fields** | — |
| **Emitted by** | [`process/_run.py:457`](../../src/mgf/common/process/_run.py#L457) |
| **What it means** | A child process did not exit within the post-`SIGKILL` wait window. The process is **likely zombie / blocked in D-state** and the OS may not have reaped it. |
| **What to do** | Inspect the host (`ps -o pid,stat,wchan -p <child_pid>`). Typical D-state cause is a stuck I/O — kernel intervention may be needed. See RA-06 closure for the test-pinned semantics. |

### `otel.sdk_unavailable`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `missing_dependency`, `endpoint_env_var` |
| **Optional fields** | `hint` |
| **Emitted by** | [`otel_setup.py:98`](../../src/mgf/common/observability/otel_setup.py#L98) |
| **What it means** | `OTEL_EXPORTER_OTLP_ENDPOINT` is set but the OpenTelemetry SDK isn't installed. Traces won't ship; the process continues without OTel. |
| **What to do** | `pip install '<app>[observability]'` to install the SDK, OR unset `OTEL_EXPORTER_OTLP_ENDPOINT` if traces aren't wanted. |

### `otel.httpx_instrument_failed`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `error_type`, `error_message` |
| **Optional fields** | `hint` |
| **Emitted by** | [`otel_setup.py:159`](../../src/mgf/common/observability/otel_setup.py#L159) |
| **What it means** | `HTTPXClientInstrumentor().instrument()` failed during OTel wiring. Usually because httpx was already instrumented by an earlier import. |
| **What to do** | Usually benign — investigate only if outgoing HTTP traces are missing. If recurrent, check for double-instrumentation from a consumer-side `instrument()` call. |

### `logging.http_sink.invalid_url`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `env_var` |
| **Optional fields** | `hint` |
| **Emitted by** | [`logging_setup.py:298`](../../src/mgf/common/observability/logging_setup.py#L298) |
| **What it means** | `<APP>_LOG_HTTP_SINK` env var is set but doesn't parse as a valid URL (missing scheme / netloc). The HTTP sink is silently skipped. |
| **What to do** | Set the env var to a valid `http(s)://<host>/<path>` URL, OR unset to disable the HTTP sink entirely. |

### `versioned.grace_mode`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `path`, `version_field`, `tolerated_as_version` |
| **Optional fields** | `hint` |
| **Emitted by** | [`versioned/_store.py:272`](../../src/mgf/common/versioned/_store.py#L272) |
| **What it means** | A `VersionedFileStore` loaded a file that has no version field. Per PAPER-09 grace mode, the loader tolerates it (treats it as `current_version` + skips migration). Fires once per load until the file is rewritten by the next `save()`. |
| **What to do** | Routine for legacy / pre-versioning files; recurring after a save() suggests the save() path isn't writing the version field — investigate. |

### `cli.command_failed`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `error_type`, `error_message`, `exit_code` |
| **Optional fields** | — |
| **Emitted by** | [`cli/_handler.py:179`](../../src/mgf/common/cli/_handler.py#L179) |
| **What it means** | A CLI command raised a typed `AppError`-subclass that mapped to a non-zero exit code. The user-facing message was already printed; this is the structured record for log aggregators. |
| **What to do** | Routine — operators triage by `error_type` and `exit_code`. The user already saw the message; no immediate operator action needed unless the rate spikes. |

### `cli.unhandled_exception`

| | |
|---|---|
| **Level** | ERROR (LOG.exception with `exc_info`) |
| **Audit?** | yes |
| **Required fields** | `error_type`, `crash_report_path` |
| **Optional fields** | `hint` |
| **Emitted by** | [`cli/_handler.py:187`](../../src/mgf/common/cli/_handler.py#L187) |
| **What it means** | A CLI command raised an exception with no mapped exit code — an unhandled bug. The crash reporter was invoked (per EH-05); the crash file path is in the record. |
| **What to do** | **Page-worthy.** Inspect the crash report (path in `crash_report_path`), file an issue with the redacted JSON attached. |

### `process.streamer_read_failure`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | no |
| **Required fields** | `command_id`, `stream` (`stdout` / `stderr`) |
| **Optional fields** | `error_type` |
| **Emitted by** | [`process/_streamer.py:166`](../../src/mgf/common/process/_streamer.py#L166) |
| **What it means** | A subprocess stream-reader thread caught an exception while reading the child's output. The thread previously died silently; per **RA-07** it now logs and continues. |
| **What to do** | Investigate the child process — the underlying cause is usually a closed FD or a decoding error on non-UTF-8 output. The parent's wait will still succeed; only the captured stream is incomplete. |

---

## §3.1 Component events (flags / plugins / health / scheduler)

### `flag.backend_configured`

| | |
|---|---|
| **Level** | INFO |
| **Audit?** | no |
| **Required fields** | `backend` |
| **Emitted by** | `src/mgf/common/flags/__init__.py` |
| **What it means** | The feature-flag backend was selected at startup. |
| **What to do** | No action — informational. |

### `flag.invalid_env_value`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | no |
| **Required fields** | `flag`, `value` |
| **Emitted by** | `src/mgf/common/flags/__init__.py` |
| **What it means** | A flag's environment-variable value couldn't be parsed; the default was used. |
| **What to do** | Fix the env var (typo / wrong type), or ignore if the default is intended. |

### `plugin.discovery`

| | |
|---|---|
| **Level** | INFO |
| **Audit?** | no |
| **Required fields** | `count` |
| **Emitted by** | `src/mgf/common/plugins/__init__.py` |
| **What it means** | Entry-point plugin discovery completed. |
| **What to do** | No action — informational. |

### `plugin.rejected`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | no |
| **Required fields** | `plugin`, `reason` |
| **Emitted by** | `src/mgf/common/plugins/__init__.py` |
| **What it means** | A discovered plugin was rejected (bad shape / failed validation). |
| **What to do** | Inspect the named plugin; a malformed or incompatible entry point. |

### `health.check_raised`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | no |
| **Required fields** | `check`, `error_type` |
| **Emitted by** | `src/mgf/common/health/__init__.py` |
| **What it means** | A health check raised instead of returning a status; treated as unhealthy. |
| **What to do** | Investigate the named check — it should return a status, not raise. |

### `schedule.task_raised`

| | |
|---|---|
| **Level** | ERROR |
| **Audit?** | no |
| **Required fields** | `task`, `error_type` |
| **Emitted by** | `src/mgf/common/schedule/__init__.py` |
| **What it means** | A scheduled task raised; the scheduler logged it and continued (best-effort, EH-10). |
| **What to do** | Investigate the named task's failure; recurring failures need a fix. |

### `retry.attempt`

| | |
|---|---|
| **Level** | WARNING |
| **Audit?** | yes |
| **Required fields** | `operation`, `attempt`, `delay_s`, `exception_type` |
| **Emitted by** | [`retry/__init__.py:55`](../../src/mgf/common/retry/__init__.py#L55) |
| **What it means** | A `retry()` / `aretry()` call caught a retryable failure and will back off for `delay_s` before the next attempt (EH-11 exponential backoff + full jitter). |
| **What to do** | Transient — usually self-heals. Persistent `retry.attempt` storms for one `operation` indicate an unhealthy upstream; check it and consider a circuit breaker (EH-13). |

---

### `retry.exhausted`

| | |
|---|---|
| **Level** | ERROR |
| **Audit?** | yes |
| **Required fields** | `operation`, `attempts` |
| **Emitted by** | [`retry/__init__.py:69`](../../src/mgf/common/retry/__init__.py#L69) |
| **What it means** | A `retry()` / `aretry()` budget was exhausted after `attempts` tries; the last exception is re-raised to the caller (EH-11 finite cap). |
| **What to do** | The operation failed durably. Investigate the upstream; the re-raised exception carries the root cause. |

---

## §4 What's NOT here

By design, the following do NOT carry an `event` field — they
fail the LG-15 "would anyone alert on this?" test:

- Routine `LOG.info("started X")` / `LOG.debug("...")` records
  without an operational-boundary meaning.
- Per-record progress noise from streamed subprocess output.
- Free-form ERROR records from translation seams that just
  re-raise (the **exception type** carries the identity; the
  log record is auxiliary).
- INFO records emitted by `setup_logging` / `setup_otel` /
  other one-shot wiring helpers (visible in startup logs;
  not alert-worthy).

When a new event name is added to mgf-common, this catalogue
gets a new row IN THE SAME COMMIT. The shape rule (rule
**LG-15**) treats event names as public-API-stable identifiers;
this file is the registry that makes them so.

---

## §5 For consumers — ship your own `docs/reference/EVENTS.md`

Every consumer that emits typed events (per **LG-15**) ships its
own per-project `EVENTS.md` in the same shape:

- The same five sections (Lifecycle / Security / Defense-in-depth /
  capacity-or-quota / What's NOT here).
- The same per-event table (Level / Audit / Required fields /
  Optional fields / Emitted by / What it means / What to do).
- The same MUST-stay-in-sync-with-code discipline (rule
  **DOC-09**).

A `bootstrap.replace` event in a consumer's process flow MAY
mean something different than mgf-common's (the consumer wraps
`bootstrap()` and adds its own `bootstrap.replace` semantics) —
the registry is what disambiguates. Document yours; don't lean
on the operator's memory.

---

## §6 Cross-references

- [`LOGGING.md`](https://codeberg.org/magogi-admin/mgf-standard/src/branch/main/docs/standards/LOGGING.md) — rule
  LG-15 (the `event` field convention), LG-16 (message-content
  discipline), LG-17 (error fingerprinting), LG-11 (operation
  start/end).
- [`SECURITY_STANDARD.md`](https://codeberg.org/magogi-admin/mgf-standard/src/branch/main/docs/standards/SECURITY_STANDARD.md)
  §9.3 — the SC-17 audit-record schema (the foundation for the
  `audit=true` flag on every event in §2 above).
- [`docs/inprogress/RELIABILITY_AND_ACCURACY.md`](../inprogress/RELIABILITY_AND_ACCURACY.md)
  — the closures that produced several of these events
  (RA-06 process reap, RA-07 streamer visibility, RA-22
  runtime-snapshot best-effort).
- [`docs/inprogress/SECURITY_HARDENING.md`](../inprogress/SECURITY_HARDENING.md)
  — SH-01 (crash redaction fail-safe → `crash.redaction_failed`).

---

*End of `EVENTS.md`.*
