Files
Genarrative/server-rs/crates/shared-logging/src/lib.rs
2026-05-16 22:44:30 +08:00

223 lines
7.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}