Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com> Reviewed-on: #3
151 lines
5.5 KiB
Rust
151 lines
5.5 KiB
Rust
//! OpenTelemetry initialization for traces and logs.
|
|
//!
|
|
//! Exports traces and logs via OTLP (gRPC) when `OTEL_EXPORTER_OTLP_ENDPOINT`
|
|
//! is set. Always includes a `tracing_subscriber::fmt` layer for console output.
|
|
//!
|
|
//! Compatible with SigNoz, Grafana Tempo/Loki, Jaeger, and any OTLP-compatible
|
|
//! collector.
|
|
//!
|
|
//! # Environment Variables
|
|
//!
|
|
//! | Variable | Description | Default |
|
|
//! |---|---|---|
|
|
//! | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint (e.g. `http://localhost:4317`) | *(disabled)* |
|
|
//! | `OTEL_SERVICE_NAME` | Service name for resource | `service_name` param |
|
|
//! | `RUST_LOG` / standard `EnvFilter` | Log level filter | `info` |
|
|
|
|
use opentelemetry::global;
|
|
use opentelemetry::trace::TracerProvider as _;
|
|
use opentelemetry::KeyValue;
|
|
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
|
|
use opentelemetry_otlp::{LogExporter, SpanExporter, WithExportConfig};
|
|
use opentelemetry_sdk::{logs::SdkLoggerProvider, trace::SdkTracerProvider, Resource};
|
|
use tracing_opentelemetry::OpenTelemetryLayer;
|
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer as _};
|
|
|
|
/// Guard that shuts down OTel providers on drop.
|
|
///
|
|
/// Must be held for the lifetime of the application. When dropped,
|
|
/// flushes and shuts down the tracer and logger providers.
|
|
pub struct TelemetryGuard {
|
|
tracer_provider: Option<SdkTracerProvider>,
|
|
logger_provider: Option<SdkLoggerProvider>,
|
|
}
|
|
|
|
impl Drop for TelemetryGuard {
|
|
fn drop(&mut self) {
|
|
if let Some(tp) = self.tracer_provider.take() {
|
|
if let Err(e) = tp.shutdown() {
|
|
eprintln!("Failed to shutdown tracer provider: {e}");
|
|
}
|
|
}
|
|
if let Some(lp) = self.logger_provider.take() {
|
|
if let Err(e) = lp.shutdown() {
|
|
eprintln!("Failed to shutdown logger provider: {e}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_resource(service_name: &str) -> Resource {
|
|
let name = std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| service_name.to_string());
|
|
Resource::builder()
|
|
.with_service_name(name)
|
|
.with_attributes([KeyValue::new("service.version", env!("CARGO_PKG_VERSION"))])
|
|
.build()
|
|
}
|
|
|
|
/// Initialize telemetry (tracing + logging).
|
|
///
|
|
/// If `OTEL_EXPORTER_OTLP_ENDPOINT` is set, traces and logs are exported
|
|
/// via OTLP/gRPC. Console fmt output is always enabled.
|
|
///
|
|
/// Returns a [`TelemetryGuard`] that must be held alive for the application
|
|
/// lifetime. Dropping it triggers a graceful shutdown of OTel providers.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the tracing subscriber cannot be initialized (e.g. called twice).
|
|
pub fn init_telemetry(service_name: &str) -> TelemetryGuard {
|
|
let otel_endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok();
|
|
|
|
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
|
|
let fmt_layer = tracing_subscriber::fmt::layer();
|
|
|
|
match otel_endpoint {
|
|
Some(ref endpoint) => {
|
|
let resource = build_resource(service_name);
|
|
|
|
// Traces
|
|
#[allow(clippy::expect_used)]
|
|
let span_exporter = SpanExporter::builder()
|
|
.with_tonic()
|
|
.with_endpoint(endpoint)
|
|
.build()
|
|
.expect("failed to create OTLP span exporter");
|
|
|
|
let tracer_provider = SdkTracerProvider::builder()
|
|
.with_batch_exporter(span_exporter)
|
|
.with_resource(resource.clone())
|
|
.build();
|
|
|
|
global::set_tracer_provider(tracer_provider.clone());
|
|
let tracer = tracer_provider.tracer(service_name.to_string());
|
|
let otel_trace_layer = OpenTelemetryLayer::new(tracer);
|
|
|
|
// Logs
|
|
#[allow(clippy::expect_used)]
|
|
let log_exporter = LogExporter::builder()
|
|
.with_tonic()
|
|
.with_endpoint(endpoint)
|
|
.build()
|
|
.expect("failed to create OTLP log exporter");
|
|
|
|
let logger_provider = SdkLoggerProvider::builder()
|
|
.with_batch_exporter(log_exporter)
|
|
.with_resource(resource)
|
|
.build();
|
|
|
|
let otel_log_layer = OpenTelemetryTracingBridge::new(&logger_provider);
|
|
|
|
// Filter to prevent telemetry-induced-telemetry loops
|
|
let otel_filter = EnvFilter::new("info")
|
|
.add_directive("hyper=off".parse().unwrap_or_default())
|
|
.add_directive("tonic=off".parse().unwrap_or_default())
|
|
.add_directive("h2=off".parse().unwrap_or_default())
|
|
.add_directive("reqwest=off".parse().unwrap_or_default());
|
|
|
|
tracing_subscriber::registry()
|
|
.with(env_filter)
|
|
.with(fmt_layer)
|
|
.with(otel_trace_layer)
|
|
.with(otel_log_layer.with_filter(otel_filter))
|
|
.init();
|
|
|
|
tracing::info!(
|
|
endpoint = endpoint.as_str(),
|
|
service = service_name,
|
|
"OpenTelemetry OTLP export enabled"
|
|
);
|
|
|
|
TelemetryGuard {
|
|
tracer_provider: Some(tracer_provider),
|
|
logger_provider: Some(logger_provider),
|
|
}
|
|
}
|
|
None => {
|
|
tracing_subscriber::registry()
|
|
.with(env_filter)
|
|
.with(fmt_layer)
|
|
.init();
|
|
|
|
tracing::info!("OpenTelemetry disabled (set OTEL_EXPORTER_OTLP_ENDPOINT to enable)");
|
|
|
|
TelemetryGuard {
|
|
tracer_provider: None,
|
|
logger_provider: None,
|
|
}
|
|
}
|
|
}
|
|
}
|