1use crate::rt::{MaybeSend, MaybeSync};
16
17pub mod pressure;
19
20pub const METRIC_REQUESTS: &str = "mkit_server_requests_total";
22pub const METRIC_LATENCY: &str = "mkit_server_request_duration_ms";
24pub const METRIC_INSPECTION_CALLS: &str = "mkit_server_inspection_calls_total";
26pub const METRIC_UPLOAD_BYTES: &str = "mkit_server_upload_bytes_total";
28pub const METRIC_RELAY_LEASE_LAG: &str = "mkit_server_relay_lease_lag_total";
30pub const METRIC_RELAY_LAG_EXCEEDED: &str = "mkit_server_relay_lag_exceeded_total";
32pub const METRIC_RELAY_BACKLOG_ROWS: &str = "mkit_server_relay_backlog_rows";
34pub const METRIC_INDEX_SLICE_SUBREQUESTS: &str = "mkit_server_index_slice_subrequests";
36pub const METRIC_INDEX_LOOKUP_CAPPED: &str = "mkit_server_index_lookup_capped_total";
38pub const METRIC_REF_POLICY_ANCESTRY_UNCHECKED: &str =
41 "mkit_server_ref_policy_ancestry_unchecked_total";
42pub const METRIC_INDEX_REJECTED_WRITE_FAILED: &str =
44 "mkit_server_index_rejected_write_failed_total";
45pub const METRIC_NAMESPACE_QUOTA_VIEW_FALLBACK: &str =
47 "mkit_server_namespace_quota_view_fallback_total";
48pub const METRIC_NAMESPACE_QUOTA_REBASE: &str = "mkit_server_namespace_quota_rebase_total";
50pub const METRIC_NAMESPACE_QUOTA_ROLLUP_ERROR: &str =
52 "mkit_server_namespace_quota_rollup_error_total";
53
54pub const NEVER_ECHO: &[&str] = &[
57 "authorization",
58 "proxy-authorization",
59 "cookie",
60 "payment-authorization",
61 "payment-signature",
62];
63
64pub 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#[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#[must_use]
88pub fn is_never_log(name: &str) -> bool {
89 NEVER_LOG.iter().any(|n| n.eq_ignore_ascii_case(name))
90}
91
92pub const REDACTED_VALUE: &str = "[redacted]";
94
95#[derive(Debug, Clone, Default)]
99pub struct Redactor {
100 extra: Vec<String>,
101}
102
103impl Redactor {
104 pub fn add_names(&mut self, names: &[String]) {
106 self.extra.extend(names.iter().cloned());
107 }
108 #[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 #[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 #[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
139pub trait Metrics: MaybeSend + MaybeSync {
141 fn incr(&self, name: &'static str, labels: &[(&'static str, &str)], by: u64);
143 fn observe_ms(&self, name: &'static str, labels: &[(&'static str, &str)], ms: f64);
145 fn gauge(&self, _name: &'static str, _labels: &[(&'static str, &str)], _value: f64) {}
147}
148
149#[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 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}