1use std::collections::HashMap;
9use std::hash::Hash;
10use std::sync::Arc;
11
12use bytes::Bytes;
13use orbit_core::{Fleet, NetId64, NodeId};
14pub use orbit_core::{OrbitTyped, RingSpec, RingTopology};
15
16pub const METRIC_SNAPSHOT_RING_CAPACITY: usize =
22 orbit_core::compile::usize_from_env(
23 option_env!("ORBIT_METRIC_SNAPSHOT_RING_CAPACITY"),
24 1_024,
25 );
26
27pub const KEYED_METRIC_RING_CAPACITY: usize =
33 orbit_core::compile::usize_from_env(
34 option_env!("ORBIT_KEYED_METRIC_RING_CAPACITY"),
35 4_096,
36 );
37
38const _: () = assert!(METRIC_SNAPSHOT_RING_CAPACITY.is_power_of_two());
39const _: () = assert!(KEYED_METRIC_RING_CAPACITY.is_power_of_two());
40
41pub trait OrbitMetricSnapshot: OrbitTyped + Sized {
47 const FAMILY: &'static str;
49
50 fn node_id(&self) -> u16;
52
53 fn captured_at_unix_secs(&self) -> u64;
55
56 fn encode(&self) -> Result<Vec<u8>, String>;
58
59 fn decode(bytes: &[u8]) -> Result<Self, String>;
61}
62
63pub trait OrbitMetricKeyedSnapshot: OrbitMetricSnapshot {
69 type Key: Eq + Hash;
70
71 fn metric_key(&self) -> Self::Key;
72}
73
74#[derive(Clone, Debug)]
76pub struct OrbitMetricSample<T> {
77 pub id: NetId64,
78 pub snapshot: T,
79}
80
81impl<T: OrbitMetricSnapshot> OrbitMetricSample<T> {
82 pub fn node_id(&self) -> u16 {
83 self.snapshot.node_id()
84 }
85
86 pub fn captured_at_unix_secs(&self) -> u64 {
87 self.snapshot.captured_at_unix_secs()
88 }
89
90 pub fn age_secs(
91 &self,
92 now_unix_secs: u64,
93 ) -> u64 {
94 now_unix_secs.saturating_sub(self.captured_at_unix_secs())
95 }
96
97 pub fn is_fresh(
98 &self,
99 now_unix_secs: u64,
100 max_age_secs: u64,
101 ) -> bool {
102 self.age_secs(now_unix_secs) <= max_age_secs
103 }
104}
105
106#[derive(Clone)]
108pub struct OrbitMetricFamily<T: OrbitMetricSnapshot> {
109 fleet: Arc<Fleet>,
110 _t: std::marker::PhantomData<T>,
111}
112
113impl<T: OrbitMetricSnapshot> OrbitMetricFamily<T> {
114 pub fn new(fleet: Arc<Fleet>) -> Self {
115 Self { fleet, _t: std::marker::PhantomData }
116 }
117
118 pub fn publisher(&self) -> OrbitMetricPublisher<T> {
119 OrbitMetricPublisher { family: self.clone() }
120 }
121
122 pub fn collector(&self) -> OrbitMetricCollector<T> {
123 OrbitMetricCollector { family: self.clone() }
124 }
125}
126
127#[derive(Clone)]
130pub struct OrbitMetricPublisher<T: OrbitMetricSnapshot> {
131 family: OrbitMetricFamily<T>,
132}
133
134impl<T: OrbitMetricSnapshot> OrbitMetricPublisher<T> {
135 pub fn new(fleet: Arc<Fleet>) -> Self {
136 Self { family: OrbitMetricFamily::new(fleet) }
137 }
138
139 pub fn publish(
143 &self,
144 snapshot: &T,
145 ) -> Result<NetId64, String> {
146 let payload = snapshot.encode()?;
147 if payload.len() > T::RING_SPEC.payload_capacity {
148 return Err(format!(
149 "orbit metrics payload too large for {}: {} > {}",
150 T::FAMILY,
151 payload.len(),
152 T::RING_SPEC.payload_capacity
153 ));
154 }
155 Ok(self.family.fleet.publish::<T>(
156 0,
157 snapshot.captured_at_unix_secs(),
158 Bytes::from(payload),
159 ))
160 }
161}
162
163#[derive(Clone)]
166pub struct OrbitMetricCollector<T: OrbitMetricSnapshot> {
167 family: OrbitMetricFamily<T>,
168}
169
170impl<T: OrbitMetricSnapshot> OrbitMetricCollector<T> {
171 pub fn new(fleet: Arc<Fleet>) -> Self {
172 Self { family: OrbitMetricFamily::new(fleet) }
173 }
174
175 pub fn latest_by_node(&self) -> HashMap<u16, OrbitMetricSample<T>> {
179 if T::RING_SPEC.topology == RingTopology::PerNode {
180 return self.latest_by_node_lanes();
181 }
182 let head = self.family.fleet.head::<T>();
183 if head == 0 {
184 return HashMap::new();
185 }
186
187 let capacity = self.family.fleet.ring_capacity::<T>() as u64;
188 let walk_count = head.min(capacity);
189 let mut samples = HashMap::new();
190 let expected_nodes = usize::from(self.family.fleet.fleet_capacity());
191
192 for i in 0..walk_count {
193 let counter = head - 1 - i;
194 let Some(frame) = self.family.fleet.read_at::<T>(counter) else {
195 if counter == 0 {
196 break;
197 }
198 continue;
199 };
200 let Ok(snapshot) = T::decode(&frame.payload) else {
201 if counter == 0 {
202 break;
203 }
204 continue;
205 };
206 samples
207 .entry(snapshot.node_id())
208 .or_insert(OrbitMetricSample { id: frame.id, snapshot });
209 if expected_nodes > 0 && samples.len() >= expected_nodes {
210 break;
211 }
212 if counter == 0 {
213 break;
214 }
215 }
216
217 samples
218 }
219
220 pub fn latest_by_key<K>(&self) -> HashMap<K, OrbitMetricSample<T>>
223 where
224 T: OrbitMetricKeyedSnapshot<Key = K>,
225 K: Eq + Hash,
226 {
227 if T::RING_SPEC.topology == RingTopology::PerNode {
228 return self.latest_by_key_lanes();
229 }
230 let head = self.family.fleet.head::<T>();
231 if head == 0 {
232 return HashMap::new();
233 }
234
235 let capacity = self.family.fleet.ring_capacity::<T>() as u64;
236 let walk_count = head.min(capacity);
237 let mut samples = HashMap::new();
238
239 for i in 0..walk_count {
240 let counter = head - 1 - i;
241 let Some(frame) = self.family.fleet.read_at::<T>(counter) else {
242 if counter == 0 {
243 break;
244 }
245 continue;
246 };
247 let Ok(snapshot) = T::decode(&frame.payload) else {
248 if counter == 0 {
249 break;
250 }
251 continue;
252 };
253 samples
254 .entry(snapshot.metric_key())
255 .or_insert(OrbitMetricSample { id: frame.id, snapshot });
256 if counter == 0 {
257 break;
258 }
259 }
260
261 samples
262 }
263
264 fn latest_by_node_lanes(&self) -> HashMap<u16, OrbitMetricSample<T>> {
265 let capacity = self.family.fleet.ring_capacity::<T>() as u64;
266 let mut samples = HashMap::new();
267
268 for node in 0..self.family.fleet.fleet_capacity() {
269 let node_id = NodeId::new(node);
270 let head = self.family.fleet.lane_head::<T>(node_id);
271 let walk_count = head.min(capacity);
272 for offset in 0..walk_count {
273 let counter = head - 1 - offset;
274 let Some(frame) =
275 self.family.fleet.read_lane_at::<T>(node_id, counter)
276 else {
277 continue;
278 };
279 let Ok(snapshot) = T::decode(&frame.payload) else {
280 continue;
281 };
282 if snapshot.node_id() != node {
283 continue;
284 }
285 samples.insert(
286 snapshot.node_id(),
287 OrbitMetricSample { id: frame.id, snapshot },
288 );
289 break;
290 }
291 }
292
293 samples
294 }
295
296 fn latest_by_key_lanes<K>(&self) -> HashMap<K, OrbitMetricSample<T>>
297 where
298 T: OrbitMetricKeyedSnapshot<Key = K>,
299 K: Eq + Hash,
300 {
301 let capacity = self.family.fleet.ring_capacity::<T>() as u64;
302 let mut samples = HashMap::new();
303
304 for node in 0..self.family.fleet.fleet_capacity() {
305 let node_id = NodeId::new(node);
306 let head = self.family.fleet.lane_head::<T>(node_id);
307 let walk_count = head.min(capacity);
308 for offset in 0..walk_count {
309 let counter = head - 1 - offset;
310 let Some(frame) =
311 self.family.fleet.read_lane_at::<T>(node_id, counter)
312 else {
313 continue;
314 };
315 let Ok(snapshot) = T::decode(&frame.payload) else {
316 continue;
317 };
318 let key = snapshot.metric_key();
319 let candidate = OrbitMetricSample { id: frame.id, snapshot };
320 match samples.entry(key) {
321 std::collections::hash_map::Entry::Vacant(entry) => {
322 entry.insert(candidate);
323 }
324 std::collections::hash_map::Entry::Occupied(mut entry)
325 if sample_is_newer(&candidate, entry.get()) =>
326 {
327 entry.insert(candidate);
328 }
329 std::collections::hash_map::Entry::Occupied(_) => {}
330 }
331 }
332 }
333
334 samples
335 }
336
337 pub fn fresh_by_key<K>(
340 &self,
341 now_unix_secs: u64,
342 max_age_secs: u64,
343 ) -> HashMap<K, OrbitMetricSample<T>>
344 where
345 T: OrbitMetricKeyedSnapshot<Key = K>,
346 K: Eq + Hash,
347 {
348 self.latest_by_key()
349 .into_iter()
350 .filter(|(_, sample)| sample.is_fresh(now_unix_secs, max_age_secs))
351 .collect()
352 }
353
354 pub fn fresh_by_node(
357 &self,
358 now_unix_secs: u64,
359 max_age_secs: u64,
360 ) -> HashMap<u16, OrbitMetricSample<T>> {
361 self.latest_by_node()
362 .into_iter()
363 .filter(|(_, sample)| sample.is_fresh(now_unix_secs, max_age_secs))
364 .collect()
365 }
366}
367
368fn sample_is_newer<T: OrbitMetricSnapshot>(
369 candidate: &OrbitMetricSample<T>,
370 current: &OrbitMetricSample<T>,
371) -> bool {
372 (
373 candidate.captured_at_unix_secs(),
374 candidate.id.node(),
375 candidate.id.counter(),
376 ) > (
377 current.captured_at_unix_secs(),
378 current.id.node(),
379 current.id.counter(),
380 )
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[derive(Clone, Debug, PartialEq, Eq)]
388 struct TestSnapshot {
389 node: u16,
390 captured_at: u64,
391 value: u64,
392 }
393
394 impl OrbitTyped for TestSnapshot {
395 const KIND: u8 = 211;
396 const RING_SPEC: RingSpec =
397 RingSpec::new(METRIC_SNAPSHOT_RING_CAPACITY, 18);
398 }
399
400 impl OrbitMetricSnapshot for TestSnapshot {
401 const FAMILY: &'static str = "test";
402
403 fn node_id(&self) -> u16 {
404 self.node
405 }
406
407 fn captured_at_unix_secs(&self) -> u64 {
408 self.captured_at
409 }
410
411 fn encode(&self) -> Result<Vec<u8>, String> {
412 let mut out = Vec::with_capacity(18);
413 out.extend_from_slice(&self.node.to_le_bytes());
414 out.extend_from_slice(&self.captured_at.to_le_bytes());
415 out.extend_from_slice(&self.value.to_le_bytes());
416 Ok(out)
417 }
418
419 fn decode(bytes: &[u8]) -> Result<Self, String> {
420 if bytes.len() != 18 {
421 return Err(format!("bad len {}", bytes.len()));
422 }
423 let node =
424 u16::from_le_bytes(bytes[0..2].try_into().expect("node bytes"));
425 let captured_at = u64::from_le_bytes(
426 bytes[2..10].try_into().expect("time bytes"),
427 );
428 let value = u64::from_le_bytes(
429 bytes[10..18].try_into().expect("value bytes"),
430 );
431 Ok(Self { node, captured_at, value })
432 }
433 }
434
435 #[test]
436 fn latest_by_node_keeps_newest_sample_per_node() {
437 let fleet = Arc::new(Fleet::join("metrics-test", 2).unwrap());
438 let family = OrbitMetricFamily::<TestSnapshot>::new(fleet);
439 let publisher = family.publisher();
440 let collector = family.collector();
441
442 publisher
443 .publish(&TestSnapshot { node: 1, captured_at: 10, value: 100 })
444 .unwrap();
445 publisher
446 .publish(&TestSnapshot { node: 2, captured_at: 11, value: 200 })
447 .unwrap();
448 publisher
449 .publish(&TestSnapshot { node: 1, captured_at: 12, value: 101 })
450 .unwrap();
451
452 let latest = collector.latest_by_node();
453 assert_eq!(latest.len(), 2);
454 assert_eq!(latest[&1].snapshot.value, 101);
455 assert_eq!(latest[&2].snapshot.value, 200);
456 }
457
458 #[cfg(unix)]
459 #[test]
460 fn per_node_family_reads_each_writer_lane() {
461 #[derive(Clone, Debug, PartialEq, Eq)]
462 struct PerNodeSnapshot(TestSnapshot);
463
464 impl OrbitTyped for PerNodeSnapshot {
465 const KIND: u8 = 209;
466 const RING_SPEC: RingSpec = RingSpec::per_node(16, 18);
467 }
468
469 impl OrbitMetricSnapshot for PerNodeSnapshot {
470 const FAMILY: &'static str = "per-node-test";
471
472 fn node_id(&self) -> u16 {
473 self.0.node_id()
474 }
475
476 fn captured_at_unix_secs(&self) -> u64 {
477 self.0.captured_at_unix_secs()
478 }
479
480 fn encode(&self) -> Result<Vec<u8>, String> {
481 self.0.encode()
482 }
483
484 fn decode(bytes: &[u8]) -> Result<Self, String> {
485 TestSnapshot::decode(bytes).map(Self)
486 }
487 }
488
489 let name =
490 Box::leak(format!("m{:x}", std::process::id()).into_boxed_str());
491 let collector_fleet =
492 Arc::new(Fleet::join_shm_as(name, 3, NodeId::new(0)).unwrap());
493 collector_fleet.reset_ring::<PerNodeSnapshot>().unwrap();
494 let node_one = OrbitMetricPublisher::<PerNodeSnapshot>::new(Arc::new(
495 Fleet::join_shm_as(name, 3, NodeId::new(1)).unwrap(),
496 ));
497 let node_two = OrbitMetricPublisher::<PerNodeSnapshot>::new(Arc::new(
498 Fleet::join_shm_as(name, 3, NodeId::new(2)).unwrap(),
499 ));
500
501 node_one
502 .publish(&PerNodeSnapshot(TestSnapshot {
503 node: 1,
504 captured_at: 10,
505 value: 100,
506 }))
507 .unwrap();
508 node_two
509 .publish(&PerNodeSnapshot(TestSnapshot {
510 node: 2,
511 captured_at: 11,
512 value: 200,
513 }))
514 .unwrap();
515 node_one
516 .publish(&PerNodeSnapshot(TestSnapshot {
517 node: 1,
518 captured_at: 12,
519 value: 101,
520 }))
521 .unwrap();
522
523 let latest = OrbitMetricCollector::<PerNodeSnapshot>::new(
524 collector_fleet.clone(),
525 )
526 .latest_by_node();
527 assert_eq!(latest.len(), 2);
528 assert_eq!(latest[&1].snapshot.0.value, 101);
529 assert_eq!(latest[&2].snapshot.0.value, 200);
530
531 collector_fleet
532 .shm_ring::<PerNodeSnapshot>()
533 .unwrap()
534 .unlink()
535 .unwrap();
536 }
537
538 #[test]
539 fn fresh_by_node_drops_stale_samples() {
540 let fleet = Arc::new(Fleet::join("metrics-fresh-test", 2).unwrap());
541 let family = OrbitMetricFamily::<TestSnapshot>::new(fleet);
542 let publisher = family.publisher();
543 let collector = family.collector();
544
545 publisher
546 .publish(&TestSnapshot { node: 1, captured_at: 10, value: 100 })
547 .unwrap();
548 publisher
549 .publish(&TestSnapshot { node: 2, captured_at: 20, value: 200 })
550 .unwrap();
551
552 let fresh = collector.fresh_by_node(25, 10);
553 assert_eq!(fresh.len(), 1);
554 assert_eq!(fresh[&2].snapshot.value, 200);
555 }
556
557 #[derive(Clone, Debug, PartialEq, Eq)]
558 struct KeyedSnapshot {
559 node: u16,
560 key: &'static str,
561 captured_at: u64,
562 value: u64,
563 }
564
565 impl OrbitTyped for KeyedSnapshot {
566 const KIND: u8 = 212;
567 const RING_SPEC: RingSpec =
568 RingSpec::new(KEYED_METRIC_RING_CAPACITY, 64);
569 }
570
571 impl OrbitMetricSnapshot for KeyedSnapshot {
572 const FAMILY: &'static str = "keyed-test";
573
574 fn node_id(&self) -> u16 {
575 self.node
576 }
577
578 fn captured_at_unix_secs(&self) -> u64 {
579 self.captured_at
580 }
581
582 fn encode(&self) -> Result<Vec<u8>, String> {
583 let mut out = Vec::with_capacity(27);
584 out.extend_from_slice(&self.node.to_le_bytes());
585 out.extend_from_slice(&self.captured_at.to_le_bytes());
586 out.extend_from_slice(&self.value.to_le_bytes());
587 let key = self.key.as_bytes();
588 out.push(key.len() as u8);
589 out.extend_from_slice(key);
590 Ok(out)
591 }
592
593 fn decode(bytes: &[u8]) -> Result<Self, String> {
594 if bytes.len() < 19 {
595 return Err(format!("bad len {}", bytes.len()));
596 }
597 let node =
598 u16::from_le_bytes(bytes[0..2].try_into().expect("node bytes"));
599 let captured_at = u64::from_le_bytes(
600 bytes[2..10].try_into().expect("time bytes"),
601 );
602 let value = u64::from_le_bytes(
603 bytes[10..18].try_into().expect("value bytes"),
604 );
605 let key_len = usize::from(bytes[18]);
606 if bytes.len() != 19 + key_len {
607 return Err(format!("bad key len {}", bytes.len()));
608 }
609 let key =
610 std::str::from_utf8(&bytes[19..]).map_err(|e| e.to_string())?;
611 let key = match key {
612 "alpha" => "alpha",
613 "beta" => "beta",
614 _ => return Err(format!("unknown key {key}")),
615 };
616 Ok(Self { node, key, captured_at, value })
617 }
618 }
619
620 impl OrbitMetricKeyedSnapshot for KeyedSnapshot {
621 type Key = String;
622
623 fn metric_key(&self) -> Self::Key {
624 self.key.to_owned()
625 }
626 }
627
628 #[test]
629 fn latest_by_key_keeps_newest_sample_per_key() {
630 let fleet = Arc::new(Fleet::join("metrics-keyed-test", 2).unwrap());
631 let family = OrbitMetricFamily::<KeyedSnapshot>::new(fleet);
632 let publisher = family.publisher();
633 let collector = family.collector();
634
635 publisher
636 .publish(&KeyedSnapshot {
637 node: 0,
638 key: "alpha",
639 captured_at: 10,
640 value: 100,
641 })
642 .unwrap();
643 publisher
644 .publish(&KeyedSnapshot {
645 node: 0,
646 key: "beta",
647 captured_at: 11,
648 value: 200,
649 })
650 .unwrap();
651 publisher
652 .publish(&KeyedSnapshot {
653 node: 0,
654 key: "alpha",
655 captured_at: 12,
656 value: 101,
657 })
658 .unwrap();
659
660 let latest = collector.latest_by_key();
661 assert_eq!(latest.len(), 2);
662 assert_eq!(latest["alpha"].snapshot.value, 101);
663 assert_eq!(latest["beta"].snapshot.value, 200);
664 }
665}