1use crate::aggregator::{AggregatedMetrics, aggregate_events, latency_stats};
2use crate::config::AggregationDimension;
3use crate::event::{EventStatus, EventType, ObservationEvent, TokenUsageSource};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct CostBreakdown {
9 pub total_usd: f64,
10 pub priced_events: u64,
11 pub unpriced_events: u64,
12}
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct TokenBreakdown {
17 pub total_input: u64,
18 pub total_output: u64,
19 pub total_tokens: u64,
20 pub estimated_events: u64,
21 pub missing_events: u64,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ObservabilityReport {
27 pub summary: ReportSummary,
28 pub configured: Vec<AggregatedMetrics>,
29 pub by_model: Vec<AggregatedMetrics>,
30 pub by_purpose: Vec<AggregatedMetrics>,
31 pub by_language: Vec<AggregatedMetrics>,
32 pub by_state: Vec<AggregatedMetrics>,
33 pub by_agent: Vec<AggregatedMetrics>,
34 pub by_orchestration_pattern: Vec<AggregatedMetrics>,
35 pub cost_breakdown: CostBreakdown,
36 pub token_breakdown: TokenBreakdown,
37 pub dropped_events: u64,
38}
39
40#[derive(Debug, Clone, Default, Serialize, Deserialize)]
42pub struct ReportSummary {
43 pub total_events: u64,
44 pub total_llm_calls: u64,
45 pub total_tool_calls: u64,
46 pub total_errors: u64,
47 pub total_tokens: u64,
48 pub total_cost_usd: f64,
49 pub avg_latency_ms: f64,
50 pub p50_latency_ms: u64,
51 pub p90_latency_ms: u64,
52 pub p99_latency_ms: u64,
53 pub router_calls: u64,
54 pub router_cost_usd: f64,
55 pub main_llm_calls: u64,
56 pub main_llm_cost_usd: f64,
57 pub unpriced_events: u64,
58 pub estimated_token_events: u64,
59}
60
61pub fn generate_report(
63 events: &[ObservationEvent],
64 configured: Vec<AggregatedMetrics>,
65 dropped_events: u64,
66) -> ObservabilityReport {
67 let latency_values: Vec<u64> = events.iter().map(|event| event.duration_ms).collect();
68 let latency = latency_stats(&latency_values);
69 let mut summary = ReportSummary {
70 total_events: events.len() as u64,
71 avg_latency_ms: latency.avg_ms,
72 p50_latency_ms: latency.p50_ms,
73 p90_latency_ms: latency.p90_ms,
74 p99_latency_ms: latency.p99_ms,
75 ..ReportSummary::default()
76 };
77 let mut token_breakdown = TokenBreakdown::default();
78 let mut cost_breakdown = CostBreakdown::default();
79
80 for event in events {
81 if matches!(event.status, EventStatus::Error) {
82 summary.total_errors += 1;
83 }
84 match event.event_type {
85 EventType::LlmCall { .. } => summary.total_llm_calls += 1,
86 EventType::ToolCall { .. } => summary.total_tool_calls += 1,
87 _ => {}
88 }
89 if let Some(tokens) = &event.tokens {
90 token_breakdown.total_input += tokens.input_tokens;
91 token_breakdown.total_output += tokens.output_tokens;
92 token_breakdown.total_tokens += tokens.total_tokens;
93 if matches!(tokens.source, TokenUsageSource::Estimated) {
94 token_breakdown.estimated_events += 1;
95 summary.estimated_token_events += 1;
96 }
97 } else {
98 token_breakdown.missing_events += 1;
99 }
100 if let Some(cost) = &event.cost {
101 cost_breakdown.total_usd += cost.total_usd;
102 cost_breakdown.priced_events += 1;
103 if event.purpose.as_label() == "router" {
104 summary.router_cost_usd += cost.total_usd;
105 }
106 if event.purpose.as_label() == "main_response" {
107 summary.main_llm_cost_usd += cost.total_usd;
108 }
109 } else {
110 cost_breakdown.unpriced_events += 1;
111 summary.unpriced_events += 1;
112 }
113 if event.purpose.as_label() == "router" {
114 summary.router_calls += 1;
115 }
116 if event.purpose.as_label() == "main_response" {
117 summary.main_llm_calls += 1;
118 }
119 }
120
121 summary.total_tokens = token_breakdown.total_tokens;
122 summary.total_cost_usd = cost_breakdown.total_usd;
123
124 ObservabilityReport {
125 summary,
126 configured,
127 by_model: aggregate_events(events, &[AggregationDimension::Model]),
128 by_purpose: aggregate_events(events, &[AggregationDimension::Purpose]),
129 by_language: aggregate_events(events, &[AggregationDimension::Language]),
130 by_state: aggregate_events(events, &[AggregationDimension::State]),
131 by_agent: aggregate_events(events, &[AggregationDimension::Agent]),
132 by_orchestration_pattern: aggregate_events(
133 events,
134 &[AggregationDimension::OrchestrationPattern],
135 ),
136 cost_breakdown,
137 token_breakdown,
138 dropped_events,
139 }
140}