mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Cover OTLP HTTP log and trace event export (#27059)
## Why The generic OTLP HTTP paths for log events and trace events need end-to-end coverage before exec-server relies on them. ## What changed - Adds loopback coverage for exporting `codex_otel.log_only` events to `/v1/logs`. - Verifies `codex_otel.trace_safe` events are present in the exported trace payload. This is a test-only PR. It does not change OTEL runtime behavior or metric APIs. ## Related work - #26091: counter descriptions - #27057: gauge instruments - #27058: second-based duration histograms This PR is independent and can land directly on `main`. ## Validation - `just test -p codex-otel` - `just fix -p codex-otel` - `just fmt`
This commit is contained in:
committed by
GitHub
Unverified
parent
5e58b6d77f
commit
bed60e3746
@@ -240,6 +240,105 @@ fn otlp_http_exporter_sends_metrics_to_collector() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn otlp_http_exporter_sends_logs_to_collector()
|
||||
-> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
|
||||
let addr = listener.local_addr().expect("local_addr");
|
||||
listener.set_nonblocking(true).expect("set_nonblocking");
|
||||
|
||||
let (tx, rx) = mpsc::channel::<Vec<CapturedRequest>>();
|
||||
let server = thread::spawn(move || {
|
||||
let mut captured = Vec::new();
|
||||
let deadline = Instant::now() + Duration::from_secs(3);
|
||||
|
||||
while Instant::now() < deadline {
|
||||
match listener.accept() {
|
||||
Ok((mut stream, _)) => {
|
||||
let result = read_http_request(&mut stream);
|
||||
let _ = write_http_response(&mut stream, "202 Accepted");
|
||||
if let Ok((path, headers, body)) = result {
|
||||
captured.push(CapturedRequest {
|
||||
path,
|
||||
content_type: headers.get("content-type").cloned(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tx.send(captured);
|
||||
});
|
||||
|
||||
let otel = OtelProvider::from(&OtelSettings {
|
||||
environment: "test".to_string(),
|
||||
service_name: "codex-cli".to_string(),
|
||||
service_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
codex_home: PathBuf::from("."),
|
||||
exporter: OtelExporter::OtlpHttp {
|
||||
endpoint: format!("http://{addr}/v1/logs"),
|
||||
headers: HashMap::new(),
|
||||
protocol: OtelHttpProtocol::Json,
|
||||
tls: None,
|
||||
},
|
||||
trace_exporter: OtelExporter::None,
|
||||
metrics_exporter: OtelExporter::None,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::new(),
|
||||
tracestate: BTreeMap::new(),
|
||||
})?
|
||||
.expect("otel provider");
|
||||
let logger_layer = otel.logger_layer().expect("logger layer");
|
||||
let subscriber = tracing_subscriber::registry().with(logger_layer);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::callsite::rebuild_interest_cache();
|
||||
tracing::event!(
|
||||
target: "codex_otel.log_only",
|
||||
tracing::Level::INFO,
|
||||
event.name = "codex.test.log_exported",
|
||||
"test OTEL log export"
|
||||
);
|
||||
});
|
||||
otel.shutdown();
|
||||
|
||||
server.join().expect("server join");
|
||||
let captured = rx.recv_timeout(Duration::from_secs(1)).expect("captured");
|
||||
|
||||
let request = captured
|
||||
.iter()
|
||||
.find(|req| req.path == "/v1/logs")
|
||||
.unwrap_or_else(|| {
|
||||
let paths = captured
|
||||
.iter()
|
||||
.map(|req| req.path.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
panic!("missing /v1/logs request; got {}: {paths}", captured.len());
|
||||
});
|
||||
let content_type = request
|
||||
.content_type
|
||||
.as_deref()
|
||||
.unwrap_or("<missing content-type>");
|
||||
assert!(
|
||||
content_type.starts_with("application/json"),
|
||||
"unexpected content-type: {content_type}"
|
||||
);
|
||||
|
||||
let body = String::from_utf8_lossy(&request.body);
|
||||
assert!(
|
||||
body.contains("codex.test.log_exported"),
|
||||
"expected exported log event not found; body prefix: {}",
|
||||
&body.chars().take(2000).collect::<String>()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn otel_provider_rejects_header_unsafe_configured_tracestate() {
|
||||
let result = OtelProvider::from(&OtelSettings {
|
||||
@@ -357,6 +456,12 @@ fn otlp_http_exporter_sends_traces_to_collector()
|
||||
let _guard = span.enter();
|
||||
let propagated_trace =
|
||||
current_span_w3c_trace_context().expect("current span should have trace context");
|
||||
tracing::event!(
|
||||
target: "codex_otel.trace_safe",
|
||||
tracing::Level::INFO,
|
||||
event.name = "codex.test.trace_event",
|
||||
"test OTEL trace event"
|
||||
);
|
||||
tracing::info!("trace loopback event");
|
||||
propagated_trace
|
||||
});
|
||||
@@ -409,6 +514,11 @@ fn otlp_http_exporter_sends_traces_to_collector()
|
||||
"expected configured span attribute not found; body prefix: {}",
|
||||
&body.chars().take(2000).collect::<String>()
|
||||
);
|
||||
assert!(
|
||||
body.contains("codex.test.trace_event"),
|
||||
"expected trace event not found; body prefix: {}",
|
||||
&body.chars().take(2000).collect::<String>()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user