Skip to main content

sozu_lib/metrics/
mod.rs

1//! Metrics façade.
2//!
3//! Defines the per-thread `Aggregator`, the `incr!`/`count!`/`gauge!`/
4//! `gauge_add!`/`time!` macros consumed across the lib + bin crates, the
5//! local-vs-network drain dispatch, and the label allow/deny filtering
6//! used to keep cardinality bounded. Gauge underflow is treated as a
7//! correctness bug (saturating clamp + warn log), not a rounding artefact.
8
9mod local_drain;
10pub mod names;
11mod network_drain;
12mod writer;
13
14use std::{
15    cell::RefCell,
16    collections::{BTreeMap, HashMap},
17    io::{self, Write},
18    net::SocketAddr,
19    num::NonZeroU64,
20    str,
21    time::{Duration, Instant},
22};
23
24use mio::net::UdpSocket;
25use sozu_command::config::MetricDetailLevel;
26use sozu_command::proto::command::{
27    FilteredMetrics, MetricsConfiguration, QueryMetricsOptions, ResponseContent,
28};
29
30use crate::metrics::{local_drain::LocalDrain, network_drain::NetworkDrain};
31
32/// Filter `(cluster_id, backend_id)` labels at emission time according to the
33/// configured [`MetricDetailLevel`]. Each level is a SUPERSET of the previous
34/// in cardinality terms:
35///
36/// - `Process`: drop both labels — proxy-only counters (smallest keyspace).
37/// - `Frontend`: same as `Process` today; reserved for the per-listener
38///   counters tracked as a follow-up. Listed in the proto + config so
39///   operators can opt in once per-listener labels land.
40/// - `Cluster`: keep `cluster_id`, drop `backend_id` — current default.
41/// - `Backend`: keep both — current historical behaviour.
42///
43/// Pure function so the filter can be unit-tested exhaustively without the
44/// drain machinery in scope.
45fn filter_labels_for_detail<'a>(
46    detail: MetricDetailLevel,
47    cluster_id: Option<&'a str>,
48    backend_id: Option<&'a str>,
49) -> (Option<&'a str>, Option<&'a str>) {
50    let (out_cluster, out_backend) = match detail {
51        MetricDetailLevel::Process | MetricDetailLevel::Frontend => (None, None),
52        MetricDetailLevel::Cluster => (cluster_id, None),
53        MetricDetailLevel::Backend => (cluster_id, backend_id),
54    };
55    // The filter only ever DROPS labels: an absent input can never become a
56    // present output, and a kept output must be the exact same borrow we
57    // were handed (no synthesised label). This is the cardinality-safety
58    // post-condition that keeps the local CLI view and the wire consistent.
59    debug_assert!(
60        cluster_id.is_some() || out_cluster.is_none(),
61        "filter must never invent a cluster_id"
62    );
63    debug_assert!(
64        backend_id.is_some() || out_backend.is_none(),
65        "filter must never invent a backend_id"
66    );
67    debug_assert!(
68        out_backend.is_none() || out_cluster.is_some(),
69        "a kept backend label implies a kept cluster label (no orphan backend)"
70    );
71    (out_cluster, out_backend)
72}
73
74/// Map a numeric HTTP status code to its dedicated counter name, if any.
75///
76/// Returns `Some("http.status.<code>")` for the eighteen status codes Sōzu
77/// either generates as a default answer or that operators routinely chart
78/// (`200/201/204`, `301/302/304`, `400/401/403/404/408/413/429`, plus the
79/// `5xx` family Sōzu can synthesise — `500/502/503/504/507`). All other
80/// codes return `None` so the bucket counter (`http.status.{1xx,…,other}`)
81/// remains the sole emission and metric cardinality stays bounded.
82///
83/// Hoisted out of the protocol modules so every emission site
84/// (`mux::stream::generate_access_log`, shared by the H1 and H2 mux paths)
85/// cannot drift on which codes get a per-code counter. It was shared with
86/// `kawa_h1::save_http_status_metric` until that unreachable H1 session was
87/// removed on 2026-09-20 (sozu#1346).
88pub(crate) fn http_status_code_metric_name(status: u16) -> Option<&'static str> {
89    match status {
90        200 => Some("http.status.200"),
91        201 => Some("http.status.201"),
92        204 => Some("http.status.204"),
93        301 => Some("http.status.301"),
94        302 => Some("http.status.302"),
95        304 => Some("http.status.304"),
96        400 => Some("http.status.400"),
97        401 => Some("http.status.401"),
98        403 => Some("http.status.403"),
99        404 => Some("http.status.404"),
100        408 => Some("http.status.408"),
101        413 => Some("http.status.413"),
102        429 => Some("http.status.429"),
103        500 => Some("http.status.500"),
104        502 => Some("http.status.502"),
105        503 => Some("http.status.503"),
106        504 => Some("http.status.504"),
107        507 => Some("http.status.507"),
108        _ => None,
109    }
110}
111
112thread_local! {
113  pub static METRICS: RefCell<Aggregator> = RefCell::new(Aggregator::new(String::from("sozu")));
114}
115
116/// Opaque identity of one active lifetime of a cluster metrics row.
117///
118/// A lifetime starts either when a legal route first references a cluster or
119/// when `AddCluster` arrives first. `AddCluster` without an intervening
120/// `RemoveCluster` keeps the identity so gauges opened by the implicit route
121/// remain balanced. A remove followed by another route or add allocates a new
122/// identity, fencing late metrics from the previous lifetime.
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub(crate) struct ClusterMetricsIncarnation(NonZeroU64);
125
126/// Identity state captured by one delayed metric owner.
127///
128/// `Missing` is the default before routing. `Invalid` records the terminal
129/// allocation-exhaustion path. Neither may be mistaken for a legal implicit
130/// cluster lifetime when labels are retained.
131#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
132pub(crate) enum ClusterMetricsCapture {
133    #[default]
134    Missing,
135    Active(ClusterMetricsIncarnation),
136    Invalid,
137}
138
139/// Capture (and, for a legal first implicit reference, allocate) the active
140/// metrics identity for `cluster_id`.
141pub(crate) fn capture_cluster_metrics(cluster_id: &str) -> ClusterMetricsCapture {
142    METRICS.with(|metrics| metrics.borrow_mut().capture_cluster_metrics(cluster_id))
143}
144
145#[derive(thiserror::Error, Debug)]
146pub enum MetricError {
147    #[error("Could not parse udp address {address}: {error}")]
148    WrongUdpAddress { address: String, error: String },
149    #[error("Could not bind to udp address {address}: {error}")]
150    UdpBind { address: String, error: String },
151    #[error("No metrics found for object with id {0}")]
152    NoMetrics(String),
153    #[error("Could not create histogram for time metric {time_metric:?}: {error}")]
154    HistogramCreation {
155        time_metric: MetricValue,
156        error: String,
157    },
158    #[error("could not record time metric {time_metric:?}: {error}")]
159    TimeMetricRecordingError {
160        time_metric: MetricValue,
161        error: String,
162    },
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub enum MetricValue {
167    Gauge(usize),
168    GaugeAdd(i64),
169    Count(i64),
170    Time(usize),
171}
172
173impl MetricValue {
174    fn is_time(&self) -> bool {
175        matches!(self, &MetricValue::Time(_))
176    }
177
178    fn update(&mut self, key: &'static str, m: MetricValue) -> bool {
179        match (self, m) {
180            (&mut MetricValue::Gauge(ref mut v1), MetricValue::Gauge(v2)) => {
181                // `changed` reflects the absolute set: true iff the stored
182                // value actually moves. Pair-assert the post-state matches
183                // the requested value and that `changed` is the equality
184                // relationship between before and after.
185                let before = *v1;
186                let changed = *v1 != v2;
187                *v1 = v2;
188                debug_assert_eq!(*v1, v2, "gauge set must store exactly the requested value");
189                debug_assert_eq!(
190                    changed,
191                    before != v2,
192                    "`changed` must report whether the gauge actually moved"
193                );
194                changed
195            }
196            (&mut MetricValue::Gauge(ref mut v1), MetricValue::GaugeAdd(v2)) => {
197                // Saturating clamp + `error!` log on underflow, symmetric
198                // with `AggregatedMetric::update` in `local_drain.rs`. The
199                // `debug_assert!` that lived here previously made debug
200                // builds panic while release silently clamped; H2
201                // `ConnectionH2::Drop` rebalance + the saturating clamps
202                // make underflow structurally impossible from live code,
203                // and a panic on a metric mismatch is too aggressive — the
204                // log line names the offending key for observability.
205                //
206                // INVARIANT (the canonical gauge-underflow guard): the post
207                // value is non-negative, equals `before + v2` on the
208                // non-underflow path, and is exactly the `0` floor whenever
209                // `before + v2` would have gone negative. We assert the
210                // *post-condition* (never panic the recovery path) rather
211                // than a `before >= -v2` pre-condition, because a real
212                // underflow is recovered (clamp + log) by design.
213                let before = *v1;
214                let changed = v2 != 0;
215                let res = before as i64 + v2;
216                *v1 = if res >= 0 {
217                    res as usize
218                } else {
219                    error!(
220                        "metric {} underflow: previous value: {}, adding: {}",
221                        key, before, v2
222                    );
223                    0
224                };
225                debug_assert!(
226                    res >= 0 || *v1 == 0,
227                    "gauge underflow must clamp to exactly 0 (never wrap)"
228                );
229                debug_assert!(
230                    res < 0 || *v1 as i64 == before as i64 + v2,
231                    "non-underflow gauge add must equal before + v2 exactly"
232                );
233
234                changed
235            }
236            (&mut MetricValue::Count(ref mut v1), MetricValue::Count(v2)) => {
237                // Counter delta: `v2` may be negative (e.g. `decr!`), so the
238                // monotonic-between-resets invariant is encoded as the exact
239                // delta — the post value equals the pre value plus `v2`.
240                let before = *v1;
241                let changed = v2 != 0;
242                *v1 += v2;
243                debug_assert_eq!(*v1, before + v2, "count update must advance by exactly v2");
244                changed
245            }
246            (s, m) => {
247                // Symmetric with `AggregatedMetric::update` in `local_drain.rs`:
248                // a type-mismatch is a coding bug (a `metric_name` was
249                // re-emitted with a different `MetricValue` variant), not a
250                // network-driven event, but log-and-continue is still the
251                // right policy — taking the worker process down on a metric
252                // bookkeeping mismatch is too aggressive for a single-bug
253                // recovery path. The log line names the offending key.
254                error!(
255                    "tried to update metric {} of value {:?} with an incompatible metric: {:?}",
256                    key, s, m
257                );
258                false
259            }
260        }
261    }
262}
263
264#[derive(Debug, Clone)]
265pub struct StoredMetricValue {
266    last_sent: Instant,
267    updated: bool,
268    data: MetricValue,
269}
270
271impl StoredMetricValue {
272    pub fn new(last_sent: Instant, data: MetricValue) -> StoredMetricValue {
273        // Symmetric with `AggregatedMetric::new`: a first emission with a
274        // negative `GaugeAdd` is a coding bug (a `-1` ran with no paired
275        // `+1` earlier in the worker's lifetime); clamp to 0 and log so the
276        // offending pattern shows up in operator logs alongside the
277        // local-drain side.
278        let data = if let MetricValue::GaugeAdd(v) = data {
279            if v >= 0 {
280                MetricValue::Gauge(v as usize)
281            } else {
282                error!(
283                    "stored metric created with negative GaugeAdd({}), clamping to 0",
284                    v
285                );
286                MetricValue::Gauge(0)
287            }
288        } else {
289            data
290        };
291        // Post-conditions: a `GaugeAdd` seed never survives as `GaugeAdd`
292        // (it is normalised to a non-negative `Gauge`), and a fresh stored
293        // value is always flagged `updated` so the next drain emits it.
294        debug_assert!(
295            !matches!(data, MetricValue::GaugeAdd(_)),
296            "GaugeAdd must be normalised to a non-negative Gauge at construction"
297        );
298        StoredMetricValue {
299            last_sent,
300            updated: true,
301            data,
302        }
303    }
304
305    pub fn update(&mut self, key: &'static str, m: MetricValue) {
306        // The `updated` flag is a monotone latch within one drain cycle: it
307        // may only go from false→true here (the drain resets it to false on
308        // emit). So once set, it stays set; and if this update reports a
309        // change, the flag must be set afterwards.
310        let was_updated = self.updated;
311        let updated = self.data.update(key, m);
312        if !self.updated {
313            self.updated = updated;
314        }
315        debug_assert!(
316            self.updated || (!was_updated && !updated),
317            "`updated` must stay set once latched; a reported change must latch it"
318        );
319        debug_assert!(
320            !was_updated || self.updated,
321            "`updated` is a monotone latch within a drain cycle (never cleared here)"
322        );
323    }
324}
325
326pub fn setup<O: Into<String>>(
327    metrics_host: &SocketAddr,
328    origin: O,
329    use_tagged_metrics: bool,
330    prefix: Option<String>,
331    detail: MetricDetailLevel,
332) -> Result<(), MetricError> {
333    let metrics_socket = udp_bind()?;
334
335    debug!(
336        "setting up metrics: local address = {:#?}",
337        metrics_socket.local_addr()
338    );
339
340    METRICS.with(|metrics| {
341        if let Some(p) = prefix {
342            (*metrics.borrow_mut()).set_up_prefix(p);
343        }
344        (*metrics.borrow_mut()).set_up_remote(metrics_socket, *metrics_host);
345        (*metrics.borrow_mut()).set_up_origin(origin.into());
346        (*metrics.borrow_mut()).set_up_tagged_metrics(use_tagged_metrics);
347        (*metrics.borrow_mut()).set_up_detail(detail);
348    });
349    Ok(())
350}
351
352pub trait Subscriber {
353    fn receive_metric(
354        &mut self,
355        label: &'static str,
356        cluster_id: Option<&str>,
357        backend_id: Option<&str>,
358        metric: MetricValue,
359    );
360}
361
362/// How often `lease_tick` actually does work; cheaper than recomputing the
363/// effective level on every metric emission. Polled once per worker event-loop
364/// iteration (which wakes at least once per one-second poll timeout) and on
365/// every worker command, so an expired lease is retired within this interval
366/// plus one poll timeout, with or without traffic or commands.
367const LEASE_TICK_INTERVAL: Duration = Duration::from_secs(5);
368
369/// Hard cap on lease TTL, mirroring the proto comment on `SetMetricDetail`.
370/// Bounds the worst-case effect of a stuck renewer.
371pub const LEASE_TTL_MAX: Duration = Duration::from_secs(300);
372
373/// Default lease TTL applied when the proto request omits `ttl_seconds` (or
374/// passes `0`). Matches the proto comment.
375pub const LEASE_TTL_DEFAULT: Duration = Duration::from_secs(60);
376
377/// Hard cap on the number of simultaneous leases held by the aggregator.
378/// `lease_apply` rejects new entries (with [`LeaseApplyOutcome::TableFull`])
379/// once the table reaches this size. Bounds the lease table's memory and
380/// neutralises the CWE-770 vector where a same-UID attacker rolls
381/// `client_id` faster than expiry to grow the map unbounded. 64 is well
382/// above any plausible TUI fleet (one TUI per operator + a handful of
383/// metric scrapers); legitimate callers renewing the same `client_id`
384/// REPLACE rather than insert and therefore don't bump the count.
385pub const LEASE_TABLE_CAP: usize = 64;
386
387/// Hard cap on `client_id` length accepted by `lease_apply`. The TUI
388/// uses `top:<pid>:<8 hex chars>` ≤ 24 bytes; 64 leaves headroom for
389/// other operator-side scrapers while keeping the lease table's per-
390/// entry memory bounded and the audit-log lease_id column small.
391pub const LEASE_CLIENT_ID_MAX_BYTES: usize = 64;
392
393/// Outcome of [`Aggregator::lease_apply`]. The success arm returns the
394/// `(previous_effective, new_effective)` pair the caller can use to
395/// decide whether to emit a `MetricDetailChanged` audit event; the
396/// failure arms surface bounded-input rejections.
397#[derive(Clone, Copy, Debug, PartialEq, Eq)]
398pub enum LeaseApplyOutcome {
399    /// Lease inserted / renewed.
400    Applied {
401        previous_effective: MetricDetailLevel,
402        new_effective: MetricDetailLevel,
403    },
404    /// `client_id` length exceeds [`LEASE_CLIENT_ID_MAX_BYTES`].
405    ClientIdTooLong,
406    /// Lease table is at [`LEASE_TABLE_CAP`] and the request is a new
407    /// insert (not a renewal). Callers MUST surface this as a `FAILURE`
408    /// to the wire so the lessor can back off.
409    TableFull,
410    /// The requested TTL exceeds [`LEASE_TTL_MAX`]. This arm is
411    /// theoretically unreachable in the normal flow because the
412    /// dispatch site rejects out-of-range values before reaching the
413    /// aggregator; surfacing it explicitly catches callers that bypass
414    /// dispatch (proto fuzzing, future internal use) instead of
415    /// silently clamping their intent.
416    TtlOutOfRange,
417    /// A renewal request was presented with a [`PeerBinding`] that
418    /// disagrees with the existing lease's apply-time binding. Returned
419    /// only when the existing binding is fully known (per
420    /// [`PeerBinding::is_known`]); unknown-binding leases (no
421    /// `SO_PEERCRED` available, pre-binding callers) continue to
422    /// accept any renewer. Closes the same-UID `client_id`-collision
423    /// takeover where an attacker re-applies a lease against a
424    /// victim's id and replaces the binding to lock the victim out of
425    /// their own clear.
426    Unauthorized,
427}
428
429/// Master-populated peer binding stored alongside each lease so subsequent
430/// `clear` requests can be authorised against the apply-time owner. The
431/// binding pairs an OS pid (from `SO_PEERCRED` on Linux, captured by the
432/// master at command-socket accept time) with the master-side connection
433/// session ULID. A clear request must present BOTH values matching the
434/// apply-time binding. A binding of `(None, None)` ("unknown") at apply
435/// time means the connection had no peer credentials available — the
436/// worker then accepts any clear for that `client_id` to preserve compat
437/// with non-Linux callers and intermediate proxies. See the proto comment
438/// on `SetMetricDetail.peer_pid` for the trust model.
439#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
440pub struct PeerBinding {
441    pub pid: Option<i32>,
442    /// Session ULID rendered as a `u128` (the master's session anchor).
443    /// Stored as the raw u128 rather than the original `Ulid` to avoid
444    /// dragging that crate into the metrics-aggregator dependencies.
445    pub session_ulid: Option<u128>,
446}
447
448impl PeerBinding {
449    /// `true` when both halves are known. A fully-known binding is the
450    /// only one against which `clear` may be authorised; partial bindings
451    /// (one half `None`) degrade to "accept any clear" per the proto
452    /// contract on `SetMetricDetail.peer_pid` / `peer_session_ulid`.
453    pub fn is_known(&self) -> bool {
454        self.pid.is_some() && self.session_ulid.is_some()
455    }
456
457    /// True when `self` and `other` are compatible (same `pid` + same
458    /// `session_ulid`) AND `self.is_known()`. Used by `lease_clear` to
459    /// reject mismatched clears.
460    pub fn matches(&self, other: &PeerBinding) -> bool {
461        self.is_known() && self.pid == other.pid && self.session_ulid == other.session_ulid
462    }
463}
464
465/// One lease entry kept inside [`Aggregator::leases`]. Carries the
466/// requested cardinality level, the absolute expiry instant, and the
467/// master-supplied [`PeerBinding`] captured at apply time.
468#[derive(Clone, Copy, Debug)]
469struct LeaseEntry {
470    level: MetricDetailLevel,
471    expires_at: Instant,
472    binding: PeerBinding,
473}
474
475/// Outcome of [`Aggregator::lease_clear`].
476#[derive(Clone, Copy, Debug, PartialEq, Eq)]
477pub enum LeaseClearOutcome {
478    /// The lease was found, the binding matched (or was unknown at apply
479    /// time), and the entry has been removed. Carries the worker's
480    /// previous effective level so the caller can decide whether to
481    /// emit a `MetricDetailChanged` audit event.
482    Cleared {
483        previous_effective: MetricDetailLevel,
484    },
485    /// No lease was held by the requested `client_id`. Silent no-op.
486    NotFound,
487    /// A lease existed but the peer binding presented with the clear did
488    /// not match the apply-time binding. The lease is left intact. The
489    /// worker MUST surface this as a `FAILURE` response to discourage
490    /// guessing attacks against another operator's lease.
491    Unauthorized,
492}
493
494pub struct Aggregator {
495    /// appended to metric keys, usually "sozu-"
496    prefix: String,
497    /// gathers metrics and sends them on a UDP socket
498    network: Option<NetworkDrain>,
499    /// gather metrics locally, queried by the CLI
500    local: LocalDrain,
501    /// Static cardinality knob — set once at boot from `MetricsConfig.detail`.
502    /// Filters `(cluster_id, backend_id)` labels at emission time. Each level
503    /// is a SUPERSET of the previous one (`Process ⊆ Frontend ⊆ Cluster ⊆ Backend`).
504    configured: MetricDetailLevel,
505    /// Effective cardinality knob actually applied at emission. Equal to
506    /// `max(configured, max(active leases))`. Recomputed only when leases
507    /// change, so the hot path (`receive_metric`) reads a single field.
508    effective: MetricDetailLevel,
509    /// Active TTL leases keyed by `client_id` from `SetMetricDetail`. A live
510    /// lease holds the worker's effective level at-or-above its requested
511    /// detail until it expires or is explicitly cleared. Multiple clients
512    /// (e.g. several `sozu top` sessions) lease independently.
513    leases: HashMap<String, LeaseEntry>,
514    /// Wall-clock anchor for the polled lease janitor. Updated on every
515    /// `lease_tick` call regardless of whether expiry happened, so the
516    /// caller's "is it time to tick?" check stays cheap.
517    last_lease_tick: Instant,
518    /// Active lifetime for each cluster id. A legal implicit route allocates
519    /// an entry before `AddCluster`; the add reuses it because only
520    /// `RemoveCluster` is a lifetime boundary. This identity deliberately
521    /// survives metrics collection being disabled or local storage being
522    /// cleared.
523    cluster_incarnations: HashMap<String, ClusterMetricsIncarnation>,
524    /// Last allocated incarnation. Zero is reserved for `Option`'s niche and
525    /// checked arithmetic makes exhaustion fail closed instead of reusing an
526    /// old identity.
527    last_cluster_incarnation: u64,
528}
529
530impl Aggregator {
531    pub fn new(prefix: String) -> Aggregator {
532        let default_detail = MetricDetailLevel::default();
533        Aggregator {
534            prefix: prefix.clone(),
535            network: None,
536            local: LocalDrain::new(prefix),
537            configured: default_detail,
538            effective: default_detail,
539            leases: HashMap::new(),
540            last_lease_tick: Instant::now(),
541            cluster_incarnations: HashMap::new(),
542            last_cluster_incarnation: 0,
543        }
544    }
545
546    pub fn set_up_prefix(&mut self, prefix: String) {
547        self.prefix = prefix;
548    }
549
550    pub fn set_up_remote(&mut self, socket: UdpSocket, addr: SocketAddr) {
551        self.network = Some(NetworkDrain::new(self.prefix.clone(), socket, addr));
552    }
553
554    pub fn set_up_origin(&mut self, origin: String) {
555        if let Some(n) = self.network.as_mut() {
556            n.origin = origin;
557        }
558    }
559
560    pub fn set_up_tagged_metrics(&mut self, tagged: bool) {
561        if let Some(n) = self.network.as_mut() {
562            n.use_tagged_metrics = tagged;
563        }
564    }
565
566    /// Set the static cardinality floor (`MetricsConfig.detail` from the TOML
567    /// configuration). Live leases applied via [`Self::lease_apply`] can
568    /// elevate the effective level at runtime; the configured floor is the
569    /// lower bound the worker falls back to when no lease is active.
570    ///
571    /// See [`MetricDetailLevel`] and `filter_labels_for_detail` for the
572    /// per-level filtering rules.
573    pub fn set_up_detail(&mut self, detail: MetricDetailLevel) {
574        self.configured = detail;
575        self.recompute_effective();
576        // Raising the configured floor can only raise/hold the effective
577        // level; the full invariant set must hold afterwards.
578        debug_assert!(
579            self.effective >= self.configured,
580            "effective must dominate the freshly-set configured floor"
581        );
582        #[cfg(debug_assertions)]
583        self.check_lease_invariants();
584    }
585
586    /// Returns the static (configured) cardinality floor. Independent of
587    /// runtime leases.
588    pub fn detail_configured(&self) -> MetricDetailLevel {
589        self.configured
590    }
591
592    /// Returns the cardinality level actually applied to emissions. Equal to
593    /// `max(configured, max(active leases))`.
594    pub fn detail_effective(&self) -> MetricDetailLevel {
595        self.effective
596    }
597
598    /// Apply or renew a runtime cardinality lease for `client_id`. If a lease
599    /// for the same client already exists it is REPLACED (used for renewals).
600    /// `ttl` values above [`LEASE_TTL_MAX`] are **rejected** with
601    /// [`LeaseApplyOutcome::TtlOutOfRange`] (NOT clamped); callers must
602    /// handle that arm or pre-validate the TTL. On success the call returns
603    /// `(previous_effective, new_effective)` so callers can decide whether
604    /// to emit a `MetricDetailChanged` audit event.
605    ///
606    /// The proto contract on `SetMetricDetail.ttl_seconds` is that the worker
607    /// **rejects** out-of-range values with a `FAILURE` response — that
608    /// enforcement lives at the dispatch site in `lib/src/server.rs::notify`.
609    /// `lease_apply` itself returns [`LeaseApplyOutcome::TtlOutOfRange`]
610    /// when called with `ttl > LEASE_TTL_MAX` so callers that bypass the
611    /// dispatch site (proto fuzzing, future internal use) see a loud
612    /// rejection instead of silently capped semantics. Same shape for
613    /// over-long `client_id` and a full lease table — see
614    /// [`LeaseApplyOutcome`] for the failure arms.
615    ///
616    /// `now` is parameterised exactly like [`Self::lease_tick`] so the whole
617    /// lease lifecycle (apply, tick) is driven off one caller-supplied clock
618    /// reading instead of `Instant::now()` on the datapath — the same
619    /// sans-io shape as `UdpManager::handle_input` — letting a deterministic
620    /// simulation drive `lease_apply` under a virtual clock. `SessionMetrics`
621    /// and the timer wheel elsewhere in this module intentionally keep the
622    /// real clock and are out of scope for this parameterisation.
623    pub fn lease_apply(
624        &mut self,
625        client_id: String,
626        level: MetricDetailLevel,
627        ttl: Duration,
628        binding: PeerBinding,
629        now: Instant,
630    ) -> LeaseApplyOutcome {
631        if client_id.len() > LEASE_CLIENT_ID_MAX_BYTES {
632            return LeaseApplyOutcome::ClientIdTooLong;
633        }
634        if ttl > LEASE_TTL_MAX {
635            return LeaseApplyOutcome::TtlOutOfRange;
636        }
637        // Cap the table size BEFORE the insert, but only when the caller
638        // is inserting a fresh entry. Renewals (same `client_id` already
639        // present) REPLACE the existing entry and therefore keep the
640        // count stable — they must always succeed so an active operator
641        // never loses their lease just because the table is full.
642        let is_renewal = self.leases.contains_key(&client_id);
643        if !is_renewal && self.leases.len() >= LEASE_TABLE_CAP {
644            return LeaseApplyOutcome::TableFull;
645        }
646        // Renewal-binding gate: when a lease already exists for this
647        // `client_id` and its apply-time binding is fully known, the
648        // renewer's presented binding MUST match. Without this check
649        // any same-UID caller that learns the `client_id` (PID from
650        // /proc, suffix from audit log) could re-apply against it,
651        // overwriting the binding to point at the attacker's session
652        // and then driving the victim's Drop-time `clear` into
653        // `Unauthorized`. Unknown apply-time bindings continue to
654        // accept any renewer per the proto contract on
655        // `SetMetricDetail.peer_pid` / `peer_session_ulid`.
656        if is_renewal
657            && let Some(entry) = self.leases.get(&client_id)
658            && entry.binding.is_known()
659            && !entry.binding.matches(&binding)
660        {
661            return LeaseApplyOutcome::Unauthorized;
662        }
663        let expires_at = now + ttl;
664        let before_len = self.leases.len();
665        self.leases.insert(
666            client_id,
667            LeaseEntry {
668                level,
669                expires_at,
670                binding,
671            },
672        );
673        // Map-accounting invariant: a renewal REPLACES in place (count
674        // stable) while a fresh insert grows the table by exactly one. The
675        // table must never exceed the cap on the insert path because the
676        // fresh-insert branch was gated by the `>= LEASE_TABLE_CAP` check
677        // above.
678        debug_assert_eq!(
679            self.leases.len(),
680            before_len + (!is_renewal) as usize,
681            "lease_apply grows the table by exactly one iff this is a fresh insert"
682        );
683        debug_assert!(
684            self.leases.len() <= LEASE_TABLE_CAP,
685            "lease table must never exceed LEASE_TABLE_CAP after an accepted apply"
686        );
687        let previous_effective = self.effective;
688        self.recompute_effective();
689        // A lease is a mechanism for a client to temporarily ELEVATE
690        // cardinality on its own behalf, not a one-way ratchet on the
691        // shared `effective` level: `effective` is, and must stay,
692        // `max(configured, max over live leases)`. A renewal at a LOWER
693        // level than the client's own previous entry is that client
694        // withdrawing part of its own request, so `effective` dropping in
695        // response is the correct outcome -- not an invariant violation.
696        // The only invariant that must hold here is the configured floor,
697        // checked below.
698        debug_assert!(
699            self.effective >= self.configured,
700            "effective must never drop below the configured floor"
701        );
702        #[cfg(debug_assertions)]
703        self.check_lease_invariants();
704        LeaseApplyOutcome::Applied {
705            previous_effective,
706            new_effective: self.effective,
707        }
708    }
709
710    /// Explicitly release a lease keyed by `client_id`. The clear is
711    /// authorised against the apply-time [`PeerBinding`] when one was
712    /// recorded — see [`LeaseClearOutcome`] for the three result states.
713    /// A clear request with `presented = PeerBinding::default()` matches
714    /// only leases whose apply-time binding was also unknown, preserving
715    /// compat with pre-binding callers and platforms without
716    /// `SO_PEERCRED`.
717    pub fn lease_clear(&mut self, client_id: &str, presented: PeerBinding) -> LeaseClearOutcome {
718        let Some(entry) = self.leases.get(client_id) else {
719            return LeaseClearOutcome::NotFound;
720        };
721        // If the apply-time binding is fully known we MUST authorise the
722        // clear against it; presenting `default()` (an empty binding) is
723        // a mismatch. If the apply-time binding is unknown (a pre-binding
724        // caller, or a non-Linux peer with no credentials), we permit
725        // any clear — there is nothing to authorise against.
726        if entry.binding.is_known() && !entry.binding.matches(&presented) {
727            return LeaseClearOutcome::Unauthorized;
728        }
729        // We only reach here after the `get` above proved the key is
730        // present, so the removal evicts exactly one entry.
731        let before_len = self.leases.len();
732        self.leases.remove(client_id);
733        debug_assert!(
734            !self.leases.contains_key(client_id),
735            "lease_clear must evict the cleared client_id"
736        );
737        debug_assert_eq!(
738            self.leases.len(),
739            before_len - 1,
740            "lease_clear removes exactly one entry on the authorised path"
741        );
742        let previous = self.effective;
743        self.recompute_effective();
744        // Clearing a lease can only LOWER or hold the effective level (one
745        // contributor to the max is gone), never raise it.
746        debug_assert!(
747            self.effective <= previous,
748            "lease_clear must not raise the effective detail level"
749        );
750        debug_assert!(
751            self.effective >= self.configured,
752            "effective must never drop below the configured floor"
753        );
754        #[cfg(debug_assertions)]
755        self.check_lease_invariants();
756        LeaseClearOutcome::Cleared {
757            previous_effective: previous,
758        }
759    }
760
761    /// Returns the number of active (non-expired-as-of-last-tick) leases.
762    /// Surfaced in `WorkerMetricDetailStatus` so the TUI can show
763    /// "another client is still leasing this level".
764    pub fn lease_count(&self) -> u32 {
765        self.leases.len() as u32
766    }
767
768    /// Polled lease-expiry janitor. Called from the worker's event loop on
769    /// every iteration and from `notify`, both gated by
770    /// [`Self::lease_tick_due`]; cheap when nothing has expired. Returns
771    /// `Some(previous_effective)` when at least one lease expired AND that
772    /// expiry actually moved the effective level (so the caller can emit a
773    /// `MetricDetailChanged` audit event), or `None` for the no-change path.
774    ///
775    /// `now` is parameterised so unit tests can advance the clock
776    /// deterministically without sleeping.
777    pub fn lease_tick(&mut self, now: Instant) -> Option<MetricDetailLevel> {
778        self.last_lease_tick = now;
779        let before = self.leases.len();
780        self.leases.retain(|_, entry| entry.expires_at > now);
781        // Expiry is a pure shrink: `retain` can only drop entries, never
782        // add them, and every surviving lease must be strictly in the
783        // future relative to `now`.
784        debug_assert!(
785            self.leases.len() <= before,
786            "lease_tick (expiry) can only shrink the table, never grow it"
787        );
788        debug_assert!(
789            self.leases.values().all(|entry| entry.expires_at > now),
790            "every surviving lease must expire strictly after `now`"
791        );
792        if self.leases.len() == before {
793            return None;
794        }
795        let previous = self.effective;
796        self.recompute_effective();
797        // Losing leases can only lower or hold the effective level.
798        debug_assert!(
799            self.effective <= previous,
800            "lease expiry must not raise the effective detail level"
801        );
802        debug_assert!(
803            self.effective >= self.configured,
804            "effective must never drop below the configured floor"
805        );
806        #[cfg(debug_assertions)]
807        self.check_lease_invariants();
808        if previous != self.effective {
809            Some(previous)
810        } else {
811            None
812        }
813    }
814
815    /// True when at least `LEASE_TICK_INTERVAL` has passed since the last
816    /// `lease_tick`. Gates the polled janitor so the worker's event loop
817    /// does not pay a HashMap walk on every iteration.
818    pub fn lease_tick_due(&self, now: Instant) -> bool {
819        now.duration_since(self.last_lease_tick) >= LEASE_TICK_INTERVAL
820    }
821
822    /// Recompute `effective = max(configured, max(active leases))`. Cheap (one
823    /// linear pass over the lease table) and only called on apply/clear/tick,
824    /// never on the metric-emission hot path.
825    fn recompute_effective(&mut self) {
826        let mut max_lease = self.configured;
827        for entry in self.leases.values() {
828            if entry.level > max_lease {
829                max_lease = entry.level;
830            }
831        }
832        self.effective = max_lease;
833        // Post-condition: effective is the floor AND every active lease's
834        // ceiling — i.e. it is at least `configured` and at least each
835        // lease level, and it equals one of those contributors (max).
836        debug_assert!(
837            self.effective >= self.configured,
838            "effective is bounded below by the configured floor"
839        );
840        debug_assert!(
841            self.leases.values().all(|e| self.effective >= e.level),
842            "effective dominates every active lease level"
843        );
844        debug_assert!(
845            self.effective == self.configured
846                || self.leases.values().any(|e| e.level == self.effective),
847            "effective must equal the configured floor or one active lease level"
848        );
849    }
850
851    /// Debug-only full invariant sweep for the lease table, called as a
852    /// run-to-completion post-condition from every lease-mutating method.
853    /// Gathers the cross-field relationships that the individual delta
854    /// assertions cannot express on their own.
855    #[cfg(debug_assertions)]
856    fn check_lease_invariants(&self) {
857        debug_assert!(
858            self.leases.len() <= LEASE_TABLE_CAP,
859            "lease table size must stay within LEASE_TABLE_CAP"
860        );
861        debug_assert!(
862            self.leases
863                .keys()
864                .all(|id| id.len() <= LEASE_CLIENT_ID_MAX_BYTES),
865            "every stored client_id must respect LEASE_CLIENT_ID_MAX_BYTES"
866        );
867        debug_assert!(
868            self.effective >= self.configured,
869            "effective is at least the configured floor"
870        );
871        debug_assert!(
872            self.leases.values().all(|e| self.effective >= e.level),
873            "effective dominates every active lease level"
874        );
875        debug_assert!(
876            self.effective == self.configured
877                || self.leases.values().any(|e| e.level == self.effective),
878            "effective equals the configured floor or some active lease level"
879        );
880    }
881
882    pub fn socket(&self) -> Option<&UdpSocket> {
883        self.network.as_ref().map(|n| &n.remote.get_ref().socket)
884    }
885
886    pub fn socket_mut(&mut self) -> Option<&mut UdpSocket> {
887        self.network
888            .as_mut()
889            .map(|n| &mut n.remote.get_mut().socket)
890    }
891
892    pub fn count_add(&mut self, key: &'static str, count_value: i64) {
893        self.receive_metric(key, None, None, MetricValue::Count(count_value));
894    }
895
896    pub fn set_gauge(&mut self, key: &'static str, gauge_value: usize) {
897        self.receive_metric(key, None, None, MetricValue::Gauge(gauge_value));
898    }
899
900    pub fn gauge_add(&mut self, key: &'static str, gauge_value: i64) {
901        self.receive_metric(key, None, None, MetricValue::GaugeAdd(gauge_value));
902    }
903
904    pub fn writable(&mut self) {
905        if let Some(ref mut net) = self.network.as_mut() {
906            net.writable();
907        }
908    }
909
910    pub fn send_data(&mut self) {
911        if let Some(ref mut net) = self.network.as_mut() {
912            net.send_metrics();
913        }
914    }
915
916    pub fn dump_local_proxy_metrics(&mut self) -> BTreeMap<String, FilteredMetrics> {
917        self.local.dump_proxy_metrics(&Vec::new())
918    }
919
920    pub fn query(&mut self, q: &QueryMetricsOptions) -> Result<ResponseContent, MetricError> {
921        self.local.query(q)
922    }
923
924    pub fn clear_local(&mut self) {
925        if let Some(ref mut net) = self.network.as_mut() {
926            net.clear();
927        }
928        self.local.clear();
929    }
930
931    pub fn configure(&mut self, config: &MetricsConfiguration) {
932        self.local.configure(config);
933    }
934
935    /// Drop all metric storage for a cluster across BOTH drains and arm
936    /// the per-drain tombstone so subsequent emissions for the cluster are
937    /// dropped on the floor instead of resurrecting the row via
938    /// `entry().or_default()`. Called from the worker IPC dispatch on
939    /// [`RequestType::RemoveCluster`](sozu_command::proto::command::request::RequestType::RemoveCluster). Network-side draining the queued
940    /// `MetricLine`s produces immediate silence on the wire (any unsent
941    /// statsd interval for the cluster is discarded).
942    ///
943    /// Worker-event-loop only — `METRICS` is a `thread_local!`, and the
944    /// mutable borrow taken here must not be re-entered from a session-
945    /// bound code path. Calling this from a metrics emission site would
946    /// deadlock the thread-local on `borrow_mut`.
947    pub fn remove_cluster(&mut self, cluster_id: &str) {
948        self.cluster_incarnations.remove(cluster_id);
949        if let Some(ref mut net) = self.network.as_mut() {
950            net.remove_cluster(cluster_id);
951        }
952        self.local.remove_cluster(cluster_id);
953    }
954
955    /// Re-arm a previously-removed cluster id across BOTH drains. Called
956    /// from the worker IPC dispatch on [`RequestType::AddCluster`](sozu_command::proto::command::request::RequestType::AddCluster).
957    /// Idempotent on ids that were never removed. Without this hook a
958    /// cluster removed then re-added would stay tombstoned forever and
959    /// every fresh metric emission would be dropped.
960    pub fn add_cluster(&mut self, cluster_id: &str) {
961        if !self.cluster_incarnations.contains_key(cluster_id) {
962            let ClusterMetricsCapture::Active(incarnation) =
963                self.allocate_cluster_metrics_incarnation(cluster_id)
964            else {
965                return;
966            };
967            self.cluster_incarnations
968                .insert(cluster_id.to_owned(), incarnation);
969        }
970        if let Some(ref mut net) = self.network.as_mut() {
971            net.add_cluster(cluster_id);
972        }
973        self.local.add_cluster(cluster_id);
974    }
975
976    fn allocate_cluster_metrics_incarnation(&mut self, cluster_id: &str) -> ClusterMetricsCapture {
977        let Some(next) = self.last_cluster_incarnation.checked_add(1) else {
978            error!(
979                "cluster metrics incarnation space exhausted; refusing metrics identity for cluster {:?}",
980                cluster_id
981            );
982            return ClusterMetricsCapture::Invalid;
983        };
984        let Some(incarnation) = NonZeroU64::new(next).map(ClusterMetricsIncarnation) else {
985            unreachable!("checked increment from a live u64 incarnation cannot yield zero");
986        };
987        self.last_cluster_incarnation = next;
988        ClusterMetricsCapture::Active(incarnation)
989    }
990
991    /// Capture the active lifetime of `cluster_id`, allocating it when a
992    /// legal frontend/backend route references the id before `AddCluster`.
993    fn capture_cluster_metrics(&mut self, cluster_id: &str) -> ClusterMetricsCapture {
994        if let Some(incarnation) = self.cluster_incarnations.get(cluster_id).copied() {
995            return ClusterMetricsCapture::Active(incarnation);
996        }
997        let capture = self.allocate_cluster_metrics_incarnation(cluster_id);
998        if let ClusterMetricsCapture::Active(incarnation) = capture {
999            self.cluster_incarnations
1000                .insert(cluster_id.to_owned(), incarnation);
1001            // This implicit route starts the replacement lifetime just as an
1002            // AddCluster would. Re-arm both drains so the new identity can
1003            // create its row while the identity gate still rejects owners
1004            // captured before RemoveCluster.
1005            if let Some(ref mut net) = self.network.as_mut() {
1006                net.add_cluster(cluster_id);
1007            }
1008            self.local.add_cluster(cluster_id);
1009        }
1010        capture
1011    }
1012
1013    /// Return the active lifetime of `cluster_id`.
1014    pub(crate) fn cluster_incarnation(
1015        &self,
1016        cluster_id: &str,
1017    ) -> Option<ClusterMetricsIncarnation> {
1018        self.cluster_incarnations.get(cluster_id).copied()
1019    }
1020
1021    /// Whether an emission for `cluster_id` captured with `capture`
1022    /// may reach the drains under the current effective detail.
1023    ///
1024    /// Process and Frontend detail discard cluster/backend labels, so every
1025    /// capture state contributes to that aggregate: a late decrement must
1026    /// balance the earlier increment. Cluster and Backend detail retain the
1027    /// label and therefore require a real captured identity equal to the
1028    /// current active identity. Missing and exhausted captures fail closed.
1029    pub(crate) fn accepts_cluster_capture(
1030        &self,
1031        cluster_id: &str,
1032        capture: ClusterMetricsCapture,
1033    ) -> bool {
1034        if !matches!(
1035            self.effective,
1036            MetricDetailLevel::Cluster | MetricDetailLevel::Backend
1037        ) {
1038            return true;
1039        }
1040        matches!(
1041            capture,
1042            ClusterMetricsCapture::Active(incarnation)
1043                if self.cluster_incarnation(cluster_id) == Some(incarnation)
1044        )
1045    }
1046
1047    /// Receive a metric from an owner that captured a cluster incarnation.
1048    ///
1049    /// The identity check intentionally precedes cardinality filtering. A
1050    /// stale event is rejected when the effective detail retains its cluster
1051    /// label, protecting a same-id replacement row. At `Process` and
1052    /// `Frontend` detail the label is deliberately removed to form a worker
1053    /// aggregate, so the event remains part of that aggregate: while detail
1054    /// stays at that level, an old connection's late `-1` still balances its
1055    /// earlier `+1`. Existing detail transitions do not migrate stored gauge
1056    /// contributions between shapes. See
1057    /// [`Self::accepts_cluster_capture`] for the identity comparison.
1058    pub(crate) fn receive_metric_for_incarnation(
1059        &mut self,
1060        label: &'static str,
1061        cluster_id: Option<&str>,
1062        backend_id: Option<&str>,
1063        capture: ClusterMetricsCapture,
1064        metric: MetricValue,
1065    ) {
1066        if cluster_id.is_none_or(|cluster_id| self.accepts_cluster_capture(cluster_id, capture)) {
1067            self.receive_metric(label, cluster_id, backend_id, metric);
1068        }
1069    }
1070
1071    /// Drop all metric storage for one backend across BOTH drains. Called
1072    /// from the worker IPC dispatch on [`RequestType::RemoveBackend`](sozu_command::proto::command::request::RequestType::RemoveBackend).
1073    /// Does NOT tombstone the cluster (only `remove_cluster` does).
1074    pub fn remove_backend(&mut self, cluster_id: &str, backend_id: &str) {
1075        if let Some(ref mut net) = self.network.as_mut() {
1076            net.remove_backend(cluster_id, backend_id);
1077        }
1078        self.local.remove_backend(cluster_id, backend_id);
1079    }
1080}
1081
1082impl Subscriber for Aggregator {
1083    fn receive_metric(
1084        &mut self,
1085        label: &'static str,
1086        cluster_id: Option<&str>,
1087        backend_id: Option<&str>,
1088        metric: MetricValue,
1089    ) {
1090        // Apply the cardinality knob BEFORE handing the metric to either
1091        // drain. Both drains see the same filtered labels, keeping the local
1092        // CLI view consistent with what statsd receives on the wire. Reads
1093        // `effective` (max of the configured floor and any active leases),
1094        // which is recomputed off the hot path.
1095        let (cluster_id, backend_id) =
1096            filter_labels_for_detail(self.effective, cluster_id, backend_id);
1097        if let Some(ref mut net) = self.network.as_mut() {
1098            net.receive_metric(label, cluster_id, backend_id, metric.to_owned());
1099        }
1100        self.local
1101            .receive_metric(label, cluster_id, backend_id, metric);
1102    }
1103}
1104
1105pub struct MetricSocket {
1106    pub addr: SocketAddr,
1107    pub socket: UdpSocket,
1108}
1109
1110impl Write for MetricSocket {
1111    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1112        self.socket.send_to(buf, self.addr)
1113    }
1114
1115    fn flush(&mut self) -> io::Result<()> {
1116        Ok(())
1117    }
1118}
1119
1120pub fn udp_bind() -> Result<UdpSocket, MetricError> {
1121    let address = "0.0.0.0:0";
1122
1123    let udp_address =
1124        address
1125            .parse::<SocketAddr>()
1126            .map_err(|parse_error| MetricError::WrongUdpAddress {
1127                address: address.to_owned(),
1128                error: parse_error.to_string(),
1129            })?;
1130
1131    UdpSocket::bind(udp_address).map_err(|parse_error| MetricError::UdpBind {
1132        address: udp_address.to_string(),
1133        error: parse_error.to_string(),
1134    })
1135}
1136
1137/// adds a value to a counter
1138#[macro_export]
1139macro_rules! count (
1140  ($key:expr, $value: expr) => ({
1141    let v = $value;
1142    $crate::metrics::METRICS.with(|metrics| {
1143      (*metrics.borrow_mut()).count_add($key, v);
1144    });
1145  })
1146);
1147
1148/// adds 1 to a counter
1149#[macro_export]
1150macro_rules! incr (
1151  ($key:expr) => (count!($key, 1));
1152  ($key:expr, $cluster_id:expr, $backend_id:expr) => {
1153    {
1154        use $crate::metrics::Subscriber;
1155
1156        $crate::metrics::METRICS.with(|metrics| {
1157          (*metrics.borrow_mut()).receive_metric($key, $cluster_id, $backend_id, $crate::metrics::MetricValue::Count(1));
1158        });
1159    }
1160  };
1161  ($key:expr, $cluster_id:expr, $backend_id:expr, $incarnation:expr) => {
1162    {
1163        $crate::metrics::METRICS.with(|metrics| {
1164          (*metrics.borrow_mut()).receive_metric_for_incarnation(
1165            $key,
1166            $cluster_id,
1167            $backend_id,
1168            $incarnation,
1169            $crate::metrics::MetricValue::Count(1),
1170          );
1171        });
1172    }
1173  }
1174);
1175
1176#[macro_export]
1177macro_rules! decr (
1178  ($key:expr) => (count!($key, -1))
1179);
1180
1181#[macro_export]
1182macro_rules! gauge (
1183  ($key:expr, $value: expr) => ({
1184    let v = $value;
1185    $crate::metrics::METRICS.with(|metrics| {
1186      (*metrics.borrow_mut()).set_gauge($key, v);
1187    });
1188  });
1189  ($key:expr, $value:expr, $cluster_id:expr, $backend_id:expr) => {
1190    {
1191        use $crate::metrics::Subscriber;
1192        let v = $value;
1193
1194        $crate::metrics::METRICS.with(|metrics| {
1195          (*metrics.borrow_mut()).receive_metric($key, $cluster_id, $backend_id, $crate::metrics::MetricValue::Gauge(v as usize));
1196        });
1197    }
1198  };
1199  ($key:expr, $value:expr, $cluster_id:expr, $backend_id:expr, $incarnation:expr) => {
1200    {
1201        let v = $value;
1202        $crate::metrics::METRICS.with(|metrics| {
1203          (*metrics.borrow_mut()).receive_metric_for_incarnation(
1204            $key,
1205            $cluster_id,
1206            $backend_id,
1207            $incarnation,
1208            $crate::metrics::MetricValue::Gauge(v as usize),
1209          );
1210        });
1211    }
1212  }
1213);
1214
1215#[macro_export]
1216macro_rules! gauge_add (
1217  ($key:expr, $value: expr) => ({
1218    let v = $value;
1219    $crate::metrics::METRICS.with(|metrics| {
1220      (*metrics.borrow_mut()).gauge_add($key, v);
1221    });
1222  });
1223  ($key:expr, $value:expr, $cluster_id:expr, $backend_id:expr) => {
1224    {
1225        use $crate::metrics::Subscriber;
1226        let v = $value;
1227
1228        $crate::metrics::METRICS.with(|metrics| {
1229          (*metrics.borrow_mut()).receive_metric($key, $cluster_id, $backend_id, $crate::metrics::MetricValue::GaugeAdd(v));
1230        });
1231    }
1232  };
1233  ($key:expr, $value:expr, $cluster_id:expr, $backend_id:expr, $incarnation:expr) => {
1234    {
1235        let v = $value;
1236        $crate::metrics::METRICS.with(|metrics| {
1237          (*metrics.borrow_mut()).receive_metric_for_incarnation(
1238            $key,
1239            $cluster_id,
1240            $backend_id,
1241            $incarnation,
1242            $crate::metrics::MetricValue::GaugeAdd(v),
1243          );
1244        });
1245    }
1246  }
1247);
1248
1249#[macro_export]
1250macro_rules! time (
1251  ($key:expr, $value: expr) => ({
1252    use $crate::metrics::{MetricValue,Subscriber};
1253    let v = $value;
1254    $crate::metrics::METRICS.with(|metrics| {
1255      let m = &mut *metrics.borrow_mut();
1256
1257      m.receive_metric($key, None, None, MetricValue::Time(v as usize));
1258    });
1259  });
1260  ($key:expr, $cluster_id:expr, $value: expr) => ({
1261    use $crate::metrics::{MetricValue,Subscriber};
1262    let v = $value;
1263    $crate::metrics::METRICS.with(|metrics| {
1264      let m = &mut *metrics.borrow_mut();
1265      let cluster: &str = $cluster_id;
1266
1267      m.receive_metric($key, Some(cluster), None, MetricValue::Time(v as usize));
1268    });
1269  });
1270  ($key:expr, $cluster_id:expr, $value:expr, $incarnation:expr) => ({
1271    use $crate::metrics::MetricValue;
1272    let v = $value;
1273    $crate::metrics::METRICS.with(|metrics| {
1274      let m = &mut *metrics.borrow_mut();
1275      let cluster: &str = $cluster_id;
1276
1277      m.receive_metric_for_incarnation(
1278        $key,
1279        Some(cluster),
1280        None,
1281        $incarnation,
1282        MetricValue::Time(v as usize),
1283      );
1284    });
1285  })
1286);
1287
1288#[macro_export]
1289macro_rules! record_backend_metrics (
1290  ($cluster_id:expr, $backend_id:expr, $response_time: expr, $backend_connection_time: expr, $backend_header_time: expr, $bin: expr, $bout: expr) => {
1291    use $crate::metrics::{MetricValue,Subscriber};
1292    $crate::metrics::METRICS.with(|metrics| {
1293      let m = &mut *metrics.borrow_mut();
1294      let cluster_id: &str = $cluster_id;
1295      let backend_id: &str = $backend_id;
1296
1297      m.receive_metric($crate::metrics::names::backend::BYTES_IN, Some(cluster_id), Some(backend_id), MetricValue::Count($bin as i64));
1298      m.receive_metric($crate::metrics::names::backend::BYTES_OUT, Some(cluster_id), Some(backend_id), MetricValue::Count($bout as i64));
1299      m.receive_metric($crate::metrics::names::backend::RESPONSE_TIME, Some(cluster_id), Some(backend_id), MetricValue::Time($response_time as usize));
1300      if let Some(t) = $backend_connection_time {
1301        m.receive_metric($crate::metrics::names::backend::CONNECTION_TIME, Some(cluster_id), Some(backend_id), MetricValue::Time(t.as_millis() as usize));
1302      }
1303      // `Option`, like the connection time and unlike the response time: a
1304      // protocol with no response headers at all (raw TCP) must record
1305      // nothing here rather than a zero.
1306      if let Some(t) = $backend_header_time {
1307        m.receive_metric($crate::metrics::names::backend::HEADER_TIME, Some(cluster_id), Some(backend_id), MetricValue::Time(t.as_millis() as usize));
1308      }
1309
1310      m.receive_metric($crate::metrics::names::backend::REQUESTS, Some(cluster_id), Some(backend_id), MetricValue::Count(1));
1311    });
1312  };
1313  ($cluster_id:expr, $backend_id:expr, $response_time:expr, $backend_connection_time:expr, $backend_header_time:expr, $bin:expr, $bout:expr, $incarnation:expr) => {
1314    use $crate::metrics::{MetricValue,Subscriber};
1315    $crate::metrics::METRICS.with(|metrics| {
1316      let m = &mut *metrics.borrow_mut();
1317      let cluster_id: &str = $cluster_id;
1318      let backend_id: &str = $backend_id;
1319
1320      // One identity lookup gates the whole burst: every emission below
1321      // shares the same cluster and captured incarnation.
1322      if m.accepts_cluster_capture(cluster_id, $incarnation) {
1323        m.receive_metric($crate::metrics::names::backend::BYTES_IN, Some(cluster_id), Some(backend_id), MetricValue::Count($bin as i64));
1324        m.receive_metric($crate::metrics::names::backend::BYTES_OUT, Some(cluster_id), Some(backend_id), MetricValue::Count($bout as i64));
1325        m.receive_metric($crate::metrics::names::backend::RESPONSE_TIME, Some(cluster_id), Some(backend_id), MetricValue::Time($response_time as usize));
1326        if let Some(t) = $backend_connection_time {
1327          m.receive_metric($crate::metrics::names::backend::CONNECTION_TIME, Some(cluster_id), Some(backend_id), MetricValue::Time(t.as_millis() as usize));
1328        }
1329        if let Some(t) = $backend_header_time {
1330          m.receive_metric($crate::metrics::names::backend::HEADER_TIME, Some(cluster_id), Some(backend_id), MetricValue::Time(t.as_millis() as usize));
1331        }
1332
1333        m.receive_metric($crate::metrics::names::backend::REQUESTS, Some(cluster_id), Some(backend_id), MetricValue::Count(1));
1334      }
1335    });
1336  }
1337);
1338
1339#[cfg(test)]
1340mod tests {
1341    use super::*;
1342    use sozu_command::proto::command::filtered_metrics;
1343
1344    #[test]
1345    fn filter_labels_process_drops_both() {
1346        assert_eq!(
1347            filter_labels_for_detail(MetricDetailLevel::Process, Some("c"), Some("b")),
1348            (None, None),
1349        );
1350    }
1351
1352    #[test]
1353    fn filter_labels_frontend_drops_both_today() {
1354        // Reserved for per-listener counters; same as Process for now.
1355        assert_eq!(
1356            filter_labels_for_detail(MetricDetailLevel::Frontend, Some("c"), Some("b")),
1357            (None, None),
1358        );
1359    }
1360
1361    #[test]
1362    fn filter_labels_cluster_keeps_cluster_drops_backend() {
1363        assert_eq!(
1364            filter_labels_for_detail(MetricDetailLevel::Cluster, Some("c"), Some("b")),
1365            (Some("c"), None),
1366        );
1367    }
1368
1369    #[test]
1370    fn cluster_incarnation_fences_labelled_rows_and_preserves_process_aggregates() {
1371        let mut aggregator = Aggregator::new(String::new());
1372        aggregator.set_up_detail(MetricDetailLevel::Backend);
1373        aggregator.add_cluster("reused");
1374        let old_token = aggregator
1375            .cluster_incarnation("reused")
1376            .expect("configured cluster has an identity");
1377        let old = ClusterMetricsCapture::Active(old_token);
1378
1379        // Neither a live AddCluster update nor local collection controls end
1380        // the configured cluster lifetime.
1381        aggregator.add_cluster("reused");
1382        assert_eq!(aggregator.cluster_incarnation("reused"), Some(old_token));
1383        aggregator.configure(&MetricsConfiguration::Disabled);
1384        aggregator.configure(&MetricsConfiguration::Enabled);
1385        aggregator.configure(&MetricsConfiguration::Clear);
1386        assert_eq!(aggregator.cluster_incarnation("reused"), Some(old_token));
1387
1388        aggregator.remove_cluster("reused");
1389        aggregator.add_cluster("reused");
1390        let replacement_token = aggregator
1391            .cluster_incarnation("reused")
1392            .expect("replacement cluster has an identity");
1393        let replacement = ClusterMetricsCapture::Active(replacement_token);
1394        assert_ne!(replacement_token, old_token);
1395
1396        // At Backend detail an obsolete event is rejected instead of being
1397        // applied to the replacement row.
1398        aggregator.receive_metric_for_incarnation(
1399            "incarnation_test",
1400            Some("reused"),
1401            Some("same-backend"),
1402            old,
1403            MetricValue::Count(7),
1404        );
1405        assert!(
1406            !aggregator
1407                .local
1408                .dump_cluster_metrics(&[])
1409                .expect("dump cluster metrics after stale emission")
1410                .contains_key("reused"),
1411            "a labelled stale event must not recreate the replacement row",
1412        );
1413
1414        // The same gate must still accept an event from the active lifetime.
1415        aggregator.receive_metric_for_incarnation(
1416            "incarnation_test",
1417            Some("reused"),
1418            Some("same-backend"),
1419            replacement,
1420            MetricValue::Count(3),
1421        );
1422        let labelled = aggregator
1423            .local
1424            .dump_cluster_metrics(&[])
1425            .expect("dump active cluster metrics");
1426        let active_backend = labelled["reused"]
1427            .backends
1428            .iter()
1429            .find(|backend| backend.backend_id == "same-backend")
1430            .expect("active backend metric row");
1431        assert_eq!(
1432            active_backend
1433                .metrics
1434                .get("incarnation_test")
1435                .and_then(|metric| metric.inner.as_ref()),
1436            Some(&filtered_metrics::Inner::Count(3)),
1437        );
1438
1439        // Process detail deliberately aggregates cluster/backend events by
1440        // removing their labels. Both old and current work contribute while
1441        // the effective detail remains at that level.
1442        aggregator.set_up_detail(MetricDetailLevel::Process);
1443        aggregator.receive_metric_for_incarnation(
1444            "incarnation_test",
1445            Some("reused"),
1446            Some("same-backend"),
1447            old,
1448            MetricValue::Count(7),
1449        );
1450        aggregator.receive_metric_for_incarnation(
1451            "incarnation_test",
1452            Some("reused"),
1453            Some("same-backend"),
1454            replacement,
1455            MetricValue::Count(3),
1456        );
1457        assert_eq!(
1458            aggregator
1459                .dump_local_proxy_metrics()
1460                .get("incarnation_test")
1461                .and_then(|metric| metric.inner.as_ref()),
1462            Some(&filtered_metrics::Inner::Count(10)),
1463        );
1464
1465        // A true proxy metric has no cluster identity and is independent of
1466        // the incarnation gate at every detail level.
1467        aggregator.receive_metric("direct_proxy_test", None, None, MetricValue::Count(5));
1468        assert_eq!(
1469            aggregator
1470                .dump_local_proxy_metrics()
1471                .get("direct_proxy_test")
1472                .and_then(|metric| metric.inner.as_ref()),
1473            Some(&filtered_metrics::Inner::Count(5)),
1474        );
1475
1476        // Exhaustion never wraps to a previously issued identity and never
1477        // re-arms the drain tombstone.
1478        let mut exhausted = Aggregator::new(String::new());
1479        exhausted.last_cluster_incarnation = u64::MAX;
1480        exhausted.add_cluster("exhausted");
1481        assert_eq!(exhausted.cluster_incarnation("exhausted"), None);
1482    }
1483
1484    /// Value of `key` in the local drain for `cluster_id`: the backend row
1485    /// when `backend_id` is given, the cluster row otherwise.
1486    fn local_labelled_value(
1487        aggregator: &mut Aggregator,
1488        cluster_id: &str,
1489        backend_id: Option<&str>,
1490        key: &str,
1491    ) -> Option<filtered_metrics::Inner> {
1492        let dump = aggregator
1493            .local
1494            .dump_cluster_metrics(&[])
1495            .expect("dump cluster metrics");
1496        let row = dump.get(cluster_id)?;
1497        let metrics = match backend_id {
1498            Some(backend_id) => {
1499                &row.backends
1500                    .iter()
1501                    .find(|backend| backend.backend_id == backend_id)?
1502                    .metrics
1503            }
1504            None => &row.cluster,
1505        };
1506        metrics.get(key)?.inner.clone()
1507    }
1508
1509    #[test]
1510    fn add_without_remove_keeps_implicit_identity_and_balances_labelled_gauge() {
1511        let mut aggregator = Aggregator::new(String::new());
1512        aggregator.set_up_detail(MetricDetailLevel::Backend);
1513        let capture = aggregator.capture_cluster_metrics("implicit-then-added");
1514        let ClusterMetricsCapture::Active(token) = capture else {
1515            panic!("implicit route must allocate an active identity");
1516        };
1517        aggregator.receive_metric_for_incarnation(
1518            "implicit_balance",
1519            Some("implicit-then-added"),
1520            Some("backend"),
1521            capture,
1522            MetricValue::GaugeAdd(1),
1523        );
1524
1525        aggregator.add_cluster("implicit-then-added");
1526        assert_eq!(
1527            aggregator.cluster_incarnation("implicit-then-added"),
1528            Some(token),
1529        );
1530        aggregator.receive_metric_for_incarnation(
1531            "implicit_balance",
1532            Some("implicit-then-added"),
1533            Some("backend"),
1534            capture,
1535            MetricValue::GaugeAdd(-1),
1536        );
1537
1538        assert_eq!(
1539            local_labelled_value(
1540                &mut aggregator,
1541                "implicit-then-added",
1542                Some("backend"),
1543                "implicit_balance",
1544            ),
1545            Some(filtered_metrics::Inner::Gauge(0)),
1546        );
1547    }
1548
1549    #[test]
1550    fn implicit_cluster_capture_is_recorded_at_process_detail() {
1551        // A frontend may route to a cluster that never received AddCluster;
1552        // its owners capture a valid implicit identity. The label-stripped
1553        // worker aggregate must still count those events.
1554        let mut aggregator = Aggregator::new(String::new());
1555        aggregator.set_up_detail(MetricDetailLevel::Process);
1556        let capture = aggregator.capture_cluster_metrics("undeclared");
1557        aggregator.receive_metric_for_incarnation(
1558            "undeclared_test",
1559            Some("undeclared"),
1560            Some("backend"),
1561            capture,
1562            MetricValue::Count(4),
1563        );
1564        assert_eq!(
1565            aggregator
1566                .dump_local_proxy_metrics()
1567                .get("undeclared_test")
1568                .and_then(|metric| metric.inner.as_ref()),
1569            Some(&filtered_metrics::Inner::Count(4)),
1570        );
1571    }
1572
1573    #[test]
1574    fn implicit_cluster_capture_is_recorded_at_labelled_detail() {
1575        for detail in [MetricDetailLevel::Cluster, MetricDetailLevel::Backend] {
1576            let mut aggregator = Aggregator::new(String::new());
1577            aggregator.set_up_detail(detail);
1578            let capture = aggregator.capture_cluster_metrics("undeclared");
1579            assert!(matches!(capture, ClusterMetricsCapture::Active(_)));
1580            aggregator.receive_metric_for_incarnation(
1581                "undeclared_test",
1582                Some("undeclared"),
1583                Some("backend"),
1584                capture,
1585                MetricValue::Count(4),
1586            );
1587            let backend_id = match detail {
1588                MetricDetailLevel::Backend => Some("backend"),
1589                _ => None,
1590            };
1591            let recorded =
1592                local_labelled_value(&mut aggregator, "undeclared", backend_id, "undeclared_test");
1593            assert_eq!(
1594                recorded,
1595                Some(filtered_metrics::Inner::Count(4)),
1596                "the active implicit identity must pass the gate at {detail:?}",
1597            );
1598        }
1599    }
1600
1601    #[test]
1602    fn mismatched_incarnation_is_dropped_at_labelled_detail() {
1603        for detail in [MetricDetailLevel::Cluster, MetricDetailLevel::Backend] {
1604            let mut aggregator = Aggregator::new(String::new());
1605            aggregator.set_up_detail(detail);
1606
1607            // Captured Some(I1), current Some(I2).
1608            aggregator.add_cluster("c");
1609            let first = aggregator
1610                .cluster_incarnation("c")
1611                .expect("first configured identity");
1612            aggregator.remove_cluster("c");
1613            aggregator.add_cluster("c");
1614            let second = aggregator
1615                .cluster_incarnation("c")
1616                .expect("replacement configured identity");
1617            assert_ne!(first, second);
1618            aggregator.receive_metric_for_incarnation(
1619                "mismatch_test",
1620                Some("c"),
1621                Some("b"),
1622                ClusterMetricsCapture::Active(first),
1623                MetricValue::Count(1),
1624            );
1625
1626            // A missing capture never adopts the current lifetime.
1627            aggregator.receive_metric_for_incarnation(
1628                "mismatch_test",
1629                Some("c"),
1630                Some("b"),
1631                ClusterMetricsCapture::Missing,
1632                MetricValue::Count(1),
1633            );
1634            assert!(
1635                !aggregator
1636                    .local
1637                    .dump_cluster_metrics(&[])
1638                    .expect("dump cluster metrics")
1639                    .contains_key("c"),
1640                "stale or missing capture must not reach the Some(I2) row at {detail:?}",
1641            );
1642
1643            // Captured I1, current identity absent under this id.
1644            aggregator.receive_metric_for_incarnation(
1645                "mismatch_test",
1646                Some("other"),
1647                Some("b"),
1648                ClusterMetricsCapture::Active(first),
1649                MetricValue::Count(1),
1650            );
1651            assert!(
1652                !aggregator
1653                    .local
1654                    .dump_cluster_metrics(&[])
1655                    .expect("dump cluster metrics")
1656                    .contains_key("other"),
1657                "Some(I1) must not reach an undeclared (None) cluster at {detail:?}",
1658            );
1659        }
1660    }
1661
1662    #[test]
1663    fn removed_implicit_capture_stays_rejected_after_clear() {
1664        let mut aggregator = Aggregator::new(String::new());
1665        aggregator.set_up_detail(MetricDetailLevel::Backend);
1666
1667        // A legal route allocates an implicit identity before this cluster
1668        // has ever received AddCluster.
1669        let captured_before_add = aggregator.capture_cluster_metrics("reviewer-reused");
1670        let ClusterMetricsCapture::Active(implicit_token) = captured_before_add else {
1671            panic!("a legal implicit route must capture an active identity");
1672        };
1673
1674        aggregator.add_cluster("reviewer-reused");
1675        assert_eq!(
1676            aggregator.cluster_incarnation("reviewer-reused"),
1677            Some(implicit_token),
1678            "AddCluster without RemoveCluster keeps the implicit lifetime",
1679        );
1680        aggregator.remove_cluster("reviewer-reused");
1681
1682        // Clear is a supported metrics operation. It clears drain storage,
1683        // including tombstones, but it must not make a removed lifetime's
1684        // earlier identity become current again.
1685        aggregator.configure(&MetricsConfiguration::Clear);
1686        aggregator.receive_metric_for_incarnation(
1687            "reviewer_late_close",
1688            Some("reviewer-reused"),
1689            Some("backend"),
1690            captured_before_add,
1691            MetricValue::Count(1),
1692        );
1693
1694        assert!(
1695            !aggregator
1696                .local
1697                .dump_cluster_metrics(&[])
1698                .expect("dump metrics after late removed emission")
1699                .contains_key("reviewer-reused"),
1700            "a removed lifetime must stay fenced after Clear removes the drain tombstone",
1701        );
1702
1703        let replacement = aggregator.capture_cluster_metrics("reviewer-reused");
1704        let ClusterMetricsCapture::Active(replacement_token) = replacement else {
1705            panic!("a route after removal must allocate a replacement identity");
1706        };
1707        assert_ne!(replacement_token, implicit_token);
1708        aggregator.add_cluster("reviewer-reused");
1709        assert_eq!(
1710            aggregator.cluster_incarnation("reviewer-reused"),
1711            Some(replacement_token),
1712        );
1713        aggregator.receive_metric_for_incarnation(
1714            "reviewer_replacement",
1715            Some("reviewer-reused"),
1716            Some("backend"),
1717            captured_before_add,
1718            MetricValue::Count(1),
1719        );
1720        aggregator.receive_metric_for_incarnation(
1721            "reviewer_replacement",
1722            Some("reviewer-reused"),
1723            Some("backend"),
1724            replacement,
1725            MetricValue::Count(2),
1726        );
1727        assert_eq!(
1728            local_labelled_value(
1729                &mut aggregator,
1730                "reviewer-reused",
1731                Some("backend"),
1732                "reviewer_replacement",
1733            ),
1734            Some(filtered_metrics::Inner::Count(2)),
1735            "only the post-remove identity may reach the replacement row",
1736        );
1737    }
1738
1739    #[test]
1740    fn implicit_route_after_remove_rearms_drains_without_add_cluster() {
1741        let mut aggregator = Aggregator::new(String::new());
1742        aggregator.set_up_detail(MetricDetailLevel::Backend);
1743
1744        let first = aggregator.capture_cluster_metrics("implicit-replacement");
1745        aggregator.receive_metric_for_incarnation(
1746            "implicit_replacement",
1747            Some("implicit-replacement"),
1748            Some("backend"),
1749            first,
1750            MetricValue::Count(1),
1751        );
1752        aggregator.remove_cluster("implicit-replacement");
1753
1754        // Frontends and backends may legally recreate this id without an
1755        // AddCluster. The first new route is therefore both the identity and
1756        // drain-lifetime boundary for the replacement.
1757        let replacement = aggregator.capture_cluster_metrics("implicit-replacement");
1758        assert_ne!(replacement, first);
1759        aggregator.receive_metric_for_incarnation(
1760            "implicit_replacement",
1761            Some("implicit-replacement"),
1762            Some("backend"),
1763            replacement,
1764            MetricValue::Count(2),
1765        );
1766        aggregator.receive_metric_for_incarnation(
1767            "implicit_replacement",
1768            Some("implicit-replacement"),
1769            Some("backend"),
1770            first,
1771            MetricValue::Count(4),
1772        );
1773
1774        assert_eq!(
1775            local_labelled_value(
1776                &mut aggregator,
1777                "implicit-replacement",
1778                Some("backend"),
1779                "implicit_replacement",
1780            ),
1781            Some(filtered_metrics::Inner::Count(2)),
1782            "the replacement implicit lifetime must be visible while the removed lifetime stays fenced",
1783        );
1784    }
1785
1786    #[test]
1787    fn exhausted_add_is_not_an_implicit_identity() {
1788        let mut aggregator = Aggregator::new(String::new());
1789        aggregator.set_up_detail(MetricDetailLevel::Backend);
1790        aggregator.last_cluster_incarnation = u64::MAX;
1791        aggregator.add_cluster("reviewer-exhausted");
1792        assert_eq!(aggregator.cluster_incarnation("reviewer-exhausted"), None);
1793
1794        aggregator.receive_metric_for_incarnation(
1795            "reviewer_exhausted",
1796            Some("reviewer-exhausted"),
1797            Some("backend"),
1798            ClusterMetricsCapture::Invalid,
1799            MetricValue::Count(1),
1800        );
1801
1802        assert!(
1803            !aggregator
1804                .local
1805                .dump_cluster_metrics(&[])
1806                .expect("dump metrics after exhausted emission")
1807                .contains_key("reviewer-exhausted"),
1808            "allocation exhaustion must fail closed instead of sharing the undeclared identity",
1809        );
1810    }
1811
1812    #[test]
1813    fn exhausted_implicit_capture_does_not_rearm_removed_drains() {
1814        let mut aggregator = Aggregator::new(String::new());
1815        aggregator.set_up_detail(MetricDetailLevel::Backend);
1816        assert!(matches!(
1817            aggregator.capture_cluster_metrics("exhausted-after-remove"),
1818            ClusterMetricsCapture::Active(_),
1819        ));
1820        aggregator.remove_cluster("exhausted-after-remove");
1821        aggregator.last_cluster_incarnation = u64::MAX;
1822
1823        let exhausted = aggregator.capture_cluster_metrics("exhausted-after-remove");
1824        assert_eq!(exhausted, ClusterMetricsCapture::Invalid);
1825        aggregator.receive_metric_for_incarnation(
1826            "exhausted_after_remove",
1827            Some("exhausted-after-remove"),
1828            Some("backend"),
1829            exhausted,
1830            MetricValue::Count(1),
1831        );
1832        // Probe the drain directly as well: the identity-aware call above is
1833        // rejected before dispatch and therefore cannot prove that allocation
1834        // exhaustion left the RemoveCluster tombstone armed.
1835        aggregator.receive_metric(
1836            "exhausted_after_remove_probe",
1837            Some("exhausted-after-remove"),
1838            Some("backend"),
1839            MetricValue::Count(1),
1840        );
1841
1842        assert!(
1843            !aggregator
1844                .local
1845                .dump_cluster_metrics(&[])
1846                .expect("dump metrics after exhausted implicit capture")
1847                .contains_key("exhausted-after-remove"),
1848            "allocation exhaustion must leave the removed drain tombstone armed",
1849        );
1850    }
1851
1852    #[test]
1853    fn every_capture_state_keeps_process_and_frontend_aggregates() {
1854        for detail in [MetricDetailLevel::Process, MetricDetailLevel::Frontend] {
1855            let mut aggregator = Aggregator::new(String::new());
1856            aggregator.set_up_detail(detail);
1857            let active = aggregator.capture_cluster_metrics("reviewer-undeclared");
1858            for capture in [
1859                active,
1860                ClusterMetricsCapture::Missing,
1861                ClusterMetricsCapture::Invalid,
1862            ] {
1863                aggregator.receive_metric_for_incarnation(
1864                    "reviewer_unconfigured_aggregate",
1865                    Some("reviewer-undeclared"),
1866                    Some("backend"),
1867                    capture,
1868                    MetricValue::Count(1),
1869                );
1870            }
1871            assert_eq!(
1872                aggregator
1873                    .dump_local_proxy_metrics()
1874                    .get("reviewer_unconfigured_aggregate")
1875                    .and_then(|metric| metric.inner.as_ref()),
1876                Some(&filtered_metrics::Inner::Count(3)),
1877                "identity state cannot suppress a label-free {detail:?} aggregate",
1878            );
1879        }
1880    }
1881
1882    #[test]
1883    fn filter_labels_backend_keeps_both() {
1884        assert_eq!(
1885            filter_labels_for_detail(MetricDetailLevel::Backend, Some("c"), Some("b")),
1886            (Some("c"), Some("b")),
1887        );
1888    }
1889
1890    #[test]
1891    fn filter_labels_none_in_none_out() {
1892        // Absent labels stay absent regardless of detail level — the filter
1893        // never invents a label, only drops.
1894        for detail in [
1895            MetricDetailLevel::Process,
1896            MetricDetailLevel::Frontend,
1897            MetricDetailLevel::Cluster,
1898            MetricDetailLevel::Backend,
1899        ] {
1900            assert_eq!(filter_labels_for_detail(detail, None, None), (None, None));
1901        }
1902    }
1903
1904    #[test]
1905    fn aggregator_default_detail_is_cluster() {
1906        // Pre-knob behaviour preserved: if a worker / process never calls
1907        // `set_up_detail`, cluster-scoped metrics still reach the drains.
1908        let agg = Aggregator::new("sozu".to_owned());
1909        assert_eq!(agg.detail_configured(), MetricDetailLevel::Cluster);
1910        assert_eq!(agg.detail_effective(), MetricDetailLevel::Cluster);
1911        assert_eq!(agg.lease_count(), 0);
1912    }
1913
1914    /// Fully-known binding used by tests that don't otherwise care about the
1915    /// peer-binding mechanic. Two distinct values (`OWNER_*` / `OTHER_*`)
1916    /// let `lease_clear` tests assert authorised vs unauthorised paths.
1917    fn owner_binding() -> PeerBinding {
1918        PeerBinding {
1919            pid: Some(1234),
1920            session_ulid: Some(0x0123_4567_89ab_cdef_0123_4567_89ab_cdefu128),
1921        }
1922    }
1923
1924    fn other_binding() -> PeerBinding {
1925        PeerBinding {
1926            pid: Some(5678),
1927            session_ulid: Some(0xfedc_ba98_7654_3210_fedc_ba98_7654_3210u128),
1928        }
1929    }
1930
1931    /// Extract `(previous_effective, new_effective)` from a successful
1932    /// `lease_apply`; panics on any failure arm so tests that don't care
1933    /// about the failure paths stay compact.
1934    fn unwrap_applied(outcome: LeaseApplyOutcome) -> (MetricDetailLevel, MetricDetailLevel) {
1935        match outcome {
1936            LeaseApplyOutcome::Applied {
1937                previous_effective,
1938                new_effective,
1939            } => (previous_effective, new_effective),
1940            other => panic!("expected LeaseApplyOutcome::Applied, got {other:?}"),
1941        }
1942    }
1943
1944    #[test]
1945    fn lease_apply_elevates_effective_above_configured() {
1946        // Configured floor stays at Cluster; a lease for Backend lifts the
1947        // effective level until the lease expires or is cleared.
1948        let mut agg = Aggregator::new("sozu".to_owned());
1949        agg.set_up_detail(MetricDetailLevel::Cluster);
1950        let (prev, new) = unwrap_applied(agg.lease_apply(
1951            "test:1".to_owned(),
1952            MetricDetailLevel::Backend,
1953            Duration::from_secs(60),
1954            PeerBinding::default(),
1955            Instant::now(),
1956        ));
1957        assert_eq!(prev, MetricDetailLevel::Cluster);
1958        assert_eq!(new, MetricDetailLevel::Backend);
1959        assert_eq!(agg.detail_configured(), MetricDetailLevel::Cluster);
1960        assert_eq!(agg.detail_effective(), MetricDetailLevel::Backend);
1961        assert_eq!(agg.lease_count(), 1);
1962    }
1963
1964    #[test]
1965    fn lease_apply_below_configured_does_not_lower_effective() {
1966        // A lease can only ELEVATE the floor, never push below `configured`.
1967        let mut agg = Aggregator::new("sozu".to_owned());
1968        agg.set_up_detail(MetricDetailLevel::Backend);
1969        let (prev, new) = unwrap_applied(agg.lease_apply(
1970            "test:1".to_owned(),
1971            MetricDetailLevel::Cluster,
1972            Duration::from_secs(60),
1973            PeerBinding::default(),
1974            Instant::now(),
1975        ));
1976        assert_eq!(prev, MetricDetailLevel::Backend);
1977        assert_eq!(new, MetricDetailLevel::Backend);
1978    }
1979
1980    #[test]
1981    fn lease_apply_rejects_client_id_over_cap() {
1982        // Defence-in-depth: even if dispatch lets a too-long id through,
1983        // the aggregator refuses to store it.
1984        let mut agg = Aggregator::new("sozu".to_owned());
1985        let too_long = "x".repeat(LEASE_CLIENT_ID_MAX_BYTES + 1);
1986        assert_eq!(
1987            agg.lease_apply(
1988                too_long,
1989                MetricDetailLevel::Backend,
1990                Duration::from_secs(60),
1991                PeerBinding::default(),
1992                Instant::now(),
1993            ),
1994            LeaseApplyOutcome::ClientIdTooLong
1995        );
1996        assert_eq!(agg.lease_count(), 0);
1997    }
1998
1999    #[test]
2000    fn lease_apply_rejects_when_table_is_full() {
2001        // Fill the table to capacity with distinct client_ids; one more
2002        // insert is refused. A RENEWAL of an existing entry must still
2003        // succeed (replaces in place, count unchanged).
2004        let mut agg = Aggregator::new("sozu".to_owned());
2005        let now = Instant::now();
2006        for i in 0..LEASE_TABLE_CAP {
2007            assert!(matches!(
2008                agg.lease_apply(
2009                    format!("client:{i:02}"),
2010                    MetricDetailLevel::Backend,
2011                    Duration::from_secs(60),
2012                    PeerBinding::default(),
2013                    now,
2014                ),
2015                LeaseApplyOutcome::Applied { .. }
2016            ));
2017        }
2018        assert_eq!(agg.lease_count() as usize, LEASE_TABLE_CAP);
2019        // New distinct client → rejected.
2020        assert_eq!(
2021            agg.lease_apply(
2022                "newcomer".to_owned(),
2023                MetricDetailLevel::Backend,
2024                Duration::from_secs(60),
2025                PeerBinding::default(),
2026                now,
2027            ),
2028            LeaseApplyOutcome::TableFull,
2029        );
2030        assert_eq!(agg.lease_count() as usize, LEASE_TABLE_CAP);
2031        // Renewal of an existing entry → still accepted.
2032        assert!(matches!(
2033            agg.lease_apply(
2034                "client:00".to_owned(),
2035                MetricDetailLevel::Backend,
2036                Duration::from_secs(120),
2037                PeerBinding::default(),
2038                now,
2039            ),
2040            LeaseApplyOutcome::Applied { .. }
2041        ));
2042        assert_eq!(agg.lease_count() as usize, LEASE_TABLE_CAP);
2043    }
2044
2045    #[test]
2046    fn lease_apply_rejects_ttl_over_max() {
2047        // The aggregator no longer silently clamps oversize TTLs.
2048        let mut agg = Aggregator::new("sozu".to_owned());
2049        assert_eq!(
2050            agg.lease_apply(
2051                "client:0".to_owned(),
2052                MetricDetailLevel::Backend,
2053                LEASE_TTL_MAX + Duration::from_secs(1),
2054                PeerBinding::default(),
2055                Instant::now(),
2056            ),
2057            LeaseApplyOutcome::TtlOutOfRange,
2058        );
2059        assert_eq!(agg.lease_count(), 0);
2060    }
2061
2062    #[test]
2063    fn lease_apply_renewal_replaces_previous_for_same_client() {
2064        // The renewer client re-sends with the same `client_id`; the entry
2065        // is REPLACED (not duplicated). Lease count stays at 1.
2066        // Unknown bindings on both sides skip the renewal-binding gate.
2067        let mut agg = Aggregator::new("sozu".to_owned());
2068        let now = Instant::now();
2069        let _ = agg.lease_apply(
2070            "renewer".to_owned(),
2071            MetricDetailLevel::Backend,
2072            Duration::from_secs(30),
2073            PeerBinding::default(),
2074            now,
2075        );
2076        let _ = agg.lease_apply(
2077            "renewer".to_owned(),
2078            MetricDetailLevel::Backend,
2079            Duration::from_secs(60),
2080            PeerBinding::default(),
2081            now,
2082        );
2083        assert_eq!(agg.lease_count(), 1);
2084    }
2085
2086    #[test]
2087    fn lease_apply_renewal_at_lower_level_lowers_effective() {
2088        // Issue #1408: a client holding the current maximum renews its own
2089        // lease at a LOWER level. A lease only ever elevates cardinality on
2090        // the client's own behalf, so a client giving part of that back is
2091        // withdrawing its own request, not violating a shared invariant.
2092        // `effective` must recompute to max(configured, max over live
2093        // leases) exactly like any other apply -- dropping here is the
2094        // correct outcome, not a bug.
2095        let mut agg = Aggregator::new("sozu".to_owned());
2096        agg.set_up_detail(MetricDetailLevel::Process);
2097        let now = Instant::now();
2098        let (_, new) = unwrap_applied(agg.lease_apply(
2099            "renewer".to_owned(),
2100            MetricDetailLevel::Backend,
2101            Duration::from_secs(60),
2102            PeerBinding::default(),
2103            now,
2104        ));
2105        assert_eq!(new, MetricDetailLevel::Backend);
2106        assert_eq!(agg.detail_effective(), MetricDetailLevel::Backend);
2107        // Same client_id renews at a strictly lower level.
2108        let (prev, new) = unwrap_applied(agg.lease_apply(
2109            "renewer".to_owned(),
2110            MetricDetailLevel::Frontend,
2111            Duration::from_secs(60),
2112            PeerBinding::default(),
2113            now,
2114        ));
2115        assert_eq!(prev, MetricDetailLevel::Backend);
2116        assert_eq!(new, MetricDetailLevel::Frontend);
2117        assert_eq!(agg.detail_effective(), MetricDetailLevel::Frontend);
2118        assert_eq!(agg.lease_count(), 1);
2119    }
2120
2121    #[test]
2122    fn lease_apply_renewal_rejects_foreign_binding() {
2123        // Same-UID `client_id` collision attack: the victim applies with
2124        // a known binding; an attacker that learns the `client_id`
2125        // attempts to renew under a different (pid, session_ulid). The
2126        // renewal must be refused so the victim's apply-time binding
2127        // remains the sole authoritative owner — both for subsequent
2128        // renewals AND for the victim's own Drop-time `clear`.
2129        let mut agg = Aggregator::new("sozu".to_owned());
2130        let now = Instant::now();
2131        let victim = PeerBinding {
2132            pid: Some(4242),
2133            session_ulid: Some(0x0123_4567_89AB_CDEF_FEDC_BA98_7654_3210),
2134        };
2135        let outcome = agg.lease_apply(
2136            "topcli".to_owned(),
2137            MetricDetailLevel::Backend,
2138            Duration::from_secs(60),
2139            victim,
2140            now,
2141        );
2142        assert!(
2143            matches!(outcome, LeaseApplyOutcome::Applied { .. }),
2144            "victim's initial apply must succeed"
2145        );
2146        let attacker = PeerBinding {
2147            pid: Some(9999),
2148            session_ulid: Some(0xDEAD_BEEF_DEAD_BEEF_DEAD_BEEF_DEAD_BEEF),
2149        };
2150        let outcome = agg.lease_apply(
2151            "topcli".to_owned(),
2152            MetricDetailLevel::Backend,
2153            Duration::from_secs(60),
2154            attacker,
2155            now,
2156        );
2157        assert_eq!(
2158            outcome,
2159            LeaseApplyOutcome::Unauthorized,
2160            "renewal with a mismatched known binding must be refused"
2161        );
2162        // The victim can still clear their own lease — proof the
2163        // refused attempt did not corrupt the stored binding.
2164        let clear = agg.lease_clear("topcli", victim);
2165        assert!(
2166            matches!(clear, LeaseClearOutcome::Cleared { .. }),
2167            "victim's original binding must still clear cleanly after \
2168             the foreign-binding renewal was refused"
2169        );
2170    }
2171
2172    #[test]
2173    fn lease_apply_renewal_with_matching_binding_succeeds() {
2174        // Symmetry case: the legitimate owner re-applies with the same
2175        // (pid, session_ulid). The renewal must succeed so the TUI's
2176        // own renewer thread keeps the lease alive across its TTL.
2177        let mut agg = Aggregator::new("sozu".to_owned());
2178        let now = Instant::now();
2179        let owner = PeerBinding {
2180            pid: Some(1234),
2181            session_ulid: Some(0xAAAA_BBBB_CCCC_DDDD_EEEE_FFFF_0000_1111),
2182        };
2183        let _ = agg.lease_apply(
2184            "topcli".to_owned(),
2185            MetricDetailLevel::Backend,
2186            Duration::from_secs(30),
2187            owner,
2188            now,
2189        );
2190        let outcome = agg.lease_apply(
2191            "topcli".to_owned(),
2192            MetricDetailLevel::Backend,
2193            Duration::from_secs(60),
2194            owner,
2195            now,
2196        );
2197        assert!(
2198            matches!(outcome, LeaseApplyOutcome::Applied { .. }),
2199            "renewal with matching binding must succeed (otherwise the \
2200             TUI's own renewer thread would be locked out)"
2201        );
2202    }
2203
2204    #[test]
2205    fn lease_apply_max_merge_two_clients() {
2206        // Two clients, two levels: effective = max(both leases, configured).
2207        // Use `Process` as the floor so the Frontend lease is observable
2208        // after the Backend lease is cleared (otherwise the configured
2209        // Cluster floor would mask the Frontend lease).
2210        let mut agg = Aggregator::new("sozu".to_owned());
2211        agg.set_up_detail(MetricDetailLevel::Process);
2212        let now = Instant::now();
2213        let _ = agg.lease_apply(
2214            "scraper".to_owned(),
2215            MetricDetailLevel::Frontend,
2216            Duration::from_secs(60),
2217            PeerBinding::default(),
2218            now,
2219        );
2220        let _ = agg.lease_apply(
2221            "topcli".to_owned(),
2222            MetricDetailLevel::Backend,
2223            Duration::from_secs(60),
2224            PeerBinding::default(),
2225            now,
2226        );
2227        assert_eq!(agg.detail_effective(), MetricDetailLevel::Backend);
2228        assert_eq!(agg.lease_count(), 2);
2229        // Clearing the higher lease drops effective back to the lower one.
2230        let outcome = agg.lease_clear("topcli", PeerBinding::default());
2231        assert_eq!(
2232            outcome,
2233            LeaseClearOutcome::Cleared {
2234                previous_effective: MetricDetailLevel::Backend,
2235            }
2236        );
2237        assert_eq!(agg.detail_effective(), MetricDetailLevel::Frontend);
2238        assert_eq!(agg.lease_count(), 1);
2239    }
2240
2241    #[test]
2242    fn lease_clear_unknown_id_is_silent_noop() {
2243        // Mismatched IDs are silently ignored (other clients' leases unaffected).
2244        let mut agg = Aggregator::new("sozu".to_owned());
2245        let _ = agg.lease_apply(
2246            "real".to_owned(),
2247            MetricDetailLevel::Backend,
2248            Duration::from_secs(60),
2249            PeerBinding::default(),
2250            Instant::now(),
2251        );
2252        assert_eq!(
2253            agg.lease_clear("ghost", PeerBinding::default()),
2254            LeaseClearOutcome::NotFound
2255        );
2256        assert_eq!(agg.detail_effective(), MetricDetailLevel::Backend);
2257        assert_eq!(agg.lease_count(), 1);
2258    }
2259
2260    #[test]
2261    fn lease_clear_with_matching_binding_authorised() {
2262        // Apply with a known binding, clear with the same binding -> Cleared.
2263        let mut agg = Aggregator::new("sozu".to_owned());
2264        let _ = agg.lease_apply(
2265            "owner-lease".to_owned(),
2266            MetricDetailLevel::Backend,
2267            Duration::from_secs(60),
2268            owner_binding(),
2269            Instant::now(),
2270        );
2271        let outcome = agg.lease_clear("owner-lease", owner_binding());
2272        assert!(matches!(outcome, LeaseClearOutcome::Cleared { .. }));
2273        assert_eq!(agg.lease_count(), 0);
2274    }
2275
2276    #[test]
2277    fn lease_clear_with_mismatched_binding_is_unauthorized() {
2278        // Apply with one binding, attempt clear with a different binding ->
2279        // Unauthorized; lease left intact.
2280        let mut agg = Aggregator::new("sozu".to_owned());
2281        let _ = agg.lease_apply(
2282            "owner-lease".to_owned(),
2283            MetricDetailLevel::Backend,
2284            Duration::from_secs(60),
2285            owner_binding(),
2286            Instant::now(),
2287        );
2288        let outcome = agg.lease_clear("owner-lease", other_binding());
2289        assert_eq!(outcome, LeaseClearOutcome::Unauthorized);
2290        assert_eq!(agg.lease_count(), 1);
2291    }
2292
2293    #[test]
2294    fn lease_clear_unknown_apply_binding_accepts_any_clear() {
2295        // Pre-binding / non-Linux apply -> any clear authorised.
2296        let mut agg = Aggregator::new("sozu".to_owned());
2297        let _ = agg.lease_apply(
2298            "legacy".to_owned(),
2299            MetricDetailLevel::Backend,
2300            Duration::from_secs(60),
2301            PeerBinding::default(),
2302            Instant::now(),
2303        );
2304        let outcome = agg.lease_clear("legacy", owner_binding());
2305        assert!(matches!(outcome, LeaseClearOutcome::Cleared { .. }));
2306        assert_eq!(agg.lease_count(), 0);
2307    }
2308
2309    #[test]
2310    fn lease_clear_known_apply_rejects_default_clear() {
2311        // Known apply binding -> a default ("unknown") clear is rejected.
2312        let mut agg = Aggregator::new("sozu".to_owned());
2313        let _ = agg.lease_apply(
2314            "owner-lease".to_owned(),
2315            MetricDetailLevel::Backend,
2316            Duration::from_secs(60),
2317            owner_binding(),
2318            Instant::now(),
2319        );
2320        let outcome = agg.lease_clear("owner-lease", PeerBinding::default());
2321        assert_eq!(outcome, LeaseClearOutcome::Unauthorized);
2322    }
2323
2324    #[test]
2325    fn lease_tick_expires_only_past_due_leases() {
2326        // `lease_tick(now)` parameterises the clock so we can test expiry
2327        // without sleeping. Setup: one lease past due, one still active.
2328        // Use `Process` as the floor so the surviving Frontend lease drives
2329        // the effective level after the Backend lease expires (otherwise
2330        // the Cluster floor would mask it).
2331        let mut agg = Aggregator::new("sozu".to_owned());
2332        agg.set_up_detail(MetricDetailLevel::Process);
2333        let now = Instant::now();
2334        // Inject directly into the table to control expires_at deterministically.
2335        agg.leases.insert(
2336            "expired".to_owned(),
2337            LeaseEntry {
2338                level: MetricDetailLevel::Backend,
2339                expires_at: now - Duration::from_secs(1),
2340                binding: PeerBinding::default(),
2341            },
2342        );
2343        agg.leases.insert(
2344            "live".to_owned(),
2345            LeaseEntry {
2346                level: MetricDetailLevel::Frontend,
2347                expires_at: now + Duration::from_secs(60),
2348                binding: PeerBinding::default(),
2349            },
2350        );
2351        agg.recompute_effective();
2352        assert_eq!(agg.detail_effective(), MetricDetailLevel::Backend);
2353        let prev = agg.lease_tick(now);
2354        assert_eq!(prev, Some(MetricDetailLevel::Backend));
2355        assert_eq!(agg.detail_effective(), MetricDetailLevel::Frontend);
2356        assert_eq!(agg.lease_count(), 1);
2357    }
2358
2359    #[test]
2360    fn lease_tick_no_change_returns_none() {
2361        // No leases -> no-op tick, no audit signal.
2362        let mut agg = Aggregator::new("sozu".to_owned());
2363        assert!(agg.lease_tick(Instant::now()).is_none());
2364    }
2365
2366    #[test]
2367    fn lease_apply_at_max_ttl_succeeds() {
2368        // Boundary: exactly LEASE_TTL_MAX is allowed; LEASE_TTL_MAX + 1ns is
2369        // rejected (covered by lease_apply_rejects_ttl_over_max above). With
2370        // `now` injected (rather than read from the host clock) the expiry
2371        // is checked for EXACT equality to `now + LEASE_TTL_MAX`, not a
2372        // fuzzy upper bound — the whole point of parameterising the clock.
2373        let mut agg = Aggregator::new("sozu".to_owned());
2374        let now = Instant::now();
2375        let outcome = agg.lease_apply(
2376            "max".to_owned(),
2377            MetricDetailLevel::Backend,
2378            LEASE_TTL_MAX,
2379            PeerBinding::default(),
2380            now,
2381        );
2382        assert!(matches!(outcome, LeaseApplyOutcome::Applied { .. }));
2383        let entry = agg.leases.get("max").unwrap();
2384        assert_eq!(entry.expires_at, now + LEASE_TTL_MAX);
2385    }
2386}