Observability¶
The mesh observes itself. Every agent host publishes structured log events
to plain NATS subjects that any subscriber can tail — a CLI, a monitoring
agent, a dashboard — with zero external infrastructure. What gets published
is controlled at runtime through a KV bucket: flip one agent to debug,
watch the output, flip it back. No restart, no redeploy.
Log events¶
The SDK instruments the agent lifecycle and invocation path automatically. Handler code needs no changes.
| Event | Level | When |
|---|---|---|
agent_registered |
info | Agent starts and registers |
agent_deregistered |
info | Agent shuts down gracefully |
agent_activated |
info | Lifecycle gate opened; agent came online |
agent_deactivated |
info | Lifecycle gate closed; agent drained and went offline |
request_received |
debug | Incoming invocation |
request_completed |
debug | Invocation completed (carries duration_ms) |
request_failed |
warn | Invocation raised an error (carries the error code) |
validation_error |
warn | Input failed Pydantic validation |
usage_reported |
info | Handler reported LLM usage via report_usage() (carries the usage fields) |
Events are published on mesh.logs.{name} — a sibling root to
mesh.errors.{name} and mesh.death.{name}, so wildcards aggregate
naturally:
mesh.logs.> # all logs, all agents
mesh.logs.nlp.> # all logs from the nlp channel
mesh.logs.nlp.summarizer
Each event is a JSON LogEvent:
{
"timestamp": "2026-07-18T14:30:00.123456+00:00",
"level": "warn",
"agent": "nlp.summarizer",
"event": "request_failed",
"request_id": "abc123",
"message": "boom",
"data": {"code": "handler_error"}
}
Log subjects are ephemeral plain NATS subjects — no JetStream, no storage, no retention configuration. If nobody is listening, events vanish. That is the deliberate trade-off for a zero-cost live-monitoring layer; historical retention belongs to the future OTel export story.
Controlling levels at runtime¶
A mesh-observability KV bucket holds the configuration. Two tiers:
| Key | Scope |
|---|---|
global |
Mesh-wide default |
{name} |
Per-agent override (wins over global) |
Levels: debug, info, warn, error, off. The default is info —
which means zero per-request publishes in steady state (the per-request
events are debug), while failures stay visible (warn).
Hosts watch the bucket and apply changes within a moment of the write:
oam observe set nlp.summarizer --log-level debug # one noisy agent
oam observe set --global --log-level warn # quiet the whole mesh
oam observe config # see what's in effect
Or from code:
await mesh.observe.set("nlp.summarizer", log_level="debug")
await mesh.observe.set_global(log_level="warn")
config = await mesh.observe.get("nlp.summarizer")
# ObserveConfig(log_level="debug", source="agent")
source tells you which tier supplied the effective value: agent,
global, or default.
Consuming logs¶
Tail from the CLI:
oam observe logs # everything
oam observe logs nlp.summarizer # one agent
oam observe logs --level warn # warn and above
Or subscribe in code — mesh.observe.logs() yields typed LogEvent
objects as they arrive:
async with AgentMesh() as mesh:
async for event in mesh.observe.logs(level="warn"):
alert(f"{event.agent}: {event.event} — {event.message}")
See the Observing the Mesh recipe for a worked debugging session.
Secured meshes¶
On a mesh secured with oam auth init (Securing the Mesh),
workers publish and read everything under their existing grants; the
observer role can tail mesh.logs.> and read the config bucket, but
only workers can change levels. Credentials minted before this feature
degrade gracefully — hosts fall back to default levels and keep serving.
What this layer is not¶
- Not tracing. Span events and cross-agent trace context are a planned increment (ADR-0048 defers them until the OTel trace-context decision).
- Not metrics. Request counters are specified to ride on heartbeats, which arrive with the liveness heartbeat layer (ADR-0016 follow-up).
- Not history. Nothing is stored. For replay and dashboards over time, export to an external backend once the OTel layer lands.