A newer version of the Gradio SDK is available: 6.20.0
ADR-0024: Observability β Structured Logging, Tracing, In-App Monitoring
Status
Accepted
Context
The engine had no cohesive observability. Each module made its own
logging.getLogger(__name__) with almost no calls (conductor.py, memory.py),
and the only structured output was the dependency-free JSON formatter for the
vLLM subprocess (modal/vllm_logging.py). There was no way to see, in one place,
the prompts passed to each model and the memory each agent had access to β the
two things that matter most when debugging a multi-agent run β nor any traces or
metrics across the api-call β inference β memory β core-loop path.
We want a complete, modular log of the application that foregrounds agent behaviour, data, and LLM/API calls, readable both in the terminal and live in the Gradio app, leveraging OpenTelemetry for logging, tracing, and basic monitoring.
Decision
Introduce a single src/observability/ package β a thin facade over OpenTelemetry
plus an in-memory store β that every layer imports as from src import observability as obs.
One stable facade.
configure(),get_logger(),log(event, **fields),span(name, **attrs),add_span_attrs(),incr()/observe()+ named helpers (record_llm_call,record_agent_turn,record_governor_trip),bind()/set_context()/current_context(), andtelemetry_store(). Call sites never touch the OTEL SDK directly, so instrumentation stays a one-liner and the wiring lives in one module.Correlation by context, not by parameter. Run / turn / agent are carried in
contextvarsand stamped onto every log record and span automatically. The conductor binds the run and turn; agents bind their name. A singlellm.callline therefore says which agent, which turn, which run, with no threading.Real OTEL tracing, self-contained backend. A real
TracerProvidermakes spans nest by context:run β turn β agent.turn β {memory.recall β memory.index.search} + {llm.call | llm.structured} + tool.call. Finished spans and all log records flow into bounded in-memory ring buffers (TelemetryStore); a console exporter can also print spans. No Jaeger / Prometheus / Grafana to deploy β the whole monitoring story lives in-process and in the live demo. Metrics (LLM calls, tokens, cost, agent-turn latency, governor trips) are in-process counters/observations feeding the UI charts.In-app surface. A Gradio "Telemetry" tab reads from
telemetry_store(): a filterable structured-log feed, metric charts, and a per-turn trace/timeline where selecting a span reveals the actual prompt + memory the agent saw.Env-driven, no secrets.
MAL_LOG_LEVEL(DEBUG surfaces full prompts + memory),MAL_LOG_FORMAT(text|json),MAL_TRACING(off|console|memory|both, defaultmemory). API keys are never logged or attached as span attributes; full prompts/memory are captured at DEBUG and truncated in stored snapshots for the UI.
OpenTelemetry (opentelemetry-api, -sdk, -semantic-conventions) is a
first-class core dependency. LLM spans use GenAI semantic-convention attributes
(gen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens /
output_tokens) alongside engine-specific ones (llm.cost_usd, llm.prompt,
llm.completion, llm.reasoning).
Consequences
- Any module gets full structured logging, tracing, and metrics by importing the facade β instrumentation is consistent and decoupled from the SDK.
- The live demo can show logs, traces, and charts with nothing extra to run; the in-memory buffers are bounded, so memory stays flat over a long session.
- OTEL is now a required dependency (the prior offline/no-new-deps stance is
relaxed for observability, by decision); when
MAL_TRACING=off, the API's no-op tracer keeps overhead negligible. setup_loggingconfigures the root logger (the engine had none); it only manages handlers it tags_mal, leaving third-party handlers intact and making re-configuration idempotent.- Future work (not in this ADR): optional OTLP export to an external collector,
and mirroring the in-process metrics to an OTEL
MeterProvider.