1use std::sync::atomic::{AtomicU64, Ordering};
10
11pub use chio_metrics_spec::{
12 CHIO_GUARD_EVALUATIONS_TOTAL, CHIO_KERNEL_DECISION_LATENCY_SECONDS,
13 DECISION_LATENCY_BUCKETS_SECONDS,
14};
15
16pub const GUARD_LABEL_HTTP_AUTHORITY: &str = "http_authority";
17
18pub const GUARD_OUTCOME_ALLOW: &str = "allow";
19pub const GUARD_OUTCOME_DENY: &str = "deny";
20pub const GUARD_OUTCOME_ERROR: &str = "error";
21
22static GUARD_EVAL_ALLOW: AtomicU64 = AtomicU64::new(0);
23static GUARD_EVAL_DENY: AtomicU64 = AtomicU64::new(0);
24static GUARD_EVAL_ERROR: AtomicU64 = AtomicU64::new(0);
25static DECISION_LATENCY_ALLOW_NS_SUM: AtomicU64 = AtomicU64::new(0);
26static DECISION_LATENCY_DENY_NS_SUM: AtomicU64 = AtomicU64::new(0);
27static DECISION_LATENCY_ERROR_NS_SUM: AtomicU64 = AtomicU64::new(0);
28static DECISION_LATENCY_ALLOW_COUNT: AtomicU64 = AtomicU64::new(0);
29static DECISION_LATENCY_DENY_COUNT: AtomicU64 = AtomicU64::new(0);
30static DECISION_LATENCY_ERROR_COUNT: AtomicU64 = AtomicU64::new(0);
31static DECISION_LATENCY_ALLOW_BUCKETS: [AtomicU64; DECISION_LATENCY_BUCKET_COUNT] = [
32 AtomicU64::new(0),
33 AtomicU64::new(0),
34 AtomicU64::new(0),
35 AtomicU64::new(0),
36 AtomicU64::new(0),
37 AtomicU64::new(0),
38 AtomicU64::new(0),
39 AtomicU64::new(0),
40 AtomicU64::new(0),
41];
42static DECISION_LATENCY_DENY_BUCKETS: [AtomicU64; DECISION_LATENCY_BUCKET_COUNT] = [
43 AtomicU64::new(0),
44 AtomicU64::new(0),
45 AtomicU64::new(0),
46 AtomicU64::new(0),
47 AtomicU64::new(0),
48 AtomicU64::new(0),
49 AtomicU64::new(0),
50 AtomicU64::new(0),
51 AtomicU64::new(0),
52];
53static DECISION_LATENCY_ERROR_BUCKETS: [AtomicU64; DECISION_LATENCY_BUCKET_COUNT] = [
54 AtomicU64::new(0),
55 AtomicU64::new(0),
56 AtomicU64::new(0),
57 AtomicU64::new(0),
58 AtomicU64::new(0),
59 AtomicU64::new(0),
60 AtomicU64::new(0),
61 AtomicU64::new(0),
62 AtomicU64::new(0),
63];
64
65const NANOS_PER_SECOND: u64 = 1_000_000_000;
66const DECISION_LATENCY_BUCKET_COUNT: usize = DECISION_LATENCY_BUCKET_UPPER_NANOS.len() + 1;
67const DECISION_LATENCY_BUCKET_UPPER_NANOS: [u64; 8] = [
68 25_000_000,
69 50_000_000,
70 75_000_000,
71 100_000_000,
72 250_000_000,
73 500_000_000,
74 1_000_000_000,
75 2_500_000_000,
76];
77
78struct DecisionLatencySeries {
79 sum: &'static AtomicU64,
80 count: &'static AtomicU64,
81 buckets: &'static [AtomicU64; DECISION_LATENCY_BUCKET_COUNT],
82}
83
84pub fn record_guard_evaluation(outcome: &str) {
88 match outcome {
89 GUARD_OUTCOME_ALLOW => {
90 GUARD_EVAL_ALLOW.fetch_add(1, Ordering::Relaxed);
91 }
92 GUARD_OUTCOME_DENY => {
93 GUARD_EVAL_DENY.fetch_add(1, Ordering::Relaxed);
94 }
95 _ => {
96 GUARD_EVAL_ERROR.fetch_add(1, Ordering::Relaxed);
97 }
98 }
99}
100
101pub fn record_dispatch_failure(surface: &str, outcome: &str) {
108 chio_metrics_spec::runtime::families::DISPATCH_FAILURE.incr(&[surface, outcome]);
109}
110
111pub fn observe_decision_latency_nanos(nanos: u64) {
113 observe_decision_latency_nanos_for_outcome(GUARD_OUTCOME_ALLOW, nanos);
114}
115
116pub fn observe_decision_latency_nanos_for_outcome(outcome: &str, nanos: u64) {
119 let series = decision_latency_series(outcome);
120 series.sum.fetch_add(nanos, Ordering::Relaxed);
121 series.count.fetch_add(1, Ordering::Relaxed);
122 for (index, upper) in DECISION_LATENCY_BUCKET_UPPER_NANOS.iter().enumerate() {
123 if nanos <= *upper {
124 series.buckets[index].fetch_add(1, Ordering::Relaxed);
125 }
126 }
127 series.buckets[DECISION_LATENCY_BUCKET_UPPER_NANOS.len()].fetch_add(1, Ordering::Relaxed);
128}
129
130#[must_use]
131pub fn guard_evaluations_total(outcome: &str) -> u64 {
132 match outcome {
133 GUARD_OUTCOME_ALLOW => GUARD_EVAL_ALLOW.load(Ordering::Relaxed),
134 GUARD_OUTCOME_DENY => GUARD_EVAL_DENY.load(Ordering::Relaxed),
135 _ => GUARD_EVAL_ERROR.load(Ordering::Relaxed),
136 }
137}
138
139#[must_use]
140pub fn decision_latency_count() -> u64 {
141 DECISION_LATENCY_ALLOW_COUNT.load(Ordering::Relaxed)
142 + DECISION_LATENCY_DENY_COUNT.load(Ordering::Relaxed)
143 + DECISION_LATENCY_ERROR_COUNT.load(Ordering::Relaxed)
144}
145
146#[must_use]
147pub fn render_http_core_metrics_prometheus() -> String {
148 let mut output = String::new();
149
150 output.push_str("# HELP ");
151 output.push_str(CHIO_GUARD_EVALUATIONS_TOTAL);
152 output.push_str(" Total guard evaluation outcomes across native and WASM guards.\n");
153 output.push_str("# TYPE ");
154 output.push_str(CHIO_GUARD_EVALUATIONS_TOTAL);
155 output.push_str(" counter\n");
156 for outcome in [GUARD_OUTCOME_ALLOW, GUARD_OUTCOME_DENY, GUARD_OUTCOME_ERROR] {
157 output.push_str(CHIO_GUARD_EVALUATIONS_TOTAL);
158 output.push_str("{guard=\"");
159 output.push_str(GUARD_LABEL_HTTP_AUTHORITY);
160 output.push_str("\",outcome=\"");
161 output.push_str(outcome);
162 output.push_str("\"} ");
163 output.push_str(&guard_evaluations_total(outcome).to_string());
164 output.push('\n');
165 }
166
167 output.push_str("# HELP ");
168 output.push_str(CHIO_KERNEL_DECISION_LATENCY_SECONDS);
169 output.push_str(" Kernel mediation decision latency in seconds.\n");
170 output.push_str("# TYPE ");
171 output.push_str(CHIO_KERNEL_DECISION_LATENCY_SECONDS);
172 output.push_str(" histogram\n");
173 for outcome in [GUARD_OUTCOME_ALLOW, GUARD_OUTCOME_DENY, GUARD_OUTCOME_ERROR] {
174 render_decision_latency_histogram(&mut output, outcome);
175 }
176
177 output
178}
179
180fn decision_latency_series(outcome: &str) -> DecisionLatencySeries {
181 match outcome {
182 GUARD_OUTCOME_ALLOW => DecisionLatencySeries {
183 sum: &DECISION_LATENCY_ALLOW_NS_SUM,
184 count: &DECISION_LATENCY_ALLOW_COUNT,
185 buckets: &DECISION_LATENCY_ALLOW_BUCKETS,
186 },
187 GUARD_OUTCOME_DENY => DecisionLatencySeries {
188 sum: &DECISION_LATENCY_DENY_NS_SUM,
189 count: &DECISION_LATENCY_DENY_COUNT,
190 buckets: &DECISION_LATENCY_DENY_BUCKETS,
191 },
192 _ => DecisionLatencySeries {
193 sum: &DECISION_LATENCY_ERROR_NS_SUM,
194 count: &DECISION_LATENCY_ERROR_COUNT,
195 buckets: &DECISION_LATENCY_ERROR_BUCKETS,
196 },
197 }
198}
199
200fn render_decision_latency_histogram(output: &mut String, outcome: &str) {
201 let series = decision_latency_series(outcome);
202 for (index, le) in DECISION_LATENCY_BUCKETS_SECONDS.iter().enumerate() {
203 render_decision_latency_bucket(
204 output,
205 outcome,
206 le,
207 series.buckets[index].load(Ordering::Relaxed),
208 );
209 }
210 render_decision_latency_bucket(
211 output,
212 outcome,
213 "+Inf",
214 series.buckets[DECISION_LATENCY_BUCKET_UPPER_NANOS.len()].load(Ordering::Relaxed),
215 );
216 output.push_str(CHIO_KERNEL_DECISION_LATENCY_SECONDS);
217 output.push_str("_sum{surface=\"");
218 output.push_str(GUARD_LABEL_HTTP_AUTHORITY);
219 output.push_str("\",outcome=\"");
220 output.push_str(outcome);
221 output.push_str("\"} ");
222 output.push_str(&format_seconds(series.sum.load(Ordering::Relaxed)));
223 output.push('\n');
224 output.push_str(CHIO_KERNEL_DECISION_LATENCY_SECONDS);
225 output.push_str("_count{surface=\"");
226 output.push_str(GUARD_LABEL_HTTP_AUTHORITY);
227 output.push_str("\",outcome=\"");
228 output.push_str(outcome);
229 output.push_str("\"} ");
230 output.push_str(&series.count.load(Ordering::Relaxed).to_string());
231 output.push('\n');
232}
233
234fn render_decision_latency_bucket(output: &mut String, outcome: &str, le: &str, count: u64) {
235 output.push_str(CHIO_KERNEL_DECISION_LATENCY_SECONDS);
236 output.push_str("_bucket{surface=\"");
237 output.push_str(GUARD_LABEL_HTTP_AUTHORITY);
238 output.push_str("\",outcome=\"");
239 output.push_str(outcome);
240 output.push_str("\",le=\"");
241 output.push_str(le);
242 output.push_str("\"} ");
243 output.push_str(&count.to_string());
244 output.push('\n');
245}
246
247fn format_seconds(nanos: u64) -> String {
248 format!("{:.9}", (nanos as f64) / (NANOS_PER_SECOND as f64))
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn registry_constants_match_spec() {
257 assert_eq!(CHIO_GUARD_EVALUATIONS_TOTAL, "chio_guard_evaluations_total");
258 assert_eq!(
259 CHIO_KERNEL_DECISION_LATENCY_SECONDS,
260 "chio_kernel_decision_latency_seconds"
261 );
262 }
263
264 #[test]
265 fn dispatch_failure_records_error_never_deny_or_allow() {
266 record_dispatch_failure(GUARD_LABEL_HTTP_AUTHORITY, "error");
270 let mut body = String::new();
271 chio_metrics_spec::runtime::families::DISPATCH_FAILURE.render(&mut body);
272 assert!(
273 body.contains(
274 "chio_dispatch_failure_total{surface=\"http_authority\",outcome=\"error\"}"
275 ),
276 "error series missing: {body}"
277 );
278 assert!(
280 !body.contains("outcome=\"denied\""),
281 "a deny must not be recorded as a dispatch failure: {body}"
282 );
283 assert!(
284 !body.contains("outcome=\"allow\""),
285 "must not record allow: {body}"
286 );
287 }
288}