har/analysis/
endpoints.rs1use crate::filter::Filter;
2use crate::model::{Capture, Entry};
3use ahash::{AHashMap, AHashSet};
4use serde::Serialize;
5use std::collections::BTreeMap;
6
7#[derive(Debug, Serialize)]
8pub struct EndpointsResult {
9 pub endpoints: Vec<EndpointStat>,
10}
11
12#[derive(Debug, Serialize)]
13pub struct EndpointStat {
14 pub host: String,
15 pub method: String,
16 pub norm_path: String,
17 pub count: usize,
18 pub statuses: BTreeMap<String, usize>,
19 pub content_types: Vec<String>,
20 pub sample_query_keys: Vec<String>,
21 pub error_count: usize,
22}
23
24struct Acc<'a> {
25 entries: Vec<&'a Entry>,
26}
27
28pub fn compute_endpoints(cap: &Capture, filter: &Filter, top: usize) -> EndpointsResult {
30 let mut by_key: AHashMap<(String, String, String), Acc> = AHashMap::new();
31 for e in cap.entries.iter().filter(|e| filter.matches(e)) {
32 let key = (
33 e.method.to_ascii_uppercase(),
34 e.host.clone(),
35 e.norm_path.clone(),
36 );
37 by_key
38 .entry(key)
39 .or_insert_with(|| Acc {
40 entries: Vec::new(),
41 })
42 .entries
43 .push(e);
44 }
45
46 let mut endpoints: Vec<EndpointStat> = by_key
47 .into_iter()
48 .map(|((method, host, norm_path), acc)| {
49 endpoint_stat(method, host, norm_path, &acc.entries)
50 })
51 .collect();
52
53 endpoints.sort_by(|a, b| {
54 b.count
55 .cmp(&a.count)
56 .then(a.host.cmp(&b.host))
57 .then(a.norm_path.cmp(&b.norm_path))
58 .then(a.method.cmp(&b.method))
59 });
60 endpoints.truncate(top);
61 EndpointsResult { endpoints }
62}
63
64fn endpoint_stat(
65 method: String,
66 host: String,
67 norm_path: String,
68 entries: &[&Entry],
69) -> EndpointStat {
70 let mut statuses: BTreeMap<String, usize> = BTreeMap::new();
71 let mut content_types: AHashSet<String> = AHashSet::new();
72 let mut query_keys: AHashSet<String> = AHashSet::new();
73 let mut error_count = 0usize;
74
75 for e in entries {
76 *statuses.entry(e.status.to_string()).or_default() += 1;
77 if let Some(ct) = &e.content_type {
78 content_types.insert(ct.clone());
79 }
80 for (k, _) in &e.query {
81 query_keys.insert(k.clone());
82 }
83 if e.is_error() {
84 error_count += 1;
85 }
86 }
87
88 let mut content_types: Vec<String> = content_types.into_iter().collect();
89 content_types.sort();
90 let mut sample_query_keys: Vec<String> = query_keys.into_iter().collect();
91 sample_query_keys.sort();
92
93 EndpointStat {
94 host,
95 method,
96 norm_path,
97 count: entries.len(),
98 statuses,
99 content_types,
100 sample_query_keys,
101 error_count,
102 }
103}
104
105pub fn render_endpoints_text(r: &EndpointsResult) -> String {
107 let mut out = String::new();
108 out.push_str("== wiretrail endpoints ==\n");
109 for e in &r.endpoints {
110 let statuses: Vec<String> = e.statuses.iter().map(|(s, c)| format!("{s}:{c}")).collect();
111 out.push_str(&format!(
112 "\n{:>4} {} {}{}\n",
113 e.count, e.method, e.host, e.norm_path
114 ));
115 out.push_str(&format!(" status: {}\n", statuses.join(" ")));
116 if !e.content_types.is_empty() {
117 out.push_str(&format!(
118 " content-types: {}\n",
119 e.content_types.join(", ")
120 ));
121 }
122 if !e.sample_query_keys.is_empty() {
123 out.push_str(&format!(
124 " query keys: {}\n",
125 e.sample_query_keys.join(", ")
126 ));
127 }
128 }
129 out
130}
131
132#[cfg(test)]
133mod tests {
134 use super::compute_endpoints;
135 use crate::filter::Filter;
136 use crate::model::{sample_capture, sample_entry};
137
138 fn cap() -> crate::model::Capture {
139 let mut e0 = sample_entry(0, "api.foo.com", "GET", "/v1/users/{id}", 200);
140 e0.query = vec![("page".into(), "1".into())];
141 let mut e1 = sample_entry(1, "api.foo.com", "GET", "/v1/users/{id}", 404);
142 e1.query = vec![("expand".into(), "true".into())];
143 let e2 = sample_entry(2, "api.foo.com", "POST", "/v1/users/{id}", 200);
144 sample_capture(vec![e0, e1, e2])
145 }
146
147 #[test]
148 fn groups_by_method_host_normpath() {
149 let r = compute_endpoints(&cap(), &Filter::parse(&[]).unwrap(), 10);
150 let get = r
152 .endpoints
153 .iter()
154 .find(|e| e.method == "GET" && e.norm_path == "/v1/users/{id}")
155 .unwrap();
156 assert_eq!(get.count, 2);
157 assert_eq!(get.statuses.get("200"), Some(&1));
158 assert_eq!(get.statuses.get("404"), Some(&1));
159 assert_eq!(get.error_count, 1);
160 assert_eq!(
162 get.sample_query_keys,
163 vec!["expand".to_string(), "page".to_string()]
164 );
165 assert!(r.endpoints.iter().any(|e| e.method == "POST"));
166 }
167
168 #[test]
169 fn sorted_by_count_desc() {
170 let r = compute_endpoints(&cap(), &Filter::parse(&[]).unwrap(), 10);
171 assert_eq!(r.endpoints[0].count, 2);
172 }
173}