Observability contract
This document defines the stable observability contract for Spooky after the refactor work that centralized reason vocabularies, outcome recording, backend lifecycle snapshots, control-plane runtime views, and admin audit events.
Purpose
Spooky has five operator-facing observability surfaces:
- Prometheus metrics
- structured operational logs
- OTLP traces
- control API runtime snapshots
- structured control-plane audit events
These surfaces should describe the same events using the same vocabulary.
The source of truth for that vocabulary is:
crates/edge/src/observability/mod.rs
This file defines the canonical enums, stable slugs, and field names that all emitters should use.
Source Of Truth
The source of truth is not a dashboard JSON file, alert rule, or one emitter implementation.
The source of truth is the canonical vocabulary in:
crates/edge/src/observability/mod.rs
Operator-packaged assets must be derived from that module's stable public meaning.
The main source-of-truth enums and helper types are:
RequestOutcomeReasonBackendHealthReasonRetryDecisionReasonHedgeDecisionReasonAdmissionDecisionReasonAdmissionOverloadCauseQuotaPolicyDecisionQuotaPolicyReasonQuotaBackendHealthReasonOperationalEventContextMetricReasonLabels
If one of those enums changes, that is an observability contract change. Dashboard queries, alert rules, runbooks, and control-plane JSON examples must be reviewed together.
Contract Rule
One operational concept should have:
- one canonical enum value
- one canonical slug string
- one consistent meaning across metrics, logs, traces, control API payloads, and audit events
The contract exists to prevent drift such as:
- one reason name in metrics
- a different prose string in logs
- a third serialized form in control-plane JSON
- a fourth ad hoc field in audit events
- a fifth trace attribute that cannot be joined back to the operator vocabulary
Canonical Reason Families
The main canonical reason families are:
RequestOutcomeReasonBackendHealthReasonRetryDecisionReasonHedgeDecisionReasonAdmissionDecisionReasonAdmissionOverloadCause
Each of these exposes a stable slug() used as the canonical external string.
Stable String Contract
The canonical slug is the external operator contract.
That means the slug should be treated as the stable value for:
- metric label values
- structured log
reason=values - trace span attributes describing the same reason
- control API serialized fields
- audit event
reasonvalues
Changing a slug is an observability contract change, not a cosmetic refactor.
Operator Correlation Contract
Operators need a stable path from an alert to a dashboard panel to an event log or audit record to a runtime snapshot.
The default correlation field set is:
request_idtrace_idspan_idevent_idgenerationlistenerrouteupstreambackendreasonfailure_classpolicycomponent
Correlation field meanings
request_ididentifies a request-path event when request-scoped logging or tracing existstrace_ididentifies the distributed trace carrying the request or control-plane actionspan_ididentifies the local operation within that traceevent_ididentifies a discrete audit or operator-significant eventgenerationidentifies the active or candidate runtime generation involved in a control-plane actionlisteneridentifies the ingress or admin listener involved in the eventrouteis a route identity when route-specific visibility existsupstreamis the canonical traffic attribution field for request-path metrics and logsbackendis the selected backend identityreasonis the canonical reason slug fromcrates/edge/src/observability/mod.rsfailure_classis a coarse refinement axis wherereasonalone is not sufficientpolicyidentifies the named policy that produced a decision when policy attribution existscomponentidentifies the subsystem such asadmission,quota,tls,runtime, orcontrol_api
Surface expectations
- metrics should use low-cardinality correlation dimensions only
- logs should emit the canonical field names when the values are known
- traces should carry the same canonical names as span attributes where request or control-plane tracing exists
- control API snapshots should expose current state using the same identities and reason slugs
- audit events should serialize the same actor, target, generation, and reason meaning used elsewhere
Absence is better than fabricated values. If a field is not known, omit it rather than inventing placeholders.
Structured Log Field Contract
Operational logs should use the canonical OperationalEventContext field set.
The canonical fields are:
request_idrouteupstreambackendreasonfailure_class
These fields are rendered in stable key=value form and unset fields are omitted.
Practical meaning
- use
upstreamfor route/upstream attribution in request-path events - use
backendfor selected backend identity - use
reasonfor the canonical reason slug - use
failure_classwhen a coarse outcome still needs finer transport or rejection detail
Do not invent new ad hoc field keys for the same concept if the canonical field already exists.
Trace Attribute Contract
Tracing is a diagnostic surface, not a separate vocabulary.
Where traces are emitted, span and event attributes should reuse the canonical names:
request_idtrace_idspan_idgenerationlistenerrouteupstreambackendreasonfailure_classpolicycomponent
Trace attributes may include additional diagnostic detail, but canonical operator fields must keep the same names and meanings as logs, metrics, control API, and audit events.
Metric Label Contract
Reasoned metric emitters should use the canonical label model:
outcomereasonfailure_class
This contract is represented by MetricReasonLabels.
Meaning of the labels
outcomeis the coarse terminal bucket such as success, failure, timeout, overload, or rate-limitedreasonis the canonical reason slugfailure_classcarries finer transport or rejection detail where needed
This is what allows dashboards to compare:
- route outcomes
- backend failures
- retries and hedges
- admission denials
without each emitter choosing different label semantics.
Audit Event Contract
The control-plane audit stream is part of the operator observability bundle, not a side channel.
Audit events should use the same identities and reason vocabulary as other surfaces, with fields oriented around:
- actor
- action
- target
- generation
- result
- reason
- event_id
- peer address
- authentication mechanism
Audit events may include control-plane-specific context, but they should not redefine canonical reason values or resource identities already established elsewhere.
Control API Snapshot Contract
The control API is not just an admin action surface. It is also a runtime introspection contract.
The runtime snapshot should describe the current active generation and shared operational state using the same meaning as logs and metrics.
Important snapshot areas include:
- active generation id and config path
- observability package metadata and contract versions
- worker expectations
- watchdog state
- adaptive admission state
- backend lifecycle inventory
- coarse metrics summary
- listener TLS inventory
Observability package metadata
The runtime snapshot is also the packaged entry point into observability for operators.
The snapshot should expose:
- the current active generation
- the active observability contract version
- the control-plane audit schema version
- a backend health summary
- a quota backend health summary
- recent tracked admin/runtime actions when runtime history is available
- stable references to shipped dashboard definition files
- stable references to operator documentation files
These references should point to repository-managed assets such as:
deploy/observability/grafana/*.jsondocs/architecture/observability-contract.mddocs/operations/control-plane.mddocs/operations/metrics-and-alerts.mddocs/operations/distributed-quota.md
This is intentionally not a UI URL contract. The control plane should expose stable package references that other operator tooling can map into local workflows.
Backend snapshot meanings
Backend lifecycle payloads expose:
- backend identity
- health
health_reason- membership
- authority host and port
- resolved addresses
- resolution generation
- last successful refresh time
- per-upstream placement details
These fields should be interpreted as the canonical backend lifecycle view, not as a best-effort debug dump.
Runtime Snapshot Boundaries
Control-plane surfaces should consume canonical runtime views, not listener-local state.
That means runtime snapshot rendering should depend on:
- the active runtime generation
- shared runtime services
- backend lifecycle inventory
- watcher/coordinator state
It should not reconstruct state by reaching into multiple unrelated listener internals.
Canonical Dimensions Versus Diagnostic Dimensions
The operator bundle must distinguish between dimensions that are safe for dashboards and alerts and dimensions that belong only in logs, traces, or targeted investigation.
Canonical operator dimensions
These are acceptable for stable metrics, dashboards, alerts, and control-plane summaries:
upstreambackendlistenerprotocolstatus_classoutcomereasonfailure_classpolicycomponentgenerationwhen bounded to active or recent generations
High-cardinality diagnostic dimensions
These should not be introduced as default metric labels or dashboard variables:
- raw request path
- user id
- tenant id
- token id
- client ip
- header values
- query strings
- certificate fingerprints for every request
- arbitrary trace baggage
- per-request nonce or session identifiers
These belong in:
- structured logs
- trace attributes
- audit events where operationally justified
- control-plane detail views
The rule is simple: if a dimension can grow with end-user traffic, it is diagnostic by default, not metric-facing by default.
Metrics vs Logs vs Control API
These surfaces serve different operational jobs.
Metrics
Metrics are for:
- time-series alerting
- SLO and capacity dashboards
- rate and latency trends
Logs
Logs are for:
- event-level diagnosis
- request-path and lifecycle debugging
- understanding the exact reason a single action happened
Traces
Traces are for:
- cross-component timing analysis
- causal sequencing within a single request or control-plane action
- joining request-path latency with retry, hedge, auth, and backend sub-operations
Control API snapshots
Control API snapshots are for:
- current-state inspection
- rollout verification
- backend and watchdog status inspection
- runtime-generation confirmation
Audit events
Audit events are for:
- operator accountability
- change history
- security-relevant admin actions
- correlating runtime changes with traffic or control-plane symptoms
They should complement each other, not contradict each other.
Cardinality Rules
The observability contract aims for stable, low-cardinality labels on default operational metrics.
Use labeled metrics for:
- upstream
- backend
- listener
- protocol
- canonical reason slugs
Avoid introducing unbounded labels such as:
- raw user IDs
- arbitrary request paths
- per-request trace tokens
If a dimension is needed for debugging but not for alerting, prefer logs or control-plane snapshots over high-cardinality metrics.
Dashboard And Alert Compatibility Promise
Shipped dashboards and alert rules are versioned operator assets that depend on this contract.
Compatibility promises:
- canonical metric names and canonical reason label values are treated as stable operator API
- dashboard panels should depend on canonical metrics or recording rules built from canonical metrics
- alert rules should depend on canonical metrics or recording rules, not prose log parsing
- control-plane examples and runbooks should use the same reason slugs and field names as the code contract
- additive fields are allowed when they do not change existing meanings
- renaming a canonical slug, field key, or low-cardinality label is a breaking observability change
When a breaking observability change is unavoidable:
- update
crates/edge/src/observability/mod.rs - update this contract document in the same change
- update shipped dashboards and alerts in the same change
- document migration notes for operators
- avoid silent drift where old dashboards continue to query non-existent or semantically changed series
Backend Lifecycle Observability
Backend health and refresh behavior should be visible consistently across surfaces.
Important canonical concepts include:
- refresh classification
- health-failure reason
- membership state
- placement state
- client rotation behavior
This lets operators answer:
- did refresh fail or just return the same addresses
- was a backend removed, suppressed, or merely unhealthy
- did traffic continue on retained addresses after refresh failure
Request Outcome Observability
Request outcome recording provides the canonical answer to:
- did the request succeed
- did it time out
- was it rejected by policy
- did backend transport/protocol/TLS fail
- was it shed by overload controls
This contract should be consistent for:
- QUIC forwarding
- bootstrap compatibility ingress
- retries and hedges
- admission and auth rejections
Reload and Runtime Observability
Runtime generation changes should be visible through:
- control API runtime generation fields
- logs around generation swaps and rejection reasons
- metrics that surface restart requests, degraded windows, and control-plane activity
Operators should not have to infer whether a reload committed from unrelated side effects.
Contributor Rules
When adding a new operational reason:
- add it to the canonical observability vocabulary
- give it one stable slug
- map local enums into the canonical reason if the local enum is richer
- reuse canonical field names in logs
- reuse canonical label names in metrics
- expose the same meaning in control-plane JSON where relevant
Do not:
- invent a new public reason string in one emitter only
- log prose-only reasons that cannot be reconciled with metrics
- serialize backend or request state under different names in different surfaces
Mental Model
The observability contract is successful when an operator can look at:
- a metric label
- a log line
- a control API field
and know they are all describing the same event using the same vocabulary.