faucet_core/observability/
timer.rs1use metrics::{KeyName, Label, SharedString, histogram};
5use std::time::Instant;
6
7#[must_use = "DurationGuard must be bound to a variable; otherwise it records elapsed=0"]
11pub struct DurationGuard {
12 name: KeyName,
13 labels: Vec<Label>,
14 started_at: Instant,
15 armed: bool,
16}
17
18impl DurationGuard {
19 pub fn new(name: impl Into<KeyName>, labels: Vec<Label>) -> Self {
20 Self {
21 name: name.into(),
22 labels,
23 started_at: Instant::now(),
24 armed: true,
25 }
26 }
27
28 pub fn disarm(&mut self) {
33 self.armed = false;
34 }
35
36 pub fn record_now(&mut self) {
43 if !self.armed {
44 return;
45 }
46 let elapsed = self.started_at.elapsed().as_secs_f64();
47 histogram!(self.name.clone(), self.labels.clone()).record(elapsed);
48 self.armed = false;
49 }
50
51 pub fn with_connector(
53 name: impl Into<KeyName>,
54 pipeline: SharedString,
55 row: SharedString,
56 connector: SharedString,
57 ) -> Self {
58 Self::new(
59 name,
60 vec![
61 Label::new("pipeline", pipeline),
62 Label::new("row", row),
63 Label::new("connector", connector),
64 ],
65 )
66 }
67}
68
69impl Drop for DurationGuard {
70 fn drop(&mut self) {
71 if !self.armed {
72 return;
73 }
74 let elapsed = self.started_at.elapsed().as_secs_f64();
75 histogram!(self.name.clone(), self.labels.clone()).record(elapsed);
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use metrics::SharedString;
83 use metrics_util::debugging::DebugValue;
84 use std::thread;
85 use std::time::Duration;
86
87 use crate::observability::decorator::source_tests::{LOCK, snapshotter};
93
94 #[test]
95 fn records_sample_on_drop() {
96 let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
97 let snap = snapshotter();
98 {
99 let _guard = DurationGuard::with_connector(
100 "test_duration_records_sample",
101 SharedString::const_str("p"),
102 SharedString::const_str("r"),
103 SharedString::const_str("c"),
104 );
105 thread::sleep(Duration::from_millis(2));
106 }
107 let snapshot = snap.snapshot();
108 let found = snapshot.into_vec().into_iter().any(|(key, _u, _d, value)| {
109 key.key().name() == "test_duration_records_sample"
110 && matches!(
111 value,
112 DebugValue::Histogram(samples)
113 if samples.first().map(|s| s.into_inner()).unwrap_or(0.0) > 0.0
114 )
115 });
116 assert!(
117 found,
118 "expected a histogram sample > 0 on test_duration_records_sample"
119 );
120 }
121
122 #[test]
123 fn record_now_records_once_and_disarms() {
124 let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
127 let snap = snapshotter();
128 {
129 let mut guard = DurationGuard::with_connector(
130 "test_duration_record_now",
131 SharedString::const_str("p"),
132 SharedString::const_str("r"),
133 SharedString::const_str("c"),
134 );
135 thread::sleep(Duration::from_millis(2));
136 guard.record_now();
137 } let snapshot = snap.snapshot();
139 let samples = snapshot
140 .into_vec()
141 .into_iter()
142 .find_map(|(key, _u, _d, value)| {
143 (key.key().name() == "test_duration_record_now").then_some(value)
144 });
145 match samples {
146 Some(DebugValue::Histogram(s)) => {
147 assert_eq!(
148 s.len(),
149 1,
150 "record_now + drop must record exactly one sample"
151 );
152 assert!(s[0].into_inner() > 0.0);
153 }
154 other => panic!("expected one histogram sample, got {other:?}"),
155 }
156 }
157
158 #[test]
159 fn records_sample_when_dropped_early() {
160 let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
163 let snap = snapshotter();
164 {
165 let _guard = DurationGuard::with_connector(
166 "test_duration_drop_early",
167 SharedString::const_str("p"),
168 SharedString::const_str("r"),
169 SharedString::const_str("c"),
170 );
171 }
172 let snapshot = snap.snapshot();
173 let found = snapshot
174 .into_vec()
175 .into_iter()
176 .any(|(key, _u, _d, _v)| key.key().name() == "test_duration_drop_early");
177 assert!(
178 found,
179 "expected a histogram entry for test_duration_drop_early"
180 );
181 }
182}