Skip to main content

dialcache/
metrics.rs

1//! The shared event-to-metric mapping of the bundled exporters.
2//!
3//! Every [`Event`] maps to exactly one [`MetricKind`]: a counter or an
4//! observation (timer, age, size, ratio). The kind fixes the metric's label
5//! set and value so the Prometheus and Datadog exporters, and the TypeScript
6//! and Go ports, publish the same wire contract. Logical cache keys never
7//! appear here: labels are bounded enumerations plus the namespace, use case
8//! and key type.
9
10use crate::observe::Event;
11
12/// The metric an [`Event`] feeds. Variant names match the TypeScript adapter
13/// method names and the Go `metricKinds` table.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum MetricKind {
16    /// Counter of lookups that reached an enabled layer ([`Event::Request`]).
17    Request,
18    /// Counter of classified misses ([`Event::Miss`]).
19    Miss,
20    /// Counter of skipped layers and no-layer calls ([`Event::Disabled`]).
21    Disabled,
22    /// Counter of bounded failure sites ([`Event::Error`]).
23    Error,
24    /// Counter of explicit invalidation attempts ([`Event::Invalidation`]).
25    Invalidation,
26    /// Counter of callers that joined an in-flight execution ([`Event::Coalesced`]).
27    Coalesced,
28    /// Counter of terminal shadow job outcomes ([`Event::ShadowValidation`]).
29    ShadowValidation,
30    /// Seconds: validated value age at a match or mismatch verdict ([`Event::ShadowValueAge`]).
31    ShadowValueAge,
32    /// Counter of confirmed-mismatch warning attempts ([`Event::ShadowMismatchLogging`]).
33    ShadowMismatchLogging,
34    /// Seconds: how far a frame is dated ahead of the observing clock
35    /// ([`Event::FutureTimestampOffset`]).
36    FutureTimestampOffset,
37    /// Counter of authorized stale-recovery checks ([`Event::StaleRecovery`]).
38    StaleRecovery,
39    /// Seconds: age of a stale value when recovery served it
40    /// ([`Event::StaleRecoveryValueAge`]).
41    StaleRecoveryValueAge,
42    /// Counter of envelope outcomes on writes and reads ([`Event::Compression`]).
43    Compression,
44    /// Seconds: layer lookup latency ([`Event::Get`]).
45    Get,
46    /// Seconds: time until the source settled or timed out ([`Event::Fallback`]).
47    Fallback,
48    /// Seconds: codec encode or decode latency ([`Event::Serialization`]).
49    Serialization,
50    /// Bytes: serializer output before compression and escaping ([`Event::Size`]).
51    Size,
52    /// Bytes: prepared payload after compression and escaping ([`Event::StoredSize`]).
53    StoredSize,
54    /// Ratio: stored over original bytes of a compressed write ([`Event::CompressionRatio`]).
55    CompressionRatio,
56    /// Seconds: zstd compress or decompress latency ([`Event::CompressionDuration`]).
57    CompressionDuration,
58}
59
60/// The base label names every layer-scoped metric starts with.
61const LABEL_CACHE_NAMESPACE: &str = "cache_namespace";
62const LABEL_USE_CASE: &str = "use_case";
63const LABEL_KEY_TYPE: &str = "key_type";
64
65impl MetricKind {
66    /// Every kind, in declaration order.
67    pub const ALL: [MetricKind; 20] = [
68        MetricKind::Request,
69        MetricKind::Miss,
70        MetricKind::Disabled,
71        MetricKind::Error,
72        MetricKind::Invalidation,
73        MetricKind::Coalesced,
74        MetricKind::ShadowValidation,
75        MetricKind::ShadowValueAge,
76        MetricKind::ShadowMismatchLogging,
77        MetricKind::FutureTimestampOffset,
78        MetricKind::StaleRecovery,
79        MetricKind::StaleRecoveryValueAge,
80        MetricKind::Compression,
81        MetricKind::Get,
82        MetricKind::Fallback,
83        MetricKind::Serialization,
84        MetricKind::Size,
85        MetricKind::StoredSize,
86        MetricKind::CompressionRatio,
87        MetricKind::CompressionDuration,
88    ];
89
90    /// The metric an event feeds.
91    pub fn of(event: &Event) -> MetricKind {
92        match event {
93            Event::Request { .. } => MetricKind::Request,
94            Event::Miss { .. } => MetricKind::Miss,
95            Event::Disabled { .. } => MetricKind::Disabled,
96            Event::Error { .. } => MetricKind::Error,
97            Event::Invalidation { .. } => MetricKind::Invalidation,
98            Event::Coalesced { .. } => MetricKind::Coalesced,
99            Event::ShadowValidation { .. } => MetricKind::ShadowValidation,
100            Event::ShadowValueAge { .. } => MetricKind::ShadowValueAge,
101            Event::ShadowMismatchLogging { .. } => MetricKind::ShadowMismatchLogging,
102            Event::FutureTimestampOffset { .. } => MetricKind::FutureTimestampOffset,
103            Event::StaleRecovery { .. } => MetricKind::StaleRecovery,
104            Event::StaleRecoveryValueAge { .. } => MetricKind::StaleRecoveryValueAge,
105            Event::Compression { .. } => MetricKind::Compression,
106            Event::Get { .. } => MetricKind::Get,
107            Event::Fallback { .. } => MetricKind::Fallback,
108            Event::Serialization { .. } => MetricKind::Serialization,
109            Event::Size { .. } => MetricKind::Size,
110            Event::StoredSize { .. } => MetricKind::StoredSize,
111            Event::CompressionRatio { .. } => MetricKind::CompressionRatio,
112            Event::CompressionDuration { .. } => MetricKind::CompressionDuration,
113        }
114    }
115
116    /// The kind's name in the TypeScript adapter and the Go `metricKinds` table.
117    pub fn as_str(self) -> &'static str {
118        match self {
119            MetricKind::Request => "request",
120            MetricKind::Miss => "miss",
121            MetricKind::Disabled => "disabled",
122            MetricKind::Error => "error",
123            MetricKind::Invalidation => "invalidation",
124            MetricKind::Coalesced => "coalesced",
125            MetricKind::ShadowValidation => "shadowValidation",
126            MetricKind::ShadowValueAge => "shadowValueAge",
127            MetricKind::ShadowMismatchLogging => "shadowMismatchLogging",
128            MetricKind::FutureTimestampOffset => "futureTimestampOffset",
129            MetricKind::StaleRecovery => "staleRecovery",
130            MetricKind::StaleRecoveryValueAge => "staleRecoveryValueAge",
131            MetricKind::Compression => "compression",
132            MetricKind::Get => "get",
133            MetricKind::Fallback => "fallback",
134            MetricKind::Serialization => "serialization",
135            MetricKind::Size => "size",
136            MetricKind::StoredSize => "storedSize",
137            MetricKind::CompressionRatio => "compressionRatio",
138            MetricKind::CompressionDuration => "compressionDuration",
139        }
140    }
141
142    /// Counters increment by one per event; every other kind records
143    /// [`value`](Self::value).
144    pub fn is_counter(self) -> bool {
145        matches!(
146            self,
147            MetricKind::Request
148                | MetricKind::Miss
149                | MetricKind::Disabled
150                | MetricKind::Error
151                | MetricKind::Invalidation
152                | MetricKind::Coalesced
153                | MetricKind::ShadowValidation
154                | MetricKind::ShadowMismatchLogging
155                | MetricKind::StaleRecovery
156                | MetricKind::Compression
157        )
158    }
159
160    /// Position of the kind in [`ALL`](Self::ALL).
161    pub fn index(self) -> usize {
162        self as usize
163    }
164
165    /// The label names of this kind, in wire order: the Prometheus label
166    /// schema and the Datadog tag set of the TypeScript adapters.
167    pub fn label_names(self) -> &'static [&'static str] {
168        const BASE: &[&str] = &[
169            LABEL_CACHE_NAMESPACE,
170            LABEL_USE_CASE,
171            LABEL_KEY_TYPE,
172            "layer",
173        ];
174        const OUTCOME: &[&str] = &[
175            LABEL_CACHE_NAMESPACE,
176            LABEL_USE_CASE,
177            LABEL_KEY_TYPE,
178            "outcome",
179        ];
180        match self {
181            MetricKind::Request
182            | MetricKind::FutureTimestampOffset
183            | MetricKind::Get
184            | MetricKind::Fallback
185            | MetricKind::Size
186            | MetricKind::StoredSize
187            | MetricKind::CompressionRatio => BASE,
188            MetricKind::Miss | MetricKind::Disabled => &[
189                LABEL_CACHE_NAMESPACE,
190                LABEL_USE_CASE,
191                LABEL_KEY_TYPE,
192                "layer",
193                "reason",
194            ],
195            MetricKind::Error => &[
196                LABEL_CACHE_NAMESPACE,
197                LABEL_USE_CASE,
198                LABEL_KEY_TYPE,
199                "layer",
200                "error",
201                "in_fallback",
202            ],
203            MetricKind::Invalidation => &[LABEL_CACHE_NAMESPACE, LABEL_KEY_TYPE, "layer"],
204            MetricKind::Coalesced => &[
205                LABEL_CACHE_NAMESPACE,
206                LABEL_USE_CASE,
207                LABEL_KEY_TYPE,
208                "scope",
209            ],
210            MetricKind::ShadowValidation
211            | MetricKind::ShadowValueAge
212            | MetricKind::ShadowMismatchLogging
213            | MetricKind::StaleRecovery
214            | MetricKind::StaleRecoveryValueAge => OUTCOME,
215            MetricKind::Compression => &[
216                LABEL_CACHE_NAMESPACE,
217                LABEL_USE_CASE,
218                LABEL_KEY_TYPE,
219                "layer",
220                "outcome",
221            ],
222            MetricKind::Serialization | MetricKind::CompressionDuration => &[
223                LABEL_CACHE_NAMESPACE,
224                LABEL_USE_CASE,
225                LABEL_KEY_TYPE,
226                "layer",
227                "operation",
228            ],
229        }
230    }
231
232    /// The labels of an event, in the order of [`label_names`](Self::label_names)
233    /// for [`of(event)`](Self::of). Values are bounded enumerations plus the
234    /// namespace, use case and key type; the logical key is never one.
235    pub fn labels(event: &Event) -> Vec<(&'static str, String)> {
236        let mut labels: Vec<(&'static str, String)> = Vec::with_capacity(6);
237        match event {
238            Event::Invalidation {
239                namespace,
240                key_type,
241                layer,
242            } => {
243                labels.push((LABEL_CACHE_NAMESPACE, namespace.to_string()));
244                labels.push((LABEL_KEY_TYPE, key_type.to_string()));
245                labels.push(("layer", layer.as_str().to_string()));
246            }
247            Event::Coalesced {
248                labels: outcome,
249                scope,
250            } => {
251                push_outcome_base(&mut labels, outcome);
252                labels.push(("scope", scope.as_str().to_string()));
253            }
254            Event::ShadowValidation {
255                labels: outcome,
256                outcome: verdict,
257            }
258            | Event::ShadowValueAge {
259                labels: outcome,
260                outcome: verdict,
261                ..
262            } => {
263                push_outcome_base(&mut labels, outcome);
264                labels.push(("outcome", verdict.as_str().to_string()));
265            }
266            Event::ShadowMismatchLogging {
267                labels: outcome,
268                outcome: logging,
269            } => {
270                push_outcome_base(&mut labels, outcome);
271                labels.push(("outcome", logging.as_str().to_string()));
272            }
273            Event::StaleRecovery {
274                labels: outcome,
275                outcome: verdict,
276            }
277            | Event::StaleRecoveryValueAge {
278                labels: outcome,
279                outcome: verdict,
280                ..
281            } => {
282                push_outcome_base(&mut labels, outcome);
283                labels.push(("outcome", verdict.as_str().to_string()));
284            }
285            Event::Request { labels: base }
286            | Event::FutureTimestampOffset { labels: base, .. }
287            | Event::Get { labels: base, .. }
288            | Event::Fallback { labels: base, .. }
289            | Event::Size { labels: base, .. }
290            | Event::StoredSize { labels: base, .. }
291            | Event::CompressionRatio { labels: base, .. } => push_layer_base(&mut labels, base),
292            Event::Miss {
293                labels: base,
294                reason,
295            } => {
296                push_layer_base(&mut labels, base);
297                labels.push(("reason", reason.as_str().to_string()));
298            }
299            Event::Disabled {
300                labels: base,
301                reason,
302            } => {
303                push_layer_base(&mut labels, base);
304                labels.push(("reason", reason.as_str().to_string()));
305            }
306            Event::Error {
307                labels: base,
308                error,
309                in_fallback,
310            } => {
311                push_layer_base(&mut labels, base);
312                labels.push(("error", error.as_str().to_string()));
313                labels.push(("in_fallback", in_fallback.to_string()));
314            }
315            Event::Compression {
316                labels: base,
317                outcome,
318            } => {
319                push_layer_base(&mut labels, base);
320                labels.push(("outcome", outcome.as_str().to_string()));
321            }
322            Event::Serialization {
323                labels: base,
324                operation,
325                ..
326            } => {
327                push_layer_base(&mut labels, base);
328                labels.push(("operation", operation.as_str().to_string()));
329            }
330            Event::CompressionDuration {
331                labels: base,
332                operation,
333                ..
334            } => {
335                push_layer_base(&mut labels, base);
336                labels.push(("operation", operation.as_str().to_string()));
337            }
338        }
339        labels
340    }
341
342    /// The value an event records: seconds for timers, ages and offsets,
343    /// bytes for sizes, the compressed-to-original ratio for
344    /// [`CompressionRatio`](Self::CompressionRatio), and the increment `1`
345    /// for counters.
346    pub fn value(event: &Event) -> f64 {
347        match event {
348            Event::ShadowValueAge { seconds, .. }
349            | Event::FutureTimestampOffset { seconds, .. }
350            | Event::StaleRecoveryValueAge { seconds, .. }
351            | Event::Get { seconds, .. }
352            | Event::Fallback { seconds, .. }
353            | Event::Serialization { seconds, .. }
354            | Event::CompressionDuration { seconds, .. } => *seconds,
355            Event::Size { bytes, .. } | Event::StoredSize { bytes, .. } => *bytes as f64,
356            Event::CompressionRatio { ratio, .. } => *ratio,
357            Event::Request { .. }
358            | Event::Miss { .. }
359            | Event::Disabled { .. }
360            | Event::Error { .. }
361            | Event::Invalidation { .. }
362            | Event::Coalesced { .. }
363            | Event::ShadowValidation { .. }
364            | Event::ShadowMismatchLogging { .. }
365            | Event::StaleRecovery { .. }
366            | Event::Compression { .. } => 1.0,
367        }
368    }
369}
370
371fn push_layer_base(labels: &mut Vec<(&'static str, String)>, base: &crate::observe::Labels) {
372    labels.push((LABEL_CACHE_NAMESPACE, base.namespace.to_string()));
373    labels.push((LABEL_USE_CASE, base.use_case.to_string()));
374    labels.push((LABEL_KEY_TYPE, base.key_type.to_string()));
375    labels.push(("layer", base.layer.as_str().to_string()));
376}
377
378fn push_outcome_base(
379    labels: &mut Vec<(&'static str, String)>,
380    base: &crate::observe::OutcomeLabels,
381) {
382    labels.push((LABEL_CACHE_NAMESPACE, base.namespace.to_string()));
383    labels.push((LABEL_USE_CASE, base.use_case.to_string()));
384    labels.push((LABEL_KEY_TYPE, base.key_type.to_string()));
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use crate::observe::{
391        CoalescingScope, CompressionOperation, CompressionOutcome, DisabledReason, ErrorKind,
392        Labels, Layer, MissReason, OutcomeLabels, RecoveryOutcome, SerializationOperation,
393        ShadowMismatchLoggingOutcome, ShadowOutcome,
394    };
395    use std::sync::Arc;
396
397    fn base() -> Labels {
398        Labels {
399            namespace: Arc::from("logical"),
400            use_case: Arc::from("lookup"),
401            key_type: Arc::from("item"),
402            layer: Layer::Remote,
403        }
404    }
405
406    fn outcome() -> OutcomeLabels {
407        OutcomeLabels {
408            namespace: Arc::from("logical"),
409            use_case: Arc::from("lookup"),
410            key_type: Arc::from("item"),
411        }
412    }
413
414    /// One event per kind with fixed, recognizable field values.
415    pub(crate) fn event_of(kind: MetricKind) -> Event {
416        match kind {
417            MetricKind::Request => Event::Request { labels: base() },
418            MetricKind::Miss => Event::Miss {
419                labels: base(),
420                reason: MissReason::Expired,
421            },
422            MetricKind::Disabled => Event::Disabled {
423                labels: base(),
424                reason: DisabledReason::RampedDown,
425            },
426            MetricKind::Error => Event::Error {
427                labels: base(),
428                error: ErrorKind::Fallback,
429                in_fallback: true,
430            },
431            MetricKind::Invalidation => Event::Invalidation {
432                namespace: Arc::from("logical"),
433                key_type: Arc::from("item"),
434                layer: Layer::Remote,
435            },
436            MetricKind::Coalesced => Event::Coalesced {
437                labels: outcome(),
438                scope: CoalescingScope::Process,
439            },
440            MetricKind::ShadowValidation => Event::ShadowValidation {
441                labels: outcome(),
442                outcome: ShadowOutcome::Mismatch,
443            },
444            MetricKind::ShadowValueAge => Event::ShadowValueAge {
445                labels: outcome(),
446                outcome: ShadowOutcome::Mismatch,
447                seconds: 0.25,
448            },
449            MetricKind::ShadowMismatchLogging => Event::ShadowMismatchLogging {
450                labels: outcome(),
451                outcome: ShadowMismatchLoggingOutcome::HookError,
452            },
453            MetricKind::FutureTimestampOffset => Event::FutureTimestampOffset {
454                labels: base(),
455                seconds: 0.25,
456            },
457            MetricKind::StaleRecovery => Event::StaleRecovery {
458                labels: outcome(),
459                outcome: RecoveryOutcome::Served,
460            },
461            MetricKind::StaleRecoveryValueAge => Event::StaleRecoveryValueAge {
462                labels: outcome(),
463                outcome: RecoveryOutcome::Served,
464                seconds: 0.25,
465            },
466            MetricKind::Compression => Event::Compression {
467                labels: base(),
468                outcome: CompressionOutcome::Compressed,
469            },
470            MetricKind::Get => Event::Get {
471                labels: base(),
472                seconds: 0.25,
473            },
474            MetricKind::Fallback => Event::Fallback {
475                labels: base(),
476                seconds: 0.25,
477            },
478            MetricKind::Serialization => Event::Serialization {
479                labels: base(),
480                operation: SerializationOperation::Dump,
481                seconds: 0.25,
482            },
483            MetricKind::Size => Event::Size {
484                labels: base(),
485                bytes: 123,
486            },
487            MetricKind::StoredSize => Event::StoredSize {
488                labels: base(),
489                bytes: 123,
490            },
491            MetricKind::CompressionRatio => Event::CompressionRatio {
492                labels: base(),
493                ratio: 0.25,
494            },
495            MetricKind::CompressionDuration => Event::CompressionDuration {
496                labels: base(),
497                operation: CompressionOperation::Compress,
498                seconds: 0.25,
499            },
500        }
501    }
502
503    #[test]
504    fn every_kind_round_trips_through_of_and_index() {
505        for (index, kind) in MetricKind::ALL.iter().enumerate() {
506            assert_eq!(MetricKind::of(&event_of(*kind)), *kind);
507            assert_eq!(kind.index(), index);
508        }
509        let names: std::collections::HashSet<&str> =
510            MetricKind::ALL.iter().map(|k| k.as_str()).collect();
511        assert_eq!(names.len(), 20);
512    }
513
514    #[test]
515    fn label_values_follow_the_declared_label_names() {
516        for kind in MetricKind::ALL {
517            let labels = MetricKind::labels(&event_of(kind));
518            let names: Vec<&str> = labels.iter().map(|(name, _)| *name).collect();
519            assert_eq!(names, kind.label_names(), "{}", kind.as_str());
520        }
521    }
522
523    #[test]
524    fn labels_derive_as_the_go_port_does() {
525        let pairs = |kind: MetricKind| -> Vec<(String, String)> {
526            MetricKind::labels(&event_of(kind))
527                .into_iter()
528                .map(|(k, v)| (k.to_string(), v))
529                .collect()
530        };
531        let owned = |items: &[(&str, &str)]| -> Vec<(String, String)> {
532            items
533                .iter()
534                .map(|(k, v)| (k.to_string(), v.to_string()))
535                .collect()
536        };
537        assert_eq!(
538            pairs(MetricKind::Error),
539            owned(&[
540                ("cache_namespace", "logical"),
541                ("use_case", "lookup"),
542                ("key_type", "item"),
543                ("layer", "remote"),
544                ("error", "fallback"),
545                ("in_fallback", "true"),
546            ])
547        );
548        assert_eq!(
549            pairs(MetricKind::Invalidation),
550            owned(&[
551                ("cache_namespace", "logical"),
552                ("key_type", "item"),
553                ("layer", "remote"),
554            ])
555        );
556        assert_eq!(
557            pairs(MetricKind::Coalesced),
558            owned(&[
559                ("cache_namespace", "logical"),
560                ("use_case", "lookup"),
561                ("key_type", "item"),
562                ("scope", "process"),
563            ])
564        );
565        assert_eq!(
566            pairs(MetricKind::ShadowValidation),
567            owned(&[
568                ("cache_namespace", "logical"),
569                ("use_case", "lookup"),
570                ("key_type", "item"),
571                ("outcome", "mismatch"),
572            ])
573        );
574        assert_eq!(
575            pairs(MetricKind::ShadowMismatchLogging),
576            owned(&[
577                ("cache_namespace", "logical"),
578                ("use_case", "lookup"),
579                ("key_type", "item"),
580                ("outcome", "hook_error"),
581            ])
582        );
583        assert_eq!(
584            pairs(MetricKind::Miss),
585            owned(&[
586                ("cache_namespace", "logical"),
587                ("use_case", "lookup"),
588                ("key_type", "item"),
589                ("layer", "remote"),
590                ("reason", "expired"),
591            ])
592        );
593        assert_eq!(
594            pairs(MetricKind::Disabled).last().unwrap(),
595            &("reason".to_string(), "ramped_down".to_string())
596        );
597        assert_eq!(
598            pairs(MetricKind::Compression).last().unwrap(),
599            &("outcome".to_string(), "compressed".to_string())
600        );
601        assert_eq!(
602            pairs(MetricKind::Serialization).last().unwrap(),
603            &("operation".to_string(), "dump".to_string())
604        );
605        assert_eq!(
606            pairs(MetricKind::CompressionDuration).last().unwrap(),
607            &("operation".to_string(), "compress".to_string())
608        );
609        let in_fallback_false = Event::Error {
610            labels: base(),
611            error: ErrorKind::CacheRead,
612            in_fallback: false,
613        };
614        assert_eq!(
615            MetricKind::labels(&in_fallback_false).last().unwrap().1,
616            "false"
617        );
618    }
619
620    #[test]
621    fn values_carry_the_documented_units() {
622        for kind in MetricKind::ALL {
623            let value = MetricKind::value(&event_of(kind));
624            let expected = match kind {
625                MetricKind::Size | MetricKind::StoredSize => 123.0,
626                _ if kind.is_counter() => 1.0,
627                _ => 0.25,
628            };
629            assert_eq!(value, expected, "{}", kind.as_str());
630        }
631        let counters: Vec<&str> = MetricKind::ALL
632            .iter()
633            .filter(|k| k.is_counter())
634            .map(|k| k.as_str())
635            .collect();
636        assert_eq!(
637            counters,
638            [
639                "request",
640                "miss",
641                "disabled",
642                "error",
643                "invalidation",
644                "coalesced",
645                "shadowValidation",
646                "shadowMismatchLogging",
647                "staleRecovery",
648                "compression",
649            ]
650        );
651    }
652}