Opt-in OTel logging for agent responses
Open 💬 4 comments Opened May 11, 2026 by chdeskur
What variant of Codex are you using?
CLI, App
What feature would you like to see?
Codex OpenTelemetry exports should support an explicit opt-in option to log the agent/assistant response content for a turn.
This feature is needed for monitoring and evaluating agent behavior, especially regarding adherence to SKILL.md directives.
Suggested behavior
Add a config option such as:
[otel]
log_agent_responses = true
or an environment/config equivalent.
When enabled, Codex should emit a log event for the final assistant message, for example:
- event.name = "codex.agent_response"
- conversation.id
- turn.id if available
- response_length
- response = "<final assistant message>"
A documented truncation limit would be acceptable.
Additional information
This feature is available in Claude Code's OTel implementation
4 Comments
Any update on this?
This would be very useful.
I would add opt-in full telemetry spans support, to point it for ex. to local/personal mlflow genai instance, check https://mlflow.org/docs/latest/genai/eval-monitor/
Claude Code has this in beta: https://code.claude.com/docs/en/monitoring-usage#traces-beta
We hit this from the observability side and have measurements plus a working implementation, so sharing the analysis here per
docs/contributing.mdrather than opening a PR.Why we need it
We host Codex as one of the agents inside a workspace product and render each agent's trajectory in our own UI — tool calls, results, messages — from telemetry the agent reports to an in-pod OTLP receiver. Every row we need is available except the agent's message, which means a Codex trajectory renders as tool steps with nothing between them: the turn's actual answer never arrives.
What we measured (codex-cli 0.146.0)
Captured OTLP/HTTP logs to a local sink from both entry points —
codex execand the interactive TUI (originator=codex-tui) — withlog_user_prompt = true:codex.user_prompt.promptand incodex.tool_result.arguments, never as agent output.codex.sse_eventfires once per stream event (221 of 224 records in a two-tool turn areevent.kind = messagecarrying onlyduration_ms); the threeresponse.completedrecords carry token counts. No content on any of them.exec-only gap.event_msg/agent_message), but reading that requires filesystem access to$CODEX_HOMEand its schema stability is an open question (#20952). Telemetry is the supported push interface, which is why we would rather consume it there.Two adjacent observations, in case they are useful:
[otel] trace_exporterproduced no exports at all in this environment — triedprotocol = "json"and"binary", and an endpoint both with and without the/v1/tracespath — while log records from the same process arrived normally. #12913's table lists traces as working for both entry points, so we may be misconfiguring something; happy to open a separate issue with a repro if that is not a known gap.arguments,output) are logged in full by default, while the agent's own message cannot be logged at all. An opt-in flag for the message would make the two consistent.Proposed shape
Mirrors the existing
log_user_promptplumbing, with no new concepts:OtelConfigToml.log_agent_message: Option<bool>→OtelConfig.log_agent_message: bool(resolved incore/src/config/otel.rs).log_agent_messagesflag onSessionTelemetryMetadata, set through awith_log_agent_messages(...)builder rather than a newSessionTelemetry::newparameter —newhas 20 call sites (2 production, 17 tests, 1 in the README), and a builder leaves all of them untouched.codex.agent_messageevent modelled directly onuser_prompt:message_lengthalways, the text gated by the flag. The trace event carries the length only, never the text, matching howuser_promptsplits log vs trace.Session::emit_turn_item_completedincore/src/session/mod.rs, matching onTurnItem::AgentMessage. That is the single funnel every completed turn item passes through — plan mode reaches it viaemit_agent_message_in_plan_mode— and it sits next to the existingrecord_turn_ttfm_metric(turn_context, &item)call, so no new plumbing is needed to reachturn_context.session_telemetry.One trap worth knowing before anyone implements this
Naming the payload attribute
messagedoes not work, and it fails silently.tracingreserves that field name, so the appender routes the value to theLogRecordbody instead of an attribute. Attribute-only consumers — including our receiver, and, we suspect, most collector pipelines people point at Codex — see the event arrive with no text on it. Straight out of the test that caught it:Naming the attribute
textfixes it, and matches the rollout's own vocabulary (agent_reasoning.text).Status
We have this implemented against
rust-v0.146.0— 6 files, +53/−1, plus two tests added tootel/tests/suite/otel_export_routing_policy.rsusing the existingInMemoryLogExporter/InMemorySpanExporterharness: one asserting the log record carries the text andmessage_lengthwhen opted in, one asserting[REDACTED]by default, both asserting the payload is an attribute rather than the record body.cargo check -p codex-config -p codex-coreis clean and the fullcodex-otelsuite passes (37 + 13 tests).Not opening a PR since contributions are invitation-only. If the team would like this upstream, we are glad to submit it against
mainwithjust write-config-schemaregenerated,just fmt/just fixclean, and an integration test undercore/suiteusingtest_codexto pin that the event fires exactly once per assistant message. Equally happy to leave the design and the trap documented here if you would rather implement it yourselves.