Skip to main content

mkit_server/
telemetry.rs

1//! Metrics facade and logging hygiene (PRD §8 M0: tracing, metrics,
2//! redaction).
3//!
4//! [`Metrics`] is a plain trait rather than a dependency on the `metrics`
5//! crate, so the core stays backend-free on wasm32; the native binary
6//! bridges it to the `metrics` facade (planner default Q20).
7//!
8//! Two header lists govern credentials (SPEC-TRANSPORT-CONNECT §5.1):
9//! [`NEVER_ECHO`] names request credentials a response never carries, and
10//! [`NEVER_LOG`] adds the receipts and signatures that a response may pass
11//! through to the client but that must stay out of logs, traces and error
12//! messages. [`Redactor`] extends [`NEVER_LOG`] with deployment-configured
13//! names, such as the headers an admission helper attaches.
14
15use crate::rt::{MaybeSend, MaybeSync};
16
17/// Physical storage pressure and alert bookkeeping shared by adapters.
18pub mod pressure;
19
20/// Counter: requests handled. Labels: `procedure`, `code`.
21pub const METRIC_REQUESTS: &str = "mkit_server_requests_total";
22/// Histogram: request latency in milliseconds. Labels: `procedure`.
23pub const METRIC_LATENCY: &str = "mkit_server_request_duration_ms";
24/// Counter: synchronous inspector calls. Label: `result`.
25pub const METRIC_INSPECTION_CALLS: &str = "mkit_server_inspection_calls_total";
26/// Counter: accepted upload bytes.
27pub const METRIC_UPLOAD_BYTES: &str = "mkit_server_upload_bytes_total";
28/// Counter: an expired shard lease remains kept beyond the relay lag bound.
29pub const METRIC_RELAY_LEASE_LAG: &str = "mkit_server_relay_lease_lag_total";
30/// Counter: an outbox row exceeded the relay lag bound. Label: `source_kind`.
31pub const METRIC_RELAY_LAG_EXCEEDED: &str = "mkit_server_relay_lag_exceeded_total";
32/// Gauge: source outbox rows inspected in the current relay window.
33pub const METRIC_RELAY_BACKLOG_ROWS: &str = "mkit_server_relay_backlog_rows";
34/// Gauge: subrequests the last scheduled-verification slice spent (WP-4.8).
35pub const METRIC_INDEX_SLICE_SUBREQUESTS: &str = "mkit_server_index_slice_subrequests";
36/// Counter: an object index lookup hit a bounded-work cap. Label: `reason`.
37pub const METRIC_INDEX_LOOKUP_CAPPED: &str = "mkit_server_index_lookup_capped_total";
38/// Counter: a fast-forward check ended unproven (a walk or lookup cap, the
39/// decode budget or a corrupt member) and its write was denied. Label: `reason`.
40pub const METRIC_REF_POLICY_ANCESTRY_UNCHECKED: &str =
41    "mkit_server_ref_policy_ancestry_unchecked_total";
42/// Counter: a content rejection could not be persisted in verification state.
43pub const METRIC_INDEX_REJECTED_WRITE_FAILED: &str =
44    "mkit_server_index_rejected_write_failed_total";
45/// Namespace quota writes admitted with no recent coordinator view.
46pub const METRIC_NAMESPACE_QUOTA_VIEW_FALLBACK: &str =
47    "mkit_server_namespace_quota_view_fallback_total";
48/// Counter: a restored shard's cumulative contribution was re-baselined.
49pub const METRIC_NAMESPACE_QUOTA_REBASE: &str = "mkit_server_namespace_quota_rebase_total";
50/// Counter: a quota rollup failed. Label: `reason`.
51pub const METRIC_NAMESPACE_QUOTA_ROLLUP_ERROR: &str =
52    "mkit_server_namespace_quota_rollup_error_total";
53
54/// Request credentials that are never set on a response, compared ignoring
55/// ASCII case. Every entry is also in [`NEVER_LOG`].
56pub const NEVER_ECHO: &[&str] = &[
57    "authorization",
58    "proxy-authorization",
59    "cookie",
60    "payment-authorization",
61    "payment-signature",
62];
63
64/// Header names whose values never reach a log, trace or error message,
65/// compared ignoring ASCII case: [`NEVER_ECHO`] plus payment receipts, the
66/// auth v2 signature and `Set-Cookie`. A response may still carry the
67/// receipts (SPEC-TRANSPORT-CONNECT §5.1).
68pub const NEVER_LOG: &[&str] = &[
69    "authorization",
70    "proxy-authorization",
71    "cookie",
72    "payment-authorization",
73    "payment-signature",
74    "payment-receipt",
75    "payment-response",
76    "x-signature",
77    "set-cookie",
78];
79
80/// Whether `name` is in [`NEVER_ECHO`], ignoring ASCII case.
81#[must_use]
82pub fn is_never_echo(name: &str) -> bool {
83    NEVER_ECHO.iter().any(|n| n.eq_ignore_ascii_case(name))
84}
85
86/// Whether `name` is in [`NEVER_LOG`], ignoring ASCII case.
87#[must_use]
88pub fn is_never_log(name: &str) -> bool {
89    NEVER_LOG.iter().any(|n| n.eq_ignore_ascii_case(name))
90}
91
92/// The placeholder logged in place of a redacted header value.
93pub const REDACTED_VALUE: &str = "[redacted]";
94
95/// Log redaction policy: [`NEVER_LOG`] plus names the deployment adds, for
96/// example its configured `admission_headers`. The default redacts
97/// [`NEVER_LOG`] only.
98#[derive(Debug, Clone, Default)]
99pub struct Redactor {
100    extra: Vec<String>,
101}
102
103impl Redactor {
104    /// Add deployment-defined credential names to the redaction set.
105    pub fn add_names(&mut self, names: &[String]) {
106        self.extra.extend(names.iter().cloned());
107    }
108    /// A redactor for [`NEVER_LOG`] plus `extra` header names.
109    #[must_use]
110    pub fn new<I, S>(extra: I) -> Self
111    where
112        I: IntoIterator<Item = S>,
113        S: Into<String>,
114    {
115        Self {
116            extra: extra.into_iter().map(Into::into).collect(),
117        }
118    }
119
120    /// Whether the value of header `name` must be redacted, ignoring ASCII
121    /// case.
122    #[must_use]
123    pub fn redacts(&self, name: &str) -> bool {
124        is_never_log(name) || self.extra.iter().any(|n| n.eq_ignore_ascii_case(name))
125    }
126
127    /// The value to log for header `name`: `value` itself, or
128    /// [`REDACTED_VALUE`].
129    #[must_use]
130    pub fn loggable<'a>(&self, name: &str, value: &'a str) -> &'a str {
131        if self.redacts(name) {
132            REDACTED_VALUE
133        } else {
134            value
135        }
136    }
137}
138
139/// Metrics sink. Label values are borrowed so a hot path allocates nothing.
140pub trait Metrics: MaybeSend + MaybeSync {
141    /// Add `by` to the counter `name`.
142    fn incr(&self, name: &'static str, labels: &[(&'static str, &str)], by: u64);
143    /// Record one observation, in milliseconds, in the histogram `name`.
144    fn observe_ms(&self, name: &'static str, labels: &[(&'static str, &str)], ms: f64);
145    /// Set a gauge. Sinks without gauge support may discard it.
146    fn gauge(&self, _name: &'static str, _labels: &[(&'static str, &str)], _value: f64) {}
147}
148
149/// A [`Metrics`] sink that discards everything.
150#[derive(Debug, Default, Clone, Copy)]
151pub struct NoopMetrics;
152
153impl Metrics for NoopMetrics {
154    fn incr(&self, _name: &'static str, _labels: &[(&'static str, &str)], _by: u64) {}
155    fn observe_ms(&self, _name: &'static str, _labels: &[(&'static str, &str)], _ms: f64) {}
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn sensitive_headers_case_insensitive() {
164        for name in NEVER_ECHO {
165            assert!(is_never_echo(name), "{name}");
166            assert!(is_never_echo(&name.to_ascii_uppercase()), "{name}");
167            assert!(NEVER_LOG.contains(name), "{name} must also be NEVER_LOG");
168        }
169        for name in NEVER_LOG {
170            assert!(is_never_log(name), "{name}");
171            assert!(is_never_log(&name.to_ascii_uppercase()), "{name}");
172        }
173        assert!(is_never_echo("Authorization"));
174        assert!(is_never_echo("PAYMENT-SIGNATURE"));
175        // Receipts pass through to the client but never reach a log.
176        for receipt in ["Payment-Receipt", "PAYMENT-RESPONSE"] {
177            assert!(!is_never_echo(receipt), "{receipt}");
178            assert!(is_never_log(receipt), "{receipt}");
179        }
180        assert!(is_never_log("X-Signature") && is_never_log("Set-Cookie"));
181        assert!(!is_never_log("WWW-Authenticate"));
182        assert!(!is_never_log("authorization-hint"));
183        assert!(!is_never_echo(""));
184    }
185
186    #[test]
187    fn redactor_extends_never_log() {
188        let base = Redactor::default();
189        assert!(base.redacts("payment-receipt"));
190        assert!(!base.redacts("X-Admission-Token"));
191        assert_eq!(base.loggable("WWW-Authenticate", "Payment x"), "Payment x");
192
193        let configured = Redactor::new(["x-admission-token"]);
194        assert!(configured.redacts("X-Admission-Token"));
195        assert!(configured.redacts("Cookie"));
196        assert_eq!(
197            configured.loggable("X-ADMISSION-TOKEN", "s3cr3t"),
198            REDACTED_VALUE
199        );
200    }
201
202    #[test]
203    fn noop_metrics_is_object_safe() {
204        let sink: &dyn Metrics = &NoopMetrics;
205        sink.incr(
206            METRIC_REQUESTS,
207            &[("procedure", "UpdateRef"), ("code", "ok")],
208            1,
209        );
210        sink.observe_ms(METRIC_LATENCY, &[("procedure", "UpdateRef")], 1.5);
211    }
212}