use std::str::FromStr;
use std::sync::OnceLock;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::{Protocol, SpanExporter, WithExportConfig};
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
use tracing::Subscriber;
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::registry::LookupSpan;
static PROVIDER: OnceLock<SdkTracerProvider> = OnceLock::new();
const SERVICE_NAME: &str = "skeg";
const TRACER_NAME: &str = "skeg-server";
const ENV_ENDPOINT: &str = "SKEG_TRACE_OTLP_ENDPOINT";
const ENV_SAMPLE_RATE: &str = "SKEG_TRACE_SAMPLE_RATE";
const ENV_RESOURCE_ATTRS: &str = "SKEG_TRACE_RESOURCE_ATTRS";
pub fn install<S>()
-> Result<Option<OpenTelemetryLayer<S, opentelemetry_sdk::trace::Tracer>>, OtlpError>
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
let Ok(endpoint) = std::env::var(ENV_ENDPOINT) else {
return Ok(None);
};
if endpoint.is_empty() {
return Ok(None);
}
let sample_rate = sample_rate_from_env();
let exporter = SpanExporter::builder()
.with_tonic()
.with_endpoint(&endpoint)
.with_protocol(Protocol::Grpc)
.build()
.map_err(OtlpError::Exporter)?;
let resource = build_resource();
let provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_sampler(Sampler::TraceIdRatioBased(sample_rate))
.with_resource(resource)
.build();
let tracer = provider.tracer(TRACER_NAME);
opentelemetry::global::set_tracer_provider(provider.clone());
let _ = PROVIDER.set(provider);
tracing::info!(
target: "skeg::tracing_otlp",
endpoint = %endpoint,
sample_rate,
"OTLP/gRPC tracing exporter installed"
);
Ok(Some(tracing_opentelemetry::layer().with_tracer(tracer)))
}
pub fn shutdown() {
if let Some(p) = PROVIDER.get()
&& let Err(e) = p.shutdown()
{
tracing::warn!(target: "skeg::tracing_otlp", error = %e, "OTLP shutdown failed");
}
}
fn sample_rate_from_env() -> f64 {
let Ok(raw) = std::env::var(ENV_SAMPLE_RATE) else {
return 1.0;
};
f64::from_str(&raw).map_or(1.0, |v| v.clamp(0.0, 1.0))
}
fn build_resource() -> Resource {
let mut builder = Resource::builder()
.with_service_name(SERVICE_NAME)
.with_attribute(opentelemetry::KeyValue::new(
"service.version",
env!("CARGO_PKG_VERSION"),
));
if let Ok(raw) = std::env::var(ENV_RESOURCE_ATTRS) {
for pair in raw.split(',') {
if let Some((k, v)) = pair.split_once('=') {
let k = k.trim().to_owned();
let v = v.trim().to_owned();
if !k.is_empty() && !v.is_empty() {
builder = builder.with_attribute(opentelemetry::KeyValue::new(k, v));
}
}
}
}
builder.build()
}
#[derive(Debug, thiserror::Error)]
pub enum OtlpError {
#[error("failed to build OTLP exporter: {0}")]
Exporter(#[from] opentelemetry_otlp::ExporterBuildError),
}