use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
pub struct TraceExporter {
endpoint: String,
client: Option<reqwest::Client>,
timeout: Duration,
}
#[derive(Debug, thiserror::Error)]
pub enum TraceExporterError {
#[error("reqwest client build failed: {0}")]
ClientBuild(#[from] reqwest::Error),
}
impl TraceExporter {
pub fn new(
endpoint: impl Into<String>,
timeout: Duration,
) -> Result<Arc<Self>, TraceExporterError> {
let endpoint = endpoint.into();
let client = if endpoint.is_empty() {
None
} else {
Some(reqwest::Client::builder().timeout(timeout).build()?)
};
Ok(Arc::new(Self {
endpoint,
client,
timeout,
}))
}
pub fn disabled() -> Arc<Self> {
Arc::new(Self {
endpoint: String::new(),
client: None,
timeout: Duration::from_secs(5),
})
}
pub fn is_enabled(&self) -> bool {
!self.endpoint.is_empty() && self.client.is_some()
}
#[allow(clippy::too_many_arguments)]
pub fn emit(
self: &Arc<Self>,
span_name: &'static str,
trace_id: nodedb_types::TraceId,
start: SystemTime,
end: SystemTime,
tenant_id: u64,
vshard_id: u32,
status_ok: bool,
) {
if !self.is_enabled() {
return;
}
let endpoint = self.endpoint.clone();
let Some(client) = self.client.clone() else {
return;
};
let timeout = self.timeout;
let start_ns = system_time_to_unix_nanos(start);
let end_ns = system_time_to_unix_nanos(end);
tokio::spawn(async move {
crate::control::otel::exporter::export_span(
&client,
timeout,
&crate::control::otel::exporter::SpanExport {
endpoint: &endpoint,
trace_id,
span_name,
start_ns,
end_ns,
tenant_id,
vshard_id,
status_ok,
},
)
.await;
});
}
}
fn system_time_to_unix_nanos(t: SystemTime) -> u64 {
t.duration_since(UNIX_EPOCH)
.map(|d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_is_noop() {
let exp = TraceExporter::disabled();
assert!(!exp.is_enabled());
let now = SystemTime::now();
exp.emit("noop", nodedb_types::TraceId::ZERO, now, now, 0, 0, true);
}
#[test]
fn endpoint_controls_enabled_flag() {
let on = TraceExporter::new("http://collector:4318", Duration::from_secs(1)).unwrap();
let off = TraceExporter::new(String::new(), Duration::from_secs(1)).unwrap();
assert!(on.is_enabled());
assert!(!off.is_enabled());
}
}