Configure telemetry via congif.toml

This commit is contained in:
Vishnu Chopra
2025-08-11 16:33:16 +01:00
committed by Anton Panasenko
parent ad78dce1d9
commit 148dda758f
9 changed files with 174 additions and 13 deletions

3
codex-rs/Cargo.lock generated
View File

@@ -743,6 +743,7 @@ dependencies = [
"maplit",
"mcp-types",
"openssl-sys",
"opentelemetry_sdk",
"os_info",
"portable-pty",
"predicates",
@@ -3247,6 +3248,7 @@ dependencies = [
"bytes",
"http",
"opentelemetry",
"reqwest",
]
[[package]]
@@ -3263,6 +3265,7 @@ dependencies = [
"opentelemetry-proto",
"opentelemetry_sdk",
"prost",
"reqwest",
"thiserror 1.0.69",
"tokio",
"tonic",

View File

@@ -32,7 +32,8 @@ tracing-subscriber = { version = "0.3", features = ["registry", "fmt"], optional
tracing-opentelemetry = { version = "0.27", optional = true }
opentelemetry = { version = "0.26", features = ["trace"], optional = true }
opentelemetry_sdk = { version = "0.26", features = ["trace", "rt-tokio"], optional = true }
opentelemetry-otlp = { version = "0.26", features = ["tonic", "http-proto"], optional = true }
# Enable HTTP exporter protocol and the reqwest HTTP client backend
opentelemetry-otlp = { version = "0.26", features = ["tonic", "http-proto", "reqwest-client"], optional = true }
opentelemetry-proto = { version = "0.26", features = ["gen-tonic"], optional = true }
prost = { version = "0.13", optional = true }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"], optional = true }

View File

@@ -64,6 +64,7 @@ uuid = { version = "1", features = ["serde", "v4"] }
which = "6"
wildmatch = "2.5.0"
codex-telemetry = { path = "../codex-telemetry", features = ["otel"] }
opentelemetry_sdk = { version = "0.26", features = ["trace"] }
[target.'cfg(target_os = "linux")'.dependencies]

View File

@@ -159,8 +159,8 @@ pub struct Config {
/// Responses API.
pub model_reasoning_effort: Option<ReasoningEffort>,
/// If not "none", the value to use for `reasoning.summary` when making a
/// request using the Responses API.
/// If true, a short textual description of the agent's internal reasoning
/// will be included in model responses.
pub model_reasoning_summary: ReasoningSummary,
/// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`).
@@ -194,6 +194,9 @@ pub struct Config {
/// All characters are inserted as they are received, and no buffering
/// or placeholder replacement will occur for fast keypress bursts.
pub disable_paste_burst: bool,
/// Telemetry configuration (exporter type, endpoint, headers, etc.).
pub telemetry: crate::config_types::TelemetryConfig,
}
impl Config {
@@ -705,6 +708,9 @@ pub struct ConfigToml {
/// All characters are inserted as they are received, and no buffering
/// or placeholder replacement will occur for fast keypress bursts.
pub disable_paste_burst: Option<bool>,
/// Telemetry configuration.
pub telemetry: Option<crate::config_types::TelemetryConfigToml>,
}
impl From<ConfigToml> for UserSavedConfig {
@@ -1053,6 +1059,24 @@ impl Config {
.as_ref()
.map(|t| t.notifications.clone())
.unwrap_or_default(),
telemetry: {
use crate::config_types::TelemetryConfig;
use crate::config_types::TelemetryConfigToml;
use crate::config_types::TelemetryExporterKind;
let t: TelemetryConfigToml = cfg.telemetry.unwrap_or_default();
let enabled = t.enabled.unwrap_or(true);
let exporter = t.exporter.unwrap_or(TelemetryExporterKind::OtlpFile);
let endpoint = t.endpoint;
let headers = t.headers.unwrap_or_default();
let rotate_mb = t.rotate_mb.or(Some(100));
TelemetryConfig {
enabled,
exporter,
endpoint,
headers,
rotate_mb,
}
},
};
Ok(config)
}
@@ -1631,6 +1655,13 @@ model_verbosity = "high"
active_profile: Some("o3".to_string()),
disable_paste_burst: false,
tui_notifications: Default::default(),
telemetry: crate::config_types::TelemetryConfig {
enabled: true,
exporter: crate::config_types::TelemetryExporterKind::OtlpFile,
endpoint: None,
headers: HashMap::new(),
rotate_mb: Some(100),
},
},
o3_profile_config
);
@@ -1689,6 +1720,13 @@ model_verbosity = "high"
active_profile: Some("gpt3".to_string()),
disable_paste_burst: false,
tui_notifications: Default::default(),
telemetry: crate::config_types::TelemetryConfig {
enabled: true,
exporter: crate::config_types::TelemetryExporterKind::OtlpFile,
endpoint: None,
headers: HashMap::new(),
rotate_mb: Some(100),
},
};
assert_eq!(expected_gpt3_profile_config, gpt3_profile_config);
@@ -1762,6 +1800,13 @@ model_verbosity = "high"
active_profile: Some("zdr".to_string()),
disable_paste_burst: false,
tui_notifications: Default::default(),
telemetry: crate::config_types::TelemetryConfig {
enabled: true,
exporter: crate::config_types::TelemetryExporterKind::OtlpFile,
endpoint: None,
headers: HashMap::new(),
rotate_mb: Some(100),
},
};
assert_eq!(expected_zdr_profile_config, zdr_profile_config);
@@ -1821,6 +1866,13 @@ model_verbosity = "high"
active_profile: Some("gpt5".to_string()),
disable_paste_burst: false,
tui_notifications: Default::default(),
telemetry: crate::config_types::TelemetryConfig {
enabled: true,
exporter: crate::config_types::TelemetryExporterKind::OtlpFile,
endpoint: None,
headers: HashMap::new(),
rotate_mb: Some(100),
},
};
assert_eq!(expected_gpt5_profile_config, gpt5_profile_config);

