use opentelemetry::KeyValue;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::trace::SdkTracerProvider;
use std::sync::OnceLock;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, fmt};
static TRACER_PROVIDER: OnceLock<SdkTracerProvider> = OnceLock::new();
#[derive(Debug, Clone)]
pub struct OtlpConfig {
pub endpoint: String,
pub service_name: String,
}
impl Default for OtlpConfig {
fn default() -> Self {
Self {
endpoint: "http://localhost:4317".to_string(),
service_name: "mikcar".to_string(),
}
}
}
impl OtlpConfig {
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
..Default::default()
}
}
#[must_use]
pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
self.service_name = name.into();
self
}
}
#[derive(Debug)]
pub enum OtlpError {
ExporterInit(String),
AlreadyInitialized,
}
impl std::fmt::Display for OtlpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExporterInit(e) => write!(f, "failed to init OTLP exporter: {e}"),
Self::AlreadyInitialized => write!(f, "OTLP already initialized"),
}
}
}
impl std::error::Error for OtlpError {}
fn init_tracer_provider(config: &OtlpConfig) -> Result<SdkTracerProvider, OtlpError> {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(&config.endpoint)
.build()
.map_err(|e| OtlpError::ExporterInit(e.to_string()))?;
let provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(
Resource::builder()
.with_attributes([KeyValue::new("service.name", config.service_name.clone())])
.build(),
)
.build();
Ok(provider)
}
pub fn init_with_otlp(config: &OtlpConfig) -> Result<(), OtlpError> {
let provider = init_tracer_provider(config)?;
let tracer = provider.tracer("mikcar");
TRACER_PROVIDER
.set(provider)
.map_err(|_| OtlpError::AlreadyInitialized)?;
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("mikcar=info,tower_http=debug"));
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer().with_target(true))
.with(otel_layer)
.init();
tracing::info!(
endpoint = %config.endpoint,
service = %config.service_name,
"OTLP tracing enabled"
);
Ok(())
}
pub fn shutdown() {
if let Some(provider) = TRACER_PROVIDER.get()
&& let Err(e) = provider.shutdown()
{
tracing::warn!("Failed to shutdown tracer provider: {e}");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_otlp_config_default() {
let config = OtlpConfig::default();
assert_eq!(config.endpoint, "http://localhost:4317");
assert_eq!(config.service_name, "mikcar");
}
#[test]
fn test_otlp_config_builder() {
let config = OtlpConfig::new("http://jaeger:4317").with_service_name("my-sidecar");
assert_eq!(config.endpoint, "http://jaeger:4317");
assert_eq!(config.service_name, "my-sidecar");
}
}