use std::str::FromStr;
use anyhow::Result;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
use opentelemetry_sdk::propagation::TraceContextPropagator;
use opentelemetry_sdk::trace::SpanExporter;
use tracing::subscriber::Subscriber;
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::layer::{Filter, SubscriberExt};
use tracing_subscriber::{Layer, Registry};
#[derive(Clone)]
pub struct TracingConfig {
pub otel: OpenTelemetry,
pub json_format: bool,
}
#[derive(Clone)]
pub enum OpenTelemetry {
Enabled {
endpoint: String,
},
Disabled,
}
impl TracingConfig {
pub fn from_otel_env() -> Self {
let endpoint = std::env::var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
.or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT"))
.ok()
.filter(|s| !s.is_empty());
let otel = match endpoint {
Some(endpoint) => OpenTelemetry::Enabled { endpoint },
None => OpenTelemetry::Disabled,
};
TracingConfig {
otel,
json_format: std::env::var("JSON_LOGGING")
.unwrap_or_else(|_| "false".to_string())
.parse()
.unwrap_or(false),
}
}
}
impl OpenTelemetry {
pub fn is_enabled(&self) -> bool {
matches!(self, OpenTelemetry::Enabled { .. })
}
}
pub fn setup_tracing(cfg: TracingConfig) -> Result<()> {
if cfg.otel.is_enabled() {
opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
setup_metrics_export(&cfg.otel)?;
}
let otel_layer = {
if let OpenTelemetry::Enabled { endpoint } = cfg.otel {
let exporter_builder =
opentelemetry_otlp::SpanExporter::builder().with_tonic().with_endpoint(endpoint);
match exporter_builder.build() {
Ok(exporter) => {
Some(open_telemetry_layer(exporter, "miden-note-transport-node".to_string()))
},
Err(_) => None,
}
} else {
None
}
};
let subscriber = Registry::default()
.with(stdout_layer(cfg.json_format).with_filter(env_or_default_filter()))
.with(otel_layer.with_filter(env_or_default_filter()));
tracing::subscriber::set_global_default(subscriber).map_err(Into::into)
}
fn setup_metrics_export(otel_cfg: &OpenTelemetry) -> Result<()> {
if let OpenTelemetry::Enabled { endpoint } = otel_cfg {
let exporter = opentelemetry_otlp::MetricExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()?;
let provider = SdkMeterProvider::builder()
.with_reader(
PeriodicReader::builder(exporter)
.with_interval(std::time::Duration::from_secs(5)) .build(),
)
.build();
opentelemetry::global::set_meter_provider(provider);
}
Ok(())
}
#[cfg(test)]
pub fn setup_test_tracing() -> Result<(
tokio::sync::mpsc::UnboundedReceiver<opentelemetry_sdk::trace::SpanData>,
tokio::sync::mpsc::UnboundedReceiver<()>,
)> {
let (exporter, rx_export, rx_shutdown) =
opentelemetry_sdk::testing::trace::new_tokio_test_exporter();
let otel_layer = open_telemetry_layer(exporter, "test-service".to_string());
let subscriber = Registry::default()
.with(stdout_layer(true).with_filter(env_or_default_filter()))
.with(otel_layer.with_filter(env_or_default_filter()));
tracing::subscriber::set_global_default(subscriber)?;
Ok((rx_export, rx_shutdown))
}
fn open_telemetry_layer<S>(
exporter: impl SpanExporter + 'static,
service_name: String,
) -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
where
S: Subscriber + Sync + Send,
for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
{
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.build();
let tracer = provider.tracer(service_name);
opentelemetry::global::set_tracer_provider(provider);
OpenTelemetryLayer::new(tracer).boxed()
}
fn stdout_layer<S>(
json_logging: bool,
) -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
where
S: Subscriber,
for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
{
use tracing_subscriber::fmt::format::FmtSpan;
if json_logging {
tracing_subscriber::fmt::layer()
.with_level(true)
.with_file(true)
.with_line_number(true)
.with_target(true)
.with_span_events(FmtSpan::CLOSE)
.json()
.boxed()
} else {
tracing_subscriber::fmt::layer()
.pretty()
.compact()
.with_level(true)
.with_file(true)
.with_line_number(true)
.with_target(true)
.with_span_events(FmtSpan::CLOSE)
.boxed()
}
}
fn env_or_default_filter<S>() -> Box<dyn Filter<S> + Send + Sync + 'static> {
use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::filter::{FilterExt, Targets};
match std::env::var(EnvFilter::DEFAULT_ENV) {
Ok(rust_log) => FilterExt::boxed(
EnvFilter::from_str(&rust_log)
.expect("RUST_LOG should contain a valid filter configuration"),
),
Err(std::env::VarError::NotUnicode(_)) => panic!("RUST_LOG contained non-unicode"),
Err(std::env::VarError::NotPresent) => {
FilterExt::boxed(
Targets::new()
.with_default(LevelFilter::INFO)
.with_target("axum::rejection", LevelFilter::TRACE),
)
},
}
}