lc_core/observability/
mod.rs1mod agent_metrics;
11mod error;
12
13pub use agent_metrics::AgentMetrics;
14pub use error::ObsError;
15
16use crate::language_models::TokenUsage;
17use serde::Serialize;
18
19#[derive(Debug, Clone, Serialize)]
21#[serde(tag = "kind", rename_all = "snake_case")]
22pub enum ObsEvent {
23 TokenUsage(TokenUsage),
25 AgentMetrics(AgentMetrics),
27 Cost(CostEvent),
29}
30
31#[derive(Debug, Clone, Serialize)]
33pub struct CostEvent {
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub scope: Option<String>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub provider: Option<String>,
40 pub model: String,
42 pub prompt_tokens: usize,
44 pub completion_tokens: usize,
46 pub cost_usd: f64,
48}
49
50#[async_trait::async_trait]
53pub trait MetricsSink: Send + Sync {
54 async fn export(&self, event: &ObsEvent) -> Result<(), ObsError>;
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use std::time::Duration;
63
64 fn sample_usage() -> TokenUsage {
65 TokenUsage {
66 prompt_tokens: 10,
67 completion_tokens: 5,
68 total_tokens: 15,
69 }
70 }
71
72 #[test]
73 fn obs_event_token_usage_serializes_with_kind_tag() {
74 let json = serde_json::to_string(&ObsEvent::TokenUsage(sample_usage())).unwrap();
75 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
76 assert_eq!(v["kind"], "token_usage");
77 assert_eq!(v["prompt_tokens"], 10);
78 assert_eq!(v["completion_tokens"], 5);
79 assert_eq!(v["total_tokens"], 15);
80 }
81
82 #[test]
83 fn obs_event_agent_metrics_serializes_with_kind_tag() {
84 let m = AgentMetrics {
85 trace_id: Some("trace-x".to_string()),
86 llm_calls: 2,
87 cache_hits: 0,
88 tool_calls: 1,
89 compactions: 1,
90 total_tokens: Some(30),
91 duration: Duration::from_millis(100),
92 };
93 let json = serde_json::to_string(&ObsEvent::AgentMetrics(m)).unwrap();
94 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
95 assert_eq!(v["kind"], "agent_metrics");
96 assert_eq!(v["llm_calls"], 2);
97 assert_eq!(v["tool_calls"], 1);
98 assert_eq!(v["total_tokens"], 30);
99 }
100
101 #[test]
102 fn obs_event_cost_serializes_with_kind_tag() {
103 let json = serde_json::to_string(&ObsEvent::Cost(CostEvent {
104 scope: Some("run-7".to_string()),
105 provider: Some("openai".to_string()),
106 model: "gpt-4o-mini".to_string(),
107 prompt_tokens: 1000,
108 completion_tokens: 1000,
109 cost_usd: 0.75,
110 }))
111 .unwrap();
112 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
113 assert_eq!(v["kind"], "cost");
114 assert_eq!(v["scope"], "run-7");
115 assert_eq!(v["provider"], "openai");
116 assert_eq!(v["model"], "gpt-4o-mini");
117 assert_eq!(v["cost_usd"], 0.75);
118 }
119}