use std::collections::HashMap; use std::time::Duration; use std::{env, fmt}; pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); pub const DEFAULT_METRIC_INTERVAL: Duration = Duration::from_secs(60); pub const DEFAULT_LOG_FILTER: &str = "info"; const TRACE_PATH: &str = "/v1/traces"; const LOG_PATH: &str = "/v1/logs"; const METRIC_PATH: &str = "/v1/metrics"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SamplerKind { AlwaysOn, AlwaysOff, ParentBasedAlwaysOn, ParentBasedAlwaysOff, TraceIdRatio, ParentBasedTraceIdRatio, } impl SamplerKind { pub fn parse(raw: &str) -> Self { match raw.trim().to_ascii_lowercase().as_str() { "always_on" => Self::AlwaysOn, "always_off" => Self::AlwaysOff, "traceidratio" => Self::TraceIdRatio, "parentbased_always_off" => Self::ParentBasedAlwaysOff, "parentbased_traceidratio" => Self::ParentBasedTraceIdRatio, _ => Self::ParentBasedAlwaysOn, } } } #[derive(Clone)] pub struct SignalConfig { pub enabled: bool, pub endpoint: Option, pub headers: HashMap, } // OTLP headers 常带 Authorization:Debug 输出里只能出现键名。 impl fmt::Debug for SignalConfig { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { let mut keys: Vec<&str> = self.headers.keys().map(String::as_str).collect(); keys.sort_unstable(); formatter .debug_struct("SignalConfig") .field("enabled", &self.enabled) .field("endpoint", &self.endpoint) .field("headers", &keys) .finish() } } impl SignalConfig { pub fn disabled() -> Self { Self { enabled: false, endpoint: None, headers: HashMap::new(), } } pub fn enabled(endpoint: impl Into) -> Self { Self { enabled: true, endpoint: Some(endpoint.into()), headers: HashMap::new(), } } pub fn endpoint(mut self, endpoint: impl Into) -> Self { self.endpoint = Some(endpoint.into()); self.enabled = true; self } pub fn header(mut self, key: impl Into, value: impl Into) -> Self { self.headers.insert(key.into(), value.into()); self } pub fn url(&self, path: &str) -> Option { if !self.enabled { return None; } self.endpoint .as_deref() .map(|base| join_endpoint(base, path)) } fn from_env( suffix: &str, common_endpoint: Option<&str>, common_headers: &HashMap, ) -> Self { let endpoint = env_var(&format!("OTEL_EXPORTER_OTLP_{suffix}_ENDPOINT")) .or_else(|| common_endpoint.map(str::to_owned)); let mut headers = common_headers.clone(); headers.extend(parse_key_values( &env_var(&format!("OTEL_EXPORTER_OTLP_{suffix}_HEADERS")).unwrap_or_default(), )); Self { enabled: endpoint.is_some(), endpoint, headers, } } } #[derive(Debug, Clone)] pub struct MetricsConfig { pub signal: SignalConfig, pub interval: Duration, } impl MetricsConfig { pub fn disabled() -> Self { Self { signal: SignalConfig::disabled(), interval: DEFAULT_METRIC_INTERVAL, } } pub fn enabled(endpoint: impl Into) -> Self { Self { signal: SignalConfig::enabled(endpoint), interval: DEFAULT_METRIC_INTERVAL, } } pub fn endpoint(mut self, endpoint: impl Into) -> Self { self.signal = self.signal.endpoint(endpoint); self } pub fn header(mut self, key: impl Into, value: impl Into) -> Self { self.signal = self.signal.header(key, value); self } pub fn interval(mut self, interval: Duration) -> Self { self.interval = interval; self } pub fn url(&self) -> Option { self.signal.url(METRIC_PATH) } } #[derive(Clone)] pub struct TelemetryConfig { pub disabled: bool, pub service_name: String, pub service_version: Option, pub environment: Option, pub attributes: Vec<(String, String)>, pub headers: HashMap, pub timeout: Duration, pub sampler: SamplerKind, pub sample_ratio: f64, pub traces: SignalConfig, pub metrics: MetricsConfig, pub logs: SignalConfig, pub log_filter: String, pub stdout: bool, } impl fmt::Debug for TelemetryConfig { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { let mut keys: Vec<&str> = self.headers.keys().map(String::as_str).collect(); keys.sort_unstable(); formatter .debug_struct("TelemetryConfig") .field("disabled", &self.disabled) .field("service_name", &self.service_name) .field("service_version", &self.service_version) .field("environment", &self.environment) .field("attributes", &self.attributes) .field("headers", &keys) .field("timeout", &self.timeout) .field("sampler", &self.sampler) .field("sample_ratio", &self.sample_ratio) .field("traces", &self.traces) .field("metrics", &self.metrics) .field("logs", &self.logs) .field("log_filter", &self.log_filter) .field("stdout", &self.stdout) .finish() } } impl TelemetryConfig { pub fn new(service_name: impl Into) -> Self { Self { disabled: false, service_name: service_name.into(), service_version: None, environment: None, attributes: Vec::new(), headers: HashMap::new(), timeout: DEFAULT_TIMEOUT, sampler: SamplerKind::ParentBasedAlwaysOn, sample_ratio: 1.0, traces: SignalConfig::disabled(), metrics: MetricsConfig::disabled(), logs: SignalConfig::disabled(), log_filter: DEFAULT_LOG_FILTER.to_owned(), stdout: true, } } pub fn from_env(service_name: impl Into) -> Self { let mut config = Self::new(service_name); if env_bool("OTEL_SDK_DISABLED").unwrap_or(false) { config.disabled = true; } if let Some(name) = env_var("OTEL_SERVICE_NAME") { config.service_name = name; } config.attributes = parse_key_values(&env_var("OTEL_RESOURCE_ATTRIBUTES").unwrap_or_default()); for (key, value) in &config.attributes { match key.as_str() { "service.version" => config.service_version = Some(value.clone()), "deployment.environment" => config.environment = Some(value.clone()), _ => {} } } let common_endpoint = env_var("OTEL_EXPORTER_OTLP_ENDPOINT"); config.headers = parse_key_values(&env_var("OTEL_EXPORTER_OTLP_HEADERS").unwrap_or_default()) .into_iter() .collect(); config.timeout = env_u64("OTEL_EXPORTER_OTLP_TIMEOUT") .map(Duration::from_millis) .unwrap_or(DEFAULT_TIMEOUT); config.sampler = SamplerKind::parse(&env_var("OTEL_TRACES_SAMPLER").unwrap_or_default()); config.sample_ratio = { let name = "OTEL_TRACES_SAMPLER_ARG"; env_var(name).and_then(|value| value.parse().ok()) } .unwrap_or(1.0); config.traces = SignalConfig::from_env("TRACES", common_endpoint.as_deref(), &config.headers); config.logs = SignalConfig::from_env("LOGS", common_endpoint.as_deref(), &config.headers); config.metrics = MetricsConfig { signal: SignalConfig::from_env("METRICS", common_endpoint.as_deref(), &config.headers), interval: env_u64("OTEL_METRIC_EXPORT_INTERVAL") .map(Duration::from_millis) .unwrap_or(DEFAULT_METRIC_INTERVAL), }; config.log_filter = env_var("OTEL_LOG_FILTER") .or_else(|| env_var("RUST_LOG")) .unwrap_or_else(|| DEFAULT_LOG_FILTER.to_owned()); config.stdout = env_bool("OTEL_LOG_STDOUT").unwrap_or(true); config } pub fn with_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self } pub fn traces_url(&self) -> Option { if self.disabled { return None; } self.traces.url(TRACE_PATH) } pub fn logs_url(&self) -> Option { if self.disabled { return None; } self.logs.url(LOG_PATH) } pub fn metrics_url(&self) -> Option { if self.disabled { return None; } self.metrics.url() } } pub fn join_endpoint(base: &str, path: &str) -> String { if base.ends_with(path) { return base.to_owned(); } match base.strip_suffix('/') { Some(trimmed) => format!("{trimmed}{path}"), None => format!("{base}{path}"), } } pub fn parse_key_values(raw: &str) -> Vec<(String, String)> { raw.split(',') .filter_map(|pair| pair.split_once('=')) .map(|(key, value)| (key.trim().to_owned(), value.trim().to_owned())) .filter(|(key, _)| !key.is_empty()) .collect() } fn env_var(name: &str) -> Option { env::var(name) .ok() .map(|value| value.trim().to_owned()) .filter(|value| !value.is_empty()) } fn env_bool(name: &str) -> Option { env_var(name).map(|value| { matches!( value.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on" ) }) } fn env_u64(name: &str) -> Option { env_var(name).and_then(|value| value.parse().ok()) }