chio_kernel/observability/
metrics.rs1use chio_metrics_spec::{
4 CHIO_AMBIGUOUS_DISPATCH_RETAINED_HOLD_TOTAL, CHIO_GUARD_DENY_TOTAL,
5 CHIO_GUARD_EVAL_DURATION_SECONDS, CHIO_GUARD_FUEL_CONSUMED_TOTAL,
6 CHIO_GUARD_HOST_CALL_DURATION_SECONDS, CHIO_GUARD_MODULE_BYTES, CHIO_GUARD_RELOAD_TOTAL,
7 CHIO_GUARD_VERDICT_TOTAL, CHIO_SETTLEMENT_UNRESOLVED_TOTAL, CHIO_SIGNING_QUEUE_BLOCK_TOTAL,
8 GUARD_EVAL_DURATION_BUCKETS_SECONDS, GUARD_HOST_CALL_DURATION_BUCKETS_SECONDS,
9};
10
11pub use chio_metrics_spec::MetricKind as PrometheusMetricKind;
12
13pub const GUARD_METRICS_PATH: &str = "/metrics";
14pub const PROMETHEUS_TEXT_CONTENT_TYPE: &str = "text/plain; version=0.0.4; charset=utf-8";
15
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct GuardMetricFamily {
18 pub name: &'static str,
19 pub help: &'static str,
20 pub kind: PrometheusMetricKind,
21 pub labels: &'static [&'static str],
22 pub buckets: &'static [&'static str],
23}
24
25const LABELS_GUARD_VERDICT: &[&str] = &["guard_id", "verdict"];
26const LABELS_GUARD_ONLY: &[&str] = &["guard_id"];
27const LABELS_GUARD_REASON_CLASS: &[&str] = &["guard_id", "reason_class"];
28const LABELS_GUARD_OUTCOME: &[&str] = &["guard_id", "outcome"];
29const LABELS_GUARD_HOST_FN: &[&str] = &["guard_id", "host_fn"];
30const LABELS_GUARD_EPOCH: &[&str] = &["guard_id", "epoch"];
31
32pub const GUARD_METRIC_FAMILIES: &[GuardMetricFamily] = &[
33 GuardMetricFamily {
34 name: CHIO_GUARD_EVAL_DURATION_SECONDS,
35 help: "WASM guard evaluation duration in seconds.",
36 kind: PrometheusMetricKind::Histogram,
37 labels: LABELS_GUARD_VERDICT,
38 buckets: GUARD_EVAL_DURATION_BUCKETS_SECONDS,
39 },
40 GuardMetricFamily {
41 name: CHIO_GUARD_FUEL_CONSUMED_TOTAL,
42 help: "Total WASM guard fuel units consumed.",
43 kind: PrometheusMetricKind::Counter,
44 labels: LABELS_GUARD_ONLY,
45 buckets: &[],
46 },
47 GuardMetricFamily {
48 name: CHIO_GUARD_VERDICT_TOTAL,
49 help: "Total WASM guard verdicts by guard and verdict.",
50 kind: PrometheusMetricKind::Counter,
51 labels: LABELS_GUARD_VERDICT,
52 buckets: &[],
53 },
54 GuardMetricFamily {
55 name: CHIO_GUARD_DENY_TOTAL,
56 help: "Total WASM guard denies by reason class.",
57 kind: PrometheusMetricKind::Counter,
58 labels: LABELS_GUARD_REASON_CLASS,
59 buckets: &[],
60 },
61 GuardMetricFamily {
62 name: CHIO_GUARD_RELOAD_TOTAL,
63 help: "Total WASM guard reload outcomes.",
64 kind: PrometheusMetricKind::Counter,
65 labels: LABELS_GUARD_OUTCOME,
66 buckets: &[],
67 },
68 GuardMetricFamily {
69 name: CHIO_GUARD_HOST_CALL_DURATION_SECONDS,
70 help: "WASM guard host-call duration in seconds.",
71 kind: PrometheusMetricKind::Histogram,
72 labels: LABELS_GUARD_HOST_FN,
73 buckets: GUARD_HOST_CALL_DURATION_BUCKETS_SECONDS,
74 },
75 GuardMetricFamily {
76 name: CHIO_GUARD_MODULE_BYTES,
77 help: "Loaded WASM guard module size in bytes.",
78 kind: PrometheusMetricKind::Gauge,
79 labels: LABELS_GUARD_EPOCH,
80 buckets: &[],
81 },
82];
83
84pub use chio_metrics_spec::CHIO_OTEL_INGRESS_DROP_TOTAL as METRIC_CHIO_OTEL_INGRESS_DROP_TOTAL;
85pub use chio_metrics_spec::CHIO_OTEL_SINK_DROP_TOTAL as METRIC_CHIO_OTEL_SINK_DROP_TOTAL;
86
87#[allow(dead_code)]
92const RUNTIME_METRIC_FAMILIES: &[GuardMetricFamily] = &[
93 GuardMetricFamily {
94 name: CHIO_SIGNING_QUEUE_BLOCK_TOTAL,
95 help: "Total receipt signing requests blocked by bounded queue capacity or byte budget.",
96 kind: PrometheusMetricKind::Counter,
97 labels: &["reason"],
98 buckets: &[],
99 },
100 GuardMetricFamily {
101 name: METRIC_CHIO_OTEL_INGRESS_DROP_TOTAL,
102 help: "Total OTEL ingress batches dropped by bounded queue admission.",
103 kind: PrometheusMetricKind::Counter,
104 labels: &[],
105 buckets: &[],
106 },
107 GuardMetricFamily {
108 name: METRIC_CHIO_OTEL_SINK_DROP_TOTAL,
109 help: "Total OTEL receipt sink batches dropped before append.",
110 kind: PrometheusMetricKind::Counter,
111 labels: &[],
112 buckets: &[],
113 },
114 GuardMetricFamily {
115 name: CHIO_SETTLEMENT_UNRESOLVED_TOTAL,
116 help: "Total settlement observer routing invocations with an unresolved outcome.",
117 kind: PrometheusMetricKind::Counter,
118 labels: &[],
119 buckets: &[],
120 },
121 GuardMetricFamily {
122 name: CHIO_AMBIGUOUS_DISPATCH_RETAINED_HOLD_TOTAL,
123 help: "Total budget or payment holds retained after an ambiguous post-dispatch outcome, labeled by whether durable reconciliation is available.",
124 kind: PrometheusMetricKind::Counter,
125 labels: &["reconciliation"],
126 buckets: &[],
127 },
128];
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct MetricsEndpointResponse {
132 pub status: u16,
133 pub content_type: &'static str,
134 pub body: String,
135}
136
137#[must_use]
138pub fn guard_metrics_endpoint(path: &str) -> Option<MetricsEndpointResponse> {
139 if path != GUARD_METRICS_PATH {
140 return None;
141 }
142
143 Some(MetricsEndpointResponse {
144 status: 200,
145 content_type: PROMETHEUS_TEXT_CONTENT_TYPE,
146 body: render_guard_metrics_prometheus(),
147 })
148}
149
150#[must_use]
158pub fn render_guard_metrics_prometheus() -> String {
159 let mut output = String::new();
160 chio_metrics_spec::runtime::render_guard_families(&mut output);
161 chio_metrics_spec::runtime::render_otel_drop_families(&mut output);
162 chio_metrics_spec::runtime::families::SIGNING_QUEUE_BLOCK.render(&mut output);
163 chio_metrics_spec::runtime::families::SETTLEMENT_UNRESOLVED.render(&mut output);
164 chio_metrics_spec::runtime::families::AMBIGUOUS_DISPATCH_RETAINED_HOLD.render(&mut output);
165 chio_metrics_spec::runtime::render_receipt_watchdog_gauges(&mut output);
166 output
167}
168
169pub fn record_receipt_health_gauges(
175 report: &crate::receipt_store::ReceiptStoreHealthReport,
176 now_unix_ms: u64,
177) {
178 let range = match (
179 report.uncheckpointed_start_seq,
180 report.uncheckpointed_end_seq,
181 ) {
182 (Some(start), Some(end)) => end.saturating_sub(start),
183 _ => 0,
184 };
185 chio_metrics_spec::runtime::families::RECEIPT_UNCHECKPOINTED_RANGE.set(&[], range);
186
187 let has_backlog = report.latest_committed_entry_seq > report.latest_checkpointed_entry_seq;
195 let age_seconds = match CHECKPOINT_PROGRESS.lock() {
196 Ok(mut state) => checkpoint_staleness_seconds(
197 &mut state,
198 report.latest_checkpointed_entry_seq,
199 has_backlog,
200 now_unix_ms,
201 ),
202 Err(_) => 0,
205 };
206 chio_metrics_spec::runtime::families::RECEIPT_CHECKPOINT_AGE_SECONDS.set(&[], age_seconds);
207}
208
209struct CheckpointProgress {
213 checkpointed_entry_seq: u64,
215 advanced_at_unix_ms: u64,
218}
219
220static CHECKPOINT_PROGRESS: std::sync::Mutex<Option<CheckpointProgress>> =
221 std::sync::Mutex::new(None);
222
223fn checkpoint_staleness_seconds(
229 state: &mut Option<CheckpointProgress>,
230 checkpointed_entry_seq: u64,
231 has_backlog: bool,
232 now_unix_ms: u64,
233) -> u64 {
234 let progressed = state
235 .as_ref()
236 .map(|prev| prev.checkpointed_entry_seq != checkpointed_entry_seq)
237 .unwrap_or(true);
238
239 if progressed || !has_backlog {
240 *state = Some(CheckpointProgress {
243 checkpointed_entry_seq,
244 advanced_at_unix_ms: now_unix_ms,
245 });
246 return 0;
247 }
248
249 state
252 .as_ref()
253 .map(|prev| now_unix_ms.saturating_sub(prev.advanced_at_unix_ms) / 1000)
254 .unwrap_or(0)
255}
256
257#[cfg(test)]
258mod checkpoint_staleness_tests {
259 use super::{checkpoint_staleness_seconds, CheckpointProgress};
260
261 #[test]
262 fn resets_when_checkpoint_advances_even_as_time_passes() {
263 let mut state: Option<CheckpointProgress> = None;
264 assert_eq!(checkpoint_staleness_seconds(&mut state, 5, true, 1_000), 0);
266 assert_eq!(checkpoint_staleness_seconds(&mut state, 9, true, 61_000), 0);
268 }
269
270 #[test]
271 fn grows_while_backlog_stays_uncheckpointed() {
272 let mut state: Option<CheckpointProgress> = None;
273 assert_eq!(checkpoint_staleness_seconds(&mut state, 5, true, 1_000), 0);
274 assert_eq!(
277 checkpoint_staleness_seconds(&mut state, 5, true, 91_000),
278 90
279 );
280 }
281
282 #[test]
283 fn stays_zero_for_a_healthy_quiet_store_without_backlog() {
284 let mut state: Option<CheckpointProgress> = None;
285 assert_eq!(checkpoint_staleness_seconds(&mut state, 5, false, 1_000), 0);
286 assert_eq!(
288 checkpoint_staleness_seconds(&mut state, 5, false, 999_000),
289 0
290 );
291 }
292}