fix: OTEL HTTP exporter panic and mTLS support (#7651)

This fixes two issues with the OTEL HTTP exporter:

1. **Runtime panic with async reqwest client**

The `opentelemetry_sdk` `BatchLogProcessor` spawns a dedicated OS thread
that uses `futures_executor::block_on()` rather than tokio's runtime.
When the async reqwest client's timeout mechanism calls
`tokio::time::sleep()`, it panics with "there is no reactor running,
must be called from the context of a Tokio 1.x runtime".

The fix is to use `reqwest::blocking::Client` instead, which doesn't
depend on tokio for timeouts. However, the blocking client creates its
own internal tokio runtime during construction, which would panic if
built from within an async context. We wrap the construction in
`tokio::task::block_in_place()` to handle this.

2. **mTLS certificate handling**

The HTTP client wasn't properly configured for mTLS, matching the fixes
previously done for the model provider client:

- Added `.tls_built_in_root_certs(false)` when using a custom CA
certificate to ensure only our CA is trusted
- Added `.https_only(true)` when using client identity
- Added `rustls-tls` feature to ensure rustls is used (required for
`Identity::from_pem()` to work correctly)
This commit is contained in:
Alexander
2025-12-05 20:46:44 -08:00
committed by GitHub
parent 93f61dbc5f
commit f521d29726
2 changed files with 25 additions and 7 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ opentelemetry-otlp = { workspace = true, features = [
"http-proto",
"http-json",
"logs",
"reqwest",
"reqwest-blocking-client",
"reqwest-rustls",
"tls",
"tls-roots",
@@ -40,7 +40,7 @@ opentelemetry_sdk = { workspace = true, features = [
"rt-tokio",
], optional = true }
http = { workspace = true }
reqwest = { workspace = true }
reqwest = { workspace = true, features = ["blocking", "rustls-tls"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
strum_macros = { workspace = true }
+23 -5
View File
@@ -182,12 +182,27 @@ fn build_grpc_tls_config(
Ok(config)
}
/// Build a blocking HTTP client with TLS configuration for the OTLP HTTP exporter.
///
/// We use `reqwest::blocking::Client` instead of the async client because the
/// `opentelemetry_sdk` `BatchLogProcessor` spawns a dedicated OS thread that uses
/// `futures_executor::block_on()` rather than tokio. When the async reqwest client's
/// timeout calls `tokio::time::sleep()`, it panics with "no reactor running".
fn build_http_client(
tls: &OtelTlsConfig,
codex_home: &Path,
) -> Result<reqwest::Client, Box<dyn Error>> {
let mut builder =
reqwest::Client::builder().timeout(resolve_otlp_timeout(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT));
) -> Result<reqwest::blocking::Client, Box<dyn Error>> {
// Wrap in block_in_place because reqwest::blocking::Client creates its own
// internal tokio runtime, which would panic if built directly from an async context.
tokio::task::block_in_place(|| build_http_client_inner(tls, codex_home))
}
fn build_http_client_inner(
tls: &OtelTlsConfig,
codex_home: &Path,
) -> Result<reqwest::blocking::Client, Box<dyn Error>> {
let mut builder = reqwest::blocking::Client::builder()
.timeout(resolve_otlp_timeout(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT));
if let Some(path) = tls.ca_certificate.as_ref() {
let (pem, location) = read_bytes(codex_home, path)?;
@@ -197,7 +212,10 @@ fn build_http_client(
location.display()
))
})?;
builder = builder.add_root_certificate(certificate);
// Disable built-in root certificates and use only our custom CA
builder = builder
.tls_built_in_root_certs(false)
.add_root_certificate(certificate);
}
match (&tls.client_certificate, &tls.client_private_key) {
@@ -212,7 +230,7 @@ fn build_http_client(
key_location.display()
))
})?;
builder = builder.identity(identity);
builder = builder.identity(identity).https_only(true);
}
(Some(_), None) | (None, Some(_)) => {
return Err(config_error(