har/analysis/
subsystems.rs1use crate::config::Config;
2use crate::filter::Filter;
3use crate::fingerprint::fingerprint;
4use crate::model::{Capture, Entry};
5use crate::render::human_ms;
6use ahash::{AHashMap, AHashSet};
7use serde::Serialize;
8
9#[derive(Debug, Serialize)]
10pub struct SubsystemsResult {
11 pub subsystems: Vec<SubsystemStat>,
12}
13
14#[derive(Debug, Serialize)]
15pub struct SubsystemStat {
16 pub name: String,
17 pub owner: Option<String>,
18 pub criticality: Option<String>,
19 pub count: usize,
20 pub hosts: Vec<String>,
21 pub error_count: usize,
22 pub duplicate_count: usize,
23 pub first_offset_ms: f64,
24 pub last_offset_ms: f64,
25}
26
27struct Acc<'a> {
28 owner: Option<String>,
29 criticality: Option<String>,
30 entries: Vec<&'a Entry>,
31 hosts: AHashSet<String>,
32}
33
34pub fn compute_subsystems(
36 cap: &Capture,
37 filter: &Filter,
38 config: &Config,
39 top: usize,
40) -> SubsystemsResult {
41 let mut by_name: AHashMap<String, Acc> = AHashMap::new();
42 for e in cap.entries.iter().filter(|e| filter.matches(e)) {
43 let sub = config.subsystem_for(e);
44 let acc = by_name.entry(sub.name.clone()).or_insert_with(|| Acc {
45 owner: sub.owner.clone(),
46 criticality: sub.criticality.clone(),
47 entries: Vec::new(),
48 hosts: AHashSet::new(),
49 });
50 acc.entries.push(e);
51 acc.hosts.insert(e.host.clone());
52 }
53
54 let mut subsystems: Vec<SubsystemStat> = by_name
55 .into_iter()
56 .map(|(name, acc)| subsystem_stat(name, acc))
57 .collect();
58
59 subsystems.sort_by(|a, b| b.count.cmp(&a.count).then(a.name.cmp(&b.name)));
60 subsystems.truncate(top);
61 SubsystemsResult { subsystems }
62}
63
64fn subsystem_stat(name: String, acc: Acc) -> SubsystemStat {
65 let mut fp_counts: AHashMap<String, usize> = AHashMap::new();
66 let mut error_count = 0usize;
67 let mut first = f64::MAX;
68 let mut last = f64::MIN;
69 for e in &acc.entries {
70 if e.is_error() {
71 error_count += 1;
72 }
73 first = first.min(e.started_offset_ms);
74 last = last.max(e.started_offset_ms);
75 *fp_counts.entry(fingerprint(e)).or_default() += 1;
76 }
77 let duplicate_count: usize = fp_counts.values().filter(|c| **c > 1).sum();
78 let mut hosts: Vec<String> = acc.hosts.into_iter().collect();
79 hosts.sort();
80
81 SubsystemStat {
82 name,
83 owner: acc.owner,
84 criticality: acc.criticality,
85 count: acc.entries.len(),
86 hosts,
87 error_count,
88 duplicate_count,
89 first_offset_ms: if first == f64::MAX { 0.0 } else { first },
90 last_offset_ms: if last == f64::MIN { 0.0 } else { last },
91 }
92}
93
94pub fn render_subsystems_text(r: &SubsystemsResult) -> String {
96 let mut out = String::new();
97 out.push_str("== wiretrail subsystems ==\n");
98 for s in &r.subsystems {
99 let owner = s.owner.as_deref().unwrap_or("-");
100 out.push_str(&format!(
101 "\n{} [{}] ({} req, {} err, {} dup)\n",
102 s.name, owner, s.count, s.error_count, s.duplicate_count
103 ));
104 out.push_str(&format!(
105 " window: {} - {}\n",
106 human_ms(s.first_offset_ms),
107 human_ms(s.last_offset_ms)
108 ));
109 out.push_str(&format!(" hosts: {}\n", s.hosts.join(", ")));
110 }
111 out
112}
113
114#[cfg(test)]
115mod tests {
116 use super::compute_subsystems;
117 use crate::config::Config;
118 use crate::filter::Filter;
119 use crate::model::{sample_capture, sample_entry};
120
121 fn cap() -> crate::model::Capture {
122 sample_capture(vec![
123 sample_entry(0, "api.github.com", "GET", "/repos/x", 200),
124 sample_entry(1, "raw.githubusercontent.com", "GET", "/y", 404),
125 sample_entry(2, "torii.nexioapp.org", "GET", "/manifest.json", 308),
126 ])
127 }
128
129 #[test]
130 fn groups_by_resolved_subsystem() {
131 let r = compute_subsystems(&cap(), &Filter::parse(&[]).unwrap(), &Config::default(), 10);
132 let gh = r.subsystems.iter().find(|s| s.name == "GitHub").unwrap();
133 assert_eq!(gh.count, 2);
135 assert_eq!(gh.error_count, 1);
136 assert_eq!(gh.hosts.len(), 2);
137 assert!(r.subsystems.iter().any(|s| s.name == "torii.nexioapp.org"));
139 }
140
141 #[test]
142 fn sorted_by_count_desc() {
143 let r = compute_subsystems(&cap(), &Filter::parse(&[]).unwrap(), &Config::default(), 10);
144 assert_eq!(r.subsystems[0].name, "GitHub");
145 }
146}