use std::sync::Arc;
use std::time::Duration;
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::error::{OTelSdkError, OTelSdkResult};
use opentelemetry_sdk::trace::{SdkTracerProvider, SpanData, SpanExporter};
use tracing_subscriber::Layer;
use tracing_subscriber::registry::LookupSpan;
use crate::metrics::Metrics;
const OTLP_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
const OTLP_SHUTDOWN_GRACE: Duration = Duration::from_secs(1);
const EXPORTED_TARGETS: &[&str] = &[
"decdn_node",
"decdn_cache",
"decdn_client",
"decdn_incentive",
"decdn_reputation",
"decdn_common",
];
const OTLP_TRANSPORT_TARGETS: &[&str] = &["h2", "hyper", "hyper_util", "tonic", "tower"];
fn export_filter() -> tracing_subscriber::filter::Targets {
use tracing::level_filters::LevelFilter;
tracing_subscriber::filter::Targets::new()
.with_default(LevelFilter::WARN)
.with_targets(
EXPORTED_TARGETS
.iter()
.map(|target| (*target, LevelFilter::INFO)),
)
.with_targets(
OTLP_TRANSPORT_TARGETS
.iter()
.map(|target| (*target, LevelFilter::OFF)),
)
}
fn is_exported_target(target: &str) -> bool {
EXPORTED_TARGETS.iter().any(|crate_name| {
target
.strip_prefix(crate_name)
.is_some_and(|rest| rest.is_empty() || rest.starts_with("::"))
})
}
pub(super) fn otel_layer<S>(provider: &SdkTracerProvider) -> impl Layer<S> + use<S>
where
S: tracing::Subscriber + for<'span> LookupSpan<'span>,
{
use tracing::level_filters::LevelFilter;
use tracing_subscriber::filter::FilterExt;
let tracer = opentelemetry::trace::TracerProvider::tracer(provider, "decdn");
let spans_from_decdn_only = tracing_subscriber::filter::filter_fn(|meta| {
meta.is_event() || is_exported_target(meta.target())
})
.with_max_level_hint(LevelFilter::TRACE);
tracing_opentelemetry::layer()
.with_tracer(tracer)
.with_filter(export_filter().and(spans_from_decdn_only))
}
#[derive(Debug)]
struct CountingSpanExporter<E> {
inner: E,
metrics: Arc<Metrics>,
}
impl<E: SpanExporter> SpanExporter for CountingSpanExporter<E> {
async fn export(&self, batch: Vec<SpanData>) -> OTelSdkResult {
let result = self.inner.export(batch).await;
if result.is_err() {
self.metrics.otlp_export_failure();
}
result
}
fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult {
self.inner.shutdown_with_timeout(timeout)
}
fn shutdown(&self) -> OTelSdkResult {
self.inner.shutdown()
}
fn force_flush(&self) -> OTelSdkResult {
self.inner.force_flush()
}
fn set_resource(&mut self, resource: &Resource) {
self.inner.set_resource(resource);
}
}
fn resource() -> Resource {
use opentelemetry::KeyValue;
Resource::builder()
.with_attributes([
KeyValue::new("service.name", "decdn"),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
])
.build()
}
pub(super) fn init_otlp_provider(
endpoint: &str,
metrics: Arc<Metrics>,
) -> anyhow::Result<SdkTracerProvider> {
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::trace::Sampler;
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()
.map_err(|e| anyhow::anyhow!("failed to build OTLP exporter: {e}"))?;
let provider = SdkTracerProvider::builder()
.with_batch_exporter(CountingSpanExporter {
inner: exporter,
metrics,
})
.with_sampler(Sampler::ParentBased(Box::new(Sampler::AlwaysOn)))
.with_resource(resource())
.build();
opentelemetry::global::set_tracer_provider(provider.clone());
Ok(provider)
}
pub(super) async fn finish_run(provider: SdkTracerProvider, result: &anyhow::Result<()>) {
if let Err(err) = result {
record_run_failure(err);
}
shutdown_tracer_provider(provider).await;
}
fn record_run_failure(err: &anyhow::Error) {
tracing::error_span!("node_run_failed").in_scope(|| {
tracing::error!(
error = %decdn_common::redact::sanitize_err_chain(err),
"node run failed"
);
});
}
async fn shutdown_tracer_provider(provider: SdkTracerProvider) {
let (tx, rx) = tokio::sync::oneshot::channel();
let spawned = std::thread::Builder::new()
.name("otlp-shutdown".into())
.spawn(move || {
let _ = tx.send(provider.shutdown_with_timeout(OTLP_SHUTDOWN_TIMEOUT));
});
if let Err(e) = spawned {
tracing::warn!(error = %e, "could not start the OTLP shutdown thread; queued spans are lost");
return;
}
let outcome = tokio::time::timeout(OTLP_SHUTDOWN_TIMEOUT + OTLP_SHUTDOWN_GRACE, rx).await;
if let Some(problem) = shutdown_problem(outcome) {
tracing::warn!("{problem}");
}
}
fn shutdown_problem(
outcome: Result<
Result<OTelSdkResult, tokio::sync::oneshot::error::RecvError>,
tokio::time::error::Elapsed,
>,
) -> Option<String> {
match outcome {
Ok(Ok(Ok(()))) => None,
Ok(Ok(Err(OTelSdkError::Timeout(after)))) => Some(format!(
"OTLP collector did not answer the exit-time flush within {after:?}; \
the in-flight batch is abandoned"
)),
Ok(Ok(Err(e))) => Some(format!(
"OTLP exit-time flush failed; its spans are lost: {e}"
)),
Ok(Err(_)) => Some(
"OTLP shutdown thread ended without reporting; queued spans may be lost".to_string(),
),
Err(_) => {
Some("OTLP shutdown did not finish within its bound; exiting without it".to_string())
}
}
}
#[cfg(test)]
mod tests;