use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventKind {
OpStart,
OpEnd,
OpYield,
Annotation,
GroupStart,
GroupEnd,
LlmUsage,
MediaRef,
}
impl EventKind {
pub fn as_str(&self) -> &'static str {
match self {
EventKind::OpStart => "op_start",
EventKind::OpEnd => "op_end",
EventKind::OpYield => "op_yield",
EventKind::Annotation => "annotation",
EventKind::GroupStart => "group_start",
EventKind::GroupEnd => "group_end",
EventKind::LlmUsage => "llm_usage",
EventKind::MediaRef => "media_ref",
}
}
}
pub type EventCtx = Vec<String>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceEvent {
pub event_id: String,
pub request_id: String,
pub kind: EventKind,
#[serde(default)]
pub op_name: Option<String>,
#[serde(default)]
pub ctx: EventCtx,
pub timestamp: DateTime<Utc>,
pub seq: u64,
#[serde(default)]
pub payload: BTreeMap<String, Value>,
}
impl TraceEvent {
pub fn sort_key(&self) -> (DateTime<Utc>, u64) {
(self.timestamp, self.seq)
}
}
impl PartialEq for TraceEvent {
fn eq(&self, other: &Self) -> bool {
self.event_id == other.event_id
}
}
impl Eq for TraceEvent {}
impl PartialOrd for TraceEvent {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for TraceEvent {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.sort_key().cmp(&other.sort_key())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn evt(seq: u64, ts: DateTime<Utc>) -> TraceEvent {
TraceEvent {
event_id: format!("e{seq}"),
request_id: "req-1".into(),
kind: EventKind::OpStart,
op_name: Some("main.x".into()),
ctx: vec!["main".into()],
timestamp: ts,
seq,
payload: BTreeMap::new(),
}
}
#[test]
fn event_kind_strings_match_python() {
assert_eq!(EventKind::OpStart.as_str(), "op_start");
assert_eq!(EventKind::OpEnd.as_str(), "op_end");
assert_eq!(EventKind::OpYield.as_str(), "op_yield");
assert_eq!(EventKind::Annotation.as_str(), "annotation");
assert_eq!(EventKind::GroupStart.as_str(), "group_start");
assert_eq!(EventKind::GroupEnd.as_str(), "group_end");
assert_eq!(EventKind::LlmUsage.as_str(), "llm_usage");
assert_eq!(EventKind::MediaRef.as_str(), "media_ref");
}
#[test]
fn event_kind_json_serializes_as_snake_case_string() {
let s = serde_json::to_string(&EventKind::OpStart).unwrap();
assert_eq!(s, "\"op_start\"");
let s = serde_json::to_string(&EventKind::LlmUsage).unwrap();
assert_eq!(s, "\"llm_usage\"");
}
#[test]
fn ordering_uses_timestamp_then_seq() {
let t0 = Utc::now();
let t1 = t0 + chrono::Duration::milliseconds(1);
let a = evt(0, t0);
let b = evt(0, t1);
let c = evt(1, t0);
assert!(a < b);
assert!(a < c); assert!(c < b); }
#[test]
fn trace_event_round_trips_through_json() {
let mut payload = BTreeMap::new();
payload.insert("inputs".into(), json!({"x": 1}));
let e = TraceEvent {
event_id: "e1".into(),
request_id: "r1".into(),
kind: EventKind::OpStart,
op_name: Some("main.x".into()),
ctx: vec!["main".into()],
timestamp: Utc::now(),
seq: 0,
payload,
};
let s = serde_json::to_string(&e).unwrap();
let back: TraceEvent = serde_json::from_str(&s).unwrap();
assert_eq!(back.event_id, e.event_id);
assert_eq!(back.kind, EventKind::OpStart);
assert_eq!(back.payload.get("inputs"), e.payload.get("inputs"));
}
}