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),
}
#[derive(Debug, Clone, Copy)]
pub struct EmitSpanParams {
pub span_name: &'static str,
pub trace_id: nodedb_types::TraceId,
pub start: SystemTime,
pub end: SystemTime,
pub tenant_id: u64,
pub vshard_id: u32,
pub status_ok: bool,
}
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()
}
pub fn emit(self: &Arc<Self>, params: EmitSpanParams) {
let EmitSpanParams {
span_name,
trace_id,
start,
end,
tenant_id,
vshard_id,
status_ok,
} = params;
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(EmitSpanParams {
span_name: "noop",
trace_id: nodedb_types::TraceId::ZERO,
start: now,
end: now,
tenant_id: 0,
vshard_id: 0,
status_ok: 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());
}
}