1use prometheus::core::Collector;
5use prometheus::{Error as PromError, IntCounter, IntGauge, Registry};
6use tracing::warn;
7
8use crate::prom::reg_or_local;
9
10fn register_collector<C: Collector + Clone + 'static>(reg: &Registry, name: &str, c: &C) {
11 match reg.register(Box::new(c.clone())) {
12 Ok(()) => {}
13 Err(PromError::AlreadyReg) => {
16 warn!(
17 metric = name,
18 "metric already registered; use separate registries per connector"
19 );
20 }
21 Err(e) => warn!(metric = name, error = ?e, "failed to register metric"),
22 }
23}
24
25#[derive(Debug, Clone)]
27#[allow(missing_docs)]
28pub struct NatsSourceMetrics {
29 pub records_total: IntCounter,
30 pub bytes_total: IntCounter,
31 pub fetch_errors_total: IntCounter,
32 pub acks_total: IntCounter,
33 pub ack_errors_total: IntCounter,
34 pub pending_acks: IntGauge,
35 pub consumer_lag: IntGauge,
37}
38
39impl NatsSourceMetrics {
40 #[must_use]
42 #[allow(clippy::missing_panics_doc)]
43 pub fn new(registry: Option<&Registry>) -> Self {
44 let mut local = None;
45 let handle = reg_or_local(registry, &mut local);
46 let reg = handle.registry();
47
48 macro_rules! reg_c {
49 ($name:expr, $help:expr) => {{
50 let c = IntCounter::new($name, $help).unwrap();
51 register_collector(reg, $name, &c);
52 c
53 }};
54 }
55 let pending_acks = IntGauge::new(
56 "nats_source_pending_acks",
57 "JetStream acks queued or in flight",
58 )
59 .unwrap();
60 register_collector(reg, "nats_source_pending_acks", &pending_acks);
61 let consumer_lag = IntGauge::new(
62 "nats_source_consumer_lag",
63 "Stream messages not yet delivered to the consumer",
64 )
65 .unwrap();
66 register_collector(reg, "nats_source_consumer_lag", &consumer_lag);
67
68 Self {
69 records_total: reg_c!("nats_source_records_total", "Records delivered"),
70 bytes_total: reg_c!("nats_source_bytes_total", "Payload bytes delivered"),
71 fetch_errors_total: reg_c!("nats_source_fetch_errors_total", "Fetch errors"),
72 acks_total: reg_c!("nats_source_acks_total", "Successful acks"),
73 ack_errors_total: reg_c!("nats_source_ack_errors_total", "Failed acks"),
74 pending_acks,
75 consumer_lag,
76 }
77 }
78
79 #[allow(missing_docs)]
80 pub fn record_poll(&self, records: u64, bytes: u64) {
81 self.records_total.inc_by(records);
82 self.bytes_total.inc_by(bytes);
83 }
84
85 #[allow(missing_docs)]
86 pub fn record_fetch_error(&self) {
87 self.fetch_errors_total.inc();
88 }
89
90 pub(crate) fn record_ack_enqueued(&self) {
91 self.pending_acks.inc();
92 }
93
94 #[allow(missing_docs)]
95 pub fn record_ack(&self) {
96 self.acks_total.inc();
97 self.pending_acks.dec();
98 }
99
100 #[allow(missing_docs)]
101 pub fn record_ack_error(&self) {
102 self.ack_errors_total.inc();
103 self.pending_acks.dec();
104 }
105
106 pub(crate) fn record_ack_enqueue_errors(&self, n: usize) {
107 self.ack_errors_total
108 .inc_by(u64::try_from(n).unwrap_or(u64::MAX));
109 }
110
111 #[allow(missing_docs, clippy::cast_possible_wrap)]
112 pub fn record_ack_abandoned(&self, n: usize) {
113 self.ack_errors_total
114 .inc_by(u64::try_from(n).unwrap_or(u64::MAX));
115 self.pending_acks.sub(n as i64);
116 }
117
118 pub(crate) fn record_abandoned_acks(&self) {
119 let pending = self.pending_acks.get();
120 if pending > 0 {
121 self.ack_errors_total
122 .inc_by(u64::try_from(pending).unwrap_or(u64::MAX));
123 self.pending_acks.set(0);
124 }
125 }
126
127 #[allow(missing_docs, clippy::cast_possible_wrap)]
128 pub fn set_consumer_lag(&self, n: u64) {
129 self.consumer_lag.set(n as i64);
130 }
131}
132
133#[derive(Debug, Clone)]
135#[allow(missing_docs)]
136pub struct NatsSinkMetrics {
137 pub records_total: IntCounter,
138 pub bytes_total: IntCounter,
139 pub publish_errors_total: IntCounter,
140 pub ack_errors_total: IntCounter,
141 pub dedup_total: IntCounter,
143 pub pending_futures: IntGauge,
144}
145
146impl NatsSinkMetrics {
147 #[must_use]
149 #[allow(clippy::missing_panics_doc)]
150 pub fn new(registry: Option<&Registry>) -> Self {
151 let mut local = None;
152 let handle = reg_or_local(registry, &mut local);
153 let reg = handle.registry();
154
155 macro_rules! reg_c {
156 ($name:expr, $help:expr) => {{
157 let c = IntCounter::new($name, $help).unwrap();
158 register_collector(reg, $name, &c);
159 c
160 }};
161 }
162 let pending_futures =
163 IntGauge::new("nats_sink_pending_futures", "Outstanding PublishAckFutures").unwrap();
164 register_collector(reg, "nats_sink_pending_futures", &pending_futures);
165
166 Self {
167 records_total: reg_c!("nats_sink_records_total", "Records published"),
168 bytes_total: reg_c!("nats_sink_bytes_total", "Payload bytes published"),
169 publish_errors_total: reg_c!("nats_sink_publish_errors_total", "Publish errors"),
170 ack_errors_total: reg_c!("nats_sink_ack_errors_total", "Publish-ack errors"),
171 dedup_total: reg_c!("nats_sink_dedup_total", "Broker-dropped duplicates"),
172 pending_futures,
173 }
174 }
175
176 #[allow(missing_docs)]
177 pub fn record_published_row(&self, bytes: u64) {
178 self.records_total.inc();
179 self.bytes_total.inc_by(bytes);
180 }
181
182 #[allow(missing_docs)]
183 pub fn record_publish_error(&self) {
184 self.publish_errors_total.inc();
185 }
186
187 #[allow(missing_docs)]
188 pub fn record_ack_error(&self) {
189 self.ack_errors_total.inc();
190 }
191
192 #[allow(missing_docs)]
193 pub fn record_dedup(&self) {
194 self.dedup_total.inc();
195 }
196
197 #[allow(missing_docs, clippy::cast_possible_wrap)]
198 pub fn set_pending_futures(&self, n: usize) {
199 self.pending_futures.set(n as i64);
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn source_initial_zero() {
209 let m = NatsSourceMetrics::new(None);
210 assert_eq!(m.records_total.get(), 0);
211 assert_eq!(m.bytes_total.get(), 0);
212 assert_eq!(m.fetch_errors_total.get(), 0);
213 assert_eq!(m.consumer_lag.get(), 0);
214 }
215
216 #[test]
217 fn source_records_increment() {
218 let m = NatsSourceMetrics::new(None);
219 m.record_poll(100, 4096);
220 m.record_poll(50, 1024);
221 m.record_ack_enqueued();
222 m.record_ack();
223 m.record_ack_enqueued();
224 m.record_ack_error();
225 m.record_ack_enqueued();
226 m.record_ack_enqueued();
227 m.record_ack_abandoned(2);
228 m.record_fetch_error();
229
230 m.set_consumer_lag(42);
231
232 assert_eq!(m.records_total.get(), 150);
233 assert_eq!(m.bytes_total.get(), 5120);
234 assert_eq!(m.fetch_errors_total.get(), 1);
235 assert_eq!(m.consumer_lag.get(), 42);
236 assert_eq!(m.acks_total.get(), 1);
237 assert_eq!(m.ack_errors_total.get(), 3);
238 assert_eq!(m.pending_acks.get(), 0);
239 }
240
241 #[test]
242 fn sink_records_and_dedup() {
243 let m = NatsSinkMetrics::new(None);
244 for _ in 0..10 {
245 m.record_published_row(200);
246 }
247 m.record_dedup();
248 m.record_dedup();
249 m.set_pending_futures(3);
250
251 assert_eq!(m.records_total.get(), 10);
252 assert_eq!(m.bytes_total.get(), 2000);
253 assert_eq!(m.pending_futures.get(), 3);
254 assert_eq!(m.dedup_total.get(), 2);
255 }
256
257 #[test]
258 fn metrics_register_on_shared_registry() {
259 let reg = Registry::new();
260 let _s = NatsSourceMetrics::new(Some(®));
261 let _k = NatsSinkMetrics::new(Some(®));
262 let names: Vec<String> = reg.gather().iter().map(|f| f.name().to_string()).collect();
263 assert!(names.contains(&"nats_source_records_total".to_string()));
264 assert!(names.contains(&"nats_sink_records_total".to_string()));
265 assert!(names.contains(&"nats_sink_dedup_total".to_string()));
266 }
267}