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`
This commit is contained in:
mcgrew-oai 2026-02-25 09:54:45 -05:00 committed by GitHub
parent 8d49e0d0c4
commit bccce0d75f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 77 additions and 7 deletions

1
codex-rs/Cargo.lock generated
View file

@ -2095,6 +2095,7 @@ dependencies = [
"codex-utils-absolute-path",
"codex-utils-string",
"eventsource-stream",
"gethostname",
"http 1.4.0",
"opentelemetry",
"opentelemetry-appender-tracing",

View file

@ -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"

View file

@ -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 = [

View file

@ -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<Option<Context>> = OnceLock::new();
@ -223,16 +225,37 @@ fn extract_traceparent_context(traceparent: String, tracestate: Option<String>)
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<KeyValue> {
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<String> {
let host_name = gethostname();
normalize_host_name(host_name.to_string_lossy().as_ref())
}
fn normalize_host_name(host_name: &str) -> Option<String> {
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,
}
}
}