warden/reports/
summary.rs1use crate::output::{Cell, Report, Table};
4use crate::store::Scanner;
5
6use super::{count, day_of, rollup, scan, ReportCtx, ReportError, Totals};
7
8pub fn build(scanner: &Scanner, ctx: &ReportCtx) -> Result<Report, ReportError> {
9 let scanned = scan(scanner, ctx)?;
10 let by_day = rollup(&scanned.events, &ctx.pricing, |event| day_of(event.ts));
11
12 let mut table = Table::new([
13 "day",
14 "sessions",
15 "requests",
16 "in",
17 "out",
18 "cache r",
19 "est. cost",
20 ]);
21 let mut rows = Vec::new();
22 let mut grand = Totals::default();
23
24 for (day, totals) in &by_day {
26 let mut row = vec![
27 Cell::text(day),
28 count(totals.sessions.len() as u64),
29 count(totals.requests),
30 ];
31 row.extend(totals.tail_cells());
32 table.push(row);
33
34 let mut json = serde_json::Map::new();
35 json.insert("day".into(), serde_json::json!(day));
36 json.insert("sessions".into(), serde_json::json!(totals.sessions.len()));
37 totals.write_json(&mut json);
38 rows.push(serde_json::Value::Object(json));
39
40 grand.merge(totals);
41 }
42
43 if !by_day.is_empty() {
44 let mut total = vec![
45 Cell::text("total"),
46 count(grand.sessions.len() as u64),
47 count(grand.requests),
48 ];
49 total.extend(grand.tail_cells());
50 table.push(total);
51 }
52
53 let mut notes = scanned.notes;
54 notes.push("the final row totals the period; daily session counts overlap when a session spans midnight, so they sum to more than the period total");
55 Ok(Report::new("summary", ctx.window, table)
56 .with_json_rows(rows)
57 .with_notes(notes.finish()))
58}
59
60#[cfg(test)]
61mod tests {
62 use super::super::testkit::*;
63 use super::*;
64 use crate::cli::TimeWindow;
65
66 fn report() -> Report {
67 let (_dir, paths) = store(&[
68 priced(used("a", ms(2026, 8, 3, 10), "acme", "m", 100, 20), 1.0),
69 priced(used("b", ms(2026, 8, 3, 11), "acme", "m", 50, 10), 0.5),
70 priced(used("c", ms(2026, 8, 4, 10), "acme", "m", 10, 2), 0.25),
71 ]);
72 build(
73 &Scanner::new(paths),
74 &ReportCtx::new(TimeWindow::all(), None, true),
75 )
76 .unwrap()
77 }
78
79 #[test]
80 fn breaks_the_period_down_by_day_in_order() {
81 let rendered = report().table.render(crate::output::Style::plain());
82 let lines: Vec<&str> = rendered.lines().collect();
83 assert!(lines[0].starts_with("DAY"));
84 assert!(lines[1].starts_with("2026-08-03"), "{rendered}");
85 assert!(lines[2].starts_with("2026-08-04"), "{rendered}");
86 assert!(lines[3].starts_with("total"), "{rendered}");
87 }
88
89 #[test]
90 fn the_total_row_sums_the_days() {
91 let report = report();
92 assert_eq!(report.json_rows.len(), 2);
93 assert_eq!(report.json_rows[0]["input_tok"], 150);
94 assert_eq!(report.json_rows[1]["input_tok"], 10);
95 assert_eq!(report.json_rows[0]["cost_est"], 1.5);
96 assert!(report
97 .table
98 .render(crate::output::Style::plain())
99 .contains("$1.75 ~"));
100 }
101}