1use std::sync::atomic::{AtomicI64, Ordering};
4use std::sync::Arc;
5
6pub trait CounterFn: Send + Sync {
8 fn increment(&self, value: u64);
10}
11
12pub trait GaugeFn: Send + Sync {
14 fn set(&self, value: i64);
16}
17
18pub trait UpDownCounterFn: Send + Sync {
20 fn increment(&self, value: i64);
22}
23
24pub trait HistogramFn: Send + Sync {
26 fn record(&self, value: f64);
28}
29
30pub trait MetricsRecorder: Send + Sync {
43 fn register_counter(
45 &self,
46 name: &str,
47 description: &str,
48 labels: &[(&str, &str)],
49 ) -> Arc<dyn CounterFn>;
50
51 fn register_gauge(
53 &self,
54 name: &str,
55 description: &str,
56 labels: &[(&str, &str)],
57 ) -> Arc<dyn GaugeFn>;
58
59 fn register_up_down_counter(
61 &self,
62 name: &str,
63 description: &str,
64 labels: &[(&str, &str)],
65 ) -> Arc<dyn UpDownCounterFn>;
66
67 fn register_histogram(
69 &self,
70 name: &str,
71 description: &str,
72 labels: &[(&str, &str)],
73 boundaries: &[f64],
74 ) -> Arc<dyn HistogramFn>;
75}
76
77#[derive(Clone, Copy, Debug, Default)]
79pub struct NoopMetricsRecorder;
80
81#[derive(Debug)]
82struct NoopMetric;
83
84impl CounterFn for NoopMetric {
85 fn increment(&self, _value: u64) {}
86}
87
88impl GaugeFn for NoopMetric {
89 fn set(&self, _value: i64) {}
90}
91
92impl UpDownCounterFn for NoopMetric {
93 fn increment(&self, _value: i64) {}
94}
95
96impl HistogramFn for NoopMetric {
97 fn record(&self, _value: f64) {}
98}
99
100impl MetricsRecorder for NoopMetricsRecorder {
101 fn register_counter(
102 &self,
103 _name: &str,
104 _description: &str,
105 _labels: &[(&str, &str)],
106 ) -> Arc<dyn CounterFn> {
107 Arc::new(NoopMetric)
108 }
109
110 fn register_gauge(
111 &self,
112 _name: &str,
113 _description: &str,
114 _labels: &[(&str, &str)],
115 ) -> Arc<dyn GaugeFn> {
116 Arc::new(NoopMetric)
117 }
118
119 fn register_up_down_counter(
120 &self,
121 _name: &str,
122 _description: &str,
123 _labels: &[(&str, &str)],
124 ) -> Arc<dyn UpDownCounterFn> {
125 Arc::new(NoopMetric)
126 }
127
128 fn register_histogram(
129 &self,
130 _name: &str,
131 _description: &str,
132 _labels: &[(&str, &str)],
133 _boundaries: &[f64],
134 ) -> Arc<dyn HistogramFn> {
135 Arc::new(NoopMetric)
136 }
137}
138
139pub(crate) struct Counter(Arc<dyn CounterFn>);
140
141impl Counter {
142 fn register(recorder: &dyn MetricsRecorder, name: &str, description: &str) -> Self {
143 Self(recorder.register_counter(name, description, &[]))
144 }
145
146 pub(crate) fn increment(&self) {
147 self.add(1);
148 }
149
150 pub(crate) fn add(&self, value: u64) {
151 self.0.increment(value);
152 }
153}
154
155pub(crate) struct Gauge(Arc<dyn GaugeFn>);
156
157impl Gauge {
158 fn register(recorder: &dyn MetricsRecorder, name: &str, description: &str) -> Self {
159 Self(recorder.register_gauge(name, description, &[]))
160 }
161
162 fn register_with_labels(
163 recorder: &dyn MetricsRecorder,
164 name: &str,
165 description: &str,
166 labels: &[(&str, &str)],
167 ) -> Self {
168 Self(recorder.register_gauge(name, description, labels))
169 }
170
171 pub(crate) fn set(&self, value: i64) {
172 self.0.set(value);
173 }
174
175 pub(crate) fn set_u64(&self, value: u64) {
176 self.set(i64::try_from(value).unwrap_or(i64::MAX));
177 }
178
179 pub(crate) fn set_usize(&self, value: usize) {
180 self.set(i64::try_from(value).unwrap_or(i64::MAX));
181 }
182}
183
184pub(crate) struct AggregateGauge {
191 total: AtomicI64,
192 gauge: Gauge,
193}
194
195impl AggregateGauge {
196 fn register(
197 recorder: &dyn MetricsRecorder,
198 name: &str,
199 description: &str,
200 labels: &[(&str, &str)],
201 ) -> Self {
202 Self {
203 total: AtomicI64::new(0),
204 gauge: Gauge::register_with_labels(recorder, name, description, labels),
205 }
206 }
207
208 pub(crate) fn add(&self, value: i64) {
209 let total = self.total.fetch_add(value, Ordering::Relaxed) + value;
210 self.gauge.set(total.max(0));
211 let current = self.total.load(Ordering::Relaxed);
212 if current != total {
213 self.gauge.set(current.max(0));
214 }
215 }
216}
217
218pub(crate) struct UpDownCounter(Arc<dyn UpDownCounterFn>);
219
220impl UpDownCounter {
221 fn register(recorder: &dyn MetricsRecorder, name: &str, description: &str) -> Self {
222 Self(recorder.register_up_down_counter(name, description, &[]))
223 }
224
225 pub(crate) fn add(&self, value: i64) {
226 self.0.increment(value);
227 }
228}
229
230const LATENCY_SECONDS_BOUNDARIES: &[f64] = &[
233 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0,
234];
235
236pub(crate) struct Histogram(Arc<dyn HistogramFn>);
237
238impl Histogram {
239 fn register(recorder: &dyn MetricsRecorder, name: &str, description: &str) -> Self {
240 Self(recorder.register_histogram(name, description, &[], LATENCY_SECONDS_BOUNDARIES))
241 }
242
243 pub(crate) fn record_duration(&self, duration: std::time::Duration) {
244 self.0.record(duration.as_secs_f64());
245 }
246}
247
248pub(crate) struct HighWaterGauge {
249 high_water: AtomicI64,
250 gauge: Gauge,
251}
252
253impl HighWaterGauge {
254 fn register(recorder: &dyn MetricsRecorder, name: &str, description: &str) -> Self {
255 Self {
256 high_water: AtomicI64::new(0),
257 gauge: Gauge::register(recorder, name, description),
258 }
259 }
260
261 pub(crate) fn update_max(&self, value: u64) {
262 let value = i64::try_from(value).unwrap_or(i64::MAX);
263 if self.high_water.fetch_max(value, Ordering::Relaxed) < value {
264 self.gauge.set(value);
265 let current = self.high_water.load(Ordering::Relaxed);
266 if current > value {
267 self.gauge.set(current);
268 }
269 }
270 }
271}
272
273pub(crate) struct Metrics {
275 pub(crate) append_records: Counter,
276 pub(crate) append_bytes: Counter,
277 pub(crate) append_failures: Counter,
278 pub(crate) committed_records: Counter,
279 pub(crate) committed_bytes: Counter,
280 pub(crate) batches_sent: Counter,
281 pub(crate) lane_retries: Counter,
282 pub(crate) lane_timeouts: Counter,
283 pub(crate) rotations_completed: Counter,
284 pub(crate) spare_provisioning_attempts: Counter,
285 pub(crate) spare_provisioning_failures: Counter,
286 pub(crate) segments_sealed: Counter,
287 pub(crate) seal_enforcement_retries: Counter,
288 pub(crate) manifest_cas_attempts: Counter,
289 pub(crate) manifest_cas_conflicts: Counter,
290 pub(crate) repair_passes: Counter,
291 pub(crate) repair_objects_repaired: Counter,
292 pub(crate) repair_transient_skips: Counter,
293 pub(crate) repair_failures: Counter,
294 pub(crate) recoveries_run: Counter,
295 pub(crate) recovery_segments_adopted: Counter,
296 pub(crate) orphan_objects_deleted: Counter,
297 pub(crate) orphan_sweeps_deferred: Counter,
298 pub(crate) truncation_cycles: Counter,
299 pub(crate) operation_failures: Counter,
300 pub(crate) max_inflight_records: HighWaterGauge,
301 pub(crate) max_inflight_bytes: HighWaterGauge,
302 pub(crate) pipeline_refills: Counter,
303 pub(crate) lane_capacity_drops: Counter,
304 pub(crate) wal_record_bytes: Counter,
305 pub(crate) replica_bytes_attempted: Counter,
306 pub(crate) open_segments: UpDownCounter,
307 pub(crate) committed_records_watermark: Gauge,
308 pub(crate) queue_depth: Gauge,
309 zone_durable_lag: Vec<AggregateGauge>,
310 pub(crate) rotation_state: Gauge,
311 maintenance_queue_depth: AggregateGauge,
312 pub(crate) manifest_directory_bytes: Gauge,
313 pub(crate) append_commit_latency: Histogram,
314 pub(crate) manifest_cas_latency: Histogram,
315 pub(crate) seal_duration: Histogram,
316}
317
318impl Metrics {
319 pub(crate) fn new(recorder: &dyn MetricsRecorder, replica_count: usize) -> Self {
320 macro_rules! counter {
321 ($name:literal, $description:literal) => {
322 Counter::register(recorder, $name, $description)
323 };
324 }
325 macro_rules! gauge {
326 ($name:literal, $description:literal) => {
327 Gauge::register(recorder, $name, $description)
328 };
329 }
330 macro_rules! high_water {
331 ($name:literal, $description:literal) => {
332 HighWaterGauge::register(recorder, $name, $description)
333 };
334 }
335 macro_rules! histogram {
336 ($name:literal, $description:literal) => {
337 Histogram::register(recorder, $name, $description)
338 };
339 }
340
341 let zone_durable_lag = (0..replica_count)
342 .map(|zone| {
343 let zone = zone.to_string();
344 AggregateGauge::register(
345 recorder,
346 "chorus.wal.replica.durable_lag_bytes",
347 "Encoded admitted bytes not yet durable in this replica zone",
348 &[("zone", zone.as_str())],
349 )
350 })
351 .collect();
352
353 Self {
354 append_records: counter!(
355 "chorus.wal.append.records",
356 "Application records admitted for append"
357 ),
358 append_bytes: counter!(
359 "chorus.wal.append.bytes",
360 "Application payload bytes admitted for append"
361 ),
362 append_failures: counter!(
363 "chorus.wal.append.failures",
364 "Admitted appends that completed with an error"
365 ),
366 committed_records: counter!(
367 "chorus.wal.append.committed_records",
368 "Records committed in contiguous sequence order"
369 ),
370 committed_bytes: counter!(
371 "chorus.wal.append.committed_bytes",
372 "Application payload bytes committed in contiguous sequence order"
373 ),
374 batches_sent: counter!(
375 "chorus.wal.batch.sent",
376 "Logical record batches submitted to replication"
377 ),
378 lane_retries: counter!(
379 "chorus.wal.lane.retries",
380 "Replica lane recovery or resend attempts"
381 ),
382 lane_timeouts: counter!(
383 "chorus.wal.lane.timeouts",
384 "Replica lanes shed after making no durable progress before their timeout"
385 ),
386 rotations_completed: counter!(
387 "chorus.wal.rotation.completed",
388 "Active-segment rotations completed"
389 ),
390 spare_provisioning_attempts: counter!(
391 "chorus.wal.rotation.spare_provisioning_attempts",
392 "Background spare provisioning attempts"
393 ),
394 spare_provisioning_failures: counter!(
395 "chorus.wal.rotation.spare_provisioning_failures",
396 "Background spare provisioning failures"
397 ),
398 segments_sealed: counter!(
399 "chorus.wal.seal.segments",
400 "Segments whose committed seal was enforced"
401 ),
402 seal_enforcement_retries: counter!(
403 "chorus.wal.seal.enforcement_retries",
404 "Committed-seal enforcement retry attempts started by maintenance"
405 ),
406 manifest_cas_attempts: counter!(
407 "chorus.wal.manifest.cas_attempts",
408 "Manifest compare-and-swap requests sent to storage"
409 ),
410 manifest_cas_conflicts: counter!(
411 "chorus.wal.manifest.cas_conflicts",
412 "Manifest compare-and-swap precondition conflicts"
413 ),
414 repair_passes: counter!(
415 "chorus.wal.repair.passes",
416 "Sealed-segment repair passes completed or skipped after an error"
417 ),
418 repair_objects_repaired: counter!(
419 "chorus.wal.repair.objects_repaired",
420 "Missing or divergent sealed objects repaired"
421 ),
422 repair_transient_skips: counter!(
423 "chorus.wal.repair.transient_skips",
424 "Repair targets skipped after transient failures"
425 ),
426 repair_failures: counter!(
427 "chorus.wal.repair.failures",
428 "Repair passes aborted by an error"
429 ),
430 recoveries_run: counter!("chorus.wal.recovery.runs", "Recovery attempts started"),
431 recovery_segments_adopted: counter!(
432 "chorus.wal.recovery.segments_adopted",
433 "Sealed segments adopted by recovery"
434 ),
435 orphan_objects_deleted: counter!(
436 "chorus.wal.orphan.objects_deleted",
437 "Dead-incarnation segment-object copies deleted by maintenance"
438 ),
439 orphan_sweeps_deferred: counter!(
440 "chorus.wal.orphan.sweeps_deferred",
441 "Dead-incarnation sweeps deferred after transient or terminal storage failures"
442 ),
443 truncation_cycles: counter!(
444 "chorus.wal.truncation.cycles",
445 "Application-triggered truncation cycles completed"
446 ),
447 operation_failures: counter!(
448 "chorus.wal.operation.failures",
449 "Engine and maintenance operations completed with errors"
450 ),
451 max_inflight_records: high_water!(
452 "chorus.wal.pipeline.max_inflight_records",
453 "High-water mark of dispatched uncommitted records"
454 ),
455 max_inflight_bytes: high_water!(
456 "chorus.wal.pipeline.max_inflight_bytes",
457 "High-water mark of admitted unresolved encoded bytes"
458 ),
459 pipeline_refills: counter!(
460 "chorus.wal.pipeline.refills",
461 "Pipeline refills before all prior records completed"
462 ),
463 lane_capacity_drops: counter!(
464 "chorus.wal.lane.capacity_drops",
465 "Replica lanes dropped after exceeding their retained-byte budget"
466 ),
467 wal_record_bytes: counter!(
468 "chorus.wal.append.encoded_bytes",
469 "Encoded durable record bytes generated before replication"
470 ),
471 replica_bytes_attempted: counter!(
472 "chorus.wal.replica.bytes_attempted",
473 "Encoded bytes handed to replica transports including retries"
474 ),
475 open_segments: UpDownCounter::register(
476 recorder,
477 "chorus.wal.catalog.open_segments",
478 "Active appendable segments owned by this client",
479 ),
480 committed_records_watermark: gauge!(
481 "chorus.wal.append.committed_watermark",
482 "Exclusive committed record boundary"
483 ),
484 queue_depth: gauge!(
485 "chorus.wal.pipeline.queue_depth",
486 "Appends waiting across the admission channel and engine queue"
487 ),
488 zone_durable_lag,
489 rotation_state: gauge!(
490 "chorus.wal.rotation.state",
491 "Rotation gate state: 0 idle, 1 due, 2 draining, 3 sealing, 4 disabled"
492 ),
493 maintenance_queue_depth: AggregateGauge::register(
494 recorder,
495 "chorus.wal.maintenance.queue_depth",
496 "Maintenance requests waiting for execution",
497 &[],
498 ),
499 manifest_directory_bytes: gauge!(
500 "chorus.wal.manifest.directory_bytes",
501 "Encoded bytes used by the sealed segment directory"
502 ),
503 append_commit_latency: histogram!(
504 "chorus.wal.append.commit_latency_seconds",
505 "Seconds from append admission to contiguous quorum commit"
506 ),
507 manifest_cas_latency: histogram!(
508 "chorus.wal.manifest.cas_latency_seconds",
509 "Latency of one manifest compare-and-swap request"
510 ),
511 seal_duration: histogram!(
512 "chorus.wal.seal.duration_seconds",
513 "Seconds to finalize a swapped-out segment"
514 ),
515 }
516 }
517
518 pub(crate) fn adjust_zone_durable_lag(&self, zone: usize, delta: i64) {
519 if let Some(gauge) = self.zone_durable_lag.get(zone) {
520 gauge.add(delta);
521 }
522 }
523
524 pub(crate) fn adjust_maintenance_queue_depth(&self, delta: i64) {
525 self.maintenance_queue_depth.add(delta);
526 }
527}
528
529#[cfg(test)]
530pub(crate) mod test_support {
531 use std::collections::HashMap;
532 use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
533 use std::sync::{Arc, Mutex};
534
535 use super::{CounterFn, GaugeFn, HistogramFn, MetricsRecorder, UpDownCounterFn};
536
537 #[derive(Default)]
538 pub(crate) struct TestMetricsRecorder {
539 counters: Mutex<HashMap<String, Arc<AtomicU64>>>,
540 gauges: Mutex<HashMap<String, Arc<AtomicI64>>>,
541 up_down_counters: Mutex<HashMap<String, Arc<AtomicI64>>>,
542 histograms: Mutex<HashMap<String, Arc<Mutex<Vec<f64>>>>>,
543 }
544
545 impl TestMetricsRecorder {
546 pub(crate) fn counter(&self, name: &str) -> u64 {
547 self.counters.lock().unwrap()[name].load(Ordering::Relaxed)
548 }
549
550 pub(crate) fn gauge(&self, name: &str) -> i64 {
551 self.gauges.lock().unwrap()[name].load(Ordering::Relaxed)
552 }
553
554 pub(crate) fn labeled_gauge(&self, name: &str, labels: &[(&str, &str)]) -> i64 {
555 self.gauges.lock().unwrap()[&metric_key(name, labels)].load(Ordering::Relaxed)
556 }
557
558 pub(crate) fn up_down_counter(&self, name: &str) -> i64 {
559 self.up_down_counters.lock().unwrap()[name].load(Ordering::Relaxed)
560 }
561
562 pub(crate) fn histogram_samples(&self, name: &str) -> usize {
563 self.histograms.lock().unwrap()[name].lock().unwrap().len()
564 }
565 }
566
567 struct TestCounter(Arc<AtomicU64>);
568
569 impl CounterFn for TestCounter {
570 fn increment(&self, value: u64) {
571 self.0.fetch_add(value, Ordering::Relaxed);
572 }
573 }
574
575 struct TestGauge(Arc<AtomicI64>);
576
577 impl GaugeFn for TestGauge {
578 fn set(&self, value: i64) {
579 self.0.store(value, Ordering::Relaxed);
580 }
581 }
582
583 struct TestUpDownCounter(Arc<AtomicI64>);
584
585 impl UpDownCounterFn for TestUpDownCounter {
586 fn increment(&self, value: i64) {
587 self.0.fetch_add(value, Ordering::Relaxed);
588 }
589 }
590
591 struct TestHistogram(Arc<Mutex<Vec<f64>>>);
592
593 impl HistogramFn for TestHistogram {
594 fn record(&self, value: f64) {
595 self.0.lock().unwrap().push(value);
596 }
597 }
598
599 impl MetricsRecorder for TestMetricsRecorder {
600 fn register_counter(
601 &self,
602 name: &str,
603 _description: &str,
604 _labels: &[(&str, &str)],
605 ) -> Arc<dyn CounterFn> {
606 let metric = self
607 .counters
608 .lock()
609 .unwrap()
610 .entry(name.to_string())
611 .or_default()
612 .clone();
613 Arc::new(TestCounter(metric))
614 }
615
616 fn register_gauge(
617 &self,
618 name: &str,
619 _description: &str,
620 labels: &[(&str, &str)],
621 ) -> Arc<dyn GaugeFn> {
622 let key = metric_key(name, labels);
623 let metric = self.gauges.lock().unwrap().entry(key).or_default().clone();
624 Arc::new(TestGauge(metric))
625 }
626
627 fn register_up_down_counter(
628 &self,
629 name: &str,
630 _description: &str,
631 _labels: &[(&str, &str)],
632 ) -> Arc<dyn UpDownCounterFn> {
633 let metric = self
634 .up_down_counters
635 .lock()
636 .unwrap()
637 .entry(name.to_string())
638 .or_default()
639 .clone();
640 Arc::new(TestUpDownCounter(metric))
641 }
642
643 fn register_histogram(
644 &self,
645 name: &str,
646 _description: &str,
647 _labels: &[(&str, &str)],
648 _boundaries: &[f64],
649 ) -> Arc<dyn HistogramFn> {
650 let metric = self
651 .histograms
652 .lock()
653 .unwrap()
654 .entry(name.to_string())
655 .or_default()
656 .clone();
657 Arc::new(TestHistogram(metric))
658 }
659 }
660
661 fn metric_key(name: &str, labels: &[(&str, &str)]) -> String {
662 labels
663 .iter()
664 .fold(name.to_string(), |mut key, (name, value)| {
665 key.push('{');
666 key.push_str(name);
667 key.push('=');
668 key.push_str(value);
669 key.push('}');
670 key
671 })
672 }
673}