Skip to main content

ai_agents_observability/
report.rs

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/// Cost totals across the current report window.
7#[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/// Token totals across the current report window.
15#[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/// User-facing report built from the rolling observation window.
25#[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/// Top-level totals for quick inspection.
41#[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
61/// Builds fixed and configured report tables from already redacted events.
62pub 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}