1#![forbid(unsafe_code)]
8
9pub mod runtime;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
13pub enum MetricKind {
14 Counter,
15 Gauge,
16 Histogram,
17}
18
19impl MetricKind {
20 #[must_use]
21 pub const fn as_str(self) -> &'static str {
22 match self {
23 Self::Counter => "counter",
24 Self::Gauge => "gauge",
25 Self::Histogram => "histogram",
26 }
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct MetricDescriptor {
33 pub name: &'static str,
34 pub help: &'static str,
35 pub kind: MetricKind,
36 pub labels: &'static [&'static str],
37 pub buckets: &'static [&'static str],
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum MetricValidationError {
43 InvalidMetricName {
44 name: &'static str,
45 },
46 InvalidLabelName {
47 metric: &'static str,
48 label: &'static str,
49 },
50 DuplicateLabelName {
51 metric: &'static str,
52 label: &'static str,
53 },
54 UnexpectedBuckets {
55 metric: &'static str,
56 kind: MetricKind,
57 },
58 MissingHistogramBuckets {
59 metric: &'static str,
60 },
61 InvalidHistogramBucket {
62 metric: &'static str,
63 bucket: &'static str,
64 },
65 NonIncreasingHistogramBucket {
66 metric: &'static str,
67 previous: &'static str,
68 bucket: &'static str,
69 },
70 DuplicateMetricName {
71 metric: &'static str,
72 },
73 RegistryNotSorted {
74 previous: &'static str,
75 current: &'static str,
76 },
77}
78
79#[macro_export]
85macro_rules! describe {
86 (
87 name = $name:expr,
88 help = $help:literal,
89 kind = $kind:ident,
90 labels = [$($label:literal),* $(,)?],
91 buckets = [$($bucket:literal),* $(,)?] $(,)?
92 ) => {
93 $crate::MetricDescriptor {
94 name: $name,
95 help: $help,
96 kind: $crate::MetricKind::$kind,
97 labels: &[$($label),*],
98 buckets: &[$($bucket),*],
99 }
100 };
101 (
102 name = $name:expr,
103 help = $help:literal,
104 kind = $kind:ident,
105 labels = [$($label:literal),* $(,)?] $(,)?
106 ) => {
107 $crate::describe!(
108 name = $name,
109 help = $help,
110 kind = $kind,
111 labels = [$($label),*],
112 buckets = []
113 )
114 };
115}
116
117pub const CHIO_ALERT_DISPATCH_TOTAL: &str = "chio_alert_dispatch_total";
118pub const CHIO_ALERT_DISPATCH_LATENCY_SECONDS: &str = "chio_alert_dispatch_latency_seconds";
119pub const CHIO_AMBIGUOUS_DISPATCH_RETAINED_HOLD_TOTAL: &str =
120 "chio_ambiguous_dispatch_retained_hold_total";
121pub const CHIO_ANCHOR_ROUND_LATENCY_SECONDS: &str = "chio_anchor_round_latency_seconds";
122pub const CHIO_CAPABILITY_REVOCATION_LAG_SECONDS: &str = "chio_capability_revocation_lag_seconds";
123pub const CHIO_DISPATCH_FAILURE_TOTAL: &str = "chio_dispatch_failure_total";
124pub const CHIO_DLQ_DEPTH: &str = "chio_dlq_depth";
125pub const CHIO_FAIL_OPEN_SUSPECTED_TOTAL: &str = "chio_fail_open_suspected_total";
126pub const CHIO_FEDERATION_HOP_LATENCY_SECONDS: &str = "chio_federation_hop_latency_seconds";
127pub const CHIO_FEDERATION_HOP_TOTAL: &str = "chio_federation_hop_total";
128pub const CHIO_FEDERATION_TRANSPORT_ACCEPT_DURATION_SECONDS: &str =
129 "chio_federation_transport_accept_duration_seconds";
130pub const CHIO_FEDERATION_TRANSPORT_ACCEPT_OPEN: &str = "chio_federation_transport_accept_open";
131pub const CHIO_FEDERATION_TRANSPORT_ADMISSION_TOTAL: &str =
132 "chio_federation_transport_admission_total";
133pub const CHIO_FEDERATION_TRANSPORT_CATCHUP_EPOCH_GAP_TOTAL: &str =
134 "chio_federation_transport_catchup_epoch_gap_total";
135pub const CHIO_FEDERATION_TRANSPORT_DIRECTORY_RELOAD_TOTAL: &str =
136 "chio_federation_transport_directory_reload_total";
137pub const CHIO_FEDERATION_TRANSPORT_LANE_TOTAL: &str = "chio_federation_transport_lane_total";
138pub const CHIO_FEDERATION_TRANSPORT_OUTBOX_TOTAL: &str = "chio_federation_transport_outbox_total";
139pub const CHIO_FEDERATION_TRANSPORT_ROUTER_ALIVE: &str = "chio_federation_transport_router_alive";
140pub const CHIO_FEDERATION_TRANSPORT_VERIFY_FAILURES_TOTAL: &str =
141 "chio_federation_transport_verify_failures_total";
142pub const CHIO_GUARD_DENY_TOTAL: &str = "chio_guard_deny_total";
143pub const CHIO_GUARD_EVAL_DURATION_SECONDS: &str = "chio_guard_eval_duration_seconds";
144pub const CHIO_GUARD_EVALUATIONS_TOTAL: &str = "chio_guard_evaluations_total";
145pub const CHIO_GUARD_FUEL_CONSUMED_TOTAL: &str = "chio_guard_fuel_consumed_total";
146pub const CHIO_GUARD_HOST_CALL_DURATION_SECONDS: &str = "chio_guard_host_call_duration_seconds";
147pub const CHIO_GUARD_MODULE_BYTES: &str = "chio_guard_module_bytes";
148pub const CHIO_GUARD_POOL_CHECKOUT_TOTAL: &str = "chio_guard_pool_checkout_total";
149pub const CHIO_GUARD_POOL_EVICT_TOTAL: &str = "chio_guard_pool_evict_total";
150pub const CHIO_GUARD_POOL_WARM_SIZE: &str = "chio_guard_pool_warm_size";
151pub const CHIO_GUARD_RELOAD_TOTAL: &str = "chio_guard_reload_total";
152pub const CHIO_GUARD_VERDICT_TOTAL: &str = "chio_guard_verdict_total";
153pub const CHIO_KERNEL_DECISION_LATENCY_SECONDS: &str = "chio_kernel_decision_latency_seconds";
154pub const CHIO_OTEL_INGRESS_DROP_TOTAL: &str = "chio_otel_ingress_drop_total";
155pub const CHIO_OTEL_SINK_DROP_TOTAL: &str = "chio_otel_sink_drop_total";
156pub const CHIO_PHEROMONE_QUEUE_OVERFLOW_TOTAL: &str = "chio_pheromone_queue_overflow_total";
157pub const CHIO_PHEROMONE_RELAY_CATCHUP_BYTES_SERVED_TOTAL: &str =
158 "chio_pheromone_relay_catchup_bytes_served_total";
159pub const CHIO_PHEROMONE_RELAY_CATCHUP_DENIES_TOTAL: &str =
160 "chio_pheromone_relay_catchup_denies_total";
161pub const CHIO_PHEROMONE_RELAY_DEAD_LETTERS_TOTAL: &str = "chio_pheromone_relay_dead_letters_total";
162pub const CHIO_PHEROMONE_RELAY_DELIVERY_TOTAL: &str = "chio_pheromone_relay_delivery_total";
163pub const CHIO_PHEROMONE_RELAY_ENDPOINT_DENIED_TOTAL: &str =
164 "chio_pheromone_relay_endpoint_denied_total";
165pub const CHIO_PHEROMONE_RELAY_LATENCY_SECONDS: &str = "chio_pheromone_relay_latency_seconds";
166pub const CHIO_PHEROMONE_RELAY_NONCE_REPLAY_CONFLICTS_TOTAL: &str =
167 "chio_pheromone_relay_nonce_replay_conflicts_total";
168pub const CHIO_PHEROMONE_RELAY_OLDEST_PENDING_AGE_SECONDS: &str =
169 "chio_pheromone_relay_oldest_pending_age_seconds";
170pub const CHIO_PHEROMONE_RELAY_QUEUE_DEPTH: &str = "chio_pheromone_relay_queue_depth";
171pub const CHIO_PHEROMONE_RELAY_REJECTIONS_TOTAL: &str = "chio_pheromone_relay_rejections_total";
172pub const CHIO_PHEROMONE_RELAY_STALE_DIRECTORIES_TOTAL: &str =
173 "chio_pheromone_relay_stale_directories_total";
174pub const CHIO_PHEROMONE_RELAY_STALE_LEASES: &str = "chio_pheromone_relay_stale_leases";
175pub const CHIO_PHEROMONE_RECEIVER_DEPOSITS_TOTAL: &str = "chio_pheromone_receiver_deposits_total";
176pub const CHIO_PHEROMONE_RECEIVER_LATENCY_SECONDS: &str = "chio_pheromone_receiver_latency_seconds";
177pub const CHIO_PHEROMONE_RECEIVER_REJECTIONS_TOTAL: &str =
178 "chio_pheromone_receiver_rejections_total";
179pub const CHIO_RECEIPT_SECONDS_SINCE_LAST_CHECKPOINT: &str =
180 "chio_receipt_seconds_since_last_checkpoint";
181pub const CHIO_RECEIPT_UNCHECKPOINTED_SEQ_RANGE: &str = "chio_receipt_uncheckpointed_seq_range";
182pub const CHIO_RECEIPT_WRITE_TOTAL: &str = "chio_receipt_write_total";
183pub const CHIO_RECEIPT_WRITE_LATENCY_SECONDS: &str = "chio_receipt_write_latency_seconds";
184pub const CHIO_SETTLEMENT_UNRESOLVED_TOTAL: &str = "chio_settlement_unresolved_total";
185pub const CHIO_SIDECAR_REQUESTS_TOTAL: &str = "chio_sidecar_requests_total";
186pub const CHIO_SIGNING_QUEUE_BLOCK_TOTAL: &str = "chio_signing_queue_block_total";
187pub const CHIO_SOC_EXPORT_TOTAL: &str = "chio_soc_export_total";
188pub const CHIO_SOC_EXPORT_LAG_SECONDS: &str = "chio_soc_export_lag_seconds";
189pub const CHIO_TRUST_CONTROL_READY: &str = "chio_trust_control_ready";
190
191pub const GUARD_EVAL_DURATION_BUCKETS_SECONDS: &[&str] = &[
192 "0.0001", "0.0005", "0.001", "0.005", "0.01", "0.025", "0.05", "0.1", "0.25", "0.5", "1.0",
193];
194pub const GUARD_HOST_CALL_DURATION_BUCKETS_SECONDS: &[&str] = &[
195 "0.00001", "0.00005", "0.0001", "0.0005", "0.001", "0.005", "0.01", "0.05", "0.1",
196];
197pub const DECISION_LATENCY_BUCKETS_SECONDS: &[&str] =
198 &["0.025", "0.05", "0.075", "0.1", "0.25", "0.5", "1.0", "2.5"];
199pub const RECEIPT_WRITE_LATENCY_BUCKETS_SECONDS: &[&str] =
200 &["0.01", "0.025", "0.05", "0.1", "0.25", "0.5", "1.0"];
201pub const ALERT_DISPATCH_LATENCY_BUCKETS_SECONDS: &[&str] =
202 &["0.25", "0.5", "1.0", "2.5", "5.0", "10.0"];
203pub const EXPORT_LAG_BUCKETS_SECONDS: &[&str] = &["30", "60", "120", "300", "600", "1800"];
204pub const ANCHOR_ROUND_LATENCY_BUCKETS_SECONDS: &[&str] =
205 &["0.1", "0.5", "1.0", "2.5", "5.0", "10.0"];
206pub const FEDERATION_HOP_LATENCY_BUCKETS_SECONDS: &[&str] =
207 &["0.01", "0.025", "0.05", "0.1", "0.25", "0.5", "1.0"];
208pub const FEDERATION_TRANSPORT_ACCEPT_DURATION_BUCKETS_SECONDS: &[&str] =
209 &["0.01", "0.025", "0.05", "0.1", "0.25", "0.5", "1.0"];
210pub const PHEROMONE_RECEIVER_LATENCY_BUCKETS_SECONDS: &[&str] = &[
211 "0.001", "0.005", "0.01", "0.025", "0.05", "0.1", "0.25", "0.5", "1.0",
212];
213
214pub const REGISTRY: &[MetricDescriptor] = &[
215 describe!(
216 name = CHIO_ALERT_DISPATCH_LATENCY_SECONDS,
217 help = "PagerDuty or OpsGenie alert dispatch latency in seconds.",
218 kind = Histogram,
219 labels = ["route", "outcome"],
220 buckets = ["0.25", "0.5", "1.0", "2.5", "5.0", "10.0"]
221 ),
222 describe!(
223 name = CHIO_ALERT_DISPATCH_TOTAL,
224 help = "Total PagerDuty or OpsGenie alert dispatch outcomes.",
225 kind = Counter,
226 labels = ["route", "outcome"]
227 ),
228 describe!(
229 name = CHIO_AMBIGUOUS_DISPATCH_RETAINED_HOLD_TOTAL,
230 help = "Total budget or payment holds retained after an ambiguous post-dispatch outcome, labeled by whether durable reconciliation is available.",
231 kind = Counter,
232 labels = ["reconciliation"]
233 ),
234 describe!(
235 name = CHIO_ANCHOR_ROUND_LATENCY_SECONDS,
236 help = "Anchor round latency in seconds.",
237 kind = Histogram,
238 labels = ["witness", "outcome"],
239 buckets = ["0.1", "0.5", "1.0", "2.5", "5.0", "10.0"]
240 ),
241 describe!(
242 name = CHIO_CAPABILITY_REVOCATION_LAG_SECONDS,
243 help = "Capability revocation propagation lag in seconds.",
244 kind = Histogram,
245 labels = ["authority"],
246 buckets = ["1", "5", "15", "30", "60", "120", "300"]
247 ),
248 describe!(
249 name = CHIO_DISPATCH_FAILURE_TOTAL,
250 help = "Total tool-dispatch failures that did not bypass mediation.",
251 kind = Counter,
252 labels = ["surface", "outcome"]
253 ),
254 describe!(
255 name = CHIO_DLQ_DEPTH,
256 help = "Dead-letter queue depth by exporter.",
257 kind = Gauge,
258 labels = ["exporter"]
259 ),
260 describe!(
261 name = CHIO_FAIL_OPEN_SUSPECTED_TOTAL,
262 help = "Total suspected fail-open paths detected by SRE guards.",
263 kind = Counter,
264 labels = ["surface"]
265 ),
266 describe!(
267 name = CHIO_FEDERATION_HOP_LATENCY_SECONDS,
268 help = "Federation hop latency in seconds.",
269 kind = Histogram,
270 labels = ["result"],
271 buckets = ["0.01", "0.025", "0.05", "0.1", "0.25", "0.5", "1.0"]
272 ),
273 describe!(
274 name = CHIO_FEDERATION_HOP_TOTAL,
275 help = "Total federation hop outcomes.",
276 kind = Counter,
277 labels = ["result"]
278 ),
279 describe!(
280 name = CHIO_FEDERATION_TRANSPORT_ACCEPT_DURATION_SECONDS,
281 help = "Iroh federation-transport per-lane accept handler duration in seconds.",
282 kind = Histogram,
283 labels = ["lane"],
284 buckets = ["0.01", "0.025", "0.05", "0.1", "0.25", "0.5", "1.0"]
285 ),
286 describe!(
287 name = CHIO_FEDERATION_TRANSPORT_ACCEPT_OPEN,
288 help = "Iroh federation-transport in-flight accept handlers by lane (slowloris gauge).",
289 kind = Gauge,
290 labels = ["lane"]
291 ),
292 describe!(
293 name = CHIO_FEDERATION_TRANSPORT_ADMISSION_TOTAL,
294 help = "Total iroh federation-transport admission-gate outcomes at after_handshake.",
295 kind = Counter,
296 labels = ["outcome"]
297 ),
298 describe!(
299 name = CHIO_FEDERATION_TRANSPORT_CATCHUP_EPOCH_GAP_TOTAL,
300 help = "Total iroh federation-transport revocation catch-up epoch gaps detected.",
301 kind = Counter,
302 labels = ["source"]
303 ),
304 describe!(
305 name = CHIO_FEDERATION_TRANSPORT_DIRECTORY_RELOAD_TOTAL,
306 help = "Total iroh federation-transport directory reload outcomes.",
307 kind = Counter,
308 labels = ["outcome"]
309 ),
310 describe!(
311 name = CHIO_FEDERATION_TRANSPORT_LANE_TOTAL,
312 help = "Total iroh federation-transport per-lane accept outcomes.",
313 kind = Counter,
314 labels = ["lane", "outcome"]
315 ),
316 describe!(
317 name = CHIO_FEDERATION_TRANSPORT_OUTBOX_TOTAL,
318 help = "Total iroh federation-transport pheromone outbox drain outcomes.",
319 kind = Counter,
320 labels = ["outcome"]
321 ),
322 describe!(
323 name = CHIO_FEDERATION_TRANSPORT_ROUTER_ALIVE,
324 help = "Iroh federation-transport router liveness (1 alive, 0 the router died).",
325 kind = Gauge,
326 labels = []
327 ),
328 describe!(
329 name = CHIO_FEDERATION_TRANSPORT_VERIFY_FAILURES_TOTAL,
330 help = "Total iroh federation-transport verification failures by seam and bounded reason.",
331 kind = Counter,
332 labels = ["seam", "reason"]
333 ),
334 describe!(
335 name = CHIO_GUARD_DENY_TOTAL,
336 help = "Total WASM guard denies by reason class.",
337 kind = Counter,
338 labels = ["guard_id", "reason_class"]
339 ),
340 describe!(
341 name = CHIO_GUARD_EVAL_DURATION_SECONDS,
342 help = "WASM guard evaluation duration in seconds.",
343 kind = Histogram,
344 labels = ["guard_id", "verdict"],
345 buckets = [
346 "0.0001", "0.0005", "0.001", "0.005", "0.01", "0.025", "0.05", "0.1", "0.25", "0.5",
347 "1.0"
348 ]
349 ),
350 describe!(
351 name = CHIO_GUARD_EVALUATIONS_TOTAL,
352 help = "Total guard evaluation outcomes across native and WASM guards.",
353 kind = Counter,
354 labels = ["guard", "outcome"]
355 ),
356 describe!(
357 name = CHIO_GUARD_FUEL_CONSUMED_TOTAL,
358 help = "Total WASM guard fuel units consumed.",
359 kind = Counter,
360 labels = ["guard_id"]
361 ),
362 describe!(
363 name = CHIO_GUARD_HOST_CALL_DURATION_SECONDS,
364 help = "WASM guard host-call duration in seconds.",
365 kind = Histogram,
366 labels = ["guard_id", "host_fn"],
367 buckets =
368 ["0.00001", "0.00005", "0.0001", "0.0005", "0.001", "0.005", "0.01", "0.05", "0.1"]
369 ),
370 describe!(
371 name = CHIO_GUARD_MODULE_BYTES,
372 help = "Loaded WASM guard module size in bytes.",
373 kind = Gauge,
374 labels = ["guard_id", "epoch"]
375 ),
376 describe!(
377 name = CHIO_GUARD_POOL_CHECKOUT_TOTAL,
378 help = "Total WASM guard pool checkout outcomes by tenant.",
379 kind = Counter,
380 labels = ["guard_id", "tenant_id"]
381 ),
382 describe!(
383 name = CHIO_GUARD_POOL_EVICT_TOTAL,
384 help = "Total WASM guard pool evictions by tenant.",
385 kind = Counter,
386 labels = ["guard_id", "tenant_id"]
387 ),
388 describe!(
389 name = CHIO_GUARD_POOL_WARM_SIZE,
390 help = "Warm WASM guard pool size by tenant.",
391 kind = Gauge,
392 labels = ["guard_id", "tenant_id"]
393 ),
394 describe!(
395 name = CHIO_GUARD_RELOAD_TOTAL,
396 help = "Total WASM guard reload outcomes.",
397 kind = Counter,
398 labels = ["guard_id", "outcome"]
399 ),
400 describe!(
401 name = CHIO_GUARD_VERDICT_TOTAL,
402 help = "Total WASM guard verdicts by guard and verdict.",
403 kind = Counter,
404 labels = ["guard_id", "verdict"]
405 ),
406 describe!(
407 name = CHIO_KERNEL_DECISION_LATENCY_SECONDS,
408 help = "Kernel mediation decision latency in seconds.",
409 kind = Histogram,
410 labels = ["surface", "outcome"],
411 buckets = ["0.025", "0.05", "0.075", "0.1", "0.25", "0.5", "1.0", "2.5"]
412 ),
413 describe!(
414 name = CHIO_OTEL_INGRESS_DROP_TOTAL,
415 help = "Total OTEL ingress batches dropped by bounded queue admission.",
416 kind = Counter,
417 labels = []
418 ),
419 describe!(
420 name = CHIO_OTEL_SINK_DROP_TOTAL,
421 help = "Total OTEL receipt sink batches dropped before append.",
422 kind = Counter,
423 labels = []
424 ),
425 describe!(
426 name = CHIO_PHEROMONE_QUEUE_OVERFLOW_TOTAL,
427 help = "Total local pheromone gossip queue overflows.",
428 kind = Counter,
429 labels = ["treaty", "recipient"]
430 ),
431 describe!(
432 name = CHIO_PHEROMONE_RECEIVER_DEPOSITS_TOTAL,
433 help = "Total local pheromone receiver deposit admission outcomes.",
434 kind = Counter,
435 labels = ["outcome"]
436 ),
437 describe!(
438 name = CHIO_PHEROMONE_RECEIVER_LATENCY_SECONDS,
439 help = "Local pheromone receiver batch verification latency in seconds.",
440 kind = Histogram,
441 labels = ["outcome"],
442 buckets = ["0.001", "0.005", "0.01", "0.025", "0.05", "0.1", "0.25", "0.5", "1.0"]
443 ),
444 describe!(
445 name = CHIO_PHEROMONE_RECEIVER_REJECTIONS_TOTAL,
446 help = "Total local pheromone receiver rejections by bounded reason.",
447 kind = Counter,
448 labels = ["reason"]
449 ),
450 describe!(
451 name = CHIO_PHEROMONE_RELAY_CATCHUP_BYTES_SERVED_TOTAL,
452 help = "Total bounded catch-up bytes served by relay.",
453 kind = Counter,
454 labels = ["responder", "treaty"]
455 ),
456 describe!(
457 name = CHIO_PHEROMONE_RELAY_CATCHUP_DENIES_TOTAL,
458 help = "Total bounded catch-up requests denied by reason.",
459 kind = Counter,
460 labels = ["reason"]
461 ),
462 describe!(
463 name = CHIO_PHEROMONE_RELAY_DEAD_LETTERS_TOTAL,
464 help = "Total relay outbox batches moved to dead letter.",
465 kind = Counter,
466 labels = ["reason"]
467 ),
468 describe!(
469 name = CHIO_PHEROMONE_RELAY_DELIVERY_TOTAL,
470 help = "Total live pheromone relay delivery outcomes.",
471 kind = Counter,
472 labels = ["recipient", "outcome"]
473 ),
474 describe!(
475 name = CHIO_PHEROMONE_RELAY_ENDPOINT_DENIED_TOTAL,
476 help = "Total relay endpoints denied by profile lint or delivery policy.",
477 kind = Counter,
478 labels = ["profile", "reason"]
479 ),
480 describe!(
481 name = CHIO_PHEROMONE_RELAY_LATENCY_SECONDS,
482 help = "Live pheromone relay delivery latency in seconds.",
483 kind = Histogram,
484 labels = ["outcome"],
485 buckets = ["0.001", "0.005", "0.01", "0.025", "0.05", "0.1", "0.25", "0.5", "1.0"]
486 ),
487 describe!(
488 name = CHIO_PHEROMONE_RELAY_NONCE_REPLAY_CONFLICTS_TOTAL,
489 help = "Total relay nonce replay conflicts.",
490 kind = Counter,
491 labels = ["peer"]
492 ),
493 describe!(
494 name = CHIO_PHEROMONE_RELAY_OLDEST_PENDING_AGE_SECONDS,
495 help = "Oldest pending relay outbox age in seconds.",
496 kind = Gauge,
497 labels = []
498 ),
499 describe!(
500 name = CHIO_PHEROMONE_RELAY_QUEUE_DEPTH,
501 help = "Relay outbox depth by bounded status.",
502 kind = Gauge,
503 labels = ["status"]
504 ),
505 describe!(
506 name = CHIO_PHEROMONE_RELAY_REJECTIONS_TOTAL,
507 help = "Total live pheromone relay rejections by bounded reason.",
508 kind = Counter,
509 labels = ["reason"]
510 ),
511 describe!(
512 name = CHIO_PHEROMONE_RELAY_STALE_DIRECTORIES_TOTAL,
513 help = "Total stale peer-directory bundles or documents rejected.",
514 kind = Counter,
515 labels = ["profile"]
516 ),
517 describe!(
518 name = CHIO_PHEROMONE_RELAY_STALE_LEASES,
519 help = "Relay scheduler leases past their expiry.",
520 kind = Gauge,
521 labels = []
522 ),
523 describe!(
524 name = CHIO_RECEIPT_SECONDS_SINCE_LAST_CHECKPOINT,
525 help = "Seconds since the receipt-store checkpoint last advanced while data was pending on the local store.",
526 kind = Gauge,
527 labels = []
528 ),
529 describe!(
530 name = CHIO_RECEIPT_UNCHECKPOINTED_SEQ_RANGE,
531 help = "Uncheckpointed receipt entry_seq range (end - start) on the local store.",
532 kind = Gauge,
533 labels = []
534 ),
535 describe!(
536 name = CHIO_RECEIPT_WRITE_LATENCY_SECONDS,
537 help = "Receipt write latency at the local store boundary in seconds.",
538 kind = Histogram,
539 labels = ["store", "outcome"],
540 buckets = ["0.01", "0.025", "0.05", "0.1", "0.25", "0.5", "1.0"]
541 ),
542 describe!(
543 name = CHIO_RECEIPT_WRITE_TOTAL,
544 help = "Total receipt write outcomes after policy or guard evaluation.",
545 kind = Counter,
546 labels = ["outcome"]
547 ),
548 describe!(
549 name = CHIO_SETTLEMENT_UNRESOLVED_TOTAL,
550 help = "Total settlement observer routing invocations with an unresolved outcome.",
551 kind = Counter,
552 labels = []
553 ),
554 describe!(
555 name = CHIO_SIDECAR_REQUESTS_TOTAL,
556 help = "Total sidecar request outcomes at the mediation edge.",
557 kind = Counter,
558 labels = ["outcome"]
559 ),
560 describe!(
561 name = CHIO_SIGNING_QUEUE_BLOCK_TOTAL,
562 help = "Total receipt signing requests blocked by bounded queue capacity or byte budget.",
563 kind = Counter,
564 labels = ["reason"]
565 ),
566 describe!(
567 name = CHIO_SOC_EXPORT_LAG_SECONDS,
568 help = "SOC export lag in seconds from receipt persistence to sink acknowledgement.",
569 kind = Histogram,
570 labels = ["exporter", "severity"],
571 buckets = ["30", "60", "120", "300", "600", "1800"]
572 ),
573 describe!(
574 name = CHIO_SOC_EXPORT_TOTAL,
575 help = "Total SOC export outcomes for audit rows.",
576 kind = Counter,
577 labels = ["exporter", "outcome"]
578 ),
579 describe!(
580 name = CHIO_TRUST_CONTROL_READY,
581 help = "Trust-control readiness state, where 1 is ready and 0 is not ready.",
582 kind = Gauge,
583 labels = []
584 ),
585];
586
587#[must_use]
588pub fn descriptor_for(name: &str) -> Option<&'static MetricDescriptor> {
589 REGISTRY.iter().find(|descriptor| descriptor.name == name)
590}
591
592#[must_use]
593pub fn is_registered_metric(name: &str) -> bool {
594 descriptor_for(name).is_some()
595}
596
597pub fn validate_metric_descriptor(
598 descriptor: &MetricDescriptor,
599) -> Result<(), MetricValidationError> {
600 if !is_prometheus_metric_name(descriptor.name) {
601 return Err(MetricValidationError::InvalidMetricName {
602 name: descriptor.name,
603 });
604 }
605
606 for (index, label) in descriptor.labels.iter().enumerate() {
607 if !is_prometheus_label_name(label) {
608 return Err(MetricValidationError::InvalidLabelName {
609 metric: descriptor.name,
610 label,
611 });
612 }
613 if descriptor.labels[..index].contains(label) {
614 return Err(MetricValidationError::DuplicateLabelName {
615 metric: descriptor.name,
616 label,
617 });
618 }
619 }
620
621 match descriptor.kind {
622 MetricKind::Histogram => validate_histogram_buckets(descriptor),
623 MetricKind::Counter | MetricKind::Gauge => {
624 if descriptor.buckets.is_empty() {
625 Ok(())
626 } else {
627 Err(MetricValidationError::UnexpectedBuckets {
628 metric: descriptor.name,
629 kind: descriptor.kind,
630 })
631 }
632 }
633 }
634}
635
636pub fn validate_registry() -> Result<(), MetricValidationError> {
637 let mut previous = None;
638 for descriptor in REGISTRY {
639 validate_metric_descriptor(descriptor)?;
640 if let Some(previous_name) = previous {
641 if descriptor.name == previous_name {
642 return Err(MetricValidationError::DuplicateMetricName {
643 metric: descriptor.name,
644 });
645 }
646 if descriptor.name < previous_name {
647 return Err(MetricValidationError::RegistryNotSorted {
648 previous: previous_name,
649 current: descriptor.name,
650 });
651 }
652 }
653 previous = Some(descriptor.name);
654 }
655 Ok(())
656}
657
658#[must_use]
659pub fn is_prometheus_metric_name(name: &str) -> bool {
660 let mut bytes = name.bytes();
661 let Some(first) = bytes.next() else {
662 return false;
663 };
664 (first.is_ascii_alphabetic() || matches!(first, b'_' | b':'))
665 && bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':'))
666}
667
668#[must_use]
669pub fn is_prometheus_label_name(name: &str) -> bool {
670 if name.starts_with("__") {
671 return false;
672 }
673 let mut bytes = name.bytes();
674 let Some(first) = bytes.next() else {
675 return false;
676 };
677 (first.is_ascii_alphabetic() || first == b'_')
678 && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
679}
680
681fn validate_histogram_buckets(descriptor: &MetricDescriptor) -> Result<(), MetricValidationError> {
682 if descriptor.buckets.is_empty() {
683 return Err(MetricValidationError::MissingHistogramBuckets {
684 metric: descriptor.name,
685 });
686 }
687
688 let mut previous_value = None;
689 let mut previous_bucket = "";
690 for bucket in descriptor.buckets {
691 let Ok(value) = bucket.parse::<f64>() else {
692 return Err(MetricValidationError::InvalidHistogramBucket {
693 metric: descriptor.name,
694 bucket,
695 });
696 };
697 if !value.is_finite() {
698 return Err(MetricValidationError::InvalidHistogramBucket {
699 metric: descriptor.name,
700 bucket,
701 });
702 }
703 if let Some(previous) = previous_value {
704 if value <= previous {
705 return Err(MetricValidationError::NonIncreasingHistogramBucket {
706 metric: descriptor.name,
707 previous: previous_bucket,
708 bucket,
709 });
710 }
711 }
712 previous_value = Some(value);
713 previous_bucket = bucket;
714 }
715
716 Ok(())
717}
718
719#[must_use]
720pub fn registry_snapshot() -> String {
721 let mut output = String::new();
722 for descriptor in REGISTRY {
723 output.push_str(descriptor.name);
724 output.push('|');
725 output.push_str(descriptor.kind.as_str());
726 output.push('|');
727 output.push_str(&descriptor.labels.join(","));
728 output.push('|');
729 output.push_str(&descriptor.buckets.join(","));
730 output.push('|');
731 output.push_str(descriptor.help);
732 output.push('\n');
733 }
734 output
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740
741 const REQUIRED_SRE_METRICS: &[&str] = &[
742 CHIO_KERNEL_DECISION_LATENCY_SECONDS,
743 CHIO_RECEIPT_WRITE_TOTAL,
744 CHIO_GUARD_EVALUATIONS_TOTAL,
745 CHIO_CAPABILITY_REVOCATION_LAG_SECONDS,
746 CHIO_ANCHOR_ROUND_LATENCY_SECONDS,
747 CHIO_FEDERATION_HOP_TOTAL,
748 CHIO_DLQ_DEPTH,
749 ];
750
751 #[test]
752 fn golden_snapshot_matches_registry() {
753 assert_eq!(registry_snapshot(), include_str!("../metrics.snapshot"));
754 }
755
756 #[test]
757 fn registry_is_sorted_and_unique() {
758 let mut previous = "";
759 for descriptor in REGISTRY {
760 assert!(
761 descriptor.name > previous,
762 "metric registry must stay sorted and unique: {} after {previous}",
763 descriptor.name
764 );
765 previous = descriptor.name;
766 }
767 }
768
769 #[test]
770 fn registry_metric_and_label_names_are_prometheus_safe() {
771 for descriptor in REGISTRY {
772 assert!(
773 is_prometheus_metric_name(descriptor.name),
774 "invalid metric name {}",
775 descriptor.name
776 );
777 for label in descriptor.labels {
778 assert!(
779 is_prometheus_label_name(label),
780 "invalid label name {label} on {}",
781 descriptor.name
782 );
783 }
784 }
785 }
786
787 #[test]
788 fn registry_descriptors_pass_runtime_validation() {
789 assert_eq!(validate_registry(), Ok(()));
790 for descriptor in REGISTRY {
791 assert_eq!(validate_metric_descriptor(descriptor), Ok(()));
792 }
793 }
794
795 #[test]
796 fn descriptor_validation_rejects_malformed_histogram_buckets() {
797 let unordered = MetricDescriptor {
798 name: "bad_latency_seconds",
799 help: "Bad latency buckets.",
800 kind: MetricKind::Histogram,
801 labels: &[],
802 buckets: &["0.1", "0.05"],
803 };
804 assert_eq!(
805 validate_metric_descriptor(&unordered),
806 Err(MetricValidationError::NonIncreasingHistogramBucket {
807 metric: "bad_latency_seconds",
808 previous: "0.1",
809 bucket: "0.05",
810 })
811 );
812
813 let invalid = MetricDescriptor {
814 name: "bad_latency_seconds",
815 help: "Bad latency buckets.",
816 kind: MetricKind::Histogram,
817 labels: &[],
818 buckets: &["NaN"],
819 };
820 assert_eq!(
821 validate_metric_descriptor(&invalid),
822 Err(MetricValidationError::InvalidHistogramBucket {
823 metric: "bad_latency_seconds",
824 bucket: "NaN",
825 })
826 );
827 }
828
829 #[test]
830 fn descriptor_validation_rejects_bucket_kind_mismatches() {
831 let counter = MetricDescriptor {
832 name: "bad_total",
833 help: "Bad counter buckets.",
834 kind: MetricKind::Counter,
835 labels: &[],
836 buckets: &["1"],
837 };
838 assert_eq!(
839 validate_metric_descriptor(&counter),
840 Err(MetricValidationError::UnexpectedBuckets {
841 metric: "bad_total",
842 kind: MetricKind::Counter,
843 })
844 );
845
846 let histogram = MetricDescriptor {
847 name: "bad_latency_seconds",
848 help: "Missing latency buckets.",
849 kind: MetricKind::Histogram,
850 labels: &[],
851 buckets: &[],
852 };
853 assert_eq!(
854 validate_metric_descriptor(&histogram),
855 Err(MetricValidationError::MissingHistogramBuckets {
856 metric: "bad_latency_seconds",
857 })
858 );
859 }
860
861 #[test]
862 fn prometheus_label_names_reject_reserved_internal_prefix() {
863 assert!(!is_prometheus_label_name("__name__"));
864 assert!(!is_prometheus_label_name("__tenant_id"));
865 }
866
867 #[test]
868 fn required_sre_metrics_are_registered() {
869 for name in REQUIRED_SRE_METRICS {
870 assert!(is_registered_metric(name), "missing required metric {name}");
871 }
872 }
873
874 #[test]
875 fn labelled_metrics_have_expected_labels() {
876 assert_eq!(
877 descriptor_for(CHIO_RECEIPT_WRITE_TOTAL).map(|metric| metric.labels),
878 Some(&["outcome"][..])
879 );
880 assert_eq!(
881 descriptor_for(CHIO_GUARD_EVALUATIONS_TOTAL).map(|metric| metric.labels),
882 Some(&["guard", "outcome"][..])
883 );
884 assert_eq!(
885 descriptor_for(CHIO_FEDERATION_HOP_TOTAL).map(|metric| metric.labels),
886 Some(&["result"][..])
887 );
888 assert_eq!(
889 descriptor_for(CHIO_DLQ_DEPTH).map(|metric| metric.labels),
890 Some(&["exporter"][..])
891 );
892 }
893}