223 lines
7.1 KiB
Rust
223 lines
7.1 KiB
Rust
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<OtelPipeline> {
|
||
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<Mutex<()>> = 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();
|
||
}
|
||
}
|