1use std::sync::Arc;
33use std::sync::atomic::{AtomicU64, Ordering};
34use std::time::{Instant, SystemTime, UNIX_EPOCH};
35
36use tsoracle_core::IgnoreReason;
37
38pub struct ReporterCounter {
39 #[cfg_attr(not(feature = "metrics"), allow(dead_code))]
42 name: &'static str,
43 local: Arc<AtomicU64>,
44}
45
46impl ReporterCounter {
47 pub(crate) fn new(name: &'static str) -> Self {
48 Self {
49 name,
50 local: Arc::new(AtomicU64::new(0)),
51 }
52 }
53
54 pub(crate) fn increment(&self, n: u64) {
55 #[cfg(feature = "metrics")]
56 metrics::counter!(self.name).increment(n);
57 self.local.fetch_add(n, Ordering::Relaxed);
58 }
59
60 #[cfg_attr(not(feature = "tracing"), allow(dead_code))]
62 pub(crate) fn snapshot(&self) -> u64 {
63 self.local.load(Ordering::Relaxed)
64 }
65}
66
67pub struct ReporterHistogram {
68 #[cfg_attr(not(feature = "metrics"), allow(dead_code))]
71 name: &'static str,
72}
73
74impl ReporterHistogram {
75 pub(crate) fn new(name: &'static str) -> Self {
76 Self { name }
77 }
78
79 pub(crate) fn record(&self, _v: f64) {
80 #[cfg(feature = "metrics")]
81 metrics::histogram!(self.name).record(_v);
82 }
83}
84
85pub struct ReporterTimestamp {
86 local: Arc<AtomicU64>,
87}
88
89impl ReporterTimestamp {
90 pub(crate) fn new() -> Self {
91 Self {
92 local: Arc::new(AtomicU64::new(0)),
93 }
94 }
95
96 pub(crate) fn touch_now(&self) {
100 self.local.store(now_unix_ms(), Ordering::Relaxed);
101 }
102
103 #[cfg_attr(not(feature = "tracing"), allow(dead_code))]
106 pub(crate) fn snapshot(&self) -> Option<u64> {
107 let v = self.local.load(Ordering::Relaxed);
108 (v != 0).then_some(v)
109 }
110}
111
112pub struct IgnoredCommitsByReason {
113 pub not_leader: ReporterCounter,
114 pub epoch_mismatch: ReporterCounter,
115 pub not_advanced: ReporterCounter,
116}
117
118impl IgnoredCommitsByReason {
119 fn new() -> Self {
120 Self {
121 not_leader: ReporterCounter::new("tsoracle.window.extensions.ignored.not_leader.total"),
122 epoch_mismatch: ReporterCounter::new(
123 "tsoracle.window.extensions.ignored.epoch_mismatch.total",
124 ),
125 not_advanced: ReporterCounter::new(
126 "tsoracle.window.extensions.ignored.not_advanced.total",
127 ),
128 }
129 }
130
131 pub(crate) fn for_reason(&self, reason: IgnoreReason) -> &ReporterCounter {
132 match reason {
133 IgnoreReason::NotLeader => &self.not_leader,
134 IgnoreReason::EpochMismatch { .. } => &self.epoch_mismatch,
135 IgnoreReason::NotAdvanced { .. } => &self.not_advanced,
136 }
137 }
138}
139
140pub struct Reporter {
141 pub get_ts_requests: ReporterCounter,
143 pub get_ts_success: ReporterCounter,
144 pub timestamps_issued: ReporterCounter,
145
146 pub get_seq_requests: ReporterCounter,
148 pub get_seq_success: ReporterCounter,
149 pub seq_values_issued: ReporterCounter,
150 pub seq_cardinality_rejected: ReporterCounter,
151
152 pub get_seq_batch_requests: ReporterCounter,
157 pub get_seq_batch_success: ReporterCounter,
158 pub seq_batch_keys: ReporterHistogram,
159 pub seq_batch_values_issued: ReporterCounter,
160 pub seq_batch_cardinality_rejected: ReporterCounter,
161
162 pub lease_acquire_requests: ReporterCounter,
164 pub lease_acquire_success: ReporterCounter,
165 pub lease_renew_requests: ReporterCounter,
166 pub lease_renew_success: ReporterCounter,
167 pub lease_release_requests: ReporterCounter,
168
169 pub not_leader: ReporterCounter,
171 pub leader_transitions: ReporterCounter,
172 pub fence_transient_retries: ReporterCounter,
173 pub fence_latency: ReporterHistogram,
174
175 pub window_extensions: ReporterCounter,
177 pub window_extension_latency: ReporterHistogram,
178 pub ignored_commits: IgnoredCommitsByReason,
179
180 pub shutdown_watch_aborted: ReporterCounter,
182 pub heartbeat_task_panicked: ReporterCounter,
183 pub last_leader_transition: ReporterTimestamp,
184 pub started_at: Instant,
185}
186
187impl Reporter {
188 pub fn new() -> Self {
189 Self {
190 get_ts_requests: ReporterCounter::new("tsoracle.get_ts.requests.total"),
191 get_ts_success: ReporterCounter::new("tsoracle.get_ts.success.total"),
192 timestamps_issued: ReporterCounter::new("tsoracle.get_ts.timestamps_issued"),
193 get_seq_requests: ReporterCounter::new("tsoracle.get_seq.requests.total"),
194 get_seq_success: ReporterCounter::new("tsoracle.get_seq.success.total"),
195 seq_values_issued: ReporterCounter::new("tsoracle.get_seq.values_issued"),
196 seq_cardinality_rejected: ReporterCounter::new(
197 "tsoracle.get_seq.cardinality_rejected.total",
198 ),
199 get_seq_batch_requests: ReporterCounter::new("tsoracle.get_seq.batch.requests.total"),
200 get_seq_batch_success: ReporterCounter::new("tsoracle.get_seq.batch.success.total"),
201 seq_batch_keys: ReporterHistogram::new("tsoracle.get_seq.batch.keys"),
202 seq_batch_values_issued: ReporterCounter::new("tsoracle.get_seq.batch.values_issued"),
203 seq_batch_cardinality_rejected: ReporterCounter::new(
204 "tsoracle.get_seq.batch.cardinality_rejected.total",
205 ),
206 lease_acquire_requests: ReporterCounter::new("tsoracle.lease.acquire.requests.total"),
207 lease_acquire_success: ReporterCounter::new("tsoracle.lease.acquire.success.total"),
208 lease_renew_requests: ReporterCounter::new("tsoracle.lease.renew.requests.total"),
209 lease_renew_success: ReporterCounter::new("tsoracle.lease.renew.success.total"),
210 lease_release_requests: ReporterCounter::new("tsoracle.lease.release.requests.total"),
211 not_leader: ReporterCounter::new("tsoracle.not_leader.total"),
212 leader_transitions: ReporterCounter::new("tsoracle.leader_transition.total"),
213 fence_transient_retries: ReporterCounter::new(
214 "tsoracle.leader_transition.fence_transient_retries.total",
215 ),
216 fence_latency: ReporterHistogram::new("tsoracle.leader_transition.fence_latency"),
217 window_extensions: ReporterCounter::new("tsoracle.window.extensions.total"),
218 window_extension_latency: ReporterHistogram::new("tsoracle.window.extension_latency"),
219 ignored_commits: IgnoredCommitsByReason::new(),
220 shutdown_watch_aborted: ReporterCounter::new("tsoracle.shutdown.watch_aborted.total"),
221 heartbeat_task_panicked: ReporterCounter::new("tsoracle.heartbeat.task_panicked.total"),
222 last_leader_transition: ReporterTimestamp::new(),
223 started_at: Instant::now(),
224 }
225 }
226
227 #[cfg(any(test, feature = "test-support"))]
231 pub fn for_tests() -> Self {
232 Self::new()
233 }
234}
235
236impl Default for Reporter {
237 fn default() -> Self {
238 Self::new()
239 }
240}
241
242pub(crate) fn now_unix_ms() -> u64 {
243 SystemTime::now()
244 .duration_since(UNIX_EPOCH)
245 .map(|d| d.as_millis() as u64)
246 .unwrap_or(0)
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn counter_increment_bumps_local_snapshot() {
255 let c = ReporterCounter::new("tsoracle.test.local");
256 assert_eq!(c.snapshot(), 0);
257 c.increment(1);
258 c.increment(4);
259 assert_eq!(c.snapshot(), 5);
260 }
261
262 #[test]
263 #[cfg(feature = "metrics")]
264 fn counter_increment_forwards_to_metrics_recorder() {
265 use metrics::Key;
266 use metrics_util::CompositeKey;
267 use metrics_util::MetricKind;
268 use metrics_util::debugging::{DebuggingRecorder, Snapshotter};
269
270 let recorder = DebuggingRecorder::new();
271 let snapshotter: Snapshotter = recorder.snapshotter();
272
273 metrics::with_local_recorder(&recorder, || {
274 let c = ReporterCounter::new("tsoracle.test.forwarded");
275 c.increment(7);
276 });
277
278 let snap = snapshotter.snapshot().into_vec();
279 let key = CompositeKey::new(
280 MetricKind::Counter,
281 Key::from_name("tsoracle.test.forwarded"),
282 );
283 let entry = snap
284 .iter()
285 .find(|(k, ..)| *k == key)
286 .expect("counter not recorded by DebuggingRecorder");
287 let (_, _, _, value) = entry;
288 assert_eq!(
289 format!("{value:?}"),
290 "Counter(7)",
291 "expected the recorder to observe Counter(7), got {value:?}"
292 );
293 }
294
295 #[test]
296 #[cfg(feature = "metrics")]
297 fn histogram_record_forwards_to_metrics_recorder() {
298 use metrics::Key;
299 use metrics_util::CompositeKey;
300 use metrics_util::MetricKind;
301 use metrics_util::debugging::DebuggingRecorder;
302
303 let recorder = DebuggingRecorder::new();
304 let snapshotter = recorder.snapshotter();
305
306 metrics::with_local_recorder(&recorder, || {
307 let h = ReporterHistogram::new("tsoracle.test.hist");
308 h.record(1.0);
309 h.record(2.0);
310 h.record(3.0);
311 });
312
313 let snap = snapshotter.snapshot().into_vec();
314 let key = CompositeKey::new(MetricKind::Histogram, Key::from_name("tsoracle.test.hist"));
315 assert!(
316 snap.iter().any(|(k, ..)| *k == key),
317 "histogram key not recorded"
318 );
319 }
320
321 #[test]
322 fn timestamp_zero_is_none() {
323 let t = ReporterTimestamp::new();
324 assert_eq!(t.snapshot(), None);
325 }
326
327 #[test]
328 fn timestamp_touch_records_now() {
329 let before = now_unix_ms();
330 let t = ReporterTimestamp::new();
331 t.touch_now();
332 let after = now_unix_ms();
333 let observed = t.snapshot().expect("touch_now should produce Some");
334 assert!(
335 observed >= before && observed <= after,
336 "timestamp {observed} not within [{before}, {after}]"
337 );
338 }
339
340 #[test]
341 #[cfg(not(feature = "metrics"))]
342 fn counter_works_without_metrics_feature() {
343 let c = ReporterCounter::new("tsoracle.test.no_metrics");
348 c.increment(3);
349 assert_eq!(c.snapshot(), 3);
350 }
351
352 #[test]
353 #[cfg(feature = "metrics")]
354 fn reporter_new_resolves_distinct_metric_names() {
355 use metrics::Key;
356 use metrics_util::CompositeKey;
357 use metrics_util::MetricKind;
358 use metrics_util::debugging::DebuggingRecorder;
359 use tsoracle_core::{Epoch, IgnoreReason};
360
361 let recorder = DebuggingRecorder::new();
362 let snapshotter = recorder.snapshotter();
363
364 metrics::with_local_recorder(&recorder, || {
365 let r = Reporter::new();
366 r.ignored_commits
367 .for_reason(IgnoreReason::NotLeader)
368 .increment(1);
369 r.ignored_commits
370 .for_reason(IgnoreReason::EpochMismatch {
371 expected: Epoch(1),
372 current: Epoch(2),
373 })
374 .increment(1);
375 r.ignored_commits
376 .for_reason(IgnoreReason::NotAdvanced {
377 persisted: 1,
378 committed: 2,
379 })
380 .increment(1);
381 });
382
383 let snap = snapshotter.snapshot().into_vec();
384 for name in [
385 "tsoracle.window.extensions.ignored.not_leader.total",
386 "tsoracle.window.extensions.ignored.epoch_mismatch.total",
387 "tsoracle.window.extensions.ignored.not_advanced.total",
388 ] {
389 let key = CompositeKey::new(MetricKind::Counter, Key::from_name(name));
390 assert!(
391 snap.iter().any(|(k, ..)| *k == key),
392 "expected metric {name} to be recorded distinctly"
393 );
394 }
395 }
396}