Skip to main content

kaizen/metrics/
quality.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Capture-quality metrics: field fill rates and trace correlation health.
3
4use crate::store::Store;
5use anyhow::Result;
6use serde::Serialize;
7
8#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
9pub struct CaptureQualityReport {
10    pub events_total: u64,
11    pub proxy_events: u64,
12    pub trace_spans_total: u64,
13    pub orphan_span_count: u64,
14    pub token_coverage_pct: u8,
15    pub cost_coverage_pct: u8,
16    pub latency_coverage_pct: u8,
17    pub context_coverage_pct: u8,
18    pub proxy_correlation_pct: u8,
19    pub cache_read_tokens: u64,
20    pub cache_creation_tokens: u64,
21}
22
23pub fn build_quality_report(
24    store: &Store,
25    workspace: &str,
26    start_ms: u64,
27    end_ms: u64,
28) -> Result<CaptureQualityReport> {
29    let rows = store.capture_quality_rows(workspace, start_ms, end_ms)?;
30    let spans = store.trace_span_quality_rows(workspace, start_ms, end_ms)?;
31    let proxy_events = rows.iter().filter(|r| r.source == "Proxy").count() as u64;
32    let correlated = proxy_events.min(spans.iter().filter(|r| r.kind == "llm").count() as u64);
33    Ok(CaptureQualityReport {
34        events_total: rows.len() as u64,
35        proxy_events,
36        trace_spans_total: spans.len() as u64,
37        orphan_span_count: spans.iter().filter(|r| r.is_orphan).count() as u64,
38        token_coverage_pct: pct(rows.len(), rows.iter().filter(|r| r.has_tokens).count()),
39        cost_coverage_pct: pct(rows.len(), rows.iter().filter(|r| r.has_cost).count()),
40        latency_coverage_pct: pct(rows.len(), rows.iter().filter(|r| r.has_latency).count()),
41        context_coverage_pct: pct(rows.len(), rows.iter().filter(|r| r.has_context).count()),
42        proxy_correlation_pct: pct(proxy_events as usize, correlated as usize),
43        cache_read_tokens: rows.iter().map(|r| r.cache_read_tokens).sum(),
44        cache_creation_tokens: rows.iter().map(|r| r.cache_creation_tokens).sum(),
45    })
46}
47
48fn pct(total: usize, good: usize) -> u8 {
49    if total == 0 {
50        return 0;
51    }
52    (((good * 100) + (total / 2)) / total).min(100) as u8
53}