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)]
80#[allow(clippy::disallowed_methods)] mod tests {
82 use super::*;
83 use metrics::SharedString;
84 use metrics_util::debugging::DebugValue;
85 use std::thread;
86 use std::time::Duration;
87
88 use crate::observability::decorator::source_tests::{LOCK, snapshotter};
94
95 #[test]
96 fn records_sample_on_drop() {
97 let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
98 let snap = snapshotter();
99 {
100 let _guard = DurationGuard::with_connector(
101 "test_duration_records_sample",
102 SharedString::const_str("p"),
103 SharedString::const_str("r"),
104 SharedString::const_str("c"),
105 );
106 thread::sleep(Duration::from_millis(2));
107 }
108 let snapshot = snap.snapshot();
109 let found = snapshot.into_vec().into_iter().any(|(key, _u, _d, value)| {
110 key.key().name() == "test_duration_records_sample"
111 && matches!(
112 value,
113 DebugValue::Histogram(samples)
114 if samples.first().map(|s| s.into_inner()).unwrap_or(0.0) > 0.0
115 )
116 });
117 assert!(
118 found,
119 "expected a histogram sample > 0 on test_duration_records_sample"
120 );
121 }
122
123 #[test]
124 fn record_now_records_once_and_disarms() {
125 let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
128 let snap = snapshotter();
129 {
130 let mut guard = DurationGuard::with_connector(
131 "test_duration_record_now",
132 SharedString::const_str("p"),
133 SharedString::const_str("r"),
134 SharedString::const_str("c"),
135 );
136 thread::sleep(Duration::from_millis(2));
137 guard.record_now();
138 } let snapshot = snap.snapshot();
140 let samples = snapshot
141 .into_vec()
142 .into_iter()
143 .find_map(|(key, _u, _d, value)| {
144 (key.key().name() == "test_duration_record_now").then_some(value)
145 });
146 match samples {
147 Some(DebugValue::Histogram(s)) => {
148 assert_eq!(
149 s.len(),
150 1,
151 "record_now + drop must record exactly one sample"
152 );
153 assert!(s[0].into_inner() > 0.0);
154 }
155 other => panic!("expected one histogram sample, got {other:?}"),
156 }
157 }
158
159 #[test]
160 fn records_sample_when_dropped_early() {
161 let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
164 let snap = snapshotter();
165 {
166 let _guard = DurationGuard::with_connector(
167 "test_duration_drop_early",
168 SharedString::const_str("p"),
169 SharedString::const_str("r"),
170 SharedString::const_str("c"),
171 );
172 }
173 let snapshot = snap.snapshot();
174 let found = snapshot
175 .into_vec()
176 .into_iter()
177 .any(|(key, _u, _d, _v)| key.key().name() == "test_duration_drop_early");
178 assert!(
179 found,
180 "expected a histogram entry for test_duration_drop_early"
181 );
182 }
183}