1#[cfg(doctest)]
6use doc_comment::doctest;
7#[cfg(doctest)]
8doctest!("../README.md");
9
10use std::fmt::Debug;
11use std::time::Duration;
12
13use batch_aint_one::{BatchStats, MetricsRecorder, MetricsRecorderFactory};
14use prometheus::{HistogramOpts, HistogramVec, IntCounterVec, IntGaugeVec, Opts, Registry};
15
16const DEFAULT_PREFIX: &str = "batch";
17
18const CHANNEL_DURATION_BUCKETS: &[f64] = &[0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1];
19const BATCH_SIZE_BUCKETS: &[f64] = &[1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0];
20
21#[derive(Debug, Clone)]
29pub struct BatchMetrics {
30 items_received_total: IntCounterVec,
31 channel_duration_seconds: HistogramVec,
32 items_rejected_total: IntCounterVec,
33 batches_completed_total: IntCounterVec,
34 batch_size: HistogramVec,
35 batch_processing_duration_seconds: HistogramVec,
36 item_latency_seconds: HistogramVec,
37 queue_duration_seconds: HistogramVec,
38 resource_acquisition_duration_seconds: HistogramVec,
39 active_keys: IntGaugeVec,
40 processing_concurrency: IntGaugeVec,
41 processing_concurrency_max_per_key: IntGaugeVec,
42 queue_depth: IntGaugeVec,
43 queue_depth_max_per_key: IntGaugeVec,
44 queue_items: IntGaugeVec,
45 queue_items_max_per_key: IntGaugeVec,
46}
47
48impl BatchMetrics {
49 pub fn new(registry: &Registry) -> prometheus::Result<Self> {
51 Self::with_prefix(registry, DEFAULT_PREFIX)
52 }
53
54 pub fn with_prefix(registry: &Registry, prefix: &str) -> prometheus::Result<Self> {
58 let batcher_labels = &["batcher"];
59 let batcher_success_labels = &["batcher", "success"];
60
61 let items_received_total = IntCounterVec::new(
62 Opts::new("items_received_total", "Items received by the batcher").namespace(prefix),
63 batcher_labels,
64 )?;
65 let channel_duration_seconds = HistogramVec::new(
66 HistogramOpts::new(
67 "channel_duration_seconds",
68 "Time items spent in the channel before the worker picked them up",
69 )
70 .namespace(prefix)
71 .buckets(CHANNEL_DURATION_BUCKETS.to_vec()),
72 batcher_labels,
73 )?;
74 let items_rejected_total = IntCounterVec::new(
75 Opts::new(
76 "items_rejected_total",
77 "Items rejected due to a full batch queue",
78 )
79 .namespace(prefix),
80 batcher_labels,
81 )?;
82 let batches_completed_total = IntCounterVec::new(
83 Opts::new(
84 "batches_completed_total",
85 "Batches that finished processing",
86 )
87 .namespace(prefix),
88 batcher_success_labels,
89 )?;
90 let batch_size = HistogramVec::new(
91 HistogramOpts::new("batch_size", "Number of items per batch")
92 .namespace(prefix)
93 .buckets(BATCH_SIZE_BUCKETS.to_vec()),
94 batcher_labels,
95 )?;
96 let batch_processing_duration_seconds = HistogramVec::new(
97 HistogramOpts::new(
98 "batch_processing_duration_seconds",
99 "Time taken to process a batch",
100 )
101 .namespace(prefix),
102 batcher_success_labels,
103 )?;
104 let item_latency_seconds = HistogramVec::new(
105 HistogramOpts::new(
106 "item_latency_seconds",
107 "End-to-end latency per item, from submission to result delivery",
108 )
109 .namespace(prefix),
110 batcher_labels,
111 )?;
112 let queue_duration_seconds = HistogramVec::new(
113 HistogramOpts::new(
114 "queue_duration_seconds",
115 "Time items spent in the batch queue before processing started",
116 )
117 .namespace(prefix),
118 batcher_labels,
119 )?;
120 let resource_acquisition_duration_seconds = HistogramVec::new(
121 HistogramOpts::new(
122 "resource_acquisition_duration_seconds",
123 "Time taken to acquire resources for a batch",
124 )
125 .namespace(prefix),
126 batcher_success_labels,
127 )?;
128 let active_keys = IntGaugeVec::new(
129 Opts::new("active_keys", "Number of keys with active batch queues").namespace(prefix),
130 batcher_labels,
131 )?;
132 let processing_concurrency = IntGaugeVec::new(
133 Opts::new(
134 "processing_concurrency",
135 "Total number of batches currently processing",
136 )
137 .namespace(prefix),
138 batcher_labels,
139 )?;
140 let processing_concurrency_max_per_key = IntGaugeVec::new(
141 Opts::new(
142 "processing_concurrency_max_per_key",
143 "Highest batch processing concurrency across any single key",
144 )
145 .namespace(prefix),
146 batcher_labels,
147 )?;
148 let queue_depth = IntGaugeVec::new(
149 Opts::new(
150 "queue_depth",
151 "Total number of batches queued for processing",
152 )
153 .namespace(prefix),
154 batcher_labels,
155 )?;
156 let queue_depth_max_per_key = IntGaugeVec::new(
157 Opts::new(
158 "queue_depth_max_per_key",
159 "Deepest batch queue across any single key",
160 )
161 .namespace(prefix),
162 batcher_labels,
163 )?;
164 let queue_items = IntGaugeVec::new(
165 Opts::new("queue_items", "Total number of items queued for processing")
166 .namespace(prefix),
167 batcher_labels,
168 )?;
169 let queue_items_max_per_key = IntGaugeVec::new(
170 Opts::new(
171 "queue_items_max_per_key",
172 "Highest number of items queued across any single key",
173 )
174 .namespace(prefix),
175 batcher_labels,
176 )?;
177
178 registry.register(Box::new(items_received_total.clone()))?;
179 registry.register(Box::new(channel_duration_seconds.clone()))?;
180 registry.register(Box::new(items_rejected_total.clone()))?;
181 registry.register(Box::new(batches_completed_total.clone()))?;
182 registry.register(Box::new(batch_size.clone()))?;
183 registry.register(Box::new(batch_processing_duration_seconds.clone()))?;
184 registry.register(Box::new(item_latency_seconds.clone()))?;
185 registry.register(Box::new(queue_duration_seconds.clone()))?;
186 registry.register(Box::new(resource_acquisition_duration_seconds.clone()))?;
187 registry.register(Box::new(active_keys.clone()))?;
188 registry.register(Box::new(processing_concurrency.clone()))?;
189 registry.register(Box::new(processing_concurrency_max_per_key.clone()))?;
190 registry.register(Box::new(queue_depth.clone()))?;
191 registry.register(Box::new(queue_depth_max_per_key.clone()))?;
192 registry.register(Box::new(queue_items.clone()))?;
193 registry.register(Box::new(queue_items_max_per_key.clone()))?;
194
195 Ok(Self {
196 items_received_total,
197 channel_duration_seconds,
198 items_rejected_total,
199 batches_completed_total,
200 batch_size,
201 batch_processing_duration_seconds,
202 item_latency_seconds,
203 queue_duration_seconds,
204 resource_acquisition_duration_seconds,
205 active_keys,
206 processing_concurrency,
207 processing_concurrency_max_per_key,
208 queue_depth,
209 queue_depth_max_per_key,
210 queue_items,
211 queue_items_max_per_key,
212 })
213 }
214
215 pub fn recorder(&self, batcher_name: &str) -> BatchMetricsRecorder {
219 BatchMetricsRecorder {
220 metrics: self.clone(),
221 batcher_name: batcher_name.to_string(),
222 }
223 }
224}
225
226impl MetricsRecorderFactory for BatchMetrics {
227 fn create_recorder(&self, batcher_name: &str) -> Box<dyn MetricsRecorder> {
228 Box::new(self.recorder(batcher_name))
229 }
230}
231
232#[derive(Debug, Clone)]
237pub struct BatchMetricsRecorder {
238 metrics: BatchMetrics,
239 batcher_name: String,
240}
241
242impl MetricsRecorder for BatchMetricsRecorder {
243 fn item_received(&self, channel_duration: Duration) {
244 let name = &self.batcher_name;
245 self.metrics
246 .items_received_total
247 .with_label_values(&[name])
248 .inc();
249 self.metrics
250 .channel_duration_seconds
251 .with_label_values(&[name])
252 .observe(channel_duration.as_secs_f64());
253 }
254
255 fn item_rejected(&self) {
256 self.metrics
257 .items_rejected_total
258 .with_label_values(&[&self.batcher_name])
259 .inc();
260 }
261
262 fn batch_completed(&self, metrics: &BatchStats) {
263 let name = self.batcher_name.as_str();
264 let success = if metrics.success { "true" } else { "false" };
265
266 self.metrics
267 .batches_completed_total
268 .with_label_values(&[name, success])
269 .inc();
270 self.metrics
271 .batch_size
272 .with_label_values(&[name])
273 .observe(metrics.size as f64);
274 self.metrics
275 .batch_processing_duration_seconds
276 .with_label_values(&[name, success])
277 .observe(metrics.processing_duration.as_secs_f64());
278
279 for latency in &metrics.item_latencies {
280 self.metrics
281 .item_latency_seconds
282 .with_label_values(&[name])
283 .observe(latency.as_secs_f64());
284 }
285 for queue_duration in &metrics.queue_durations {
286 self.metrics
287 .queue_duration_seconds
288 .with_label_values(&[name])
289 .observe(queue_duration.as_secs_f64());
290 }
291 }
292
293 fn resource_acquisition_completed(&self, duration: Duration, success: bool) {
294 let success = if success { "true" } else { "false" };
295 self.metrics
296 .resource_acquisition_duration_seconds
297 .with_label_values(&[self.batcher_name.as_str(), success])
298 .observe(duration.as_secs_f64());
299 }
300
301 fn active_keys_changed(&self, count: usize) {
302 self.metrics
303 .active_keys
304 .with_label_values(&[&self.batcher_name])
305 .set(count as i64);
306 }
307
308 fn processing_concurrency_changed(&self, total: usize, max_per_key: usize) {
309 let name = &self.batcher_name;
310 self.metrics
311 .processing_concurrency
312 .with_label_values(&[name])
313 .set(total as i64);
314 self.metrics
315 .processing_concurrency_max_per_key
316 .with_label_values(&[name])
317 .set(max_per_key as i64);
318 }
319
320 fn queue_depth_changed(&self, total: usize, max_per_key: usize) {
321 let name = &self.batcher_name;
322 self.metrics
323 .queue_depth
324 .with_label_values(&[name])
325 .set(total as i64);
326 self.metrics
327 .queue_depth_max_per_key
328 .with_label_values(&[name])
329 .set(max_per_key as i64);
330 }
331
332 fn queue_items_changed(&self, total: usize, max_per_key: usize) {
333 let name = &self.batcher_name;
334 self.metrics
335 .queue_items
336 .with_label_values(&[name])
337 .set(total as i64);
338 self.metrics
339 .queue_items_max_per_key
340 .with_label_values(&[name])
341 .set(max_per_key as i64);
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use prometheus::proto::MetricFamily;
348
349 use super::*;
350
351 fn get_metric<'a>(families: &'a [MetricFamily], name: &str) -> Option<&'a MetricFamily> {
352 families.iter().find(|f| f.name() == name)
353 }
354
355 fn get_counter_value(families: &[MetricFamily], name: &str, labels: &[(&str, &str)]) -> f64 {
356 let family =
357 get_metric(families, name).unwrap_or_else(|| panic!("metric {name} not found"));
358 for metric in family.get_metric() {
359 if labels_match(metric, labels) {
360 return metric.get_counter().value();
361 }
362 }
363 panic!("no metric {name} with labels {labels:?}");
364 }
365
366 fn get_gauge_value(families: &[MetricFamily], name: &str, labels: &[(&str, &str)]) -> f64 {
367 let family =
368 get_metric(families, name).unwrap_or_else(|| panic!("metric {name} not found"));
369 for metric in family.get_metric() {
370 if labels_match(metric, labels) {
371 return metric.get_gauge().value();
372 }
373 }
374 panic!("no metric {name} with labels {labels:?}");
375 }
376
377 fn get_histogram_count(families: &[MetricFamily], name: &str, labels: &[(&str, &str)]) -> u64 {
378 let family =
379 get_metric(families, name).unwrap_or_else(|| panic!("metric {name} not found"));
380 for metric in family.get_metric() {
381 if labels_match(metric, labels) {
382 return metric.get_histogram().get_sample_count();
383 }
384 }
385 panic!("no metric {name} with labels {labels:?}");
386 }
387
388 fn get_histogram_sum(families: &[MetricFamily], name: &str, labels: &[(&str, &str)]) -> f64 {
389 let family =
390 get_metric(families, name).unwrap_or_else(|| panic!("metric {name} not found"));
391 for metric in family.get_metric() {
392 if labels_match(metric, labels) {
393 return metric.get_histogram().get_sample_sum();
394 }
395 }
396 panic!("no metric {name} with labels {labels:?}");
397 }
398
399 fn labels_match(metric: &prometheus::proto::Metric, expected: &[(&str, &str)]) -> bool {
400 expected.iter().all(|(k, v)| {
401 metric
402 .get_label()
403 .iter()
404 .any(|l| l.name() == *k && l.value() == *v)
405 })
406 }
407
408 #[test]
409 fn records_item_received() {
410 let registry = Registry::new();
411 let metrics = BatchMetrics::new(®istry).unwrap();
412 let recorder = metrics.recorder("test");
413
414 recorder.item_received(Duration::from_millis(5));
415 recorder.item_received(Duration::from_millis(10));
416
417 let families = registry.gather();
418 let labels = &[("batcher", "test")];
419
420 assert_eq!(
421 get_counter_value(&families, "batch_items_received_total", labels),
422 2.0,
423 );
424 assert_eq!(
425 get_histogram_count(&families, "batch_channel_duration_seconds", labels),
426 2,
427 );
428 assert!(get_histogram_sum(&families, "batch_channel_duration_seconds", labels) > 0.0,);
429 }
430
431 #[test]
432 fn records_item_rejected() {
433 let registry = Registry::new();
434 let metrics = BatchMetrics::new(®istry).unwrap();
435 let recorder = metrics.recorder("test");
436
437 recorder.item_rejected();
438
439 let families = registry.gather();
440 assert_eq!(
441 get_counter_value(
442 &families,
443 "batch_items_rejected_total",
444 &[("batcher", "test")]
445 ),
446 1.0,
447 );
448 }
449
450 #[test]
451 fn records_batch_completed() {
452 let registry = Registry::new();
453 let metrics = BatchMetrics::new(®istry).unwrap();
454 let recorder = metrics.recorder("test");
455
456 let batch_metrics = BatchStats::new(
457 3,
458 Duration::from_millis(50),
459 true,
460 vec![
461 Duration::from_millis(100),
462 Duration::from_millis(110),
463 Duration::from_millis(120),
464 ],
465 vec![
466 Duration::from_millis(40),
467 Duration::from_millis(30),
468 Duration::from_millis(20),
469 ],
470 );
471 recorder.batch_completed(&batch_metrics);
472
473 let families = registry.gather();
474 let success_labels = &[("batcher", "test"), ("success", "true")];
475 let batcher_labels = &[("batcher", "test")];
476
477 assert_eq!(
478 get_counter_value(&families, "batch_batches_completed_total", success_labels),
479 1.0,
480 );
481 assert_eq!(
482 get_histogram_count(&families, "batch_batch_size", batcher_labels),
483 1,
484 );
485 assert_eq!(
486 get_histogram_sum(&families, "batch_batch_size", batcher_labels),
487 3.0,
488 );
489 assert_eq!(
490 get_histogram_count(
491 &families,
492 "batch_batch_processing_duration_seconds",
493 success_labels,
494 ),
495 1,
496 );
497 assert_eq!(
498 get_histogram_count(&families, "batch_item_latency_seconds", batcher_labels),
499 3,
500 );
501 assert_eq!(
502 get_histogram_count(&families, "batch_queue_duration_seconds", batcher_labels),
503 3,
504 );
505 }
506
507 #[test]
508 fn records_resource_acquisition() {
509 let registry = Registry::new();
510 let metrics = BatchMetrics::new(®istry).unwrap();
511 let recorder = metrics.recorder("test");
512
513 recorder.resource_acquisition_completed(Duration::from_millis(25), true);
514 recorder.resource_acquisition_completed(Duration::from_millis(100), false);
515
516 let families = registry.gather();
517
518 assert_eq!(
519 get_histogram_count(
520 &families,
521 "batch_resource_acquisition_duration_seconds",
522 &[("batcher", "test"), ("success", "true")],
523 ),
524 1,
525 );
526 assert_eq!(
527 get_histogram_count(
528 &families,
529 "batch_resource_acquisition_duration_seconds",
530 &[("batcher", "test"), ("success", "false")],
531 ),
532 1,
533 );
534 }
535
536 #[test]
537 fn records_gauge_metrics() {
538 let registry = Registry::new();
539 let metrics = BatchMetrics::new(®istry).unwrap();
540 let recorder = metrics.recorder("test");
541
542 recorder.active_keys_changed(5);
543 recorder.processing_concurrency_changed(3, 2);
544 recorder.queue_depth_changed(10, 4);
545 recorder.queue_items_changed(200, 80);
546
547 let families = registry.gather();
548 let labels = &[("batcher", "test")];
549
550 assert_eq!(get_gauge_value(&families, "batch_active_keys", labels), 5.0);
551 assert_eq!(
552 get_gauge_value(&families, "batch_processing_concurrency", labels),
553 3.0,
554 );
555 assert_eq!(
556 get_gauge_value(
557 &families,
558 "batch_processing_concurrency_max_per_key",
559 labels
560 ),
561 2.0,
562 );
563 assert_eq!(
564 get_gauge_value(&families, "batch_queue_depth", labels),
565 10.0
566 );
567 assert_eq!(
568 get_gauge_value(&families, "batch_queue_depth_max_per_key", labels),
569 4.0,
570 );
571 assert_eq!(
572 get_gauge_value(&families, "batch_queue_items", labels),
573 200.0
574 );
575 assert_eq!(
576 get_gauge_value(&families, "batch_queue_items_max_per_key", labels),
577 80.0,
578 );
579 }
580
581 #[test]
582 fn multiple_batchers_have_separate_labels() {
583 let registry = Registry::new();
584 let metrics = BatchMetrics::new(®istry).unwrap();
585 let recorder_a = metrics.recorder("batcher-a");
586 let recorder_b = metrics.recorder("batcher-b");
587
588 recorder_a.item_received(Duration::from_millis(1));
589 recorder_a.item_received(Duration::from_millis(1));
590 recorder_b.item_received(Duration::from_millis(1));
591
592 let families = registry.gather();
593
594 assert_eq!(
595 get_counter_value(
596 &families,
597 "batch_items_received_total",
598 &[("batcher", "batcher-a")],
599 ),
600 2.0,
601 );
602 assert_eq!(
603 get_counter_value(
604 &families,
605 "batch_items_received_total",
606 &[("batcher", "batcher-b")],
607 ),
608 1.0,
609 );
610 }
611
612 #[test]
613 fn custom_prefix() {
614 let registry = Registry::new();
615 let metrics = BatchMetrics::with_prefix(®istry, "myapp").unwrap();
616 let recorder = metrics.recorder("test");
617
618 recorder.item_received(Duration::from_millis(1));
619
620 let families = registry.gather();
621 assert!(get_metric(&families, "myapp_items_received_total").is_some());
622 assert!(get_metric(&families, "batch_items_received_total").is_none());
623 }
624}