use chrono::Utc;
use std::collections::HashMap;
use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::OnceLock;
use std::time::Instant;
use tracing::field::Field;
use tracing::field::Visit;
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::Layer;
use uuid::Uuid;
use crate::Client;
use crate::MetricPoint;
pub const SPAN_DURATION_METRIC: &str = "span_duration";
const BATCH_SIZE: usize = 50;
const FLUSH_INTERVAL_MS: u64 = 500;
const LABEL_FIELDS: &[&str] = &[
"session_id",
"task_id",
"model",
"model_name",
"provider",
"agent_role",
"tool_name",
"status",
];
fn trace_id_store() -> &'static Mutex<HashMap<u64, Uuid>> {
static STORE: OnceLock<Mutex<HashMap<u64, Uuid>>> = OnceLock::new();
STORE.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn current_trace_id() -> Option<Uuid> {
let span = tracing::Span::current();
let id = span.id()?;
let key = id.into_u64();
trace_id_store().lock().ok()?.get(&key).copied()
}
#[derive(Clone)]
pub struct XtraceLayer {
inner: Arc<XtraceLayerInner>,
}
struct XtraceLayerInner {
tx: mpsc::SyncSender<MetricPoint>,
span_records: Mutex<Vec<SpanRecord>>,
}
struct SpanRecord {
span_id: u64,
created_at: Instant,
name: String,
}
impl XtraceLayer {
pub fn new(client: Client) -> Self {
let (tx, rx) = mpsc::sync_channel(1000);
let inner = Arc::new(XtraceLayerInner {
tx,
span_records: Mutex::new(Vec::new()),
});
let client = client.clone();
std::thread::spawn(move || {
let rt = match tokio::runtime::Runtime::new() {
Ok(r) => r,
Err(_) => return,
};
let mut batch = Vec::with_capacity(BATCH_SIZE);
let mut last_flush = Instant::now();
let flush_interval = std::time::Duration::from_millis(FLUSH_INTERVAL_MS);
loop {
match rx.recv_timeout(flush_interval) {
Ok(point) => {
batch.push(point);
if batch.len() >= BATCH_SIZE {
flush_batch(&rt, &client, &mut batch);
last_flush = Instant::now();
}
}
Err(mpsc::RecvTimeoutError::Timeout) => {
if !batch.is_empty() && last_flush.elapsed() >= flush_interval {
flush_batch(&rt, &client, &mut batch);
last_flush = Instant::now();
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
if !batch.is_empty() {
flush_batch(&rt, &client, &mut batch);
}
});
Self { inner }
}
fn try_send(&self, point: MetricPoint) {
let _ = self.inner.tx.try_send(point);
}
}
fn flush_batch(
rt: &tokio::runtime::Runtime,
client: &Client,
batch: &mut Vec<MetricPoint>,
) {
if batch.is_empty() {
return;
}
let points = std::mem::take(batch);
let client = client.clone();
rt.block_on(async move {
let _ = client.push_metrics(&points).await;
});
}
impl<S> Layer<S> for XtraceLayer
where
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
{
fn on_new_span(
&self,
attrs: &tracing::span::Attributes<'_>,
id: &tracing::Id,
ctx: Context<'_, S>,
) {
let name = attrs.metadata().name().to_string();
let key = id.clone().into_u64();
let mut tid_visitor = TraceIdVisitor::default();
attrs.record(&mut tid_visitor);
let trace_id = if let Some(explicit) = tid_visitor.trace_id {
explicit
} else {
let parent_id = if attrs.is_root() {
None
} else if let Some(parent) = attrs.parent() {
Some(parent.clone())
} else {
ctx.current_span().id().cloned()
};
let parent_tid = parent_id.and_then(|pid| {
trace_id_store()
.lock()
.ok()?
.get(&pid.into_u64())
.copied()
});
parent_tid.unwrap_or_else(Uuid::new_v4)
};
if let Ok(mut store) = trace_id_store().lock() {
store.insert(key, trace_id);
}
let mut guard = self.inner.span_records.lock().unwrap();
guard.push(SpanRecord {
span_id: key,
created_at: Instant::now(),
name,
});
}
fn on_event(&self, event: &tracing::Event<'_>, ctx: Context<'_, S>) {
let mut visitor = MetricEventVisitor::default();
event.record(&mut visitor);
if let Some(mut point) = visitor.into_metric_point() {
if let Some(tid) = ctx
.current_span()
.id()
.and_then(|sid| trace_id_store().lock().ok()?.get(&sid.into_u64()).copied())
{
point
.labels
.entry("trace_id".to_string())
.or_insert_with(|| tid.to_string());
}
self.try_send(point);
}
}
fn on_close(&self, id: tracing::Id, _ctx: Context<'_, S>) {
let key = id.into_u64();
let trace_id = trace_id_store().lock().ok().and_then(|mut s| s.remove(&key));
let (duration_secs, span_name) = {
let mut guard = self.inner.span_records.lock().unwrap();
let pos = guard.iter().position(|r| r.span_id == key);
if let Some(pos) = pos {
let rec = guard.remove(pos);
(rec.created_at.elapsed().as_secs_f64(), rec.name)
} else {
return;
}
};
let mut labels = HashMap::new();
labels.insert("span_name".to_string(), span_name);
if let Some(tid) = trace_id {
labels.insert("trace_id".to_string(), tid.to_string());
}
let point = MetricPoint {
name: SPAN_DURATION_METRIC.to_string(),
labels,
value: duration_secs,
timestamp: Utc::now(),
};
self.try_send(point);
}
}
#[derive(Default)]
struct TraceIdVisitor {
trace_id: Option<Uuid>,
}
impl Visit for TraceIdVisitor {
fn record_debug(&mut self, _field: &Field, _value: &dyn std::fmt::Debug) {}
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "trace_id" || field.name() == "xtrace.trace_id" {
if let Ok(id) = Uuid::parse_str(value) {
self.trace_id = Some(id);
}
}
}
}
#[derive(Default)]
struct MetricEventVisitor {
metric: Option<String>,
value: Option<f64>,
labels: HashMap<String, String>,
}
impl MetricEventVisitor {
fn into_metric_point(self) -> Option<MetricPoint> {
let metric = self.metric?;
let value = self.value.unwrap_or(0.0);
Some(MetricPoint {
name: metric,
labels: self.labels,
value,
timestamp: Utc::now(),
})
}
}
impl Visit for MetricEventVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if LABEL_FIELDS.contains(&field.name()) {
self.labels
.insert(field.name().to_string(), format!("{:?}", value));
}
}
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "metric" {
self.metric = Some(value.to_string());
} else if LABEL_FIELDS.contains(&field.name()) {
self.labels
.insert(field.name().to_string(), value.to_string());
}
}
fn record_f64(&mut self, field: &Field, value: f64) {
if field.name() == "value" {
self.value = Some(value);
} else if LABEL_FIELDS.contains(&field.name()) {
self.labels
.insert(field.name().to_string(), value.to_string());
}
}
fn record_i64(&mut self, field: &Field, value: i64) {
if field.name() == "value" {
self.value = Some(value as f64);
} else if LABEL_FIELDS.contains(&field.name()) {
self.labels
.insert(field.name().to_string(), value.to_string());
}
}
fn record_u64(&mut self, field: &Field, value: u64) {
if field.name() == "value" {
self.value = Some(value as f64);
} else if LABEL_FIELDS.contains(&field.name()) {
self.labels
.insert(field.name().to_string(), value.to_string());
}
}
}