Skip to main content

phi_telemetry/
storage.rs

1//! Metrics persistence — save, load, and list session metrics from disk.
2
3use std::path::Path;
4
5use anyhow::{Context, Result};
6
7use crate::types::{SessionMetrics, SessionSummary};
8
9/// Metrics file name within a session directory.
10const METRICS_FILE: &str = "session_metrics.json";
11
12/// Incrementally write session metrics to `session_metrics.json`.
13/// Overwrites the file on each call — safe for per-turn writes.
14pub fn save_metrics(metrics: &SessionMetrics, session_dir: &Path) -> Result<()> {
15    let path = session_dir.join(METRICS_FILE);
16    let json = serde_json::to_string_pretty(metrics)?;
17    std::fs::write(&path, json)?;
18    tracing::debug!(path = %path.display(), turns = metrics.total_turns, "session_metrics saved");
19    Ok(())
20}
21
22/// Load session metrics from `session_metrics.json` in a session directory.
23pub fn load_metrics(session_dir: &Path) -> Result<SessionMetrics> {
24    let path = session_dir.join(METRICS_FILE);
25    let content = std::fs::read_to_string(&path)
26        .with_context(|| format!("Failed to read metrics file: {}", path.display()))?;
27    let metrics: SessionMetrics = serde_json::from_str(&content)
28        .with_context(|| format!("Failed to parse metrics file: {}", path.display()))?;
29    Ok(metrics)
30}
31
32/// Try to load session metrics, returning `None` if the file doesn't exist.
33pub fn try_load_metrics(session_dir: &Path) -> Option<SessionMetrics> {
34    load_metrics(session_dir).ok()
35}
36
37/// List all session summaries by scanning the sessions directory.
38/// Reads each `session_metrics.json` and extracts summary fields.
39pub fn list_all_metrics(base_dir: &Path) -> Result<Vec<SessionSummary>> {
40    let sessions_dir = base_dir.join("sessions");
41    if !sessions_dir.exists() {
42        return Ok(Vec::new());
43    }
44
45    let mut summaries = Vec::new();
46    for entry in std::fs::read_dir(&sessions_dir)? {
47        let entry = entry?;
48        let path = entry.path();
49        if !path.is_dir() {
50            continue;
51        }
52
53        if let Some(metrics) = try_load_metrics(&path) {
54            let product = metrics
55                .custom
56                .get("product")
57                .and_then(|v| v.as_str())
58                .map(|s| s.to_string());
59
60            summaries.push(SessionSummary {
61                session_id: metrics.session_id,
62                node_id: metrics.node_id,
63                created_at: metrics.created_at,
64                model: metrics.model,
65                total_turns: metrics.total_turns,
66                total_chars: metrics.total_chars,
67                outcome: metrics.outcome,
68                product,
69            });
70        }
71    }
72
73    // Sort by created_at descending (newest first)
74    summaries.sort_by(|a, b| b.created_at.cmp(&a.created_at));
75    Ok(summaries)
76}