View File

@@ -76,6 +76,48 @@ pub enum HistoryPersistence {
None,
}
// ===== Telemetry configuration =====
/// Which telemetry exporter to use.
#[derive(Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum TelemetryExporterKind {
None,
OtlpFile,
OtlpHttp,
OtlpGrpc,
}
/// Telemetry settings loaded from config.toml. Fields are optional so we can apply defaults.
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub struct TelemetryConfigToml {
/// Enable or disable telemetry entirely. Defaults to true.
pub enabled: Option<bool>,
/// Exporter to use. Defaults to `otlp-file`.
pub exporter: Option<TelemetryExporterKind>,
/// Endpoint for HTTP/GRPC exporters. Example: "http://localhost:4318".
pub endpoint: Option<String>,
/// Optional headers for HTTP/GRPC exporters.
#[serde(default)]
pub headers: Option<HashMap<String, String>>,
/// Rotation size (MiB) for file exporter.
pub rotate_mb: Option<u64>,
}
/// Effective telemetry settings after defaults are applied.
#[derive(Debug, Clone, PartialEq)]
pub struct TelemetryConfig {
pub enabled: bool,
pub exporter: TelemetryExporterKind,
pub endpoint: Option<String>,
pub headers: HashMap<String, String>,
pub rotate_mb: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
pub enum Notifications {

View File

@@ -100,3 +100,5 @@ pub use codex_protocol::models::LocalShellExecAction;
pub use codex_protocol::models::LocalShellStatus;
pub use codex_protocol::models::ReasoningItemContent;
pub use codex_protocol::models::ResponseItem;
pub mod telemetry_init;

View File

@@ -0,0 +1,58 @@
use std::path::PathBuf;
use opentelemetry_sdk::trace::Tracer;
use crate::config::Config;
use crate::config_types::TelemetryExporterKind as Kind;
use codex_telemetry as telemetry;
/// Build an OpenTelemetry tracer and guard from the app Config.
///
/// Returns `None` when telemetry is disabled.
pub fn build_otel_layer_from_config(
config: &Config,
service_name: &str,
service_version: &str,
) -> Option<(telemetry::Guard, Tracer)> {
let exporter = match config.telemetry.exporter {
Kind::None => telemetry::Exporter::None,
Kind::OtlpFile => telemetry::Exporter::OtlpFile {
path: PathBuf::new(),
rotate_mb: config.telemetry.rotate_mb,
},
Kind::OtlpHttp => telemetry::Exporter::OtlpHttp {
endpoint: config
.telemetry
.endpoint
.clone()
.unwrap_or_else(|| "http://localhost:4318".to_string()),
headers: config
.telemetry
.headers
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
},
Kind::OtlpGrpc => telemetry::Exporter::OtlpGrpc {
endpoint: config
.telemetry
.endpoint
.clone()
.unwrap_or_else(|| "http://localhost:4317".to_string()),
headers: config
.telemetry
.headers
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
},
};
telemetry::build_layer(&telemetry::Settings {
enabled: config.telemetry.enabled,
exporter,
service_name: service_name.to_string(),
service_version: service_version.to_string(),
codex_home: Some(config.codex_home.clone()),
})
}

View File

@@ -169,16 +169,11 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?;
// Build OTEL layer and compose into subscriber.
let telemetry = telemetry::build_layer(&telemetry::Settings {
enabled: true,
exporter: telemetry::Exporter::OtlpFile {
path: PathBuf::new(),
rotate_mb: Some(100),
},
service_name: "codex".to_string(),
service_version: env!("CARGO_PKG_VERSION").to_string(),
codex_home: Some(config.codex_home.clone()),
});
let telemetry = codex_core::telemetry_init::build_otel_layer_from_config(
&config,
"codex",
env!("CARGO_PKG_VERSION"),
);
let _telemetry_guard = if let Some((guard, tracer)) = telemetry {
let otel_layer = tracing_opentelemetry::OpenTelemetryLayer::new(tracer);
// Build env_filter separately and attach via with_filter.

View File

@@ -165,6 +165,13 @@ pub async fn run_main(
}
};
// Build OTEL layer and compose into subscriber.
let telemetry = codex_core::telemetry_init::build_otel_layer_from_config(
&config,
"codex",
env!("CARGO_PKG_VERSION"),
);
// we load config.toml here to determine project state.
#[allow(clippy::print_stderr)]
let config_toml = {