From bccce0d75f836750c32ce4fc3dde6504fa2f591c Mon Sep 17 00:00:00 2001 From: mcgrew-oai <146999853+mcgrew-oai@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:54:45 -0500 Subject: [PATCH] otel: add host.name resource attribute to logs/traces via gethostname (#12352) **PR Summary** This PR adds the OpenTelemetry `host.name` resource attribute to Codex OTEL exports so every OTEL log (and trace, via the shared resource) carries the machine hostname. **What changed** - Added `host.name` to the shared OTEL `Resource` in `/Users/michael.mcgrew/code/codex/codex-rs/otel/src/otel_provider.rs` - This applies to both: - OTEL logs (`SdkLoggerProvider`) - OTEL traces (`SdkTracerProvider`) - Hostname is now resolved via `gethostname::gethostname()` (best-effort) - Value is trimmed - Empty values are omitted (non-fatal) - Added focused unit tests for: - including `host.name` when present - omitting `host.name` when missing/empty **Why** - `host.name` is host/process metadata and belongs on the OTEL `resource`, not per-event attributes. - Attaching it in the shared resource is the smallest change that guarantees coverage across all exported OTEL logs/traces. **Scope / Non-goals** - No public API changes - No changes to metrics behavior (this PR only updates log/trace resource metadata) **Dependency updates** - Added `gethostname` as a workspace dependency and `codex-otel` dependency - `Cargo.lock` updated accordingly - `MODULE.bazel.lock` unchanged after refresh/check **Validation** - `just fmt` - `cargo test -p codex-otel` - `just bazel-lock-update` - `just bazel-lock-check` --- codex-rs/Cargo.lock | 1 + codex-rs/Cargo.toml | 1 + codex-rs/otel/Cargo.toml | 1 + codex-rs/otel/src/otel_provider.rs | 81 +++++++++++++++++++++++++++--- 4 files changed, 77 insertions(+), 7 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 45402ed66..05e0ea297 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2095,6 +2095,7 @@ dependencies = [ "codex-utils-absolute-path", "codex-utils-string", "eventsource-stream", + "gethostname", "http 1.4.0", "opentelemetry", "opentelemetry-appender-tracing", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index e648572ec..3bb436ef0 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -178,6 +178,7 @@ env_logger = "0.11.9" eventsource-stream = "0.2.3" futures = { version = "0.3", default-features = false } globset = "0.4" +gethostname = "1.1.0" http = "1.3.1" icu_decimal = "2.1" icu_locale_core = "2.1" diff --git a/codex-rs/otel/Cargo.toml b/codex-rs/otel/Cargo.toml index 6e6321d2e..0fa14ff54 100644 --- a/codex-rs/otel/Cargo.toml +++ b/codex-rs/otel/Cargo.toml @@ -26,6 +26,7 @@ codex-utils-string = { workspace = true } codex-api = { workspace = true } codex-protocol = { workspace = true } eventsource-stream = { workspace = true } +gethostname = { workspace = true } opentelemetry = { workspace = true, features = ["logs", "metrics", "trace"] } opentelemetry-appender-tracing = { workspace = true } opentelemetry-otlp = { workspace = true, features = [ diff --git a/codex-rs/otel/src/otel_provider.rs b/codex-rs/otel/src/otel_provider.rs index b1ea099fa..6f2dc5a09 100644 --- a/codex-rs/otel/src/otel_provider.rs +++ b/codex-rs/otel/src/otel_provider.rs @@ -3,6 +3,7 @@ use crate::config::OtelHttpProtocol; use crate::config::OtelSettings; use crate::metrics::MetricsClient; use crate::metrics::MetricsConfig; +use gethostname::gethostname; use opentelemetry::Context; use opentelemetry::KeyValue; use opentelemetry::context::ContextGuard; @@ -40,6 +41,7 @@ use tracing_subscriber::Layer; use tracing_subscriber::registry::LookupSpan; const ENV_ATTRIBUTE: &str = "env"; +const HOST_NAME_ATTRIBUTE: &str = "host.name"; const TRACEPARENT_ENV_VAR: &str = "TRACEPARENT"; const TRACESTATE_ENV_VAR: &str = "TRACESTATE"; static TRACEPARENT_CONTEXT: OnceLock> = OnceLock::new(); @@ -223,16 +225,37 @@ fn extract_traceparent_context(traceparent: String, tracestate: Option) fn make_resource(settings: &OtelSettings) -> Resource { Resource::builder() .with_service_name(settings.service_name.clone()) - .with_attributes(vec![ - KeyValue::new( - semconv::attribute::SERVICE_VERSION, - settings.service_version.clone(), - ), - KeyValue::new(ENV_ATTRIBUTE, settings.environment.clone()), - ]) + .with_attributes(resource_attributes( + settings, + detected_host_name().as_deref(), + )) .build() } +fn resource_attributes(settings: &OtelSettings, host_name: Option<&str>) -> Vec { + let mut attributes = vec![ + KeyValue::new( + semconv::attribute::SERVICE_VERSION, + settings.service_version.clone(), + ), + KeyValue::new(ENV_ATTRIBUTE, settings.environment.clone()), + ]; + if let Some(host_name) = host_name.and_then(normalize_host_name) { + attributes.push(KeyValue::new(HOST_NAME_ATTRIBUTE, host_name)); + } + attributes +} + +fn detected_host_name() -> Option { + let host_name = gethostname(); + normalize_host_name(host_name.to_string_lossy().as_ref()) +} + +fn normalize_host_name(host_name: &str) -> Option { + let host_name = host_name.trim(); + (!host_name.is_empty()).then(|| host_name.to_owned()) +} + fn build_logger( resource: &Resource, exporter: &OtelExporter, @@ -377,6 +400,8 @@ mod tests { use opentelemetry::trace::SpanId; use opentelemetry::trace::TraceContextExt; use opentelemetry::trace::TraceId; + use pretty_assertions::assert_eq; + use std::path::PathBuf; #[test] fn parses_valid_traceparent() { @@ -398,4 +423,46 @@ mod tests { fn invalid_traceparent_returns_none() { assert!(extract_traceparent_context("not-a-traceparent".to_string(), None).is_none()); } + + #[test] + fn resource_attributes_include_host_name_when_present() { + let attrs = resource_attributes(&test_otel_settings(), Some("opentelemetry-test")); + + let host_name = attrs + .iter() + .find(|kv| kv.key.as_str() == HOST_NAME_ATTRIBUTE) + .map(|kv| kv.value.as_str().to_string()); + + assert_eq!(host_name, Some("opentelemetry-test".to_string())); + } + + #[test] + fn resource_attributes_omit_host_name_when_missing_or_empty() { + let missing = resource_attributes(&test_otel_settings(), None); + let empty = resource_attributes(&test_otel_settings(), Some(" ")); + + assert!( + !missing + .iter() + .any(|kv| kv.key.as_str() == HOST_NAME_ATTRIBUTE) + ); + assert!( + !empty + .iter() + .any(|kv| kv.key.as_str() == HOST_NAME_ATTRIBUTE) + ); + } + + fn test_otel_settings() -> OtelSettings { + OtelSettings { + environment: "test".to_string(), + service_name: "codex-test".to_string(), + service_version: "0.0.0".to_string(), + codex_home: PathBuf::from("."), + exporter: OtelExporter::None, + trace_exporter: OtelExporter::None, + metrics_exporter: OtelExporter::None, + runtime_metrics: false, + } + } }