dinio_sdk/models/stats.rs
1//! Statistics from Relay telemetry ([`Stats`](crate::api::Stats)). Relay reports in
2//! 10-minute windows. Unlike the rest of the API, every time here is in **milliseconds**
3//! since the epoch; only `step` is in seconds.
4
5use std::collections::BTreeMap;
6
7use serde::{Deserialize, Serialize};
8
9/// A chart-ready set of series over one time range (zone queries, client events and
10/// rcodes).
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct StatsSeriesResult {
13 /// Start of the range, ms, aligned to 10 minutes.
14 pub from: i64,
15 /// End of the range, ms, exclusive.
16 pub to: i64,
17 /// Step length in seconds (600 = one Relay window).
18 pub step: u32,
19 /// What point values are: `count` per step, `rate` per second, or `share` of all
20 /// series at that step (0–1).
21 pub mode: String,
22 /// Sum of every series over the range (a count, whatever the mode).
23 pub total: f64,
24 /// `total` per second of the range.
25 pub rate_per_s: f64,
26 /// With `compare`: `total` of the preceding range of equal length.
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub previous_total: Option<f64>,
29 /// With `compare`: `(total - previous_total) / previous_total` (`0.25` = +25 %);
30 /// `None` also when `previous_total` is 0.
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub change: Option<f64>,
33 /// Busiest first. Beyond `limit` the rest is merged into key `__other`.
34 pub series: Vec<StatsSeries>,
35}
36
37/// One series of a [`StatsSeriesResult`].
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct StatsSeries {
40 /// Raw key, e.g. rcode `0`, a qtype, a client identity uuid; `__other` for the merged
41 /// rest, `__nxdomain` for NXDOMAIN in a zone's qtype/geo/name grouping.
42 pub key: String,
43 /// Readable name, e.g. `NOERROR`, `AAAA`, the client's `identify`.
44 pub label: String,
45 /// Sum over the range (a count, whatever the mode).
46 pub total: f64,
47 /// Part of the result's `total` (0–1).
48 pub share: f64,
49 /// `total` per second of the range.
50 pub rate_per_s: f64,
51 /// The busiest step; `None` without data.
52 #[serde(default)]
53 pub peak: Option<StatsPeak>,
54 /// With `compare`, as on the result.
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub previous_total: Option<f64>,
57 /// With `compare`, as on the result.
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub change: Option<f64>,
60 /// `(t, value)` per step: `t` the start of the step in ms, `value` per `mode`. A step
61 /// without data is `0` (with `fill`, the default) or absent.
62 pub points: Vec<(i64, f64)>,
63}
64
65/// The busiest step of a [`StatsSeries`].
66#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
67pub struct StatsPeak {
68 /// Start of the step, ms.
69 pub t: i64,
70 /// Count in that step.
71 pub v: f64,
72}
73
74/// NXDOMAIN answers under a zone ([`Stats::zone_nx`](crate::api::Stats::zone_nx)).
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub struct ZoneNxStats {
77 /// NXDOMAIN answers over time (one series, `all`).
78 pub count: StatsSeriesResult,
79 /// `(t ms, n)`: distinct non-existent names per step, the highest per-Relay estimate,
80 /// so a lower bound. Never add these up.
81 pub unique_max: Vec<(i64, f64)>,
82}
83
84/// A top list (non-existent names of a zone, names blocked for clients).
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct StatsTopList {
87 /// ms.
88 pub from: i64,
89 /// ms, exclusive.
90 pub to: i64,
91 /// Most frequent first.
92 pub items: Vec<StatsTopItem>,
93}
94
95/// One entry of a [`StatsTopList`]. Counts are estimates summed over Relays and windows.
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97pub struct StatsTopItem {
98 /// The name.
99 pub key: String,
100 /// Upper bound of the count.
101 pub count: f64,
102 /// Lower bound of the count.
103 pub count_min: f64,
104 /// How many Relays reported it.
105 pub relays: u32,
106}
107
108/// A zone's busiest names ([`Stats::zone_names`](crate::api::Stats::zone_names)).
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub struct ZoneNamesStats {
111 /// ms.
112 pub from: i64,
113 /// ms, exclusive.
114 pub to: i64,
115 /// Most queried first.
116 pub items: Vec<ZoneNameStats>,
117}
118
119/// One name of [`ZoneNamesStats`].
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121pub struct ZoneNameStats {
122 /// The queried name.
123 pub name: String,
124 /// Queries over the range.
125 pub queries: f64,
126 /// Queries by rcode label (`NOERROR`, `SERVFAIL`, …).
127 pub rcodes: BTreeMap<String, f64>,
128 /// Queries by qtype label (`A`, `AAAA`, …).
129 pub qtypes: BTreeMap<String, f64>,
130 /// Part of the queries answered with anything but NOERROR (0–1).
131 pub error_share: f64,
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn series_result_deserializes() {
140 let result: StatsSeriesResult = serde_json::from_value(serde_json::json!({
141 "from": 1_793_088_000_000_i64, "to": 1_793_174_400_000_i64, "step": 3600,
142 "mode": "count", "total": 120, "rate_per_s": 0.0014,
143 "previous_total": 0, "change": null,
144 "series": [{
145 "key": "0", "label": "NOERROR", "total": 120, "share": 1, "rate_per_s": 0.0014,
146 "peak": { "t": 1_793_088_000_000_i64, "v": 40 },
147 "points": [[1_793_088_000_000_i64, 40], [1_793_091_600_000_i64, 0]]
148 }, {
149 "key": "__other", "label": "Other", "total": 0, "share": 0, "rate_per_s": 0,
150 "peak": null, "points": []
151 }]
152 }))
153 .unwrap();
154 assert_eq!(result.change, None);
155 assert_eq!(result.series[0].points[0], (1_793_088_000_000, 40.0));
156 assert_eq!(result.series[1].peak, None);
157 }
158}