1use std::{
8 collections::{BTreeMap, HashMap},
9 sync::{
10 Arc, Mutex, MutexGuard, PoisonError,
11 atomic::{AtomicI64, AtomicU64, Ordering},
12 },
13 time::{SystemTime, UNIX_EPOCH},
14};
15
16use super::otlp::{
17 AggregationTemporality, KeyValue, Metric, MetricData, MetricsData, NumberDataPoint,
18 NumberValue, Resource, ResourceMetrics, ScopeMetrics,
19};
20
21pub const TELEMETRY_METRIC_DOMAIN: &str = "org.apache.kafka";
24
25const EXCLUDED_ATTRIBUTE: &str = "client_id";
28
29#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub struct MetricKey {
32 pub name: String,
34 pub tags: BTreeMap<String, String>,
36}
37
38impl MetricKey {
39 #[must_use]
52 pub fn kafka<'a>(
53 group: &str,
54 name: &str,
55 tags: impl IntoIterator<Item = (&'a str, &'a str)>,
56 ) -> Self {
57 let group = clean(group, ".").replace(".metrics", "");
58 Self {
59 name: format!("{TELEMETRY_METRIC_DOMAIN}.{group}.{}", clean(name, ".")),
60 tags: tags
61 .into_iter()
62 .map(|(key, value)| (clean(key, "_"), value.to_owned()))
63 .collect(),
64 }
65 }
66}
67
68fn clean(raw: &str, joiner: &str) -> String {
69 raw.to_lowercase().replace('-', joiner)
70}
71
72#[derive(Clone, Debug, Default)]
75pub struct Counter(Arc<AtomicU64>);
76
77impl Counter {
78 pub fn add(&self, amount: u64) {
80 self.0.fetch_add(amount, Ordering::Relaxed);
81 }
82
83 #[must_use]
85 pub fn value(&self) -> u64 {
86 self.0.load(Ordering::Relaxed)
87 }
88}
89
90#[derive(Clone, Debug, Default)]
92pub struct Gauge(Arc<AtomicI64>);
93
94impl Gauge {
95 pub fn add(&self, delta: i64) {
97 self.0.fetch_add(delta, Ordering::Relaxed);
98 }
99
100 pub fn set(&self, value: i64) {
102 self.0.store(value, Ordering::Relaxed);
103 }
104
105 #[must_use]
107 pub fn value(&self) -> i64 {
108 self.0.load(Ordering::Relaxed)
109 }
110}
111
112#[derive(Clone, Debug)]
113enum Cell {
114 Counter(Counter),
115 Gauge(Gauge),
116}
117
118#[derive(Clone, Debug)]
119struct Registered {
120 cell: Cell,
121 added_unix_nanos: u64,
123}
124
125#[derive(Clone, Debug, Default)]
127pub struct ClientMetrics {
128 registered: Arc<Mutex<BTreeMap<MetricKey, Registered>>>,
129}
130
131impl ClientMetrics {
132 #[must_use]
134 pub fn new() -> Self {
135 Self::default()
136 }
137
138 #[must_use]
142 pub fn counter(&self, key: MetricKey) -> Counter {
143 let mut registered = self.lock();
144 if let Some(Registered {
145 cell: Cell::Counter(counter),
146 ..
147 }) = registered.get(&key)
148 {
149 return counter.clone();
150 }
151 let counter = Counter::default();
152 registered.insert(
153 key,
154 Registered {
155 cell: Cell::Counter(counter.clone()),
156 added_unix_nanos: unix_nanos_now(),
157 },
158 );
159 counter
160 }
161
162 #[must_use]
165 pub fn gauge(&self, key: MetricKey) -> Gauge {
166 let mut registered = self.lock();
167 if let Some(Registered {
168 cell: Cell::Gauge(gauge),
169 ..
170 }) = registered.get(&key)
171 {
172 return gauge.clone();
173 }
174 let gauge = Gauge::default();
175 registered.insert(
176 key,
177 Registered {
178 cell: Cell::Gauge(gauge.clone()),
179 added_unix_nanos: unix_nanos_now(),
180 },
181 );
182 gauge
183 }
184
185 pub fn remove(&self, key: &MetricKey) {
187 self.lock().remove(key);
188 }
189
190 fn lock(&self) -> MutexGuard<'_, BTreeMap<MetricKey, Registered>> {
191 self.registered
192 .lock()
193 .unwrap_or_else(PoisonError::into_inner)
194 }
195
196 fn snapshot(&self) -> Vec<(MetricKey, Registered)> {
197 self.lock()
198 .iter()
199 .map(|(key, registered)| (key.clone(), registered.clone()))
200 .collect()
201 }
202}
203
204pub fn unix_nanos_now() -> u64 {
206 SystemTime::now()
207 .duration_since(UNIX_EPOCH)
208 .ok()
209 .and_then(|elapsed| u64::try_from(elapsed.as_nanos()).ok())
210 .unwrap_or(0)
211}
212
213#[derive(Clone, Debug)]
217pub struct NetworkMetrics {
218 connection_creation_total: Counter,
219 connection_close_total: Counter,
220 connection_count: Gauge,
221 request_total: Counter,
222 outgoing_byte_total: Counter,
223 response_total: Counter,
224 incoming_byte_total: Counter,
225}
226
227impl NetworkMetrics {
228 #[must_use]
231 pub fn register(metrics: &ClientMetrics, group: &str) -> Self {
232 let counter = |name| metrics.counter(MetricKey::kafka(group, name, []));
233 Self {
234 connection_creation_total: counter("connection-creation-total"),
235 connection_close_total: counter("connection-close-total"),
236 connection_count: metrics.gauge(MetricKey::kafka(group, "connection-count", [])),
237 request_total: counter("request-total"),
238 outgoing_byte_total: counter("outgoing-byte-total"),
239 response_total: counter("response-total"),
240 incoming_byte_total: counter("incoming-byte-total"),
241 }
242 }
243
244 pub(crate) fn opened(&self) -> ConnectionMetrics {
247 self.connection_creation_total.add(1);
248 self.connection_count.add(1);
249 ConnectionMetrics(self.clone())
250 }
251}
252
253#[derive(Debug)]
255pub struct ConnectionMetrics(NetworkMetrics);
256
257impl ConnectionMetrics {
258 pub(crate) fn request(&self, frame_len: usize) {
261 self.0.request_total.add(1);
262 self.0.outgoing_byte_total.add(wire_len(frame_len));
263 }
264
265 pub(crate) fn response(&self, body_len: usize) {
268 self.0.response_total.add(1);
269 self.0
270 .incoming_byte_total
271 .add(wire_len(body_len).saturating_add(4));
272 }
273}
274
275fn wire_len(len: usize) -> u64 {
276 u64::try_from(len).unwrap_or(u64::MAX).saturating_add(4)
277}
278
279impl Drop for ConnectionMetrics {
280 fn drop(&mut self) {
281 self.0.connection_close_total.add(1);
282 self.0.connection_count.add(-1);
283 }
284}
285
286#[derive(Clone, Debug, PartialEq, Eq)]
289pub enum MetricSelector {
290 NoMetrics,
292 AllMetrics,
294 Prefixes(Vec<String>),
296}
297
298impl MetricSelector {
299 pub(crate) fn from_requested(requested: &[String]) -> Self {
300 match requested {
301 [] => Self::NoMetrics,
302 [all] if all == "*" => Self::AllMetrics,
303 prefixes => Self::Prefixes(prefixes.to_vec()),
304 }
305 }
306
307 pub(crate) fn matches(&self, name: &str) -> bool {
308 match self {
309 Self::NoMetrics => false,
310 Self::AllMetrics => true,
311 Self::Prefixes(prefixes) => prefixes.iter().any(|prefix| name.starts_with(prefix)),
312 }
313 }
314}
315
316#[derive(Debug)]
319pub struct Collector {
320 metrics: ClientMetrics,
321 resource: Resource,
322 last_values: HashMap<MetricKey, (u64, f64)>,
324 added: HashMap<MetricKey, u64>,
326 reset: bool,
328}
329
330impl Collector {
331 pub(crate) fn new(metrics: ClientMetrics, resource: Resource) -> Self {
332 Self {
333 metrics,
334 resource,
335 last_values: HashMap::new(),
336 added: HashMap::new(),
337 reset: false,
338 }
339 }
340
341 pub(crate) fn reset(&mut self) {
344 self.last_values.clear();
345 self.added.clear();
346 self.reset = true;
347 }
348
349 pub(crate) fn collect(
352 &mut self,
353 selector: &MetricSelector,
354 delta_temporality: bool,
355 now_unix_nanos: u64,
356 ) -> MetricsData {
357 let mut resource_metrics = Vec::new();
358 for (key, registered) in self.metrics.snapshot() {
359 if !selector.matches(&key.name) {
360 continue;
361 }
362 let attributes = key
363 .tags
364 .iter()
365 .filter(|(tag, _)| tag.as_str() != EXCLUDED_ATTRIBUTE)
366 .map(|(tag, value)| KeyValue::new(tag, value))
367 .collect::<Vec<_>>();
368 let data = match registered.cell {
369 Cell::Gauge(gauge) => MetricData::Gauge {
370 data_points: vec![NumberDataPoint {
371 attributes,
372 start_time_unix_nano: 0,
373 time_unix_nano: now_unix_nanos,
374 value: Some(NumberValue::Double(i64_to_f64(gauge.value()))),
375 }],
376 },
377 Cell::Counter(counter) => {
378 let value = u64_to_f64(counter.value());
379 let added = *self.added.entry(key.clone()).or_insert(if self.reset {
380 now_unix_nanos
381 } else {
382 registered.added_unix_nanos
383 });
384 let (start, value, aggregation_temporality) = if delta_temporality {
385 match self
386 .last_values
387 .insert(key.clone(), (now_unix_nanos, value))
388 {
389 Some((last_time, last_value)) => {
390 (last_time, value - last_value, AggregationTemporality::Delta)
391 }
392 None => (added, value, AggregationTemporality::Delta),
393 }
394 } else {
395 (added, value, AggregationTemporality::Cumulative)
396 };
397 MetricData::Sum {
398 data_points: vec![NumberDataPoint {
399 attributes,
400 start_time_unix_nano: start,
401 time_unix_nano: now_unix_nanos,
402 value: Some(NumberValue::Double(value)),
403 }],
404 aggregation_temporality,
405 is_monotonic: true,
406 }
407 }
408 };
409 resource_metrics.push(ResourceMetrics {
410 resource: Some(self.resource.clone()),
411 scope_metrics: vec![ScopeMetrics {
412 metrics: vec![Metric {
413 name: key.name,
414 data: Some(data),
415 }],
416 }],
417 });
418 }
419 MetricsData { resource_metrics }
420 }
421}
422
423const TWO_POW_32: f64 = 4_294_967_296.0;
424
425fn u64_to_f64(value: u64) -> f64 {
427 let high = u32::try_from(value >> 32).unwrap_or(u32::MAX);
428 let low = u32::try_from(value & 0xffff_ffff).unwrap_or(u32::MAX);
429 f64::from(high).mul_add(TWO_POW_32, f64::from(low))
430}
431
432fn i64_to_f64(value: i64) -> f64 {
433 let high = i32::try_from(value >> 32).unwrap_or(0);
434 let low = u32::try_from(value & 0xffff_ffff).unwrap_or(0);
435 f64::from(high).mul_add(TWO_POW_32, f64::from(low))
436}
437
438#[cfg(test)]
439mod tests {
440 use assert2::assert;
441
442 use super::*;
443
444 #[test]
445 fn metric_keys_follow_kafkas_naming_convention() {
446 let cases = [
447 (
448 ("producer-metrics", "record-send-rate", vec![]),
449 "org.apache.kafka.producer.record.send.rate",
450 vec![],
451 ),
452 (
453 ("admin-client-metrics", "request-total", vec![]),
454 "org.apache.kafka.admin.client.request.total",
455 vec![],
456 ),
457 (
458 (
459 "consumer-fetch-manager-metrics",
460 "Records-Lag-Max",
461 vec![("client-id", "c"), ("topic", "t")],
462 ),
463 "org.apache.kafka.consumer.fetch.manager.records.lag.max",
464 vec![("client_id", "c"), ("topic", "t")],
465 ),
466 (
467 (
468 "producer-node-metrics",
469 "request-latency-avg",
470 vec![("node-id", "node-1")],
471 ),
472 "org.apache.kafka.producer.node.request.latency.avg",
473 vec![("node_id", "node-1")],
474 ),
475 ];
476 for ((group, name, tags), expected_name, expected_tags) in cases {
477 let expected = MetricKey {
478 name: expected_name.to_owned(),
479 tags: expected_tags
480 .into_iter()
481 .map(|(key, value)| (key.to_owned(), value.to_owned()))
482 .collect(),
483 };
484 assert!(MetricKey::kafka(group, name, tags) == expected);
485 }
486 }
487
488 #[test]
489 fn selector_follows_the_requested_metrics() {
490 let name = "org.apache.kafka.producer.request.total";
491 let cases: [(&[&str], MetricSelector, bool); 5] = [
492 (&[], MetricSelector::NoMetrics, false),
493 (&["*"], MetricSelector::AllMetrics, true),
494 (
495 &["org.apache.kafka.producer"],
496 MetricSelector::Prefixes(vec!["org.apache.kafka.producer".into()]),
497 true,
498 ),
499 (
500 &["org.apache.kafka.consumer", "*"],
501 MetricSelector::Prefixes(vec!["org.apache.kafka.consumer".into(), "*".into()]),
502 false,
503 ),
504 (&[""], MetricSelector::Prefixes(vec![String::new()]), true),
505 ];
506 for (requested, expected, matches) in cases {
507 let requested = requested
508 .iter()
509 .map(|s| (*s).to_owned())
510 .collect::<Vec<_>>();
511 let selector = MetricSelector::from_requested(&requested);
512 assert!(selector == expected, "{requested:?}");
513 assert!(selector.matches(name) == matches, "{requested:?}");
514 }
515 }
516
517 #[test]
518 fn numbers_convert_to_the_nearest_double() {
519 for value in [0u64, 1, 42, u64::from(u32::MAX) + 1, 1 << 53, u64::MAX] {
520 assert!(
521 u64_to_f64(value).to_bits() == format!("{value}").parse::<f64>().unwrap().to_bits()
522 );
523 }
524 for value in [0i64, 1, -1, i64::from(i32::MIN) - 5, i64::MAX, i64::MIN] {
525 assert!(
526 i64_to_f64(value).to_bits() == format!("{value}").parse::<f64>().unwrap().to_bits()
527 );
528 }
529 }
530
531 fn sum_point(
532 name: &str,
533 start: u64,
534 time: u64,
535 value: f64,
536 aggregation_temporality: AggregationTemporality,
537 ) -> ResourceMetrics {
538 ResourceMetrics {
539 resource: Some(Resource {
540 attributes: vec![KeyValue::new("transactional_id", "tx")],
541 }),
542 scope_metrics: vec![ScopeMetrics {
543 metrics: vec![Metric {
544 name: name.to_owned(),
545 data: Some(MetricData::Sum {
546 data_points: vec![NumberDataPoint {
547 attributes: vec![KeyValue::new("topic", "t")],
548 start_time_unix_nano: start,
549 time_unix_nano: time,
550 value: Some(NumberValue::Double(value)),
551 }],
552 aggregation_temporality,
553 is_monotonic: true,
554 }),
555 }],
556 }],
557 }
558 }
559
560 #[test]
564 fn collector_pushes_sums_with_the_subscribed_temporality() {
565 let metrics = ClientMetrics::new();
566 let key = MetricKey::kafka(
567 "producer-topic-metrics",
568 "record-send-total",
569 [("client-id", "p"), ("topic", "t")],
570 );
571 let name = key.name.clone();
572 let counter = metrics.counter(key.clone());
573 let added = metrics.lock()[&key].added_unix_nanos;
574 let mut collector = Collector::new(
575 metrics,
576 Resource {
577 attributes: vec![KeyValue::new("transactional_id", "tx")],
578 },
579 );
580 let all = MetricSelector::AllMetrics;
581 let mut pushes = Vec::new();
582 counter.add(5);
583 pushes.push(collector.collect(&all, false, 100));
584 counter.add(7);
585 pushes.push(collector.collect(&all, false, 200));
586 pushes.push(collector.collect(&all, true, 300));
587 counter.add(3);
588 pushes.push(collector.collect(&all, true, 400));
589 collector.reset();
590 pushes.push(collector.collect(&all, false, 500));
591 pushes.push(collector.collect(&MetricSelector::NoMetrics, false, 600));
592
593 let one = |point| MetricsData {
594 resource_metrics: vec![point],
595 };
596 let cumulative = AggregationTemporality::Cumulative;
597 let delta = AggregationTemporality::Delta;
598 assert!(
599 pushes
600 == [
601 one(sum_point(&name, added, 100, 5.0, cumulative)),
602 one(sum_point(&name, added, 200, 12.0, cumulative)),
603 one(sum_point(&name, added, 300, 12.0, delta)),
604 one(sum_point(&name, 300, 400, 3.0, delta)),
605 one(sum_point(&name, 500, 500, 15.0, cumulative)),
606 MetricsData::default(),
607 ]
608 );
609 }
610
611 #[test]
612 fn network_metrics_count_connections_requests_and_bytes() {
613 let metrics = ClientMetrics::new();
614 let network = NetworkMetrics::register(&metrics, "consumer-metrics");
615 let first = network.opened();
616 let second = network.opened();
617 first.request(96);
618 first.response(20);
619 second.request(10);
620 drop(first);
621
622 let mut collector = Collector::new(metrics, Resource::default());
623 let values = collector
624 .collect(&MetricSelector::AllMetrics, false, 1)
625 .resource_metrics
626 .into_iter()
627 .flat_map(|resource| resource.scope_metrics)
628 .flat_map(|scope| scope.metrics)
629 .map(|metric| {
630 let value = match metric.data {
631 Some(
632 MetricData::Gauge { data_points } | MetricData::Sum { data_points, .. },
633 ) => data_points[0].value,
634 None => None,
635 };
636 (metric.name, value)
637 })
638 .collect::<Vec<_>>();
639 let expected = [
640 ("connection.close.total", 1.0),
641 ("connection.count", 1.0),
642 ("connection.creation.total", 2.0),
643 ("incoming.byte.total", 28.0),
644 ("outgoing.byte.total", 114.0),
645 ("request.total", 2.0),
646 ("response.total", 1.0),
647 ]
648 .map(|(name, value)| {
649 (
650 format!("org.apache.kafka.consumer.{name}"),
651 Some(NumberValue::Double(value)),
652 )
653 });
654 assert!(values == expected);
655 }
656}