use std::time::Duration;
use anyhow::{Context, Result};
use opentelemetry::KeyValue;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::{Protocol, SpanExporter, WithExportConfig as _};
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::trace::{SdkTracer, SdkTracerProvider};
use parking_lot::Mutex;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::{EnvFilter, Layer};
enum ProviderState {
Uninitialized,
Active(SdkTracerProvider),
ShutDown,
}
static PROVIDER: Mutex<ProviderState> = Mutex::new(ProviderState::Uninitialized);
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
pub fn layer<S>() -> Result<Option<impl Layer<S>>>
where
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
{
let Some(tracer) = initialize_if_configured()? else {
return Ok(None);
};
Ok(Some(
tracing_opentelemetry::layer()
.with_tracer(tracer)
.with_location(false)
.with_threads(false)
.with_tracked_inactivity(false)
.with_filter(EnvFilter::new("rivetkit::telemetry=info")),
))
}
fn initialize_if_configured() -> Result<Option<SdkTracer>> {
if !export_is_configured() {
return Ok(None);
}
let mut stored_provider = PROVIDER.lock();
match &*stored_provider {
ProviderState::Active(provider) => return Ok(Some(provider.tracer("rivetkit"))),
ProviderState::ShutDown => return Ok(None),
ProviderState::Uninitialized => {}
}
let exporter = match configured_protocol()? {
Protocol::Grpc => SpanExporter::builder()
.with_tonic()
.build()
.context("build otlp span exporter")?,
protocol @ (Protocol::HttpBinary | Protocol::HttpJson) => SpanExporter::builder()
.with_http()
.with_protocol(protocol)
.build()
.context("build otlp span exporter")?,
};
let resource = Resource::builder()
.with_attribute(KeyValue::new("rivetkit.version", env!("CARGO_PKG_VERSION")))
.build();
let provider = SdkTracerProvider::builder()
.with_resource(resource)
.with_batch_exporter(exporter)
.build();
let tracer = provider.tracer("rivetkit");
*stored_provider = ProviderState::Active(provider);
Ok(Some(tracer))
}
fn configured_protocol() -> Result<Protocol> {
let configured = std::env::var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL")
.or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL"))
.unwrap_or_else(|_| "http/protobuf".to_owned());
match configured.as_str() {
"grpc" => Ok(Protocol::Grpc),
"http/protobuf" => Ok(Protocol::HttpBinary),
"http/json" => Ok(Protocol::HttpJson),
other => anyhow::bail!(
"native trace export supports grpc, http/protobuf and http/json, got {other:?}"
),
}
}
fn export_is_configured() -> bool {
if std::env::var("OTEL_SDK_DISABLED").is_ok_and(|value| value.eq_ignore_ascii_case("true"))
|| std::env::var("OTEL_TRACES_EXPORTER")
.is_ok_and(|value| value.eq_ignore_ascii_case("none"))
{
return false;
}
[
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_ENDPOINT",
]
.into_iter()
.any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
}
pub async fn shutdown_best_effort() {
let provider = {
let mut state = PROVIDER.lock();
match std::mem::replace(&mut *state, ProviderState::ShutDown) {
ProviderState::Active(provider) => Some(provider),
ProviderState::Uninitialized | ProviderState::ShutDown => None,
}
};
let Some(provider) = provider else {
return;
};
let shutdown = tokio::task::spawn_blocking(move || provider.shutdown());
match tokio::time::timeout(SHUTDOWN_TIMEOUT, shutdown).await {
Ok(Ok(Ok(()))) => {}
Ok(Ok(Err(error))) => tracing::warn!(
?error,
"OpenTelemetry trace provider shutdown failed; queued spans may be lost"
),
Ok(Err(error)) => tracing::warn!(
?error,
"OpenTelemetry trace provider shutdown task failed; queued spans may be lost"
),
Err(_) => tracing::warn!(
"OpenTelemetry trace provider shutdown timed out; queued spans may be lost"
),
}
}