1use std::fmt;
21use std::sync::Arc;
22
23use ::prometheus::core::Collector;
24use ::prometheus::{HistogramOpts, HistogramVec, IntCounterVec, Opts, Registry};
25
26use crate::metrics::MetricKind;
27use crate::observe::{Event, Observer};
28
29const TIMER_BUCKETS: &[f64] = &[
30 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
31];
32const SIZE_BUCKETS: &[f64] = &[
33 100.0,
34 1_000.0,
35 10_000.0,
36 100_000.0,
37 1_000_000.0,
38 10_000_000.0,
39];
40const RATIO_BUCKETS: &[f64] = &[0.05, 0.1, 0.2, 0.3, 0.5, 0.7, 0.9, 1.0];
41const VALUE_AGE_BUCKETS: &[f64] = &[
43 1.0, 5.0, 15.0, 60.0, 300.0, 900.0, 3_600.0, 10_800.0, 43_200.0, 86_400.0, 259_200.0, 604_800.0,
44];
45const FUTURE_TIMESTAMP_OFFSET_BUCKETS: &[f64] = &[
46 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 5.0, 15.0, 60.0, 300.0, 900.0, 3_600.0,
47 10_800.0, 43_200.0,
48];
49
50#[derive(Debug, Clone, PartialEq)]
52pub struct CollectorSchema {
53 pub kind: MetricKind,
55 pub name: String,
57 pub help: &'static str,
59 pub labels: &'static [&'static str],
61 pub buckets: &'static [f64],
63}
64
65impl CollectorSchema {
66 pub fn is_counter(&self) -> bool {
68 self.kind.is_counter()
69 }
70}
71
72pub fn schemas(prefix: &str) -> Vec<CollectorSchema> {
75 let schema = |kind: MetricKind,
76 suffix: &str,
77 help: &'static str,
78 buckets: &'static [f64]|
79 -> CollectorSchema {
80 CollectorSchema {
81 kind,
82 name: format!("{prefix}dialcache_{suffix}"),
83 help,
84 labels: kind.label_names(),
85 buckets,
86 }
87 };
88 vec![
89 schema(
90 MetricKind::Disabled,
91 "disabled_counter",
92 "Requests where DialCache skipped a cache layer.",
93 &[],
94 ),
95 schema(
96 MetricKind::Miss,
97 "miss_counter",
98 "DialCache cache misses.",
99 &[],
100 ),
101 schema(
102 MetricKind::Request,
103 "request_counter",
104 "Total DialCache cache-layer requests.",
105 &[],
106 ),
107 schema(
108 MetricKind::Error,
109 "error_counter",
110 "Errors during DialCache cache operations or fallback execution.",
111 &[],
112 ),
113 schema(
114 MetricKind::Invalidation,
115 "invalidation_counter",
116 "DialCache invalidation calls by key type and layer.",
117 &[],
118 ),
119 schema(
120 MetricKind::Coalesced,
121 "coalesced_counter",
122 "DialCache requests coalesced onto in-flight work by sharing scope.",
123 &[],
124 ),
125 schema(
126 MetricKind::ShadowValidation,
127 "shadow_validation_counter",
128 "Sampled DialCache Redis shadow-validation outcomes.",
129 &[],
130 ),
131 schema(
132 MetricKind::ShadowValueAge,
133 "shadow_value_age_histogram",
134 "Age in seconds of the validated Redis value at DialCache shadow verdict time.",
135 VALUE_AGE_BUCKETS,
136 ),
137 schema(
138 MetricKind::ShadowMismatchLogging,
139 "shadow_mismatch_logging_counter",
140 "DialCache confirmed shadow-mismatch warning outcomes.",
141 &[],
142 ),
143 schema(
144 MetricKind::FutureTimestampOffset,
145 "future_timestamp_offset_histogram",
146 "Positive offset in seconds of Redis frames dated after the observing DialCache process clock.",
147 FUTURE_TIMESTAMP_OFFSET_BUCKETS,
148 ),
149 schema(
150 MetricKind::StaleRecovery,
151 "stale_recovery_counter",
152 "DialCache stale-on-error Redis recovery outcomes.",
153 &[],
154 ),
155 schema(
156 MetricKind::StaleRecoveryValueAge,
157 "stale_recovery_value_age_histogram",
158 "Age in seconds of Redis values served by DialCache stale-on-error recovery.",
159 VALUE_AGE_BUCKETS,
160 ),
161 schema(
162 MetricKind::Compression,
163 "compression_counter",
164 "DialCache Redis payload compression and decompression outcomes.",
165 &[],
166 ),
167 schema(
168 MetricKind::Get,
169 "get_timer",
170 "DialCache cache get latency in seconds.",
171 TIMER_BUCKETS,
172 ),
173 schema(
174 MetricKind::Fallback,
175 "fallback_timer",
176 "Time DialCache waited for the fallback function in seconds.",
177 TIMER_BUCKETS,
178 ),
179 schema(
180 MetricKind::Serialization,
181 "serialization_timer",
182 "DialCache serialization latency in seconds.",
183 TIMER_BUCKETS,
184 ),
185 schema(
186 MetricKind::Size,
187 "size_histogram",
188 "Serialized DialCache value sizes in bytes.",
189 SIZE_BUCKETS,
190 ),
191 schema(
192 MetricKind::StoredSize,
193 "stored_size_histogram",
194 "Stored DialCache payload sizes in bytes, after compression and escaping.",
195 SIZE_BUCKETS,
196 ),
197 schema(
198 MetricKind::CompressionRatio,
199 "compression_ratio_histogram",
200 "Compressed-to-original DialCache payload size ratio for compressed writes.",
201 RATIO_BUCKETS,
202 ),
203 schema(
204 MetricKind::CompressionDuration,
205 "compression_timer",
206 "DialCache payload compression and decompression latency in seconds.",
207 TIMER_BUCKETS,
208 ),
209 ]
210}
211
212#[derive(Debug)]
214pub enum PrometheusError {
215 Conflict {
221 name: String,
223 source: ::prometheus::Error,
225 },
226 InvalidCollector {
229 name: String,
231 source: ::prometheus::Error,
233 },
234}
235
236impl fmt::Display for PrometheusError {
237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238 match self {
239 PrometheusError::Conflict { name, source } => write!(
240 f,
241 "Prometheus collector {name:?} already exists with an incompatible or externally \
242 owned schema; use a unique prefix or registry: {source}"
243 ),
244 PrometheusError::InvalidCollector { name, source } => {
245 write!(f, "invalid Prometheus collector {name:?}: {source}")
246 }
247 }
248 }
249}
250
251impl std::error::Error for PrometheusError {
252 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
253 match self {
254 PrometheusError::Conflict { source, .. }
255 | PrometheusError::InvalidCollector { source, .. } => Some(source),
256 }
257 }
258}
259
260#[derive(Clone)]
261enum Vector {
262 Counter(IntCounterVec),
263 Histogram(HistogramVec),
264}
265
266impl Vector {
267 fn boxed(&self) -> Box<dyn Collector> {
268 match self {
269 Vector::Counter(vector) => Box::new(vector.clone()),
270 Vector::Histogram(vector) => Box::new(vector.clone()),
271 }
272 }
273}
274
275struct Group {
277 vectors: Vec<Vector>,
278}
279
280impl Group {
281 fn build(prefix: &str) -> Result<Group, PrometheusError> {
282 let mut vectors: Vec<Option<Vector>> = vec![None; MetricKind::ALL.len()];
283 for schema in schemas(prefix) {
284 let vector = if schema.is_counter() {
285 IntCounterVec::new(Opts::new(&schema.name, schema.help), schema.labels)
286 .map(Vector::Counter)
287 } else {
288 HistogramVec::new(
289 HistogramOpts::new(&schema.name, schema.help).buckets(schema.buckets.to_vec()),
290 schema.labels,
291 )
292 .map(Vector::Histogram)
293 }
294 .map_err(|source| PrometheusError::InvalidCollector {
295 name: schema.name.clone(),
296 source,
297 })?;
298 vectors[schema.kind.index()] = Some(vector);
299 }
300 Ok(Group {
301 vectors: vectors
302 .into_iter()
303 .map(|vector| vector.expect("every kind has a schema"))
304 .collect(),
305 })
306 }
307
308 fn register(&self, registry: &Registry, prefix: &str) -> Result<(), PrometheusError> {
310 let mut registered: Vec<&Vector> = Vec::new();
311 for schema in schemas(prefix) {
312 let vector = &self.vectors[schema.kind.index()];
313 if let Err(source) = registry.register(vector.boxed()) {
314 for done in registered {
315 let _ = registry.unregister(done.boxed());
316 }
317 return Err(PrometheusError::Conflict {
318 name: schema.name,
319 source,
320 });
321 }
322 registered.push(vector);
323 }
324 Ok(())
325 }
326}
327
328#[derive(Clone)]
333pub struct PrometheusObserver {
334 group: Arc<Group>,
335 prefix: String,
336}
337
338impl fmt::Debug for PrometheusObserver {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 f.debug_struct("PrometheusObserver")
341 .field("prefix", &self.prefix)
342 .finish_non_exhaustive()
343 }
344}
345
346impl PrometheusObserver {
347 pub fn new(registry: &Registry, prefix: &str) -> Result<PrometheusObserver, PrometheusError> {
351 let group = Arc::new(Group::build(prefix)?);
352 group.register(registry, prefix)?;
353 Ok(PrometheusObserver {
354 group,
355 prefix: prefix.to_string(),
356 })
357 }
358
359 pub fn schemas(&self) -> Vec<CollectorSchema> {
361 schemas(&self.prefix)
362 }
363}
364
365impl Observer for PrometheusObserver {
366 fn observe(&self, event: &Event) {
367 let kind = MetricKind::of(event);
368 let labels = MetricKind::labels(event);
369 let values: Vec<&str> = labels.iter().map(|(_, value)| value.as_str()).collect();
370 let result = match &self.group.vectors[kind.index()] {
371 Vector::Counter(vector) => vector
372 .get_metric_with_label_values(&values)
373 .map(|metric| metric.inc()),
374 Vector::Histogram(vector) => vector
375 .get_metric_with_label_values(&values)
376 .map(|metric| metric.observe(MetricKind::value(event))),
377 };
378 if let Err(error) = result {
379 log::warn!(
380 target: "dialcache",
381 "Dropped DialCache {} metric with labels {:?}: {error}",
382 kind.as_str(),
383 labels
384 );
385 }
386 }
387
388 fn observes_shadow_outcomes(&self) -> bool {
389 true
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn schemas_cover_every_kind_once_with_matching_label_names() {
399 let all = schemas("p_");
400 assert_eq!(all.len(), MetricKind::ALL.len());
401 let mut seen = std::collections::HashSet::new();
402 for schema in &all {
403 assert!(seen.insert(schema.kind), "{:?} twice", schema.kind);
404 assert!(schema.name.starts_with("p_dialcache_"));
405 assert_eq!(schema.labels, schema.kind.label_names());
406 assert_eq!(schema.buckets.is_empty(), schema.is_counter());
407 }
408 }
409
410 #[test]
411 fn invalid_prefix_is_an_error_not_a_panic() {
412 let registry = Registry::new();
413 let error = PrometheusObserver::new(®istry, "bad prefix ").unwrap_err();
414 assert!(
415 matches!(error, PrometheusError::InvalidCollector { .. }),
416 "{error}"
417 );
418 assert!(registry.gather().is_empty());
419 }
420}