use std::io; use opentelemetry::{KeyValue, global, trace::TracerProvider}; use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::{ Resource, logs::SdkLoggerProvider, metrics::SdkMeterProvider, trace::SdkTracerProvider, }; use tracing::warn; use tracing_subscriber::{ EnvFilter, Layer, filter::LevelFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt, }; #[derive(Clone, Copy, Debug, Default)] pub struct OtelConfig { pub enabled: bool, } // 统一解析工作区日志过滤器,优先环境变量,其次回落到调用方传入的默认值。 pub fn resolve_env_filter(default_filter: &str) -> EnvFilter { EnvFilter::try_from_default_env() .or_else(|_| EnvFilter::try_new(default_filter)) .unwrap_or_else(|_| EnvFilter::new("info")) } // 统一初始化 tracing subscriber,避免各入口重复散落相同配置。 pub fn init_tracing(default_filter: &str, otel_config: OtelConfig) -> Result<(), io::Error> { let env_filter = resolve_env_filter(default_filter); let fmt_layer = fmt::layer().with_target(true).with_ansi(false).compact(); if !otel_config.enabled { return tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) .try_init() .map_err(|error| io::Error::other(format!("初始化 tracing subscriber 失败:{error}"))); } let Some(otel) = build_otel_pipeline() else { return tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) .try_init() .map_err(|error| io::Error::other(format!("初始化 tracing subscriber 失败:{error}"))); }; tracing_subscriber::registry() .with(env_filter) .with(fmt_layer) .with( tracing_opentelemetry::layer() .with_tracer(otel.tracer_provider.tracer("genarrative-api")), ) .with( OpenTelemetryTracingBridge::new(&otel.logger_provider).with_filter(LevelFilter::INFO), ) .try_init() .map_err(|error| io::Error::other(format!("初始化 tracing subscriber 失败:{error}"))) } struct OtelPipeline { tracer_provider: SdkTracerProvider, _meter_provider: SdkMeterProvider, logger_provider: SdkLoggerProvider, } fn build_otel_pipeline() -> Option { let resource = Resource::builder() .with_service_name(read_env_or_default("OTEL_SERVICE_NAME", "genarrative-api")) .with_attribute(KeyValue::new("service.namespace", "genarrative")) .build(); let span_exporter = match opentelemetry_otlp::SpanExporter::builder() .with_http() .with_endpoint(resolve_otlp_http_signal_endpoint( "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "/v1/traces", )) .build() { Ok(exporter) => exporter, Err(error) => { warn!(%error, "OpenTelemetry span exporter 初始化失败,已回退为本地日志"); return None; } }; let metric_exporter = match opentelemetry_otlp::MetricExporter::builder() .with_http() .with_endpoint(resolve_otlp_http_signal_endpoint( "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "/v1/metrics", )) .build() { Ok(exporter) => exporter, Err(error) => { warn!(%error, "OpenTelemetry metric exporter 初始化失败,已回退为本地日志"); return None; } }; let log_exporter = match opentelemetry_otlp::LogExporter::builder() .with_http() .with_endpoint(resolve_otlp_http_signal_endpoint( "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "/v1/logs", )) .build() { Ok(exporter) => exporter, Err(error) => { warn!(%error, "OpenTelemetry log exporter 初始化失败,已回退为本地日志"); return None; } }; let tracer_provider = SdkTracerProvider::builder() .with_resource(resource.clone()) .with_batch_exporter(span_exporter) .build(); let meter_provider = SdkMeterProvider::builder() .with_resource(resource) .with_periodic_exporter(metric_exporter) .build(); let logger_provider = SdkLoggerProvider::builder() .with_resource(Resource::builder() .with_service_name(read_env_or_default("OTEL_SERVICE_NAME", "genarrative-api")) .with_attribute(KeyValue::new("service.namespace", "genarrative")) .build()) .with_batch_exporter(log_exporter) .build(); global::set_tracer_provider(tracer_provider.clone()); global::set_meter_provider(meter_provider.clone()); Some(OtelPipeline { tracer_provider, _meter_provider: meter_provider, logger_provider, }) } fn read_env_or_default(key: &str, default_value: &str) -> String { std::env::var(key) .ok() .filter(|value| !value.trim().is_empty()) .unwrap_or_else(|| default_value.to_string()) } fn resolve_otlp_http_signal_endpoint(signal_key: &str, signal_path: &str) -> String { if let Ok(value) = std::env::var(signal_key) && !value.trim().is_empty() { return value; } append_otlp_signal_path( &read_env_or_default("OTEL_EXPORTER_OTLP_ENDPOINT", "http://127.0.0.1:4318"), signal_path, ) } fn append_otlp_signal_path(base_endpoint: &str, signal_path: &str) -> String { let base_endpoint = base_endpoint.trim_end_matches('/'); let signal_path = signal_path.trim_start_matches('/'); format!("{base_endpoint}/{signal_path}") } #[cfg(test)] mod tests { use std::sync::{Mutex, OnceLock}; use super::resolve_otlp_http_signal_endpoint; const OTEL_ENDPOINT_ENV_KEYS: [&str; 4] = [ "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", ]; fn env_lock() -> std::sync::MutexGuard<'static, ()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| Mutex::new(())).lock().unwrap() } fn clear_otel_endpoint_env() { unsafe { for key in OTEL_ENDPOINT_ENV_KEYS { std::env::remove_var(key); } } } #[test] fn generic_otlp_http_endpoint_expands_to_signal_paths() { let _guard = env_lock(); clear_otel_endpoint_env(); unsafe { std::env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", "http://127.0.0.1:4318"); } assert_eq!( resolve_otlp_http_signal_endpoint("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "/v1/traces"), "http://127.0.0.1:4318/v1/traces" ); assert_eq!( resolve_otlp_http_signal_endpoint("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "/v1/metrics"), "http://127.0.0.1:4318/v1/metrics" ); assert_eq!( resolve_otlp_http_signal_endpoint("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "/v1/logs"), "http://127.0.0.1:4318/v1/logs" ); clear_otel_endpoint_env(); } }