OpenTelemetry events - #2103
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR implements OpenTelemetry (OTEL) tracing integration for the Codex application. The work adds telemetry configuration, a new telemetry crate with OTLP exporters, and session span tracking to enable observability across the TUI and exec binaries.
- Adds
codex-telemetrycrate with OTLP file, HTTP, and gRPC exporters - Integrates telemetry layers into TUI and exec binaries with configurable exporters
- Implements session span tracking with graceful shutdown to ensure trace export
Reviewed Changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| codex-rs/codex-telemetry/src/lib.rs | New telemetry crate implementing OTLP exporters and tracing layer builders |
| codex-rs/core/src/telemetry_init.rs | Configuration adapter between app config and telemetry crate |
| codex-rs/core/src/config_types.rs | Telemetry configuration types for TOML deserialization |
| codex-rs/core/src/config.rs | Integration of telemetry config with defaults and test updates |
| codex-rs/tui/src/lib.rs | TUI binary telemetry integration with conditional layer composition |
| codex-rs/exec/src/lib.rs | Exec binary telemetry integration and graceful shutdown handling |
| codex-rs/core/src/codex.rs | Session span tracking with proper span lifecycle management |
Comments suppressed due to low confidence (1)
codex-rs/codex-telemetry/src/lib.rs:20
- [nitpick] Using PathBuf::new() creates an empty path which will be ignored according to the comment. Consider using a more explicit type like Option or a dedicated enum variant to make this intent clearer.
use opentelemetry_sdk::export::trace::SpanData;
|
Hi @vishnu-oai, this PR has been quiet for a while. Is there any plan at OpenAI to prioritise and merge it? Thanks for any update you can share. |
0c1fffd to
4cae7be
Compare
Hi @regismesquita , We are working on this and next week will make more progress. Stay tuned. Thanks |
5606d4c to
8d3967a
Compare
d74f8b6 to
9d3e066
Compare
|
|
||
| if let Some(provider) = otel.as_ref() { | ||
| let otel_layer = OpenTelemetryTracingBridge::new(&provider.logger).with_filter( | ||
| tracing_subscriber::filter::filter_fn(codex_core::otel_init::codex_export_filter), |
There was a problem hiding this comment.
can build_provider also return a layer with filter applied or have a method to update tracing_subscriber::registry ? so we don't spread duplicates of registration code and keep it encapsulated?
There was a problem hiding this comment.
with_filter produce very complicated return type, I can try to come up with something later in follow up.
| #[cfg(feature = "otel")] | ||
| pub mod otel_provider; | ||
|
|
||
| #[cfg(not(feature = "otel"))] |
There was a problem hiding this comment.
why do we have a feature for this?
There was a problem hiding this comment.
Looks like we always enable it.
There was a problem hiding this comment.
some people might want to build cli without otel, but I can drop this optionality.
| @@ -89,7 +109,8 @@ async fn run_stream(sse_body: &str) -> Vec<ResponseEvent> { | |||
| while let Some(event) = stream.next().await { | |||
| match event { | |||
| Ok(ev) => events.push(ev), | |||
| Err(e) => panic!("stream event error: {e}"), | |||
| // We still collect the error to exercise telemetry and complete the task. | |||
| Err(_e) => break, | |||
There was a problem hiding this comment.
should the containing method return Result?
There was a problem hiding this comment.
it will return results, here we breaking while loop of whiting on stream of events
There was a problem hiding this comment.
More things to say on the PR but the general comment is why creating everything directly on OTEL. I feel like using tracing::instrument and maybe some prometheuse metrics (not 100% on this one, maybe the current OTEL is better but worth it to try) would be way simpler and handle a lot of things for us
| user_explicitly_approved_this_action: false, | ||
| }) | ||
| } | ||
| SafetyCheck::AutoApprove { |
There was a problem hiding this comment.
What's the link with this PR ?
There was a problem hiding this comment.
Sorry, what do you mean?
While tracing::instrument is indeed simpler to apply (I even used it in a prior commit), this change is more focused on event logging than on tracing code execution. The goal is to keep these events stable and treat them as part of a contract. By handling logging manually, we gain finer control over what gets logged and when. |
bf9c740 to
8f04205
Compare
8f04205 to
12f2846
Compare
| debug!("failed to parse ErrorResponse: {e}"); | ||
| let error = format!("failed to parse ErrorResponse: {e}"); | ||
| debug!(error); | ||
| response_error = Some(CodexErr::Stream(error, None)) |
There was a problem hiding this comment.
Why was this change made to set response_error in this case?
There was a problem hiding this comment.
to catch those errors and report them as error events.
Title
otel
Codex can emit OpenTelemetry log events that
describe each run: outbound API requests, streamed responses, user input,
tool-approval decisions, and the result of every tool invocation. Export is
disabled by default so local runs remain self-contained. Opt in by adding an
[otel]table and choosing an exporter.Codex tags every exported event with
service.name = "codex-cli", the CLIversion, and an
envattribute so downstream collectors can distinguishdev/staging/prod traffic. Only telemetry produced inside the
codex_otelcrate—the events listed below—is forwarded to the exporter.
Event catalog
Every event shares a common set of metadata fields:
event.timestamp,conversation.id,app.version,auth_mode(when available),user.account_id(when available),terminal.type,model, andslug.With OTEL enabled Codex emits the following event types (in addition to the
metadata above):
codex.api_requestcf_ray(optional)attemptduration_mshttp.response.status_code(optional)error.message(failures)codex.sse_eventevent.kindduration_mserror.message(failures)input_token_count(completion only)output_token_count(completion only)cached_token_count(completion only, optional)reasoning_token_count(completion only, optional)tool_token_count(completion only)codex.user_promptprompt_lengthprompt(redacted unlesslog_user_prompt = true)codex.tool_decisiontool_namecall_iddecision(approved,approved_for_session,denied, orabort)source(configoruser)codex.tool_resulttool_namecall_idargumentsduration_ms(execution time for the tool)success("true"or"false")outputChoosing an exporter
Set
otel.exporterto control where events go:none– leaves instrumentation active but skips exporting. This is thedefault.
otlp-http– posts OTLP log records to an OTLP/HTTP collector. Specify theendpoint, protocol, and headers your collector expects:
otlp-grpc– streams OTLP log records over gRPC. Provide the endpoint and anymetadata headers:
If the exporter is
nonenothing is written anywhere; otherwise you must run or point to yourown collector. All exporters run on a background batch worker that is flushed on
shutdown.
If you build Codex from source the OTEL crate is still behind an
otelfeatureflag; the official prebuilt binaries ship with the feature enabled. When the
feature is disabled the telemetry hooks become no-ops so the CLI continues to
function without the extra dependencies.