1mod 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
32fn 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 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
74pub(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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub(crate) struct ClusterMetricsIncarnation(NonZeroU64);
125
126#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
132pub(crate) enum ClusterMetricsCapture {
133 #[default]
134 Missing,
135 Active(ClusterMetricsIncarnation),
136 Invalid,
137}
138
139pub(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 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 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 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 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 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 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 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
362const LEASE_TICK_INTERVAL: Duration = Duration::from_secs(5);
368
369pub const LEASE_TTL_MAX: Duration = Duration::from_secs(300);
372
373pub const LEASE_TTL_DEFAULT: Duration = Duration::from_secs(60);
376
377pub const LEASE_TABLE_CAP: usize = 64;
386
387pub const LEASE_CLIENT_ID_MAX_BYTES: usize = 64;
392
393#[derive(Clone, Copy, Debug, PartialEq, Eq)]
398pub enum LeaseApplyOutcome {
399 Applied {
401 previous_effective: MetricDetailLevel,
402 new_effective: MetricDetailLevel,
403 },
404 ClientIdTooLong,
406 TableFull,
410 TtlOutOfRange,
417 Unauthorized,
427}
428
429#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
440pub struct PeerBinding {
441 pub pid: Option<i32>,
442 pub session_ulid: Option<u128>,
446}
447
448impl PeerBinding {
449 pub fn is_known(&self) -> bool {
454 self.pid.is_some() && self.session_ulid.is_some()
455 }
456
457 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#[derive(Clone, Copy, Debug)]
469struct LeaseEntry {
470 level: MetricDetailLevel,
471 expires_at: Instant,
472 binding: PeerBinding,
473}
474
475#[derive(Clone, Copy, Debug, PartialEq, Eq)]
477pub enum LeaseClearOutcome {
478 Cleared {
483 previous_effective: MetricDetailLevel,
484 },
485 NotFound,
487 Unauthorized,
492}
493
494pub struct Aggregator {
495 prefix: String,
497 network: Option<NetworkDrain>,
499 local: LocalDrain,
501 configured: MetricDetailLevel,
505 effective: MetricDetailLevel,
509 leases: HashMap<String, LeaseEntry>,
514 last_lease_tick: Instant,
518 cluster_incarnations: HashMap<String, ClusterMetricsIncarnation>,
524 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 pub fn set_up_detail(&mut self, detail: MetricDetailLevel) {
574 self.configured = detail;
575 self.recompute_effective();
576 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 pub fn detail_configured(&self) -> MetricDetailLevel {
589 self.configured
590 }
591
592 pub fn detail_effective(&self) -> MetricDetailLevel {
595 self.effective
596 }
597
598 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 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 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 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 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 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 entry.binding.is_known() && !entry.binding.matches(&presented) {
727 return LeaseClearOutcome::Unauthorized;
728 }
729 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 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 pub fn lease_count(&self) -> u32 {
765 self.leases.len() as u32
766 }
767
768 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 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 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 pub fn lease_tick_due(&self, now: Instant) -> bool {
819 now.duration_since(self.last_lease_tick) >= LEASE_TICK_INTERVAL
820 }
821
822 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 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 #[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 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 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 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 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 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 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 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 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 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#[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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let mut agg = Aggregator::new("sozu".to_owned());
2332 agg.set_up_detail(MetricDetailLevel::Process);
2333 let now = Instant::now();
2334 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 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 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}