oxicode_sdk/observability/
trace.rs1use parking_lot::RwLock;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::sync::Arc;
7use tokio::sync::broadcast;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct TraceId(u64);
14
15impl TraceId {
16 pub fn new() -> Self {
18 Self(fastrand::u64(1..))
19 }
20 pub fn zero() -> Self {
22 Self(0)
23 }
24}
25
26impl Default for TraceId {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl std::fmt::Display for TraceId {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 write!(f, "{:016x}", self.0)
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub struct SpanId(u64);
43
44impl SpanId {
45 pub fn new() -> Self {
47 Self(fastrand::u64(1..))
48 }
49 pub fn zero() -> Self {
51 Self(0)
52 }
53}
54
55impl Default for SpanId {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl std::fmt::Display for SpanId {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 write!(f, "{:016x}", self.0)
64 }
65}
66
67#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
71pub enum SpanKind {
72 Agent,
74 #[default]
76 Tool,
77 Llm,
79 Internal,
81}
82
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
87pub enum SpanStatus {
88 #[default]
90 Ok,
91 Error {
93 message: String,
95 },
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SpanContext {
103 pub trace_id: TraceId,
105 pub span_id: SpanId,
107 pub parent_span_id: Option<SpanId>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SpanEvent {
116 pub name: String,
118 pub timestamp_ms: u64,
120 #[serde(default)]
122 pub attributes: Vec<(String, serde_json::Value)>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct Span {
130 pub context: SpanContext,
132 pub name: String,
134 pub kind: SpanKind,
136 pub start_ms: u64,
138 pub end_ms: Option<u64>,
140 pub status: SpanStatus,
142 #[serde(default)]
144 pub attributes: HashMap<String, serde_json::Value>,
145 #[serde(default)]
147 pub events: Vec<SpanEvent>,
148 #[serde(default)]
150 pub links: Vec<SpanContext>,
151}
152
153impl Span {
154 pub fn duration_ms(&self) -> Option<u64> {
156 self.end_ms.map(|end| end.saturating_sub(self.start_ms))
157 }
158 pub fn is_complete(&self) -> bool {
160 self.end_ms.is_some()
161 }
162}
163
164#[derive(Debug)]
168pub struct Tracer {
169 spans: Arc<RwLock<Vec<Span>>>,
170 completed_tx: broadcast::Sender<Span>,
171}
172
173impl Tracer {
174 pub fn new() -> Self {
176 let (tx, _) = broadcast::channel(256);
177 Self {
178 spans: Arc::new(RwLock::new(Vec::new())),
179 completed_tx: tx,
180 }
181 }
182
183 pub fn start(self: &Arc<Self>, name: &str, kind: SpanKind) -> SpanGuard {
185 self.start_with_parent(name, kind, None)
186 }
187
188 pub fn start_with_parent(
190 self: &Arc<Self>,
191 name: &str,
192 kind: SpanKind,
193 parent: Option<&SpanContext>,
194 ) -> SpanGuard {
195 let trace_id = parent.map(|c| c.trace_id).unwrap_or_default();
196 let span_id = SpanId::new();
197 let context = SpanContext {
198 trace_id,
199 span_id,
200 parent_span_id: parent.map(|c| c.span_id),
201 };
202 let span = Span {
203 context,
204 name: name.to_string(),
205 kind,
206 start_ms: now_ms(),
207 end_ms: None,
208 status: SpanStatus::Ok,
209 attributes: HashMap::new(),
210 events: Vec::new(),
211 links: Vec::new(),
212 };
213 SpanGuard {
214 tracer: Arc::clone(self),
215 span,
216 }
217 }
218
219 fn record(&self, span: Span) {
220 self.spans.write().push(span.clone());
221 let _ = self.completed_tx.send(span);
222 }
223
224 pub fn trace(&self, trace_id: TraceId) -> Vec<Span> {
226 self.spans
227 .read()
228 .iter()
229 .filter(|s| s.context.trace_id == trace_id)
230 .cloned()
231 .collect()
232 }
233
234 pub fn subscribe(&self) -> broadcast::Receiver<Span> {
236 self.completed_tx.subscribe()
237 }
238}
239
240impl Clone for Tracer {
241 fn clone(&self) -> Self {
242 Self {
243 spans: Arc::clone(&self.spans),
244 completed_tx: self.completed_tx.clone(),
245 }
246 }
247}
248
249impl Default for Tracer {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255pub struct SpanGuard {
259 tracer: Arc<Tracer>,
260 span: Span,
261}
262
263impl SpanGuard {
264 pub fn context(&self) -> &SpanContext {
266 &self.span.context
267 }
268
269 pub fn trace_id(&self) -> TraceId {
271 self.span.context.trace_id
272 }
273
274 pub fn span_id(&self) -> SpanId {
276 self.span.context.span_id
277 }
278
279 pub fn set_attribute(&mut self, key: &str, value: serde_json::Value) {
281 self.span.attributes.insert(key.to_string(), value);
282 }
283
284 pub fn add_event(&mut self, name: &str) {
286 self.span.events.push(SpanEvent {
287 name: name.to_string(),
288 timestamp_ms: now_ms(),
289 attributes: vec![],
290 });
291 }
292
293 pub fn set_error(&mut self, message: &str) {
295 self.span.status = SpanStatus::Error {
296 message: message.to_string(),
297 };
298 }
299}
300
301impl Drop for SpanGuard {
302 fn drop(&mut self) {
303 let mut span = self.span.clone();
304 span.end_ms = Some(now_ms());
305 self.tracer.record(span);
306 }
307}
308
309fn now_ms() -> u64 {
312 std::time::SystemTime::now()
313 .duration_since(std::time::UNIX_EPOCH)
314 .map(|d| d.as_millis() as u64)
315 .unwrap_or(0)
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 fn assert_send_static<T: Send + 'static>() {}
323
324 #[test]
325 fn span_guard_is_send_and_static() {
326 assert_send_static::<SpanGuard>();
327 }
328
329 #[tokio::test]
330 async fn smoke() {
331 let tracer = Arc::new(Tracer::new());
332 let guard = tracer.start("s", SpanKind::Agent);
333 let tid = guard.trace_id();
334 drop(guard);
335 let spans = tracer.trace(tid);
336 assert!(!spans.is_empty());
337 assert_eq!(spans[0].name, "s");
338 assert!(spans[0].is_complete());
339 }
340
341 #[tokio::test]
342 async fn child_span() {
343 let tracer = Arc::new(Tracer::new());
344 let parent = tracer.start("parent", SpanKind::Agent);
345 let parent_ctx = parent.context().clone();
346 drop(parent);
347 let child = tracer.start_with_parent("child", SpanKind::Tool, Some(&parent_ctx));
348 let tid = child.trace_id();
349 drop(child);
350 let spans = tracer.trace(tid);
351 assert_eq!(spans.len(), 2);
352 }
353}