ai-agents-observability 1.0.0-rc.15

Observability and tracing for AI Agents framework
Documentation
use crate::aggregator::{AggregatedMetrics, aggregate_events, latency_stats};
use crate::config::AggregationDimension;
use crate::event::{EventStatus, EventType, ObservationEvent, TokenUsageSource};
use serde::{Deserialize, Serialize};

/// Cost totals across the current report window.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CostBreakdown {
    pub total_usd: f64,
    pub priced_events: u64,
    pub unpriced_events: u64,
}

/// Token totals across the current report window.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TokenBreakdown {
    pub total_input: u64,
    pub total_output: u64,
    pub total_tokens: u64,
    pub estimated_events: u64,
    pub missing_events: u64,
}

/// User-facing report built from the rolling observation window.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObservabilityReport {
    pub summary: ReportSummary,
    pub configured: Vec<AggregatedMetrics>,
    pub by_model: Vec<AggregatedMetrics>,
    pub by_purpose: Vec<AggregatedMetrics>,
    pub by_language: Vec<AggregatedMetrics>,
    pub by_state: Vec<AggregatedMetrics>,
    pub by_agent: Vec<AggregatedMetrics>,
    pub by_orchestration_pattern: Vec<AggregatedMetrics>,
    pub cost_breakdown: CostBreakdown,
    pub token_breakdown: TokenBreakdown,
    pub dropped_events: u64,
}

/// Top-level totals for quick inspection.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReportSummary {
    pub total_events: u64,
    pub total_llm_calls: u64,
    pub total_tool_calls: u64,
    pub total_errors: u64,
    pub total_tokens: u64,
    pub total_cost_usd: f64,
    pub avg_latency_ms: f64,
    pub p50_latency_ms: u64,
    pub p90_latency_ms: u64,
    pub p99_latency_ms: u64,
    pub router_calls: u64,
    pub router_cost_usd: f64,
    pub main_llm_calls: u64,
    pub main_llm_cost_usd: f64,
    pub unpriced_events: u64,
    pub estimated_token_events: u64,
}

/// Builds fixed and configured report tables from already redacted events.
pub fn generate_report(
    events: &[ObservationEvent],
    configured: Vec<AggregatedMetrics>,
    dropped_events: u64,
) -> ObservabilityReport {
    let latency_values: Vec<u64> = events.iter().map(|event| event.duration_ms).collect();
    let latency = latency_stats(&latency_values);
    let mut summary = ReportSummary {
        total_events: events.len() as u64,
        avg_latency_ms: latency.avg_ms,
        p50_latency_ms: latency.p50_ms,
        p90_latency_ms: latency.p90_ms,
        p99_latency_ms: latency.p99_ms,
        ..ReportSummary::default()
    };
    let mut token_breakdown = TokenBreakdown::default();
    let mut cost_breakdown = CostBreakdown::default();

    for event in events {
        if matches!(event.status, EventStatus::Error) {
            summary.total_errors += 1;
        }
        match event.event_type {
            EventType::LlmCall { .. } => summary.total_llm_calls += 1,
            EventType::ToolCall { .. } => summary.total_tool_calls += 1,
            _ => {}
        }
        if let Some(tokens) = &event.tokens {
            token_breakdown.total_input += tokens.input_tokens;
            token_breakdown.total_output += tokens.output_tokens;
            token_breakdown.total_tokens += tokens.total_tokens;
            if matches!(tokens.source, TokenUsageSource::Estimated) {
                token_breakdown.estimated_events += 1;
                summary.estimated_token_events += 1;
            }
        } else {
            token_breakdown.missing_events += 1;
        }
        if let Some(cost) = &event.cost {
            cost_breakdown.total_usd += cost.total_usd;
            cost_breakdown.priced_events += 1;
            if event.purpose.as_label() == "router" {
                summary.router_cost_usd += cost.total_usd;
            }
            if event.purpose.as_label() == "main_response" {
                summary.main_llm_cost_usd += cost.total_usd;
            }
        } else {
            cost_breakdown.unpriced_events += 1;
            summary.unpriced_events += 1;
        }
        if event.purpose.as_label() == "router" {
            summary.router_calls += 1;
        }
        if event.purpose.as_label() == "main_response" {
            summary.main_llm_calls += 1;
        }
    }

    summary.total_tokens = token_breakdown.total_tokens;
    summary.total_cost_usd = cost_breakdown.total_usd;

    ObservabilityReport {
        summary,
        configured,
        by_model: aggregate_events(events, &[AggregationDimension::Model]),
        by_purpose: aggregate_events(events, &[AggregationDimension::Purpose]),
        by_language: aggregate_events(events, &[AggregationDimension::Language]),
        by_state: aggregate_events(events, &[AggregationDimension::State]),
        by_agent: aggregate_events(events, &[AggregationDimension::Agent]),
        by_orchestration_pattern: aggregate_events(
            events,
            &[AggregationDimension::OrchestrationPattern],
        ),
        cost_breakdown,
        token_breakdown,
        dropped_events,
    }
}