Skip to main content

difflore_core/observability/
injection_log.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8struct InjectionEntry {
9    ts_ms: i64,
10    path: String,
11    rules_injected: usize,
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    file_path: Option<String>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18struct InjectionLog {
19    version: u32,
20    entries: Vec<InjectionEntry>,
21}
22
23#[derive(Debug, Clone, Default)]
24pub struct InjectionPathSummary {
25    pub count_24h: usize,
26    pub by_path: BTreeMap<String, usize>,
27    pub injected_by_path: BTreeMap<String, usize>,
28    pub total_rules_injected: usize,
29    pub path: Option<PathBuf>,
30    pub detail: Option<String>,
31}
32
33fn now_ms() -> i64 {
34    use std::time::{SystemTime, UNIX_EPOCH};
35    SystemTime::now()
36        .duration_since(UNIX_EPOCH)
37        .map_or(0, |d| d.as_millis() as i64)
38}
39
40fn log_path() -> Option<PathBuf> {
41    crate::paths::data_home()
42        .ok()
43        .map(|dir| dir.join("injection-paths.json"))
44}
45
46pub fn record(path_name: &str, rules_injected: usize, file_path: Option<&str>) {
47    let Some(path) = log_path() else {
48        return;
49    };
50    let cutoff = now_ms().saturating_sub(24 * 60 * 60 * 1000);
51    let mut log = std::fs::read_to_string(&path)
52        .ok()
53        .and_then(|raw| serde_json::from_str::<InjectionLog>(&raw).ok())
54        .unwrap_or(InjectionLog {
55            version: 1,
56            entries: Vec::new(),
57        });
58    log.entries.retain(|entry| entry.ts_ms >= cutoff);
59    log.entries.push(InjectionEntry {
60        ts_ms: now_ms(),
61        path: path_name.to_owned(),
62        rules_injected,
63        file_path: file_path.map(|p| {
64            if p.len() > 200 {
65                p.chars().take(200).collect()
66            } else {
67                p.to_owned()
68            }
69        }),
70    });
71    if log.entries.len() > 2_000 {
72        let keep_from = log.entries.len().saturating_sub(2_000);
73        log.entries = log.entries.split_off(keep_from);
74    }
75    if let Some(parent) = path.parent() {
76        let _ = std::fs::create_dir_all(parent);
77    }
78    if let Ok(json) = serde_json::to_string_pretty(&log) {
79        let _ = std::fs::write(path, json);
80    }
81}
82
83pub fn summary_24h() -> InjectionPathSummary {
84    let Some(path) = log_path() else {
85        return InjectionPathSummary {
86            detail: Some("could not resolve DIFFLORE_HOME".into()),
87            ..InjectionPathSummary::default()
88        };
89    };
90    let Ok(raw) = std::fs::read_to_string(&path) else {
91        return InjectionPathSummary {
92            path: Some(path),
93            detail: Some("no injection path log yet".into()),
94            ..InjectionPathSummary::default()
95        };
96    };
97    let log = match serde_json::from_str::<InjectionLog>(&raw) {
98        Ok(log) => log,
99        Err(e) => {
100            return InjectionPathSummary {
101                path: Some(path),
102                detail: Some(format!("injection path log is unreadable: {e}")),
103                ..InjectionPathSummary::default()
104            };
105        }
106    };
107    let cutoff = now_ms().saturating_sub(24 * 60 * 60 * 1000);
108    let mut summary = InjectionPathSummary {
109        path: Some(path),
110        ..InjectionPathSummary::default()
111    };
112    for entry in log
113        .entries
114        .into_iter()
115        .filter(|entry| entry.ts_ms >= cutoff)
116    {
117        summary.count_24h += 1;
118        *summary.by_path.entry(entry.path.clone()).or_insert(0) += 1;
119        if entry.rules_injected > 0 {
120            *summary.injected_by_path.entry(entry.path).or_insert(0) += 1;
121            summary.total_rules_injected += entry.rules_injected;
122        }
123    }
124    summary
125}