1pub mod frontend_perf;
10pub mod prometheus_names;
11pub mod request_plane;
12pub mod tokio_perf;
13pub mod transport_metrics;
14pub mod work_handler_perf;
15pub mod work_handler_pool;
16
17use parking_lot::Mutex;
18use std::collections::HashSet;
19use std::sync::Arc;
20
21use crate::component::ComponentBuilder;
22use anyhow;
23use once_cell::sync::Lazy;
24use regex::Regex;
25use std::any::Any;
26use std::collections::HashMap;
27
28use prometheus_names::{
30 build_component_metric_name, labels, name_prefix, sanitize_prometheus_label,
31 sanitize_prometheus_name, work_handler,
32};
33
34use crate::pipeline::{
36 AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, ResponseStream, SingleIn, async_trait,
37 network::Ingress,
38};
39use crate::protocols::annotated::Annotated;
40use crate::stream;
41use crate::stream::StreamExt;
42
43use prometheus::Encoder;
45
46fn validate_no_duplicate_label_keys(labels: &[(&str, &str)]) -> anyhow::Result<()> {
49 let mut seen_keys = std::collections::HashSet::new();
50 for (key, _) in labels {
51 if !seen_keys.insert(*key) {
52 return Err(anyhow::anyhow!(
53 "Duplicate label key '{}' found in labels",
54 key
55 ));
56 }
57 }
58 Ok(())
59}
60
61pub trait PrometheusMetric: prometheus::core::Collector + Clone + Send + Sync + 'static {
66 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error>
68 where
69 Self: Sized;
70
71 fn with_histogram_opts_and_buckets(
74 _opts: prometheus::HistogramOpts,
75 _buckets: Option<Vec<f64>>,
76 ) -> Result<Self, prometheus::Error>
77 where
78 Self: Sized,
79 {
80 panic!("with_histogram_opts_and_buckets is not implemented for this metric type");
81 }
82
83 fn with_histogram_opts_buckets_and_label_names(
85 _opts: prometheus::HistogramOpts,
86 _buckets: Option<Vec<f64>>,
87 _label_names: &[&str],
88 ) -> Result<Self, prometheus::Error>
89 where
90 Self: Sized,
91 {
92 panic!(
93 "with_histogram_opts_buckets_and_label_names is not implemented for this metric type"
94 );
95 }
96
97 fn with_opts_and_label_names(
100 _opts: prometheus::Opts,
101 _label_names: &[&str],
102 ) -> Result<Self, prometheus::Error>
103 where
104 Self: Sized,
105 {
106 panic!("with_opts_and_label_names is not implemented for this metric type");
107 }
108}
109
110impl PrometheusMetric for prometheus::Counter {
112 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
113 prometheus::Counter::with_opts(opts)
114 }
115}
116
117impl PrometheusMetric for prometheus::IntCounter {
118 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
119 prometheus::IntCounter::with_opts(opts)
120 }
121}
122
123impl PrometheusMetric for prometheus::Gauge {
124 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
125 prometheus::Gauge::with_opts(opts)
126 }
127}
128
129impl PrometheusMetric for prometheus::IntGauge {
130 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
131 prometheus::IntGauge::with_opts(opts)
132 }
133}
134
135impl PrometheusMetric for prometheus::GaugeVec {
136 fn with_opts(_opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
137 Err(prometheus::Error::Msg(
138 "GaugeVec requires label names, use with_opts_and_label_names instead".to_string(),
139 ))
140 }
141
142 fn with_opts_and_label_names(
143 opts: prometheus::Opts,
144 label_names: &[&str],
145 ) -> Result<Self, prometheus::Error> {
146 prometheus::GaugeVec::new(opts, label_names)
147 }
148}
149
150impl PrometheusMetric for prometheus::IntGaugeVec {
151 fn with_opts(_opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
152 Err(prometheus::Error::Msg(
153 "IntGaugeVec requires label names, use with_opts_and_label_names instead".to_string(),
154 ))
155 }
156
157 fn with_opts_and_label_names(
158 opts: prometheus::Opts,
159 label_names: &[&str],
160 ) -> Result<Self, prometheus::Error> {
161 prometheus::IntGaugeVec::new(opts, label_names)
162 }
163}
164
165impl PrometheusMetric for prometheus::IntCounterVec {
166 fn with_opts(_opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
167 Err(prometheus::Error::Msg(
168 "IntCounterVec requires label names, use with_opts_and_label_names instead".to_string(),
169 ))
170 }
171
172 fn with_opts_and_label_names(
173 opts: prometheus::Opts,
174 label_names: &[&str],
175 ) -> Result<Self, prometheus::Error> {
176 prometheus::IntCounterVec::new(opts, label_names)
177 }
178}
179
180impl PrometheusMetric for prometheus::Histogram {
182 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
183 let histogram_opts = prometheus::HistogramOpts::new(opts.name, opts.help);
185 prometheus::Histogram::with_opts(histogram_opts)
186 }
187
188 fn with_histogram_opts_and_buckets(
189 mut opts: prometheus::HistogramOpts,
190 buckets: Option<Vec<f64>>,
191 ) -> Result<Self, prometheus::Error> {
192 if let Some(custom_buckets) = buckets {
193 opts = opts.buckets(custom_buckets);
194 }
195 prometheus::Histogram::with_opts(opts)
196 }
197}
198
199impl PrometheusMetric for prometheus::HistogramVec {
200 fn with_opts(_opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
201 Err(prometheus::Error::Msg(
202 "HistogramVec requires histogram options and label names".to_string(),
203 ))
204 }
205
206 fn with_histogram_opts_buckets_and_label_names(
207 mut opts: prometheus::HistogramOpts,
208 buckets: Option<Vec<f64>>,
209 label_names: &[&str],
210 ) -> Result<Self, prometheus::Error> {
211 if let Some(custom_buckets) = buckets {
212 opts = opts.buckets(custom_buckets);
213 }
214 prometheus::HistogramVec::new(opts, label_names)
215 }
216}
217
218impl PrometheusMetric for prometheus::CounterVec {
220 fn with_opts(_opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
221 panic!("CounterVec requires label names, use with_opts_and_label_names instead");
223 }
224
225 fn with_opts_and_label_names(
226 opts: prometheus::Opts,
227 label_names: &[&str],
228 ) -> Result<Self, prometheus::Error> {
229 prometheus::CounterVec::new(opts, label_names)
230 }
231}
232
233pub fn create_metric<T: PrometheusMetric, H: MetricsHierarchy + ?Sized>(
238 hierarchy: &H,
239 metric_name: &str,
240 metric_desc: &str,
241 labels: &[(&str, &str)],
242 buckets: Option<Vec<f64>>,
243 const_labels: Option<&[&str]>,
244) -> anyhow::Result<T> {
245 validate_no_duplicate_label_keys(labels)?;
247 let basename = hierarchy.basename();
250 let parent_hierarchies = hierarchy.parent_hierarchies();
251
252 let mut hierarchy_names: Vec<String> =
254 parent_hierarchies.iter().map(|p| p.basename()).collect();
255 hierarchy_names.push(basename.clone());
256
257 let metric_name = build_component_metric_name(metric_name);
258
259 let mut updated_labels: Vec<(String, String)> = Vec::new();
261
262 for (key, _) in labels {
269 if *key == labels::NAMESPACE
270 || *key == labels::COMPONENT
271 || *key == labels::ENDPOINT
272 || *key == labels::WORKER_ID
273 {
274 return Err(anyhow::anyhow!(
275 "Label '{}' is automatically added by auto-label injection and cannot be manually set",
276 key
277 ));
278 }
279 }
280
281 if let Some(label_names) = const_labels {
285 for name in label_names.iter() {
286 if *name == labels::NAMESPACE
287 || *name == labels::COMPONENT
288 || *name == labels::ENDPOINT
289 || *name == labels::WORKER_ID
290 {
291 return Err(anyhow::anyhow!(
292 "Variable label name '{}' conflicts with auto-injected const label and cannot be used",
293 name
294 ));
295 }
296 }
297 }
298
299 if hierarchy_names.len() > 1 {
302 let namespace = &hierarchy_names[1];
303 if !namespace.is_empty() {
304 let valid_namespace = sanitize_prometheus_label(namespace)?;
305 if !valid_namespace.is_empty() {
306 updated_labels.push((labels::NAMESPACE.to_string(), valid_namespace));
307 }
308 }
309 }
310 if hierarchy_names.len() > 2 {
311 let component = &hierarchy_names[2];
312 if !component.is_empty() {
313 let valid_component = sanitize_prometheus_label(component)?;
314 if !valid_component.is_empty() {
315 updated_labels.push((labels::COMPONENT.to_string(), valid_component));
316 }
317 }
318 }
319 if hierarchy_names.len() > 3 {
320 let endpoint = &hierarchy_names[3];
321 if !endpoint.is_empty() {
322 let valid_endpoint = sanitize_prometheus_label(endpoint)?;
323 if !valid_endpoint.is_empty() {
324 updated_labels.push((labels::ENDPOINT.to_string(), valid_endpoint));
325 }
326 }
327 }
328
329 if let Some(conn_id) = hierarchy.connection_id() {
333 updated_labels.push((labels::WORKER_ID.to_string(), format!("{:x}", conn_id)));
334 }
335
336 updated_labels.extend(
338 labels
339 .iter()
340 .map(|(k, v)| ((*k).to_string(), (*v).to_string())),
341 );
342 let prometheus_metric = if std::any::TypeId::of::<T>()
346 == std::any::TypeId::of::<prometheus::CounterVec>()
347 {
348 if buckets.is_some() {
351 return Err(anyhow::anyhow!(
352 "buckets parameter is not valid for CounterVec"
353 ));
354 }
355 let mut opts = prometheus::Opts::new(&metric_name, metric_desc);
356 for (key, value) in &updated_labels {
357 opts = opts.const_label(key.clone(), value.clone());
358 }
359 let label_names = const_labels
360 .ok_or_else(|| anyhow::anyhow!("CounterVec requires const_labels parameter"))?;
361 T::with_opts_and_label_names(opts, label_names)?
362 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<prometheus::GaugeVec>() {
363 if buckets.is_some() {
366 return Err(anyhow::anyhow!(
367 "buckets parameter is not valid for GaugeVec"
368 ));
369 }
370 let mut opts = prometheus::Opts::new(&metric_name, metric_desc);
371 for (key, value) in &updated_labels {
372 opts = opts.const_label(key.clone(), value.clone());
373 }
374 let label_names = const_labels
375 .ok_or_else(|| anyhow::anyhow!("GaugeVec requires const_labels parameter"))?;
376 T::with_opts_and_label_names(opts, label_names)?
377 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<prometheus::HistogramVec>() {
378 let mut opts = prometheus::HistogramOpts::new(&metric_name, metric_desc);
379 for (key, value) in &updated_labels {
380 opts = opts.const_label(key.clone(), value.clone());
381 }
382 let label_names = const_labels
383 .ok_or_else(|| anyhow::anyhow!("HistogramVec requires const_labels parameter"))?;
384 T::with_histogram_opts_buckets_and_label_names(opts, buckets, label_names)?
385 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<prometheus::Histogram>() {
386 if const_labels.is_some() {
389 return Err(anyhow::anyhow!(
390 "const_labels parameter is not valid for Histogram"
391 ));
392 }
393 let mut opts = prometheus::HistogramOpts::new(&metric_name, metric_desc);
394 for (key, value) in &updated_labels {
395 opts = opts.const_label(key.clone(), value.clone());
396 }
397 T::with_histogram_opts_and_buckets(opts, buckets)?
398 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<prometheus::IntCounterVec>() {
399 if buckets.is_some() {
402 return Err(anyhow::anyhow!(
403 "buckets parameter is not valid for IntCounterVec"
404 ));
405 }
406 let mut opts = prometheus::Opts::new(&metric_name, metric_desc);
407 for (key, value) in &updated_labels {
408 opts = opts.const_label(key.clone(), value.clone());
409 }
410 let label_names = const_labels
411 .ok_or_else(|| anyhow::anyhow!("IntCounterVec requires const_labels parameter"))?;
412 T::with_opts_and_label_names(opts, label_names)?
413 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<prometheus::IntGaugeVec>() {
414 if buckets.is_some() {
417 return Err(anyhow::anyhow!(
418 "buckets parameter is not valid for IntGaugeVec"
419 ));
420 }
421 let mut opts = prometheus::Opts::new(&metric_name, metric_desc);
422 for (key, value) in &updated_labels {
423 opts = opts.const_label(key.clone(), value.clone());
424 }
425 let label_names = const_labels
426 .ok_or_else(|| anyhow::anyhow!("IntGaugeVec requires const_labels parameter"))?;
427 T::with_opts_and_label_names(opts, label_names)?
428 } else {
429 if buckets.is_some() {
432 return Err(anyhow::anyhow!(
433 "buckets parameter is not valid for Counter, IntCounter, Gauge, or IntGauge"
434 ));
435 }
436 if const_labels.is_some() {
437 return Err(anyhow::anyhow!(
438 "const_labels parameter is not valid for Counter, IntCounter, Gauge, or IntGauge"
439 ));
440 }
441 let mut opts = prometheus::Opts::new(&metric_name, metric_desc);
442 for (key, value) in &updated_labels {
443 opts = opts.const_label(key.clone(), value.clone());
444 }
445 T::with_opts(opts)?
446 };
447
448 let collector: Box<dyn prometheus::core::Collector> = Box::new(prometheus_metric.clone());
449 hierarchy.get_metrics_registry().add_metric(collector)?;
450
451 Ok(prometheus_metric)
452}
453
454pub struct Metrics<H: MetricsHierarchy> {
457 hierarchy: H,
458}
459
460impl<H: MetricsHierarchy> Metrics<H> {
461 pub fn new(hierarchy: H) -> Self {
462 Self { hierarchy }
463 }
464
465 pub fn create_counter(
488 &self,
489 name: &str,
490 description: &str,
491 labels: &[(&str, &str)],
492 ) -> anyhow::Result<prometheus::Counter> {
493 create_metric(&self.hierarchy, name, description, labels, None, None)
494 }
495
496 pub fn create_countervec(
498 &self,
499 name: &str,
500 description: &str,
501 const_labels: &[&str],
502 const_label_values: &[(&str, &str)],
503 ) -> anyhow::Result<prometheus::CounterVec> {
504 create_metric(
505 &self.hierarchy,
506 name,
507 description,
508 const_label_values,
509 None,
510 Some(const_labels),
511 )
512 }
513
514 pub fn create_gauge(
516 &self,
517 name: &str,
518 description: &str,
519 labels: &[(&str, &str)],
520 ) -> anyhow::Result<prometheus::Gauge> {
521 create_metric(&self.hierarchy, name, description, labels, None, None)
522 }
523
524 pub fn create_gaugevec(
526 &self,
527 name: &str,
528 description: &str,
529 const_labels: &[&str],
530 const_label_values: &[(&str, &str)],
531 ) -> anyhow::Result<prometheus::GaugeVec> {
532 create_metric(
533 &self.hierarchy,
534 name,
535 description,
536 const_label_values,
537 None,
538 Some(const_labels),
539 )
540 }
541
542 pub fn create_histogram(
544 &self,
545 name: &str,
546 description: &str,
547 labels: &[(&str, &str)],
548 buckets: Option<Vec<f64>>,
549 ) -> anyhow::Result<prometheus::Histogram> {
550 create_metric(&self.hierarchy, name, description, labels, buckets, None)
551 }
552
553 pub fn create_histogramvec(
555 &self,
556 name: &str,
557 description: &str,
558 label_names: &[&str],
559 labels: &[(&str, &str)],
560 buckets: Option<Vec<f64>>,
561 ) -> anyhow::Result<prometheus::HistogramVec> {
562 create_metric(
563 &self.hierarchy,
564 name,
565 description,
566 labels,
567 buckets,
568 Some(label_names),
569 )
570 }
571
572 pub fn create_intcounter(
574 &self,
575 name: &str,
576 description: &str,
577 labels: &[(&str, &str)],
578 ) -> anyhow::Result<prometheus::IntCounter> {
579 create_metric(&self.hierarchy, name, description, labels, None, None)
580 }
581
582 pub fn create_intcountervec(
584 &self,
585 name: &str,
586 description: &str,
587 const_labels: &[&str],
588 const_label_values: &[(&str, &str)],
589 ) -> anyhow::Result<prometheus::IntCounterVec> {
590 create_metric(
591 &self.hierarchy,
592 name,
593 description,
594 const_label_values,
595 None,
596 Some(const_labels),
597 )
598 }
599
600 pub fn create_intgauge(
602 &self,
603 name: &str,
604 description: &str,
605 labels: &[(&str, &str)],
606 ) -> anyhow::Result<prometheus::IntGauge> {
607 create_metric(&self.hierarchy, name, description, labels, None, None)
608 }
609
610 pub fn create_intgaugevec(
612 &self,
613 name: &str,
614 description: &str,
615 const_labels: &[&str],
616 const_label_values: &[(&str, &str)],
617 ) -> anyhow::Result<prometheus::IntGaugeVec> {
618 create_metric(
619 &self.hierarchy,
620 name,
621 description,
622 const_label_values,
623 None,
624 Some(const_labels),
625 )
626 }
627
628 pub fn prometheus_expfmt(&self) -> anyhow::Result<String> {
630 self.hierarchy
631 .get_metrics_registry()
632 .prometheus_expfmt_combined()
633 }
634}
635
636use crate::traits::DistributedRuntimeProvider;
640
641pub trait MetricsHierarchy: Send + Sync {
642 fn basename(&self) -> String;
648
649 fn parent_hierarchies(&self) -> Vec<&dyn MetricsHierarchy>;
653
654 fn get_metrics_registry(&self) -> &MetricsRegistry;
656
657 fn connection_id(&self) -> Option<u64> {
667 None
668 }
669
670 fn metrics(&self) -> Metrics<&Self>
673 where
674 Self: Sized,
675 {
676 Metrics::new(self)
677 }
678}
679
680impl<T: MetricsHierarchy + ?Sized> MetricsHierarchy for &T {
682 fn basename(&self) -> String {
683 (**self).basename()
684 }
685
686 fn parent_hierarchies(&self) -> Vec<&dyn MetricsHierarchy> {
687 (**self).parent_hierarchies()
688 }
689
690 fn get_metrics_registry(&self) -> &MetricsRegistry {
691 (**self).get_metrics_registry()
692 }
693
694 fn connection_id(&self) -> Option<u64> {
695 (**self).connection_id()
696 }
697}
698
699pub type PrometheusUpdateCallback = Arc<dyn Fn() -> anyhow::Result<()> + Send + Sync + 'static>;
708
709pub type PrometheusExpositionFormatCallback =
711 Arc<dyn Fn() -> anyhow::Result<String> + Send + Sync + 'static>;
712
713#[derive(Clone)]
718pub struct MetricsRegistry {
719 pub prometheus_registry: Arc<std::sync::RwLock<prometheus::Registry>>,
722
723 child_registries: Arc<std::sync::RwLock<Vec<MetricsRegistry>>>,
737
738 pub prometheus_update_callbacks: Arc<std::sync::RwLock<Vec<PrometheusUpdateCallback>>>,
741
742 pub prometheus_expfmt_callbacks:
745 Arc<std::sync::RwLock<Vec<PrometheusExpositionFormatCallback>>>,
746}
747
748impl std::fmt::Debug for MetricsRegistry {
749 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
750 f.debug_struct("MetricsRegistry")
751 .field("prometheus_registry", &"<RwLock<Registry>>")
752 .field(
753 "prometheus_update_callbacks",
754 &format!(
755 "<RwLock<Vec<Callback>>> with {} callbacks",
756 self.prometheus_update_callbacks.read().unwrap().len()
757 ),
758 )
759 .field(
760 "prometheus_expfmt_callbacks",
761 &format!(
762 "<RwLock<Vec<Callback>>> with {} callbacks",
763 self.prometheus_expfmt_callbacks.read().unwrap().len()
764 ),
765 )
766 .finish()
767 }
768}
769
770impl MetricsRegistry {
771 pub fn new() -> Self {
773 Self {
774 prometheus_registry: Arc::new(std::sync::RwLock::new(prometheus::Registry::new())),
775 child_registries: Arc::new(std::sync::RwLock::new(Vec::new())),
776 prometheus_update_callbacks: Arc::new(std::sync::RwLock::new(Vec::new())),
777 prometheus_expfmt_callbacks: Arc::new(std::sync::RwLock::new(Vec::new())),
778 }
779 }
780
781 pub fn add_child_registry(&self, child: &MetricsRegistry) {
785 let child_ptr = Arc::as_ptr(&child.prometheus_registry);
786 let mut guard = self.child_registries.write().unwrap();
787 if guard
788 .iter()
789 .any(|r| Arc::as_ptr(&r.prometheus_registry) == child_ptr)
790 {
791 return;
792 }
793 guard.push(child.clone());
794 }
795
796 fn registries_for_combined_scrape(&self) -> Vec<MetricsRegistry> {
797 fn visit(
803 registry: &MetricsRegistry,
804 out: &mut Vec<MetricsRegistry>,
805 seen: &mut HashSet<*const std::sync::RwLock<prometheus::Registry>>,
806 ) {
807 let ptr = Arc::as_ptr(®istry.prometheus_registry);
808 if !seen.insert(ptr) {
809 return;
810 }
811
812 out.push(registry.clone());
813
814 let children: Vec<MetricsRegistry> = registry
815 .child_registries
816 .read()
817 .unwrap()
818 .iter()
819 .cloned()
820 .collect();
821 for child in children {
822 visit(&child, out, seen);
823 }
824 }
825
826 let mut out = Vec::new();
827 let mut seen: HashSet<*const std::sync::RwLock<prometheus::Registry>> = HashSet::new();
828 visit(self, &mut out, &mut seen);
829 out
830 }
831
832 pub fn prometheus_expfmt_combined(&self) -> anyhow::Result<String> {
838 let registries = self.registries_for_combined_scrape();
839
840 for registry in ®istries {
842 for result in registry.execute_update_callbacks() {
843 if let Err(e) = result {
844 tracing::error!("Error executing metrics callback: {e}");
845 }
846 }
847 }
848
849 let mut by_name: HashMap<String, prometheus::proto::MetricFamily> = HashMap::new();
851 let mut seen_series: HashSet<String> = HashSet::new();
852
853 for (registry_idx, registry) in registries.iter().enumerate() {
854 let families = registry.get_prometheus_registry().gather();
855 for mut family in families {
856 let name = family.name().to_string();
857
858 let entry = by_name.entry(name.clone()).or_insert_with(|| {
859 let mut out = prometheus::proto::MetricFamily::new();
860 out.set_name(name.clone());
861 out.set_help(family.help().to_string());
862 out.set_field_type(family.get_field_type());
863 out
864 });
865
866 if entry.help() != family.help()
867 || entry.get_field_type() != family.get_field_type()
868 {
869 return Err(anyhow::anyhow!(
870 "Metric family '{}' has inconsistent help/type across registries (idx={})",
871 name,
872 registry_idx
873 ));
874 }
875
876 let mut metrics = family.take_metric();
877 for metric in metrics.drain(..) {
878 let mut labels: Vec<(String, String)> = metric
879 .get_label()
880 .iter()
881 .map(|lp| (lp.name().to_string(), lp.value().to_string()))
882 .collect();
883 labels.sort_by(|(ka, va), (kb, vb)| (ka, va).cmp(&(kb, vb)));
884
885 let key = format!(
886 "{}|{}",
887 name,
888 labels
889 .iter()
890 .map(|(k, v)| format!("{}={}", k, v))
891 .collect::<Vec<_>>()
892 .join(",")
893 );
894
895 if !seen_series.insert(key) {
896 tracing::warn!(
897 metric_name = %name,
898 labels = ?labels,
899 registry_idx,
900 "Duplicate Prometheus series while merging registries; dropping later sample"
901 );
902 continue;
903 }
904
905 entry.mut_metric().push(metric);
906 }
907 }
908 }
909
910 let mut merged: Vec<prometheus::proto::MetricFamily> = by_name.into_values().collect();
911 merged.sort_by(|a, b| a.name().cmp(b.name()));
912
913 let encoder = prometheus::TextEncoder::new();
914 let mut buffer = Vec::new();
915 encoder.encode(&merged, &mut buffer)?;
916 let mut result = String::from_utf8(buffer)?;
917
918 let mut expfmt = String::new();
920 for registry in registries {
921 let text = registry.execute_expfmt_callbacks();
922 if !text.is_empty() {
923 if !expfmt.is_empty() && !expfmt.ends_with('\n') {
924 expfmt.push('\n');
925 }
926 expfmt.push_str(&text);
927 }
928 }
929
930 if !expfmt.is_empty() {
931 if !result.ends_with('\n') {
932 result.push('\n');
933 }
934 result.push_str(&expfmt);
935 }
936
937 Ok(result)
938 }
939
940 pub fn add_update_callback(&self, callback: PrometheusUpdateCallback) {
942 self.prometheus_update_callbacks
943 .write()
944 .unwrap()
945 .push(callback);
946 }
947
948 pub fn add_expfmt_callback(&self, callback: PrometheusExpositionFormatCallback) {
950 self.prometheus_expfmt_callbacks
951 .write()
952 .unwrap()
953 .push(callback);
954 }
955
956 pub fn execute_update_callbacks(&self) -> Vec<anyhow::Result<()>> {
958 self.prometheus_update_callbacks
959 .read()
960 .unwrap()
961 .iter()
962 .map(|callback| callback())
963 .collect()
964 }
965
966 pub fn execute_expfmt_callbacks(&self) -> String {
968 let callbacks = self.prometheus_expfmt_callbacks.read().unwrap();
969 let mut result = String::new();
970 for callback in callbacks.iter() {
971 match callback() {
972 Ok(text) => {
973 if !text.is_empty() {
974 if !result.is_empty() && !result.ends_with('\n') {
975 result.push('\n');
976 }
977 result.push_str(&text);
978 }
979 }
980 Err(e) => {
981 tracing::error!("Error executing exposition text callback: {e}");
982 }
983 }
984 }
985 result
986 }
987
988 pub fn add_metric(
990 &self,
991 collector: Box<dyn prometheus::core::Collector>,
992 ) -> anyhow::Result<()> {
993 self.prometheus_registry
994 .write()
995 .unwrap()
996 .register(collector)
997 .map_err(|e| anyhow::anyhow!("Failed to register metric: {}", e))
998 }
999
1000 pub fn add_metric_or_warn(&self, collector: Box<dyn prometheus::core::Collector>, name: &str) {
1002 if let Err(e) = self.add_metric(collector) {
1003 tracing::warn!(error = %e, metric = name, "Failed to register metric");
1004 }
1005 }
1006
1007 pub fn get_prometheus_registry(&self) -> std::sync::RwLockReadGuard<'_, prometheus::Registry> {
1009 self.prometheus_registry.read().unwrap()
1010 }
1011
1012 pub fn has_metric_named(&self, metric_name: &str) -> bool {
1014 self.prometheus_registry
1015 .read()
1016 .unwrap()
1017 .gather()
1018 .iter()
1019 .any(|mf| mf.name() == metric_name)
1020 }
1021}
1022
1023impl Default for MetricsRegistry {
1024 fn default() -> Self {
1025 Self::new()
1026 }
1027}
1028
1029#[cfg(test)]
1030mod test_helpers {
1031 use super::prometheus_names::name_prefix;
1032 use super::*;
1033
1034 fn filter_prometheus_lines<F>(input: &str, mut predicate: F) -> Vec<String>
1037 where
1038 F: FnMut(&str) -> bool,
1039 {
1040 input
1041 .lines()
1042 .filter(|line| predicate(line))
1043 .map(|line| line.to_string())
1044 .collect::<Vec<_>>()
1045 }
1046
1047 pub fn extract_metrics(input: &str) -> Vec<String> {
1050 filter_prometheus_lines(input, |line| {
1051 line.starts_with(&format!("{}_", name_prefix::COMPONENT))
1052 && !line.starts_with("#")
1053 && !line.trim().is_empty()
1054 })
1055 }
1056
1057 pub fn parse_prometheus_metric(
1069 line: &str,
1070 ) -> Option<(String, std::collections::HashMap<String, String>, f64)> {
1071 if line.trim().is_empty() || line.starts_with('#') {
1072 return None;
1073 }
1074
1075 let parts: Vec<&str> = line.split_whitespace().collect();
1076 if parts.len() < 2 {
1077 return None;
1078 }
1079
1080 let metric_part = parts[0];
1081 let value: f64 = parts[1].parse().ok()?;
1082
1083 let (name, labels) = if metric_part.contains('{') {
1084 let brace_start = metric_part.find('{').unwrap();
1085 let brace_end = metric_part.rfind('}').unwrap_or(metric_part.len());
1086 let name = &metric_part[..brace_start];
1087 let labels_str = &metric_part[brace_start + 1..brace_end];
1088
1089 let mut labels = std::collections::HashMap::new();
1090 for pair in labels_str.split(',') {
1091 if let Some((k, v)) = pair.split_once('=') {
1092 let v = v.trim_matches('"');
1093 labels.insert(k.trim().to_string(), v.to_string());
1094 }
1095 }
1096 (name.to_string(), labels)
1097 } else {
1098 (metric_part.to_string(), std::collections::HashMap::new())
1099 };
1100
1101 Some((name, labels, value))
1102 }
1103
1104 pub fn inject_worker_id(expected: &str, wid: &str) -> String {
1110 let wid_label = format!(",worker_id=\"{}\"", wid);
1111 expected
1112 .lines()
1113 .map(|line| {
1114 if line.starts_with('#') || line.trim().is_empty() || !line.contains('{') {
1115 line.to_string()
1116 } else if let Some(le_pos) = line.find(",le=") {
1117 let mut s = line.to_string();
1120 s.insert_str(le_pos, &wid_label);
1121 s
1122 } else {
1123 line.replacen("}", &format!("{}}}", wid_label), 1)
1124 }
1125 })
1126 .collect::<Vec<_>>()
1127 .join("\n")
1128 }
1129}
1130
1131#[cfg(test)]
1132mod test_metricsregistry_units {
1133 use super::*;
1134
1135 #[test]
1136 fn test_build_component_metric_name_with_prefix() {
1137 let result = build_component_metric_name("requests");
1139 assert_eq!(result, "dynamo_component_requests");
1140
1141 let result = build_component_metric_name("counter");
1142 assert_eq!(result, "dynamo_component_counter");
1143 }
1144
1145 #[test]
1146 fn test_parse_prometheus_metric() {
1147 use super::test_helpers::parse_prometheus_metric;
1148 use std::collections::HashMap;
1149
1150 let line = "http_requests_total{method=\"GET\",status=\"200\"} 1234";
1152 let parsed = parse_prometheus_metric(line);
1153 assert!(parsed.is_some());
1154
1155 let (name, labels, value) = parsed.unwrap();
1156 assert_eq!(name, "http_requests_total");
1157
1158 let mut expected_labels = HashMap::new();
1159 expected_labels.insert("method".to_string(), "GET".to_string());
1160 expected_labels.insert("status".to_string(), "200".to_string());
1161 assert_eq!(labels, expected_labels);
1162
1163 assert_eq!(value, 1234.0);
1164
1165 let line = "cpu_usage 98.5";
1167 let parsed = parse_prometheus_metric(line);
1168 assert!(parsed.is_some());
1169
1170 let (name, labels, value) = parsed.unwrap();
1171 assert_eq!(name, "cpu_usage");
1172 assert!(labels.is_empty());
1173 assert_eq!(value, 98.5);
1174
1175 let line = "response_time{service=\"api\"} 0.123";
1177 let parsed = parse_prometheus_metric(line);
1178 assert!(parsed.is_some());
1179
1180 let (name, labels, value) = parsed.unwrap();
1181 assert_eq!(name, "response_time");
1182
1183 let mut expected_labels = HashMap::new();
1184 expected_labels.insert("service".to_string(), "api".to_string());
1185 assert_eq!(labels, expected_labels);
1186
1187 assert_eq!(value, 0.123);
1188
1189 assert!(parse_prometheus_metric("").is_none()); assert!(parse_prometheus_metric("# HELP metric description").is_none()); assert!(parse_prometheus_metric("# TYPE metric counter").is_none()); assert!(parse_prometheus_metric("metric_name").is_none()); println!("✓ Prometheus metric parsing works correctly!");
1196 }
1197
1198 #[test]
1199 fn test_metrics_registry_entry_callbacks() {
1200 use crate::MetricsRegistry;
1201 use std::sync::atomic::{AtomicUsize, Ordering};
1202
1203 {
1205 let registry = MetricsRegistry::new();
1206 let counter = Arc::new(AtomicUsize::new(0));
1207
1208 for increment in [1, 10, 100] {
1210 let counter_clone = counter.clone();
1211 registry.add_update_callback(Arc::new(move || {
1212 counter_clone.fetch_add(increment, Ordering::SeqCst);
1213 Ok(())
1214 }));
1215 }
1216
1217 assert_eq!(counter.load(Ordering::SeqCst), 0);
1219
1220 let results = registry.execute_update_callbacks();
1222 assert_eq!(results.len(), 3);
1223 assert!(results.iter().all(|r| r.is_ok()));
1224 assert_eq!(counter.load(Ordering::SeqCst), 111); let results = registry.execute_update_callbacks();
1228 assert_eq!(results.len(), 3);
1229 assert_eq!(counter.load(Ordering::SeqCst), 222); let cloned = registry.clone();
1233 assert_eq!(cloned.execute_update_callbacks().len(), 3);
1234 assert_eq!(counter.load(Ordering::SeqCst), 333); registry.execute_update_callbacks();
1238 assert_eq!(counter.load(Ordering::SeqCst), 444); }
1240
1241 {
1243 let registry = MetricsRegistry::new();
1244 let counter = Arc::new(AtomicUsize::new(0));
1245
1246 let counter_clone = counter.clone();
1248 registry.add_update_callback(Arc::new(move || {
1249 counter_clone.fetch_add(1, Ordering::SeqCst);
1250 Ok(())
1251 }));
1252
1253 registry.add_update_callback(Arc::new(|| Err(anyhow::anyhow!("Simulated error"))));
1255
1256 let counter_clone = counter.clone();
1258 registry.add_update_callback(Arc::new(move || {
1259 counter_clone.fetch_add(10, Ordering::SeqCst);
1260 Ok(())
1261 }));
1262
1263 let results = registry.execute_update_callbacks();
1265 assert_eq!(results.len(), 3);
1266 assert!(results[0].is_ok());
1267 assert!(results[1].is_err());
1268 assert!(results[2].is_ok());
1269
1270 assert_eq!(
1272 results[1].as_ref().unwrap_err().to_string(),
1273 "Simulated error"
1274 );
1275
1276 assert_eq!(counter.load(Ordering::SeqCst), 11); let results = registry.execute_update_callbacks();
1281 assert!(results[1].is_err());
1282 assert_eq!(counter.load(Ordering::SeqCst), 22); }
1284
1285 {
1287 let registry = MetricsRegistry::new();
1288 let results = registry.execute_update_callbacks();
1289 assert_eq!(results.len(), 0);
1290 }
1291 }
1292}
1293
1294#[cfg(feature = "integration")]
1295#[cfg(test)]
1296mod test_metricsregistry_prefixes {
1297 use super::*;
1298 use crate::distributed::distributed_test_utils::create_test_drt_async;
1299 use prometheus::core::Collector;
1300
1301 #[tokio::test]
1302 async fn test_hierarchical_prefixes_and_parent_hierarchies() {
1303 let drt = create_test_drt_async().await;
1304
1305 const DRT_NAME: &str = "";
1306 const NAMESPACE_NAME: &str = "ns901";
1307 const COMPONENT_NAME: &str = "comp901";
1308 const ENDPOINT_NAME: &str = "ep901";
1309 let namespace = drt.namespace(NAMESPACE_NAME).unwrap();
1310 let component = namespace.component(COMPONENT_NAME).unwrap();
1311 let endpoint = component.endpoint(ENDPOINT_NAME);
1312
1313 assert_eq!(drt.basename(), DRT_NAME);
1315 assert_eq!(drt.parent_hierarchies().len(), 0);
1316 assert_eq!(namespace.basename(), NAMESPACE_NAME);
1320 assert_eq!(namespace.parent_hierarchies().len(), 1);
1321 assert_eq!(namespace.parent_hierarchies()[0].basename(), DRT_NAME);
1322 assert_eq!(component.basename(), COMPONENT_NAME);
1326 assert_eq!(component.parent_hierarchies().len(), 2);
1327 assert_eq!(component.parent_hierarchies()[0].basename(), DRT_NAME);
1328 assert_eq!(component.parent_hierarchies()[1].basename(), NAMESPACE_NAME);
1329 assert_eq!(endpoint.basename(), ENDPOINT_NAME);
1333 assert_eq!(endpoint.parent_hierarchies().len(), 3);
1334 assert_eq!(endpoint.parent_hierarchies()[0].basename(), DRT_NAME);
1335 assert_eq!(endpoint.parent_hierarchies()[1].basename(), NAMESPACE_NAME);
1336 assert_eq!(endpoint.parent_hierarchies()[2].basename(), COMPONENT_NAME);
1337 assert!(
1341 namespace
1342 .parent_hierarchies()
1343 .iter()
1344 .any(|h| h.basename() == drt.basename())
1345 );
1346 assert!(
1347 component
1348 .parent_hierarchies()
1349 .iter()
1350 .any(|h| h.basename() == namespace.basename())
1351 );
1352 assert!(
1353 endpoint
1354 .parent_hierarchies()
1355 .iter()
1356 .any(|h| h.basename() == component.basename())
1357 );
1358
1359 assert_eq!(drt.parent_hierarchies().len(), 0);
1361 assert_eq!(namespace.parent_hierarchies().len(), 1);
1362 assert_eq!(component.parent_hierarchies().len(), 2);
1363 assert_eq!(endpoint.parent_hierarchies().len(), 3);
1364
1365 let invalid_namespace = drt.namespace("@@123").unwrap();
1369 let result =
1370 invalid_namespace
1371 .metrics()
1372 .create_counter("test_counter", "A test counter", &[]);
1373 assert!(result.is_ok());
1374 if let Ok(counter) = &result {
1375 let desc = counter.desc();
1377 let namespace_label = desc[0]
1378 .const_label_pairs
1379 .iter()
1380 .find(|l| l.name() == "dynamo_namespace")
1381 .expect("Should have dynamo_namespace label");
1382 assert_eq!(namespace_label.value(), "_123");
1383 }
1384
1385 let valid_namespace = drt.namespace("ns567").unwrap();
1387 assert!(
1388 valid_namespace
1389 .metrics()
1390 .create_counter("test_counter", "A test counter", &[])
1391 .is_ok()
1392 );
1393 }
1394
1395 #[tokio::test]
1396 async fn test_expfmt_callback_only_registered_on_endpoint_is_included_once() {
1397 let drt = create_test_drt_async().await;
1401 let namespace = drt.namespace("ns_expfmt_ep_only").unwrap();
1402 let component = namespace.component("comp_expfmt_ep_only").unwrap();
1403 let endpoint = component.endpoint("ep_expfmt_ep_only");
1404
1405 let metric_line = "dynamo_component_active_decode_blocks{dp_rank=\"0\"} 0\n";
1406 let callback: PrometheusExpositionFormatCallback =
1407 Arc::new(move || Ok(metric_line.to_string()));
1408
1409 endpoint
1410 .get_metrics_registry()
1411 .add_expfmt_callback(callback);
1412
1413 let output = drt.metrics().prometheus_expfmt().unwrap();
1414 let occurrences = output
1415 .lines()
1416 .filter(|line| line == &metric_line.trim_end_matches('\n'))
1417 .count();
1418
1419 assert_eq!(
1420 occurrences, 1,
1421 "endpoint-registered exposition callback should appear once, got {} occurrences\n\n{}",
1422 occurrences, output
1423 );
1424 }
1425
1426 #[tokio::test]
1427 async fn test_recursive_namespace() {
1428 let drt = create_test_drt_async().await;
1430
1431 let ns1 = drt.namespace("ns1").unwrap();
1433 let ns2 = ns1.namespace("ns2").unwrap();
1434 let ns3 = ns2.namespace("ns3").unwrap();
1435
1436 let component = ns3.component("test-component").unwrap();
1438
1439 assert_eq!(ns1.basename(), "ns1");
1441 assert_eq!(ns1.parent_hierarchies().len(), 1);
1442 assert_eq!(ns1.parent_hierarchies()[0].basename(), "");
1443 assert_eq!(ns2.basename(), "ns2");
1446 assert_eq!(ns2.parent_hierarchies().len(), 2);
1447 assert_eq!(ns2.parent_hierarchies()[0].basename(), "");
1448 assert_eq!(ns2.parent_hierarchies()[1].basename(), "ns1");
1449 assert_eq!(ns3.basename(), "ns3");
1452 assert_eq!(ns3.parent_hierarchies().len(), 3);
1453 assert_eq!(ns3.parent_hierarchies()[0].basename(), "");
1454 assert_eq!(ns3.parent_hierarchies()[1].basename(), "ns1");
1455 assert_eq!(ns3.parent_hierarchies()[2].basename(), "ns2");
1456 assert_eq!(component.basename(), "test-component");
1459 assert_eq!(component.parent_hierarchies().len(), 4);
1460 assert_eq!(component.parent_hierarchies()[0].basename(), "");
1461 assert_eq!(component.parent_hierarchies()[1].basename(), "ns1");
1462 assert_eq!(component.parent_hierarchies()[2].basename(), "ns2");
1463 assert_eq!(component.parent_hierarchies()[3].basename(), "ns3");
1464 println!("✓ Chained namespace test passed - all prefixes correct");
1467 }
1468}
1469
1470#[cfg(feature = "integration")]
1471#[cfg(test)]
1472mod test_metricsregistry_prometheus_fmt_outputs {
1473 use super::prometheus_names::name_prefix;
1474 use super::*;
1475 use crate::distributed::distributed_test_utils::create_test_drt_async;
1476 use prometheus::Counter;
1477 use std::sync::Arc;
1478
1479 #[tokio::test]
1480 async fn test_prometheusfactory_using_metrics_registry_trait() {
1481 let drt = create_test_drt_async().await;
1483
1484 let namespace_name = "ns345";
1486
1487 let namespace = drt.namespace(namespace_name).unwrap();
1488 let component = namespace.component("comp345").unwrap();
1489 let endpoint = component.endpoint("ep345");
1490
1491 let counter = endpoint
1493 .metrics()
1494 .create_counter("testcounter", "A test counter", &[])
1495 .unwrap();
1496 counter.inc_by(123.456789);
1497 let epsilon = 0.01;
1498 assert!((counter.get() - 123.456789).abs() < epsilon);
1499
1500 let endpoint_output_raw = endpoint.metrics().prometheus_expfmt().unwrap();
1501 println!("Endpoint output:");
1502 println!("{}", endpoint_output_raw);
1503
1504 let wid = format!("{:x}", drt.connection_id());
1507 use super::test_helpers::inject_worker_id;
1508
1509 let expected_endpoint_output = inject_worker_id(
1510 r#"# HELP dynamo_component_testcounter A test counter
1511# TYPE dynamo_component_testcounter counter
1512dynamo_component_testcounter{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345"} 123.456789"#,
1513 &wid,
1514 );
1515
1516 assert_eq!(
1517 endpoint_output_raw.trim_end_matches('\n'),
1518 expected_endpoint_output.trim_end_matches('\n'),
1519 "\n=== ENDPOINT COMPARISON FAILED ===\n\
1520 Actual:\n{}\n\
1521 Expected:\n{}\n\
1522 ==============================",
1523 endpoint_output_raw,
1524 expected_endpoint_output
1525 );
1526
1527 let gauge = component
1529 .metrics()
1530 .create_gauge("testgauge", "A test gauge", &[])
1531 .unwrap();
1532 gauge.set(50000.0);
1533 assert_eq!(gauge.get(), 50000.0);
1534
1535 let component_output_raw = component.metrics().prometheus_expfmt().unwrap();
1537 println!("Component output:");
1538 println!("{}", component_output_raw);
1539
1540 let expected_component_output = inject_worker_id(
1541 r#"# HELP dynamo_component_testcounter A test counter
1542# TYPE dynamo_component_testcounter counter
1543dynamo_component_testcounter{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345"} 123.456789
1544# HELP dynamo_component_testgauge A test gauge
1545# TYPE dynamo_component_testgauge gauge
1546dynamo_component_testgauge{dynamo_component="comp345",dynamo_namespace="ns345"} 50000"#,
1547 &wid,
1548 );
1549
1550 assert_eq!(
1551 component_output_raw.trim_end_matches('\n'),
1552 expected_component_output.trim_end_matches('\n'),
1553 "\n=== COMPONENT COMPARISON FAILED ===\n\
1554 Actual:\n{}\n\
1555 Expected:\n{}\n\
1556 ==============================",
1557 component_output_raw,
1558 expected_component_output
1559 );
1560
1561 let intcounter = namespace
1562 .metrics()
1563 .create_intcounter("testintcounter", "A test int counter", &[])
1564 .unwrap();
1565 intcounter.inc_by(12345);
1566 assert_eq!(intcounter.get(), 12345);
1567
1568 let namespace_output_raw = namespace.metrics().prometheus_expfmt().unwrap();
1570 println!("Namespace output:");
1571 println!("{}", namespace_output_raw);
1572
1573 let expected_namespace_output = inject_worker_id(
1574 r#"# HELP dynamo_component_testcounter A test counter
1575# TYPE dynamo_component_testcounter counter
1576dynamo_component_testcounter{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345"} 123.456789
1577# HELP dynamo_component_testgauge A test gauge
1578# TYPE dynamo_component_testgauge gauge
1579dynamo_component_testgauge{dynamo_component="comp345",dynamo_namespace="ns345"} 50000
1580# HELP dynamo_component_testintcounter A test int counter
1581# TYPE dynamo_component_testintcounter counter
1582dynamo_component_testintcounter{dynamo_namespace="ns345"} 12345"#,
1583 &wid,
1584 );
1585
1586 assert_eq!(
1587 namespace_output_raw.trim_end_matches('\n'),
1588 expected_namespace_output.trim_end_matches('\n'),
1589 "\n=== NAMESPACE COMPARISON FAILED ===\n\
1590 Actual:\n{}\n\
1591 Expected:\n{}\n\
1592 ==============================",
1593 namespace_output_raw,
1594 expected_namespace_output
1595 );
1596
1597 let intgauge = namespace
1599 .metrics()
1600 .create_intgauge("testintgauge", "A test int gauge", &[])
1601 .unwrap();
1602 intgauge.set(42);
1603 assert_eq!(intgauge.get(), 42);
1604
1605 let intgaugevec = namespace
1607 .metrics()
1608 .create_intgaugevec(
1609 "testintgaugevec",
1610 "A test int gauge vector",
1611 &["instance", "status"],
1612 &[("service", "api")],
1613 )
1614 .unwrap();
1615 intgaugevec
1616 .with_label_values(&["server1", "active"])
1617 .set(10);
1618 intgaugevec
1619 .with_label_values(&["server2", "inactive"])
1620 .set(0);
1621
1622 let countervec = endpoint
1624 .metrics()
1625 .create_countervec(
1626 "testcountervec",
1627 "A test counter vector",
1628 &["method", "status"],
1629 &[("service", "api")],
1630 )
1631 .unwrap();
1632 countervec.with_label_values(&["GET", "200"]).inc_by(10.0);
1633 countervec.with_label_values(&["POST", "201"]).inc_by(5.0);
1634
1635 let histogram = component
1637 .metrics()
1638 .create_histogram("testhistogram", "A test histogram", &[], None)
1639 .unwrap();
1640 histogram.observe(1.0);
1641 histogram.observe(2.5);
1642 histogram.observe(4.0);
1643
1644 let drt_output_raw = drt.metrics().prometheus_expfmt().unwrap();
1646 println!("DRT output:");
1647 println!("{}", drt_output_raw);
1648
1649 let expected_drt_output_without_uptime = inject_worker_id(
1652 r#"# HELP dynamo_component_testcounter A test counter
1653# TYPE dynamo_component_testcounter counter
1654dynamo_component_testcounter{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345"} 123.456789
1655# HELP dynamo_component_testcountervec A test counter vector
1656# TYPE dynamo_component_testcountervec counter
1657dynamo_component_testcountervec{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345",method="GET",service="api",status="200"} 10
1658dynamo_component_testcountervec{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345",method="POST",service="api",status="201"} 5
1659# HELP dynamo_component_testgauge A test gauge
1660# TYPE dynamo_component_testgauge gauge
1661dynamo_component_testgauge{dynamo_component="comp345",dynamo_namespace="ns345"} 50000
1662# HELP dynamo_component_testhistogram A test histogram
1663# TYPE dynamo_component_testhistogram histogram
1664dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.005"} 0
1665dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.01"} 0
1666dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.025"} 0
1667dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.05"} 0
1668dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.1"} 0
1669dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.25"} 0
1670dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.5"} 0
1671dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="1"} 1
1672dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="2.5"} 2
1673dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="5"} 3
1674dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="10"} 3
1675dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="+Inf"} 3
1676dynamo_component_testhistogram_sum{dynamo_component="comp345",dynamo_namespace="ns345"} 7.5
1677dynamo_component_testhistogram_count{dynamo_component="comp345",dynamo_namespace="ns345"} 3
1678# HELP dynamo_component_testintcounter A test int counter
1679# TYPE dynamo_component_testintcounter counter
1680dynamo_component_testintcounter{dynamo_namespace="ns345"} 12345
1681# HELP dynamo_component_testintgauge A test int gauge
1682# TYPE dynamo_component_testintgauge gauge
1683dynamo_component_testintgauge{dynamo_namespace="ns345"} 42
1684# HELP dynamo_component_testintgaugevec A test int gauge vector
1685# TYPE dynamo_component_testintgaugevec gauge
1686dynamo_component_testintgaugevec{dynamo_namespace="ns345",instance="server1",service="api",status="active"} 10
1687dynamo_component_testintgaugevec{dynamo_namespace="ns345",instance="server2",service="api",status="inactive"} 0"#,
1688 &wid,
1689 );
1690
1691 let mut non_uptime_lines = Vec::new();
1695 let mut saw_uptime_value = false;
1696 for line in drt_output_raw.trim_end_matches('\n').lines() {
1697 if line.starts_with("dynamo_component_uptime_seconds") && !line.starts_with('#') {
1698 let val_str = line.split_whitespace().last().unwrap();
1699 val_str.parse::<f64>().expect("uptime should be a float");
1700 saw_uptime_value = true;
1701 } else if line.starts_with("# HELP dynamo_component_uptime_seconds")
1702 || line.starts_with("# TYPE dynamo_component_uptime_seconds")
1703 {
1704 } else {
1706 non_uptime_lines.push(line);
1707 }
1708 }
1709 assert!(
1710 saw_uptime_value,
1711 "uptime_seconds metric should be present in initial scrape"
1712 );
1713
1714 let actual_without_uptime = non_uptime_lines.join("\n");
1715 assert_eq!(
1716 actual_without_uptime,
1717 expected_drt_output_without_uptime.trim_end_matches('\n'),
1718 "\n=== DRT COMPARISON FAILED (excluding uptime) ===\n\
1719 Expected:\n{}\n\
1720 Actual:\n{}\n\
1721 ==============================",
1722 expected_drt_output_without_uptime,
1723 actual_without_uptime
1724 );
1725
1726 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1728 let drt_output_after = drt.metrics().prometheus_expfmt().unwrap();
1729 let uptime_line = drt_output_after
1730 .lines()
1731 .find(|l| l.starts_with("dynamo_component_uptime_seconds") && !l.starts_with('#'))
1732 .expect("uptime_seconds metric should be present after sleep");
1733 let uptime_after: f64 = uptime_line
1734 .split_whitespace()
1735 .last()
1736 .unwrap()
1737 .parse()
1738 .expect("uptime should be a float");
1739 assert!(
1740 uptime_after > 0.0,
1741 "uptime_seconds should be > 0 after 10ms sleep, got {}",
1742 uptime_after
1743 );
1744
1745 println!("✓ All Prometheus format outputs verified successfully!");
1746 }
1747
1748 #[test]
1749 fn test_refactored_filter_functions() {
1750 let test_input = r#"# HELP dynamo_component_requests Total requests
1752# TYPE dynamo_component_requests counter
1753dynamo_component_requests 42
1754# HELP dynamo_component_latency Response latency
1755# TYPE dynamo_component_latency histogram
1756dynamo_component_latency_bucket{le="0.1"} 10
1757dynamo_component_latency_bucket{le="0.5"} 25
1758dynamo_component_errors_total 5"#;
1759
1760 let metrics_only = super::test_helpers::extract_metrics(test_input);
1762 assert_eq!(metrics_only.len(), 4); assert!(
1764 metrics_only
1765 .iter()
1766 .all(|line| line.starts_with("dynamo_component") && !line.starts_with("#"))
1767 );
1768
1769 println!("✓ All refactored filter functions work correctly!");
1770 }
1771
1772 #[tokio::test]
1773 async fn test_same_metric_name_different_endpoints() {
1774 let drt = create_test_drt_async().await;
1778 let namespace = drt.namespace("ns_test").unwrap();
1779 let component = namespace.component("comp_test").unwrap();
1780
1781 let ep1 = component.endpoint("ep1");
1783 let ep2 = component.endpoint("ep2");
1784
1785 let counter1 = ep1
1786 .metrics()
1787 .create_counter("requests_total", "Total requests", &[])
1788 .unwrap();
1789 counter1.inc_by(100.0);
1790
1791 let counter2 = ep2
1792 .metrics()
1793 .create_counter("requests_total", "Total requests", &[])
1794 .unwrap();
1795 counter2.inc_by(200.0);
1796
1797 let output = component.metrics().prometheus_expfmt().unwrap();
1799
1800 let wid = format!("{:x}", drt.connection_id());
1801 use super::test_helpers::inject_worker_id;
1802
1803 let expected_output = inject_worker_id(
1804 r#"# HELP dynamo_component_requests_total Total requests
1805# TYPE dynamo_component_requests_total counter
1806dynamo_component_requests_total{dynamo_component="comp_test",dynamo_endpoint="ep1",dynamo_namespace="ns_test"} 100
1807dynamo_component_requests_total{dynamo_component="comp_test",dynamo_endpoint="ep2",dynamo_namespace="ns_test"} 200"#,
1808 &wid,
1809 );
1810
1811 assert_eq!(
1812 output.trim_end_matches('\n'),
1813 expected_output.trim_end_matches('\n'),
1814 "\n=== MULTI-REGISTRY COMPARISON FAILED ===\n\
1815 Actual:\n{}\n\
1816 Expected:\n{}\n\
1817 ==============================",
1818 output,
1819 expected_output
1820 );
1821
1822 println!("✓ Multi-registry prevents Prometheus collisions!");
1823 }
1824
1825 #[tokio::test]
1826 async fn test_duplicate_series_warning() {
1827 let drt = create_test_drt_async().await;
1830 let namespace = drt.namespace("ns_dup").unwrap();
1831 let component = namespace.component("comp_dup").unwrap();
1832
1833 let ep1 = component.endpoint("ep_same");
1835 let ep2 = component.endpoint("ep_same"); let counter1 = ep1
1838 .metrics()
1839 .create_counter("dup_metric", "Duplicate metric test", &[])
1840 .unwrap();
1841 counter1.inc_by(50.0);
1842
1843 let counter2 = ep2
1844 .metrics()
1845 .create_counter("dup_metric", "Duplicate metric test", &[])
1846 .unwrap();
1847 counter2.inc_by(75.0);
1848
1849 let output = component.metrics().prometheus_expfmt().unwrap();
1851
1852 let wid = format!("{:x}", drt.connection_id());
1853 use super::test_helpers::inject_worker_id;
1854
1855 let expected_output = inject_worker_id(
1856 r#"# HELP dynamo_component_dup_metric Duplicate metric test
1857# TYPE dynamo_component_dup_metric counter
1858dynamo_component_dup_metric{dynamo_component="comp_dup",dynamo_endpoint="ep_same",dynamo_namespace="ns_dup"} 50"#,
1859 &wid,
1860 );
1861
1862 assert_eq!(
1863 output.trim_end_matches('\n'),
1864 expected_output.trim_end_matches('\n'),
1865 "\n=== DEDUPLICATION COMPARISON FAILED ===\n\
1866 Actual:\n{}\n\
1867 Expected:\n{}\n\
1868 ==============================",
1869 output,
1870 expected_output
1871 );
1872
1873 println!("✓ Duplicate series detection and deduplication works!");
1874 }
1875}