use crate::error::Result;
pub type BoxedLayer =
Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync>;
#[derive(Clone, Debug)]
pub struct OtelConfig {
pub service: String,
pub version: String,
pub env: String,
pub endpoint: Option<String>,
pub env_var_endpoint: String,
pub env_var_protocol: String,
pub env_var_env: String,
}
impl OtelConfig {
pub fn from_app_name(app_name: &str, version: &str) -> Self {
let prefix = app_name.to_uppercase().replace('-', "_");
let env_var_env = format!("{prefix}_ENV");
let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
.ok()
.filter(|v| !v.is_empty());
let env = std::env::var(&env_var_env)
.ok()
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "dev".to_string());
Self {
service: app_name.to_string(),
version: version.to_string(),
env,
endpoint,
env_var_endpoint: "OTEL_EXPORTER_OTLP_ENDPOINT".to_string(),
env_var_protocol: "OTEL_EXPORTER_OTLP_PROTOCOL".to_string(),
env_var_env,
}
}
#[must_use]
pub fn with_endpoint(mut self, endpoint: Option<String>) -> Self {
if self.endpoint.is_none() {
self.endpoint = endpoint;
}
self
}
}
pub struct OtelGuard {
provider: opentelemetry_sdk::trace::SdkTracerProvider,
}
impl Drop for OtelGuard {
fn drop(&mut self) {
if let Err(e) = self.provider.shutdown() {
eprintln!("Error shutting down tracer provider: {e}");
}
}
}
pub fn build_otel_layer(cfg: &OtelConfig) -> Result<(Option<BoxedLayer>, Option<OtelGuard>)> {
let endpoint = match cfg.endpoint.as_deref() {
Some(ep) if !ep.is_empty() => ep,
_ => return Ok((None, None)),
};
let resource = opentelemetry_sdk::Resource::builder()
.with_attributes([
opentelemetry::KeyValue::new("service.name", cfg.service.clone()),
opentelemetry::KeyValue::new("deployment.environment", cfg.env.clone()),
opentelemetry::KeyValue::new("service.version", cfg.version.clone()),
])
.build();
let protocol = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL")
.ok()
.unwrap_or_default();
let exporter = build_exporter(endpoint, &protocol)?;
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(resource)
.build();
use opentelemetry::trace::TracerProvider as _;
let tracer = provider.tracer(cfg.service.clone());
let layer = tracing_opentelemetry::layer().with_tracer(tracer);
let boxed: BoxedLayer = Box::new(layer);
Ok((Some(boxed), Some(OtelGuard { provider })))
}
fn build_exporter(endpoint: &str, protocol: &str) -> Result<opentelemetry_otlp::SpanExporter> {
use opentelemetry_otlp::WithExportConfig as _;
match protocol {
#[cfg(feature = "otel-grpc")]
"grpc" => opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()
.map_err(crate::Error::OtelInit),
_ => opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_endpoint(endpoint)
.build()
.map_err(crate::Error::OtelInit),
}
}