1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Copyright 2026 Encypher Corporation
// SPDX-License-Identifier: Apache-2.0
//! Lightweight, dependency-free verification metrics.
//!
//! A process-wide [`Metrics`] of atomic counters the host (sidecar/PyO3 shim)
//! can scrape and export to Prometheus/OTel without this crate taking a metrics
//! dependency. Counters cover outcomes (valid/invalid/trusted/no-manifest),
//! errors, and contained panics — the signals an operator needs at billions/day.
use std::sync::atomic::{AtomicU64, Ordering};
/// Process-wide verification counters. Construct one [`Metrics`] per process
/// (or use [`global`]) and read snapshots via [`Metrics::snapshot`].
#[derive(Default)]
pub struct Metrics {
/// Total verify calls completed (any outcome).
pub verifications: AtomicU64,
/// Results with state `Trusted`.
pub trusted: AtomicU64,
/// Results with state `Valid` (not trusted).
pub valid: AtomicU64,
/// Results with state `Invalid`.
pub invalid: AtomicU64,
/// Assets with no C2PA manifest.
pub no_manifest: AtomicU64,
/// Verify calls that returned a [`crate::c2pa_validate::ValidateError`].
pub errors: AtomicU64,
/// Contained panics (defence-in-depth; should always be 0 in practice).
pub panics: AtomicU64,
}
/// A point-in-time, plain-data snapshot of [`Metrics`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MetricsSnapshot {
/// See [`Metrics::verifications`].
pub verifications: u64,
/// See [`Metrics::trusted`].
pub trusted: u64,
/// See [`Metrics::valid`].
pub valid: u64,
/// See [`Metrics::invalid`].
pub invalid: u64,
/// See [`Metrics::no_manifest`].
pub no_manifest: u64,
/// See [`Metrics::errors`].
pub errors: u64,
/// See [`Metrics::panics`].
pub panics: u64,
}
impl Metrics {
/// A fresh zeroed metrics set.
pub const fn new() -> Self {
Metrics {
verifications: AtomicU64::new(0),
trusted: AtomicU64::new(0),
valid: AtomicU64::new(0),
invalid: AtomicU64::new(0),
no_manifest: AtomicU64::new(0),
errors: AtomicU64::new(0),
panics: AtomicU64::new(0),
}
}
/// Record a completed verification by its state.
pub fn record_state(&self, state: crate::c2pa_validate::ValidationState) {
self.verifications.fetch_add(1, Ordering::Relaxed);
match state {
crate::c2pa_validate::ValidationState::Trusted => &self.trusted,
crate::c2pa_validate::ValidationState::Valid => &self.valid,
crate::c2pa_validate::ValidationState::Invalid => &self.invalid,
// No provenance: count alongside the other no-manifest outcomes.
crate::c2pa_validate::ValidationState::None => &self.no_manifest,
}
.fetch_add(1, Ordering::Relaxed);
}
/// Record an asset with no manifest.
pub fn record_no_manifest(&self) {
self.no_manifest.fetch_add(1, Ordering::Relaxed);
}
/// Record a verify error.
pub fn record_error(&self) {
self.errors.fetch_add(1, Ordering::Relaxed);
}
/// Record a contained panic.
pub fn record_panic(&self) {
self.panics.fetch_add(1, Ordering::Relaxed);
}
/// Read a consistent-enough snapshot (relaxed; for export, not accounting).
pub fn snapshot(&self) -> MetricsSnapshot {
MetricsSnapshot {
verifications: self.verifications.load(Ordering::Relaxed),
trusted: self.trusted.load(Ordering::Relaxed),
valid: self.valid.load(Ordering::Relaxed),
invalid: self.invalid.load(Ordering::Relaxed),
no_manifest: self.no_manifest.load(Ordering::Relaxed),
errors: self.errors.load(Ordering::Relaxed),
panics: self.panics.load(Ordering::Relaxed),
}
}
}
/// The process-global metrics instance (for hosts that prefer a singleton).
pub fn global() -> &'static Metrics {
static GLOBAL: Metrics = Metrics::new();
&GLOBAL
}
#[cfg(test)]
mod tests {
use super::*;
use crate::c2pa_validate::ValidationState;
#[test]
fn counts_states() {
let m = Metrics::new();
m.record_state(ValidationState::Trusted);
m.record_state(ValidationState::Valid);
m.record_state(ValidationState::Invalid);
m.record_state(ValidationState::Invalid);
m.record_no_manifest();
m.record_error();
m.record_panic();
let s = m.snapshot();
assert_eq!(s.verifications, 4);
assert_eq!(s.trusted, 1);
assert_eq!(s.valid, 1);
assert_eq!(s.invalid, 2);
assert_eq!(s.no_manifest, 1);
assert_eq!(s.errors, 1);
assert_eq!(s.panics, 1);
}
#[test]
fn global_is_shared() {
let before = global().snapshot().verifications;
global().record_state(ValidationState::Valid);
assert_eq!(global().snapshot().verifications, before + 1);
}
}