Skip to main content

rust_web_server/traffic/
summary.rs

1//! The Traffic page's data: totals for a period and the one before it, a
2//! series (hourly up to 48 hours, daily beyond), and the top lists.
3
4use std::collections::BTreeMap;
5
6use super::record::{snapshot, Day, Top};
7
8fn quote(s: &str) -> String {
9    let mut out = String::with_capacity(s.len() + 2);
10    out.push('"');
11    for c in s.chars() {
12        match c {
13            '"' => out.push_str("\\\""),
14            '\\' => out.push_str("\\\\"),
15            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
16            c => out.push(c),
17        }
18    }
19    out.push('"');
20    out
21}
22
23fn pairs(top: &Top, n: usize, host: Option<&str>) -> String {
24    let mut filtered = Top::default();
25    for (key, count) in &top.0 {
26        if host.is_none_or(|h| key.starts_with(h) || key == "(other)") {
27            filtered.0.insert(key.clone(), *count);
28        }
29    }
30    let rows: Vec<String> = filtered.sorted(n).iter().map(|(k, v)| format!("[{},{v}]", quote(k))).collect();
31    format!("[{}]", rows.join(","))
32}
33
34/// The JSON for the last `hours` hours (1–720), optionally one host's.
35pub fn json(hours: u64, host: Option<&str>, now: u64) -> String {
36    let hours = hours.clamp(1, 24 * 90);
37    let (buckets, days) = snapshot(now);
38    let names = super::record::host_names();
39    let host_index = host.and_then(|h| names.iter().position(|n| n.eq_ignore_ascii_case(h))).map(|i| i as u8);
40    let wanted = |h: u8| host.is_none() || Some(h) == host_index;
41    let this_hour = now / 3600;
42    let start = this_hour + 1 - hours;
43    let previous_start = start.saturating_sub(hours);
44
45    let mut totals = [[0u64; 2]; 3];
46    let mut previous = [[0u64; 2]; 3];
47    let daily = hours > 48;
48    let mut series: BTreeMap<u64, [u64; 3]> = BTreeMap::new();
49    let mut first_hour = None;
50    for ((hour, h), counts) in &buckets {
51        if !wanted(*h) {
52            continue;
53        }
54        first_hour = Some(first_hour.map_or(*hour, |f: u64| f.min(*hour)));
55        let target = if *hour >= start && *hour <= this_hour {
56            Some(&mut totals)
57        } else if *hour >= previous_start && *hour < start {
58            Some(&mut previous)
59        } else {
60            None
61        };
62        if let Some(t) = target {
63            for g in 0..3 {
64                t[g][0] += counts[g].requests;
65                t[g][1] += counts[g].pages;
66            }
67        }
68        if *hour >= start && *hour <= this_hour {
69            let key = if daily { hour / 24 * 24 } else { *hour };
70            let point = series.entry(key).or_default();
71            for g in 0..3 {
72                point[g] += counts[g].requests;
73            }
74        }
75    }
76    // Every bucket of the period, empty ones included.
77    let step = if daily { 24 } else { 1 };
78    let mut t = if daily { start / 24 * 24 } else { start };
79    while t <= this_hour {
80        series.entry(t).or_default();
81        t += step;
82    }
83
84    let start_day = start / 24;
85    let previous_day = previous_start / 24;
86    let mut lists = Day::default();
87    let mut visitors = [0u64; 3];
88    let mut previous_visitors = [0u64; 3];
89    for (day, d) in &days {
90        if *day >= start_day {
91            lists.merge(d);
92            for g in 0..3 {
93                visitors[g] += d.visitors[g];
94            }
95        } else if *day >= previous_day {
96            for g in 0..3 {
97                previous_visitors[g] += d.visitors[g];
98            }
99        }
100    }
101
102    let group = |t: &[[u64; 2]; 3], v: &[u64; 3], g: usize| format!(r#"{{"requests":{},"pages":{},"visitors":{}}}"#, t[g][0], t[g][1], v[g]);
103    let groups = |t: &[[u64; 2]; 3], v: &[u64; 3]| format!(r#"{{"people":{},"ai":{},"bots":{}}}"#, group(t, v, 0), group(t, v, 1), group(t, v, 2));
104    let points: Vec<String> = series.iter().map(|(t, p)| format!(r#"{{"t":{},"people":{},"ai":{},"bots":{}}}"#, t * 3600, p[0], p[1], p[2])).collect();
105    let agents: Vec<String> = lists
106        .ai_agents
107        .sorted(25)
108        .iter()
109        .map(|(key, n)| {
110            let mut parts = key.split('\t');
111            let (name, operator, kind, verified) = (parts.next().unwrap_or(""), parts.next().unwrap_or(""), parts.next().unwrap_or(""), parts.next() == Some("true"));
112            format!(r#"{{"name":{},"operator":{},"kind":{},"verified":{verified},"requests":{n}}}"#, quote(name), quote(operator), quote(kind))
113        })
114        .collect();
115    let bots: Vec<String> = lists
116        .bots
117        .sorted(25)
118        .iter()
119        .map(|(key, n)| {
120            let (kind, name) = key.split_once('\t').unwrap_or((key, ""));
121            format!(r#"{{"kind":{},"name":{},"requests":{n}}}"#, quote(kind), quote(name))
122        })
123        .collect();
124    format!(
125        r#"{{"hours":{hours},"bucket":"{}","since":{},"hosts":[{}],"host":{},"totals":{},"previous":{},"series":[{}],"people_pages":{},"ai_pages":{},"ai_agents":[{}],"bots":[{}],"referrers":{},"not_found":{}}}"#,
126        if daily { "day" } else { "hour" },
127        first_hour.map_or("null".to_string(), |h| (h * 3600).to_string()),
128        names.iter().map(|n| quote(n)).collect::<Vec<_>>().join(","),
129        host.map_or("null".to_string(), quote),
130        groups(&totals, &visitors),
131        groups(&previous, &previous_visitors),
132        points.join(","),
133        pairs(&lists.people_pages, 25, host),
134        pairs(&lists.ai_pages, 25, host),
135        agents.join(","),
136        bots.join(","),
137        pairs(&lists.referrers, 25, None),
138        pairs(&lists.not_found, 25, host),
139    )
140}