1pub mod frontend_perf;
10pub mod otlp_export;
11pub mod prom_typed;
12pub mod prometheus_names;
13pub mod quic_response;
14pub mod request_plane;
15pub mod tokio_perf;
16pub mod transport_metrics;
17pub mod work_handler_perf;
18pub mod work_handler_pool;
19
20use parking_lot::Mutex;
21use std::collections::HashSet;
22use std::sync::Arc;
23
24use crate::component::ComponentBuilder;
25use anyhow;
26use anyhow::Context as _;
27use once_cell::sync::Lazy;
28use regex::Regex;
29use std::any::Any;
30use std::collections::HashMap;
31
32use prometheus_names::{
34 build_component_metric_name, labels, name_prefix, sanitize_prometheus_label,
35 sanitize_prometheus_name, work_handler,
36};
37
38use crate::pipeline::{
40 AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, ResponseStream, SingleIn, async_trait,
41 network::Ingress,
42};
43use crate::protocols::annotated::Annotated;
44use crate::stream;
45use crate::stream::StreamExt;
46
47use prometheus::Encoder;
49
50fn validate_no_duplicate_label_keys(labels: &[(&str, &str)]) -> anyhow::Result<()> {
53 let mut seen_keys = std::collections::HashSet::new();
54 for (key, _) in labels {
55 if !seen_keys.insert(*key) {
56 return Err(anyhow::anyhow!(
57 "Duplicate label key '{}' found in labels",
58 key
59 ));
60 }
61 }
62 Ok(())
63}
64
65pub trait PrometheusMetric: prometheus::core::Collector + Clone + Send + Sync + 'static {
70 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error>
72 where
73 Self: Sized;
74
75 fn with_histogram_opts_and_buckets(
78 _opts: prometheus::HistogramOpts,
79 _buckets: Option<Vec<f64>>,
80 ) -> Result<Self, prometheus::Error>
81 where
82 Self: Sized,
83 {
84 panic!("with_histogram_opts_and_buckets is not implemented for this metric type");
85 }
86
87 fn with_histogram_opts_buckets_and_label_names(
89 _opts: prometheus::HistogramOpts,
90 _buckets: Option<Vec<f64>>,
91 _label_names: &[&str],
92 ) -> Result<Self, prometheus::Error>
93 where
94 Self: Sized,
95 {
96 panic!(
97 "with_histogram_opts_buckets_and_label_names is not implemented for this metric type"
98 );
99 }
100
101 fn with_opts_and_label_names(
104 _opts: prometheus::Opts,
105 _label_names: &[&str],
106 ) -> Result<Self, prometheus::Error>
107 where
108 Self: Sized,
109 {
110 panic!("with_opts_and_label_names is not implemented for this metric type");
111 }
112}
113
114impl PrometheusMetric for prometheus::Counter {
116 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
117 prometheus::Counter::with_opts(opts)
118 }
119}
120
121impl PrometheusMetric for prometheus::IntCounter {
122 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
123 prometheus::IntCounter::with_opts(opts)
124 }
125}
126
127impl PrometheusMetric for prometheus::Gauge {
128 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
129 prometheus::Gauge::with_opts(opts)
130 }
131}
132
133impl PrometheusMetric for prometheus::IntGauge {
134 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
135 prometheus::IntGauge::with_opts(opts)
136 }
137}
138
139impl PrometheusMetric for prometheus::GaugeVec {
140 fn with_opts(_opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
141 Err(prometheus::Error::Msg(
142 "GaugeVec requires label names, use with_opts_and_label_names instead".to_string(),
143 ))
144 }
145
146 fn with_opts_and_label_names(
147 opts: prometheus::Opts,
148 label_names: &[&str],
149 ) -> Result<Self, prometheus::Error> {
150 prometheus::GaugeVec::new(opts, label_names)
151 }
152}
153
154impl PrometheusMetric for prometheus::IntGaugeVec {
155 fn with_opts(_opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
156 Err(prometheus::Error::Msg(
157 "IntGaugeVec requires label names, use with_opts_and_label_names instead".to_string(),
158 ))
159 }
160
161 fn with_opts_and_label_names(
162 opts: prometheus::Opts,
163 label_names: &[&str],
164 ) -> Result<Self, prometheus::Error> {
165 prometheus::IntGaugeVec::new(opts, label_names)
166 }
167}
168
169impl PrometheusMetric for prometheus::IntCounterVec {
170 fn with_opts(_opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
171 Err(prometheus::Error::Msg(
172 "IntCounterVec requires label names, use with_opts_and_label_names instead".to_string(),
173 ))
174 }
175
176 fn with_opts_and_label_names(
177 opts: prometheus::Opts,
178 label_names: &[&str],
179 ) -> Result<Self, prometheus::Error> {
180 prometheus::IntCounterVec::new(opts, label_names)
181 }
182}
183
184impl PrometheusMetric for prometheus::Histogram {
186 fn with_opts(opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
187 let histogram_opts = prometheus::HistogramOpts::new(opts.name, opts.help);
189 prometheus::Histogram::with_opts(histogram_opts)
190 }
191
192 fn with_histogram_opts_and_buckets(
193 mut opts: prometheus::HistogramOpts,
194 buckets: Option<Vec<f64>>,
195 ) -> Result<Self, prometheus::Error> {
196 if let Some(custom_buckets) = buckets {
197 opts = opts.buckets(custom_buckets);
198 }
199 prometheus::Histogram::with_opts(opts)
200 }
201}
202
203impl PrometheusMetric for prometheus::HistogramVec {
204 fn with_opts(_opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
205 Err(prometheus::Error::Msg(
206 "HistogramVec requires histogram options and label names".to_string(),
207 ))
208 }
209
210 fn with_histogram_opts_buckets_and_label_names(
211 mut opts: prometheus::HistogramOpts,
212 buckets: Option<Vec<f64>>,
213 label_names: &[&str],
214 ) -> Result<Self, prometheus::Error> {
215 if let Some(custom_buckets) = buckets {
216 opts = opts.buckets(custom_buckets);
217 }
218 prometheus::HistogramVec::new(opts, label_names)
219 }
220}
221
222impl PrometheusMetric for prometheus::CounterVec {
224 fn with_opts(_opts: prometheus::Opts) -> Result<Self, prometheus::Error> {
225 panic!("CounterVec requires label names, use with_opts_and_label_names instead");
227 }
228
229 fn with_opts_and_label_names(
230 opts: prometheus::Opts,
231 label_names: &[&str],
232 ) -> Result<Self, prometheus::Error> {
233 prometheus::CounterVec::new(opts, label_names)
234 }
235}
236
237pub fn create_metric<T: PrometheusMetric, H: MetricsHierarchy + ?Sized>(
242 hierarchy: &H,
243 metric_name: &str,
244 metric_desc: &str,
245 labels: &[(&str, &str)],
246 buckets: Option<Vec<f64>>,
247 const_labels: Option<&[&str]>,
248) -> anyhow::Result<T> {
249 validate_no_duplicate_label_keys(labels)?;
251 let basename = hierarchy.basename();
254 let parent_hierarchies = hierarchy.parent_hierarchies();
255
256 let mut hierarchy_names: Vec<String> =
258 parent_hierarchies.iter().map(|p| p.basename()).collect();
259 hierarchy_names.push(basename.clone());
260
261 let metric_name = build_component_metric_name(metric_name);
262
263 let mut updated_labels: Vec<(String, String)> = Vec::new();
265
266 for (key, _) in labels {
273 if *key == labels::NAMESPACE
274 || *key == labels::COMPONENT
275 || *key == labels::ENDPOINT
276 || *key == labels::WORKER_ID
277 {
278 return Err(anyhow::anyhow!(
279 "Label '{}' is automatically added by auto-label injection and cannot be manually set",
280 key
281 ));
282 }
283 }
284
285 if let Some(label_names) = const_labels {
289 for name in label_names.iter() {
290 if *name == labels::NAMESPACE
291 || *name == labels::COMPONENT
292 || *name == labels::ENDPOINT
293 || *name == labels::WORKER_ID
294 {
295 return Err(anyhow::anyhow!(
296 "Variable label name '{}' conflicts with auto-injected const label and cannot be used",
297 name
298 ));
299 }
300 }
301 }
302
303 if hierarchy_names.len() > 1 {
306 let namespace = &hierarchy_names[1];
307 if !namespace.is_empty() {
308 let valid_namespace = sanitize_prometheus_label(namespace)?;
309 if !valid_namespace.is_empty() {
310 updated_labels.push((labels::NAMESPACE.to_string(), valid_namespace));
311 }
312 }
313 }
314 if hierarchy_names.len() > 2 {
315 let component = &hierarchy_names[2];
316 if !component.is_empty() {
317 let valid_component = sanitize_prometheus_label(component)?;
318 if !valid_component.is_empty() {
319 updated_labels.push((labels::COMPONENT.to_string(), valid_component));
320 }
321 }
322 }
323 if hierarchy_names.len() > 3 {
324 let endpoint = &hierarchy_names[3];
325 if !endpoint.is_empty() {
326 let valid_endpoint = sanitize_prometheus_label(endpoint)?;
327 if !valid_endpoint.is_empty() {
328 updated_labels.push((labels::ENDPOINT.to_string(), valid_endpoint));
329 }
330 }
331 }
332
333 if let Some(conn_id) = hierarchy.connection_id() {
337 updated_labels.push((labels::WORKER_ID.to_string(), format!("{:x}", conn_id)));
338 }
339
340 updated_labels.extend(
342 labels
343 .iter()
344 .map(|(k, v)| ((*k).to_string(), (*v).to_string())),
345 );
346 let prometheus_metric = if std::any::TypeId::of::<T>()
350 == std::any::TypeId::of::<prometheus::CounterVec>()
351 {
352 if buckets.is_some() {
355 return Err(anyhow::anyhow!(
356 "buckets parameter is not valid for CounterVec"
357 ));
358 }
359 let mut opts = prometheus::Opts::new(&metric_name, metric_desc);
360 for (key, value) in &updated_labels {
361 opts = opts.const_label(key.clone(), value.clone());
362 }
363 let label_names = const_labels
364 .ok_or_else(|| anyhow::anyhow!("CounterVec requires const_labels parameter"))?;
365 T::with_opts_and_label_names(opts, label_names)?
366 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<prometheus::GaugeVec>() {
367 if buckets.is_some() {
370 return Err(anyhow::anyhow!(
371 "buckets parameter is not valid for GaugeVec"
372 ));
373 }
374 let mut opts = prometheus::Opts::new(&metric_name, metric_desc);
375 for (key, value) in &updated_labels {
376 opts = opts.const_label(key.clone(), value.clone());
377 }
378 let label_names = const_labels
379 .ok_or_else(|| anyhow::anyhow!("GaugeVec requires const_labels parameter"))?;
380 T::with_opts_and_label_names(opts, label_names)?
381 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<prometheus::HistogramVec>() {
382 let mut opts = prometheus::HistogramOpts::new(&metric_name, metric_desc);
383 for (key, value) in &updated_labels {
384 opts = opts.const_label(key.clone(), value.clone());
385 }
386 let label_names = const_labels
387 .ok_or_else(|| anyhow::anyhow!("HistogramVec requires const_labels parameter"))?;
388 T::with_histogram_opts_buckets_and_label_names(opts, buckets, label_names)?
389 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<prometheus::Histogram>() {
390 if const_labels.is_some() {
393 return Err(anyhow::anyhow!(
394 "const_labels parameter is not valid for Histogram"
395 ));
396 }
397 let mut opts = prometheus::HistogramOpts::new(&metric_name, metric_desc);
398 for (key, value) in &updated_labels {
399 opts = opts.const_label(key.clone(), value.clone());
400 }
401 T::with_histogram_opts_and_buckets(opts, buckets)?
402 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<prometheus::IntCounterVec>() {
403 if buckets.is_some() {
406 return Err(anyhow::anyhow!(
407 "buckets parameter is not valid for IntCounterVec"
408 ));
409 }
410 let mut opts = prometheus::Opts::new(&metric_name, metric_desc);
411 for (key, value) in &updated_labels {
412 opts = opts.const_label(key.clone(), value.clone());
413 }
414 let label_names = const_labels
415 .ok_or_else(|| anyhow::anyhow!("IntCounterVec requires const_labels parameter"))?;
416 T::with_opts_and_label_names(opts, label_names)?
417 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<prometheus::IntGaugeVec>() {
418 if buckets.is_some() {
421 return Err(anyhow::anyhow!(
422 "buckets parameter is not valid for IntGaugeVec"
423 ));
424 }
425 let mut opts = prometheus::Opts::new(&metric_name, metric_desc);
426 for (key, value) in &updated_labels {
427 opts = opts.const_label(key.clone(), value.clone());
428 }
429 let label_names = const_labels
430 .ok_or_else(|| anyhow::anyhow!("IntGaugeVec requires const_labels parameter"))?;
431 T::with_opts_and_label_names(opts, label_names)?
432 } else {
433 if buckets.is_some() {
436 return Err(anyhow::anyhow!(
437 "buckets parameter is not valid for Counter, IntCounter, Gauge, or IntGauge"
438 ));
439 }
440 if const_labels.is_some() {
441 return Err(anyhow::anyhow!(
442 "const_labels parameter is not valid for Counter, IntCounter, Gauge, or IntGauge"
443 ));
444 }
445 let mut opts = prometheus::Opts::new(&metric_name, metric_desc);
446 for (key, value) in &updated_labels {
447 opts = opts.const_label(key.clone(), value.clone());
448 }
449 T::with_opts(opts)?
450 };
451
452 let collector: Box<dyn prometheus::core::Collector> = Box::new(prometheus_metric.clone());
453 hierarchy.get_metrics_registry().add_metric(collector)?;
454
455 Ok(prometheus_metric)
456}
457
458pub struct Metrics<H: MetricsHierarchy> {
461 hierarchy: H,
462}
463
464impl<H: MetricsHierarchy> Metrics<H> {
465 pub fn new(hierarchy: H) -> Self {
466 Self { hierarchy }
467 }
468
469 pub fn create_counter(
492 &self,
493 name: &str,
494 description: &str,
495 labels: &[(&str, &str)],
496 ) -> anyhow::Result<prometheus::Counter> {
497 create_metric(&self.hierarchy, name, description, labels, None, None)
498 }
499
500 pub fn create_countervec(
502 &self,
503 name: &str,
504 description: &str,
505 const_labels: &[&str],
506 const_label_values: &[(&str, &str)],
507 ) -> anyhow::Result<prometheus::CounterVec> {
508 create_metric(
509 &self.hierarchy,
510 name,
511 description,
512 const_label_values,
513 None,
514 Some(const_labels),
515 )
516 }
517
518 pub fn create_gauge(
520 &self,
521 name: &str,
522 description: &str,
523 labels: &[(&str, &str)],
524 ) -> anyhow::Result<prometheus::Gauge> {
525 create_metric(&self.hierarchy, name, description, labels, None, None)
526 }
527
528 pub fn create_gaugevec(
530 &self,
531 name: &str,
532 description: &str,
533 const_labels: &[&str],
534 const_label_values: &[(&str, &str)],
535 ) -> anyhow::Result<prometheus::GaugeVec> {
536 create_metric(
537 &self.hierarchy,
538 name,
539 description,
540 const_label_values,
541 None,
542 Some(const_labels),
543 )
544 }
545
546 pub fn create_histogram(
548 &self,
549 name: &str,
550 description: &str,
551 labels: &[(&str, &str)],
552 buckets: Option<Vec<f64>>,
553 ) -> anyhow::Result<prometheus::Histogram> {
554 create_metric(&self.hierarchy, name, description, labels, buckets, None)
555 }
556
557 pub fn create_histogramvec(
559 &self,
560 name: &str,
561 description: &str,
562 label_names: &[&str],
563 labels: &[(&str, &str)],
564 buckets: Option<Vec<f64>>,
565 ) -> anyhow::Result<prometheus::HistogramVec> {
566 create_metric(
567 &self.hierarchy,
568 name,
569 description,
570 labels,
571 buckets,
572 Some(label_names),
573 )
574 }
575
576 pub fn create_intcounter(
578 &self,
579 name: &str,
580 description: &str,
581 labels: &[(&str, &str)],
582 ) -> anyhow::Result<prometheus::IntCounter> {
583 create_metric(&self.hierarchy, name, description, labels, None, None)
584 }
585
586 pub fn create_intcountervec(
588 &self,
589 name: &str,
590 description: &str,
591 const_labels: &[&str],
592 const_label_values: &[(&str, &str)],
593 ) -> anyhow::Result<prometheus::IntCounterVec> {
594 create_metric(
595 &self.hierarchy,
596 name,
597 description,
598 const_label_values,
599 None,
600 Some(const_labels),
601 )
602 }
603
604 pub fn create_intgauge(
606 &self,
607 name: &str,
608 description: &str,
609 labels: &[(&str, &str)],
610 ) -> anyhow::Result<prometheus::IntGauge> {
611 create_metric(&self.hierarchy, name, description, labels, None, None)
612 }
613
614 pub fn create_intgaugevec(
616 &self,
617 name: &str,
618 description: &str,
619 const_labels: &[&str],
620 const_label_values: &[(&str, &str)],
621 ) -> anyhow::Result<prometheus::IntGaugeVec> {
622 create_metric(
623 &self.hierarchy,
624 name,
625 description,
626 const_label_values,
627 None,
628 Some(const_labels),
629 )
630 }
631
632 pub fn prometheus_expfmt(&self) -> anyhow::Result<String> {
634 self.hierarchy
635 .get_metrics_registry()
636 .prometheus_expfmt_combined()
637 }
638}
639
640use crate::traits::DistributedRuntimeProvider;
644
645pub trait MetricsHierarchy: Send + Sync {
646 fn basename(&self) -> String;
652
653 fn parent_hierarchies(&self) -> Vec<&dyn MetricsHierarchy>;
657
658 fn get_metrics_registry(&self) -> &MetricsRegistry;
660
661 fn connection_id(&self) -> Option<u64> {
671 None
672 }
673
674 fn metrics(&self) -> Metrics<&Self>
677 where
678 Self: Sized,
679 {
680 Metrics::new(self)
681 }
682}
683
684impl<T: MetricsHierarchy + ?Sized> MetricsHierarchy for &T {
686 fn basename(&self) -> String {
687 (**self).basename()
688 }
689
690 fn parent_hierarchies(&self) -> Vec<&dyn MetricsHierarchy> {
691 (**self).parent_hierarchies()
692 }
693
694 fn get_metrics_registry(&self) -> &MetricsRegistry {
695 (**self).get_metrics_registry()
696 }
697
698 fn connection_id(&self) -> Option<u64> {
699 (**self).connection_id()
700 }
701}
702
703pub type PrometheusUpdateCallback = Arc<dyn Fn() -> anyhow::Result<()> + Send + Sync + 'static>;
712
713pub struct CollectedFamilies {
720 pub families: Vec<prometheus::proto::MetricFamily>,
721 pub units: HashMap<String, String>,
723 pub prom_types: HashMap<String, String>,
727}
728
729pub type PrometheusExpositionFormatCallback =
731 Arc<dyn Fn() -> anyhow::Result<String> + Send + Sync + 'static>;
732
733pub type PrometheusTypedCallback = Arc<
736 dyn Fn() -> anyhow::Result<Vec<crate::metrics::prom_typed::BuiltFamily>>
737 + Send
738 + Sync
739 + 'static,
740>;
741
742#[derive(Clone)]
747pub struct MetricsRegistry {
748 pub prometheus_registry: Arc<std::sync::RwLock<prometheus::Registry>>,
751
752 child_registries: Arc<std::sync::RwLock<Vec<MetricsRegistry>>>,
766
767 pub prometheus_update_callbacks: Arc<std::sync::RwLock<Vec<PrometheusUpdateCallback>>>,
770
771 #[allow(clippy::type_complexity)]
773 pub prometheus_expfmt_callbacks:
774 Arc<std::sync::RwLock<Vec<PrometheusExpositionFormatCallback>>>,
775
776 pub prometheus_typed_callbacks: Arc<std::sync::RwLock<Vec<PrometheusTypedCallback>>>,
780}
781
782impl std::fmt::Debug for MetricsRegistry {
783 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
784 f.debug_struct("MetricsRegistry")
785 .field("prometheus_registry", &"<RwLock<Registry>>")
786 .field(
787 "prometheus_update_callbacks",
788 &format!(
789 "<RwLock<Vec<Callback>>> with {} callbacks",
790 self.prometheus_update_callbacks.read().unwrap().len()
791 ),
792 )
793 .field(
794 "prometheus_expfmt_callbacks",
795 &format!(
796 "<RwLock<Vec<Callback>>> with {} callbacks",
797 self.prometheus_expfmt_callbacks.read().unwrap().len()
798 ),
799 )
800 .field(
801 "prometheus_typed_callbacks",
802 &format!(
803 "<RwLock<Vec<Callback>>> with {} callbacks",
804 self.prometheus_typed_callbacks.read().unwrap().len()
805 ),
806 )
807 .finish()
808 }
809}
810
811impl MetricsRegistry {
812 pub fn new() -> Self {
814 Self {
815 prometheus_registry: Arc::new(std::sync::RwLock::new(prometheus::Registry::new())),
816 child_registries: Arc::new(std::sync::RwLock::new(Vec::new())),
817 prometheus_update_callbacks: Arc::new(std::sync::RwLock::new(Vec::new())),
818 prometheus_expfmt_callbacks: Arc::new(std::sync::RwLock::new(Vec::new())),
819 prometheus_typed_callbacks: Arc::new(std::sync::RwLock::new(Vec::new())),
820 }
821 }
822
823 pub fn add_child_registry(&self, child: &MetricsRegistry) {
827 let child_ptr = Arc::as_ptr(&child.prometheus_registry);
828 let mut guard = self.child_registries.write().unwrap();
829 if guard
830 .iter()
831 .any(|r| Arc::as_ptr(&r.prometheus_registry) == child_ptr)
832 {
833 return;
834 }
835 guard.push(child.clone());
836 }
837
838 fn registries_for_combined_scrape(&self) -> Vec<MetricsRegistry> {
839 fn visit(
845 registry: &MetricsRegistry,
846 out: &mut Vec<MetricsRegistry>,
847 seen: &mut HashSet<*const std::sync::RwLock<prometheus::Registry>>,
848 ) {
849 let ptr = Arc::as_ptr(®istry.prometheus_registry);
850 if !seen.insert(ptr) {
851 return;
852 }
853
854 out.push(registry.clone());
855
856 let children: Vec<MetricsRegistry> = registry
857 .child_registries
858 .read()
859 .unwrap()
860 .iter()
861 .cloned()
862 .collect();
863 for child in children {
864 visit(&child, out, seen);
865 }
866 }
867
868 let mut out = Vec::new();
869 let mut seen: HashSet<*const std::sync::RwLock<prometheus::Registry>> = HashSet::new();
870 visit(self, &mut out, &mut seen);
871 out
872 }
873
874 pub fn prometheus_expfmt_combined(&self) -> anyhow::Result<String> {
884 let registries = self.registries_for_combined_scrape();
885 run_update_callbacks(®istries);
886
887 let merged = FamilyMerger::from_gathered(®istries)?.into_sorted();
888
889 let encoder = prometheus::TextEncoder::new();
890 let mut buffer = Vec::new();
891 encoder.encode(&merged, &mut buffer)?;
892 let mut result = String::from_utf8(buffer)?;
893
894 let mut expfmt = String::new();
896 for registry in ®istries {
897 let text = registry.execute_expfmt_callbacks();
898 if !text.is_empty() {
899 if !expfmt.is_empty() && !expfmt.ends_with('\n') {
900 expfmt.push('\n');
901 }
902 expfmt.push_str(&text);
903 }
904 }
905
906 if !expfmt.is_empty() {
907 if !result.ends_with('\n') {
908 result.push('\n');
909 }
910 result.push_str(&expfmt);
911 }
912
913 Ok(result)
914 }
915
916 pub fn metric_families_combined(&self) -> anyhow::Result<CollectedFamilies> {
926 let registries = self.registries_for_combined_scrape();
927 run_update_callbacks(®istries);
928
929 let mut merger = FamilyMerger::from_gathered(®istries)?;
930
931 let mut units: HashMap<String, String> = HashMap::new();
934 let mut prom_types: HashMap<String, String> = HashMap::new();
935 for registry in ®istries {
936 for built in registry.execute_typed_callbacks() {
937 let name = built.family.name().to_string();
938 if let Err(error) = merger.add_family(built.family) {
944 tracing::warn!(metric_name = %name, %error, "skipping typed metric family");
945 continue;
946 }
947 if !built.unit.is_empty() {
954 units.entry(name.clone()).or_insert(built.unit);
955 }
956 prom_types.entry(name).or_insert(built.prom_type);
957 }
958 }
959
960 Ok(CollectedFamilies {
961 families: merger.into_sorted(),
962 units,
963 prom_types,
964 })
965 }
966
967 pub fn add_update_callback(&self, callback: PrometheusUpdateCallback) {
969 self.prometheus_update_callbacks
970 .write()
971 .unwrap()
972 .push(callback);
973 }
974
975 pub fn add_expfmt_callback(&self, callback: PrometheusExpositionFormatCallback) {
980 self.prometheus_expfmt_callbacks
981 .write()
982 .unwrap()
983 .push(callback);
984 }
985
986 pub fn execute_expfmt_callbacks(&self) -> String {
988 let callbacks: Vec<PrometheusExpositionFormatCallback> = self
992 .prometheus_expfmt_callbacks
993 .read()
994 .unwrap()
995 .iter()
996 .cloned()
997 .collect();
998 let mut result = String::new();
999 for callback in callbacks.iter() {
1000 match callback() {
1001 Ok(text) => {
1002 if !text.is_empty() {
1003 if !result.is_empty() && !result.ends_with('\n') {
1004 result.push('\n');
1005 }
1006 result.push_str(&text);
1007 }
1008 }
1009 Err(e) => {
1010 tracing::error!("Error executing exposition text callback: {e}");
1011 }
1012 }
1013 }
1014 result
1015 }
1016
1017 pub fn add_typed_callback(&self, callback: PrometheusTypedCallback) {
1021 self.prometheus_typed_callbacks
1022 .write()
1023 .unwrap()
1024 .push(callback);
1025 }
1026
1027 pub fn execute_typed_callbacks(&self) -> Vec<crate::metrics::prom_typed::BuiltFamily> {
1030 let callbacks: Vec<PrometheusTypedCallback> = self
1035 .prometheus_typed_callbacks
1036 .read()
1037 .unwrap()
1038 .iter()
1039 .cloned()
1040 .collect();
1041
1042 callbacks
1043 .iter()
1044 .filter_map(|callback| match callback() {
1045 Ok(families) => Some(families),
1046 Err(error) => {
1047 tracing::warn!(%error, "typed metrics callback failed");
1048 None
1049 }
1050 })
1051 .flatten()
1052 .collect()
1053 }
1054
1055 pub fn execute_update_callbacks(&self) -> Vec<anyhow::Result<()>> {
1057 let callbacks: Vec<PrometheusUpdateCallback> = self
1062 .prometheus_update_callbacks
1063 .read()
1064 .unwrap()
1065 .iter()
1066 .cloned()
1067 .collect();
1068
1069 callbacks.iter().map(|callback| callback()).collect()
1070 }
1071
1072 pub fn add_metric(
1074 &self,
1075 collector: Box<dyn prometheus::core::Collector>,
1076 ) -> anyhow::Result<()> {
1077 self.prometheus_registry
1078 .write()
1079 .unwrap()
1080 .register(collector)
1081 .map_err(|e| anyhow::anyhow!("Failed to register metric: {}", e))
1082 }
1083
1084 pub fn add_metric_or_warn(&self, collector: Box<dyn prometheus::core::Collector>, name: &str) {
1086 if let Err(e) = self.add_metric(collector) {
1087 tracing::warn!(error = %e, metric = name, "Failed to register metric");
1088 }
1089 }
1090
1091 pub fn get_prometheus_registry(&self) -> std::sync::RwLockReadGuard<'_, prometheus::Registry> {
1093 self.prometheus_registry.read().unwrap()
1094 }
1095
1096 pub fn has_metric_named(&self, metric_name: &str) -> bool {
1098 self.prometheus_registry
1099 .read()
1100 .unwrap()
1101 .gather()
1102 .iter()
1103 .any(|mf| mf.name() == metric_name)
1104 }
1105}
1106
1107fn run_update_callbacks(registries: &[MetricsRegistry]) {
1110 for registry in registries {
1111 for result in registry.execute_update_callbacks() {
1112 if let Err(error) = result {
1113 tracing::error!(%error, "metrics update callback failed");
1114 }
1115 }
1116 }
1117}
1118
1119struct FamilyMerger {
1125 by_name: HashMap<String, prometheus::proto::MetricFamily>,
1126 seen_series: HashSet<String>,
1127}
1128
1129impl FamilyMerger {
1130 fn from_gathered(registries: &[MetricsRegistry]) -> anyhow::Result<Self> {
1131 let mut merger = Self {
1132 by_name: HashMap::new(),
1133 seen_series: HashSet::new(),
1134 };
1135 for (idx, registry) in registries.iter().enumerate() {
1136 for family in registry.get_prometheus_registry().gather() {
1137 merger
1138 .add_family(family)
1139 .with_context(|| format!("gathering from registry {idx}"))?;
1140 }
1141 }
1142 Ok(merger)
1143 }
1144
1145 fn add_family(&mut self, mut family: prometheus::proto::MetricFamily) -> anyhow::Result<()> {
1146 let name = family.name().to_string();
1147
1148 let entry = self.by_name.entry(name.clone()).or_insert_with(|| {
1149 let mut out = prometheus::proto::MetricFamily::new();
1150 out.set_name(name.clone());
1151 out.set_help(family.help().to_string());
1152 out.set_field_type(family.get_field_type());
1153 out
1154 });
1155
1156 if entry.help() != family.help() || entry.get_field_type() != family.get_field_type() {
1157 return Err(anyhow::anyhow!(
1158 "Metric family '{}' has inconsistent help/type across sources",
1159 name
1160 ));
1161 }
1162
1163 for metric in family.take_metric() {
1164 let mut labels: Vec<(String, String)> = metric
1165 .get_label()
1166 .iter()
1167 .map(|lp| (lp.name().to_string(), lp.value().to_string()))
1168 .collect();
1169 labels.sort_by(|(ka, va), (kb, vb)| (ka, va).cmp(&(kb, vb)));
1170
1171 let key = format!(
1172 "{}|{}",
1173 name,
1174 labels
1175 .iter()
1176 .map(|(k, v)| format!("{}={}", k, v))
1177 .collect::<Vec<_>>()
1178 .join(",")
1179 );
1180
1181 if !self.seen_series.insert(key) {
1182 tracing::warn!(
1183 metric_name = %name,
1184 labels = ?labels,
1185 "Duplicate Prometheus series while merging; dropping later sample"
1186 );
1187 continue;
1188 }
1189
1190 entry.mut_metric().push(metric);
1191 }
1192
1193 Ok(())
1194 }
1195
1196 fn into_sorted(self) -> Vec<prometheus::proto::MetricFamily> {
1197 let mut merged: Vec<prometheus::proto::MetricFamily> = self.by_name.into_values().collect();
1198 merged.sort_by(|a, b| a.name().cmp(b.name()));
1199 merged
1200 }
1201}
1202
1203impl Default for MetricsRegistry {
1204 fn default() -> Self {
1205 Self::new()
1206 }
1207}
1208
1209#[cfg(test)]
1210mod test_helpers {
1211 use super::prometheus_names::name_prefix;
1212 use super::*;
1213
1214 fn filter_prometheus_lines<F>(input: &str, mut predicate: F) -> Vec<String>
1217 where
1218 F: FnMut(&str) -> bool,
1219 {
1220 input
1221 .lines()
1222 .filter(|line| predicate(line))
1223 .map(|line| line.to_string())
1224 .collect::<Vec<_>>()
1225 }
1226
1227 pub fn extract_metrics(input: &str) -> Vec<String> {
1230 filter_prometheus_lines(input, |line| {
1231 line.starts_with(&format!("{}_", name_prefix::COMPONENT))
1232 && !line.starts_with("#")
1233 && !line.trim().is_empty()
1234 })
1235 }
1236
1237 pub fn parse_prometheus_metric(
1249 line: &str,
1250 ) -> Option<(String, std::collections::HashMap<String, String>, f64)> {
1251 if line.trim().is_empty() || line.starts_with('#') {
1252 return None;
1253 }
1254
1255 let parts: Vec<&str> = line.split_whitespace().collect();
1256 if parts.len() < 2 {
1257 return None;
1258 }
1259
1260 let metric_part = parts[0];
1261 let value: f64 = parts[1].parse().ok()?;
1262
1263 let (name, labels) = if metric_part.contains('{') {
1264 let brace_start = metric_part.find('{').unwrap();
1265 let brace_end = metric_part.rfind('}').unwrap_or(metric_part.len());
1266 let name = &metric_part[..brace_start];
1267 let labels_str = &metric_part[brace_start + 1..brace_end];
1268
1269 let mut labels = std::collections::HashMap::new();
1270 for pair in labels_str.split(',') {
1271 if let Some((k, v)) = pair.split_once('=') {
1272 let v = v.trim_matches('"');
1273 labels.insert(k.trim().to_string(), v.to_string());
1274 }
1275 }
1276 (name.to_string(), labels)
1277 } else {
1278 (metric_part.to_string(), std::collections::HashMap::new())
1279 };
1280
1281 Some((name, labels, value))
1282 }
1283
1284 pub fn inject_worker_id(expected: &str, wid: &str) -> String {
1290 let wid_label = format!(",worker_id=\"{}\"", wid);
1291 expected
1292 .lines()
1293 .map(|line| {
1294 if line.starts_with('#') || line.trim().is_empty() || !line.contains('{') {
1295 line.to_string()
1296 } else if let Some(le_pos) = line.find(",le=") {
1297 let mut s = line.to_string();
1300 s.insert_str(le_pos, &wid_label);
1301 s
1302 } else {
1303 line.replacen("}", &format!("{}}}", wid_label), 1)
1304 }
1305 })
1306 .collect::<Vec<_>>()
1307 .join("\n")
1308 }
1309}
1310
1311#[cfg(test)]
1312mod test_metricsregistry_units {
1313 use super::*;
1314
1315 #[test]
1316 fn test_build_component_metric_name_with_prefix() {
1317 let result = build_component_metric_name("requests");
1319 assert_eq!(result, "dynamo_component_requests");
1320
1321 let result = build_component_metric_name("counter");
1322 assert_eq!(result, "dynamo_component_counter");
1323 }
1324
1325 #[test]
1326 fn test_parse_prometheus_metric() {
1327 use super::test_helpers::parse_prometheus_metric;
1328 use std::collections::HashMap;
1329
1330 let line = "http_requests_total{method=\"GET\",status=\"200\"} 1234";
1332 let parsed = parse_prometheus_metric(line);
1333 assert!(parsed.is_some());
1334
1335 let (name, labels, value) = parsed.unwrap();
1336 assert_eq!(name, "http_requests_total");
1337
1338 let mut expected_labels = HashMap::new();
1339 expected_labels.insert("method".to_string(), "GET".to_string());
1340 expected_labels.insert("status".to_string(), "200".to_string());
1341 assert_eq!(labels, expected_labels);
1342
1343 assert_eq!(value, 1234.0);
1344
1345 let line = "cpu_usage 98.5";
1347 let parsed = parse_prometheus_metric(line);
1348 assert!(parsed.is_some());
1349
1350 let (name, labels, value) = parsed.unwrap();
1351 assert_eq!(name, "cpu_usage");
1352 assert!(labels.is_empty());
1353 assert_eq!(value, 98.5);
1354
1355 let line = "response_time{service=\"api\"} 0.123";
1357 let parsed = parse_prometheus_metric(line);
1358 assert!(parsed.is_some());
1359
1360 let (name, labels, value) = parsed.unwrap();
1361 assert_eq!(name, "response_time");
1362
1363 let mut expected_labels = HashMap::new();
1364 expected_labels.insert("service".to_string(), "api".to_string());
1365 assert_eq!(labels, expected_labels);
1366
1367 assert_eq!(value, 0.123);
1368
1369 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!");
1376 }
1377
1378 #[test]
1379 fn test_metrics_registry_entry_callbacks() {
1380 use crate::MetricsRegistry;
1381 use std::sync::atomic::{AtomicUsize, Ordering};
1382
1383 {
1385 let registry = MetricsRegistry::new();
1386 let counter = Arc::new(AtomicUsize::new(0));
1387
1388 for increment in [1, 10, 100] {
1390 let counter_clone = counter.clone();
1391 registry.add_update_callback(Arc::new(move || {
1392 counter_clone.fetch_add(increment, Ordering::SeqCst);
1393 Ok(())
1394 }));
1395 }
1396
1397 assert_eq!(counter.load(Ordering::SeqCst), 0);
1399
1400 let results = registry.execute_update_callbacks();
1402 assert_eq!(results.len(), 3);
1403 assert!(results.iter().all(|r| r.is_ok()));
1404 assert_eq!(counter.load(Ordering::SeqCst), 111); let results = registry.execute_update_callbacks();
1408 assert_eq!(results.len(), 3);
1409 assert_eq!(counter.load(Ordering::SeqCst), 222); let cloned = registry.clone();
1413 assert_eq!(cloned.execute_update_callbacks().len(), 3);
1414 assert_eq!(counter.load(Ordering::SeqCst), 333); registry.execute_update_callbacks();
1418 assert_eq!(counter.load(Ordering::SeqCst), 444); }
1420
1421 {
1423 let registry = MetricsRegistry::new();
1424 let counter = Arc::new(AtomicUsize::new(0));
1425
1426 let counter_clone = counter.clone();
1428 registry.add_update_callback(Arc::new(move || {
1429 counter_clone.fetch_add(1, Ordering::SeqCst);
1430 Ok(())
1431 }));
1432
1433 registry.add_update_callback(Arc::new(|| Err(anyhow::anyhow!("Simulated error"))));
1435
1436 let counter_clone = counter.clone();
1438 registry.add_update_callback(Arc::new(move || {
1439 counter_clone.fetch_add(10, Ordering::SeqCst);
1440 Ok(())
1441 }));
1442
1443 let results = registry.execute_update_callbacks();
1445 assert_eq!(results.len(), 3);
1446 assert!(results[0].is_ok());
1447 assert!(results[1].is_err());
1448 assert!(results[2].is_ok());
1449
1450 assert_eq!(
1452 results[1].as_ref().unwrap_err().to_string(),
1453 "Simulated error"
1454 );
1455
1456 assert_eq!(counter.load(Ordering::SeqCst), 11); let results = registry.execute_update_callbacks();
1461 assert!(results[1].is_err());
1462 assert_eq!(counter.load(Ordering::SeqCst), 22); }
1464
1465 {
1467 let registry = MetricsRegistry::new();
1468 let results = registry.execute_update_callbacks();
1469 assert_eq!(results.len(), 0);
1470 }
1471 }
1472}
1473
1474#[cfg(feature = "integration")]
1475#[cfg(test)]
1476mod test_metricsregistry_prefixes {
1477 use super::*;
1478 use crate::distributed::distributed_test_utils::create_test_drt_async;
1479 use prometheus::core::Collector;
1480
1481 #[tokio::test]
1482 async fn test_hierarchical_prefixes_and_parent_hierarchies() {
1483 let drt = create_test_drt_async().await;
1484
1485 const DRT_NAME: &str = "";
1486 const NAMESPACE_NAME: &str = "ns901";
1487 const COMPONENT_NAME: &str = "comp901";
1488 const ENDPOINT_NAME: &str = "ep901";
1489 let namespace = drt.namespace(NAMESPACE_NAME).unwrap();
1490 let component = namespace.component(COMPONENT_NAME).unwrap();
1491 let endpoint = component.endpoint(ENDPOINT_NAME);
1492
1493 assert_eq!(drt.basename(), DRT_NAME);
1495 assert_eq!(drt.parent_hierarchies().len(), 0);
1496 assert_eq!(namespace.basename(), NAMESPACE_NAME);
1500 assert_eq!(namespace.parent_hierarchies().len(), 1);
1501 assert_eq!(namespace.parent_hierarchies()[0].basename(), DRT_NAME);
1502 assert_eq!(component.basename(), COMPONENT_NAME);
1506 assert_eq!(component.parent_hierarchies().len(), 2);
1507 assert_eq!(component.parent_hierarchies()[0].basename(), DRT_NAME);
1508 assert_eq!(component.parent_hierarchies()[1].basename(), NAMESPACE_NAME);
1509 assert_eq!(endpoint.basename(), ENDPOINT_NAME);
1513 assert_eq!(endpoint.parent_hierarchies().len(), 3);
1514 assert_eq!(endpoint.parent_hierarchies()[0].basename(), DRT_NAME);
1515 assert_eq!(endpoint.parent_hierarchies()[1].basename(), NAMESPACE_NAME);
1516 assert_eq!(endpoint.parent_hierarchies()[2].basename(), COMPONENT_NAME);
1517 assert!(
1521 namespace
1522 .parent_hierarchies()
1523 .iter()
1524 .any(|h| h.basename() == drt.basename())
1525 );
1526 assert!(
1527 component
1528 .parent_hierarchies()
1529 .iter()
1530 .any(|h| h.basename() == namespace.basename())
1531 );
1532 assert!(
1533 endpoint
1534 .parent_hierarchies()
1535 .iter()
1536 .any(|h| h.basename() == component.basename())
1537 );
1538
1539 assert_eq!(drt.parent_hierarchies().len(), 0);
1541 assert_eq!(namespace.parent_hierarchies().len(), 1);
1542 assert_eq!(component.parent_hierarchies().len(), 2);
1543 assert_eq!(endpoint.parent_hierarchies().len(), 3);
1544
1545 let invalid_namespace = drt.namespace("@@123").unwrap();
1549 let result =
1550 invalid_namespace
1551 .metrics()
1552 .create_counter("test_counter", "A test counter", &[]);
1553 assert!(result.is_ok());
1554 if let Ok(counter) = &result {
1555 let desc = counter.desc();
1557 let namespace_label = desc[0]
1558 .const_label_pairs
1559 .iter()
1560 .find(|l| l.name() == "dynamo_namespace")
1561 .expect("Should have dynamo_namespace label");
1562 assert_eq!(namespace_label.value(), "_123");
1563 }
1564
1565 let valid_namespace = drt.namespace("ns567").unwrap();
1567 assert!(
1568 valid_namespace
1569 .metrics()
1570 .create_counter("test_counter", "A test counter", &[])
1571 .is_ok()
1572 );
1573 }
1574
1575 #[tokio::test]
1576 async fn test_typed_callback_only_registered_on_endpoint_is_collected_once() {
1577 let drt = create_test_drt_async().await;
1582 let namespace = drt.namespace("ns_typed_ep_only").unwrap();
1583 let component = namespace.component("comp_typed_ep_only").unwrap();
1584 let endpoint = component.endpoint("ep_typed_ep_only");
1585
1586 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1587 let counter = calls.clone();
1588 let callback: PrometheusTypedCallback = Arc::new(move || {
1589 counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1590 let mut family = prometheus::proto::MetricFamily::new();
1591 family.set_name("dynamo_component_active_decode_blocks".to_string());
1592 family.set_field_type(prometheus::proto::MetricType::GAUGE);
1593 let mut metric = prometheus::proto::Metric::new();
1594 let mut label = prometheus::proto::LabelPair::new();
1595 label.set_name("dp_rank".to_string());
1596 label.set_value("0".to_string());
1597 metric.set_label(vec![label]);
1598 metric.set_gauge(prometheus::proto::Gauge::new());
1599 family.mut_metric().push(metric);
1600 Ok(vec![crate::metrics::prom_typed::BuiltFamily {
1601 family,
1602 unit: String::new(),
1603 prom_type: "gauge".to_string(),
1604 }])
1605 });
1606
1607 endpoint.get_metrics_registry().add_typed_callback(callback);
1608
1609 let families = drt
1611 .get_metrics_registry()
1612 .metric_families_combined()
1613 .unwrap();
1614
1615 assert_eq!(
1616 calls.load(std::sync::atomic::Ordering::SeqCst),
1617 1,
1618 "endpoint-registered typed callback should be collected once"
1619 );
1620 assert!(
1621 families
1622 .families
1623 .iter()
1624 .any(|f| f.name() == "dynamo_component_active_decode_blocks"),
1625 "typed family missing from the combined collection"
1626 );
1627 }
1628
1629 #[tokio::test]
1630 async fn test_recursive_namespace() {
1631 let drt = create_test_drt_async().await;
1633
1634 let ns1 = drt.namespace("ns1").unwrap();
1636 let ns2 = ns1.namespace("ns2").unwrap();
1637 let ns3 = ns2.namespace("ns3").unwrap();
1638
1639 let component = ns3.component("test-component").unwrap();
1641
1642 assert_eq!(ns1.basename(), "ns1");
1644 assert_eq!(ns1.parent_hierarchies().len(), 1);
1645 assert_eq!(ns1.parent_hierarchies()[0].basename(), "");
1646 assert_eq!(ns2.basename(), "ns2");
1649 assert_eq!(ns2.parent_hierarchies().len(), 2);
1650 assert_eq!(ns2.parent_hierarchies()[0].basename(), "");
1651 assert_eq!(ns2.parent_hierarchies()[1].basename(), "ns1");
1652 assert_eq!(ns3.basename(), "ns3");
1655 assert_eq!(ns3.parent_hierarchies().len(), 3);
1656 assert_eq!(ns3.parent_hierarchies()[0].basename(), "");
1657 assert_eq!(ns3.parent_hierarchies()[1].basename(), "ns1");
1658 assert_eq!(ns3.parent_hierarchies()[2].basename(), "ns2");
1659 assert_eq!(component.basename(), "test-component");
1662 assert_eq!(component.parent_hierarchies().len(), 4);
1663 assert_eq!(component.parent_hierarchies()[0].basename(), "");
1664 assert_eq!(component.parent_hierarchies()[1].basename(), "ns1");
1665 assert_eq!(component.parent_hierarchies()[2].basename(), "ns2");
1666 assert_eq!(component.parent_hierarchies()[3].basename(), "ns3");
1667 println!("✓ Chained namespace test passed - all prefixes correct");
1670 }
1671}
1672
1673#[cfg(feature = "integration")]
1674#[cfg(test)]
1675mod test_metricsregistry_prometheus_fmt_outputs {
1676 use super::prometheus_names::name_prefix;
1677 use super::*;
1678 use crate::distributed::distributed_test_utils::create_test_drt_async;
1679 use prometheus::Counter;
1680 use std::sync::Arc;
1681
1682 #[tokio::test]
1683 async fn test_prometheusfactory_using_metrics_registry_trait() {
1684 let drt = create_test_drt_async().await;
1686
1687 let namespace_name = "ns345";
1689
1690 let namespace = drt.namespace(namespace_name).unwrap();
1691 let component = namespace.component("comp345").unwrap();
1692 let endpoint = component.endpoint("ep345");
1693
1694 let counter = endpoint
1696 .metrics()
1697 .create_counter("testcounter", "A test counter", &[])
1698 .unwrap();
1699 counter.inc_by(123.456789);
1700 let epsilon = 0.01;
1701 assert!((counter.get() - 123.456789).abs() < epsilon);
1702
1703 let endpoint_output_raw = endpoint.metrics().prometheus_expfmt().unwrap();
1704 println!("Endpoint output:");
1705 println!("{}", endpoint_output_raw);
1706
1707 let wid = format!("{:x}", drt.connection_id());
1710 use super::test_helpers::inject_worker_id;
1711
1712 let expected_endpoint_output = inject_worker_id(
1713 r#"# HELP dynamo_component_testcounter A test counter
1714# TYPE dynamo_component_testcounter counter
1715dynamo_component_testcounter{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345"} 123.456789"#,
1716 &wid,
1717 );
1718
1719 assert_eq!(
1720 endpoint_output_raw.trim_end_matches('\n'),
1721 expected_endpoint_output.trim_end_matches('\n'),
1722 "\n=== ENDPOINT COMPARISON FAILED ===\n\
1723 Actual:\n{}\n\
1724 Expected:\n{}\n\
1725 ==============================",
1726 endpoint_output_raw,
1727 expected_endpoint_output
1728 );
1729
1730 let gauge = component
1732 .metrics()
1733 .create_gauge("testgauge", "A test gauge", &[])
1734 .unwrap();
1735 gauge.set(50000.0);
1736 assert_eq!(gauge.get(), 50000.0);
1737
1738 let component_output_raw = component.metrics().prometheus_expfmt().unwrap();
1740 println!("Component output:");
1741 println!("{}", component_output_raw);
1742
1743 let expected_component_output = inject_worker_id(
1744 r#"# HELP dynamo_component_testcounter A test counter
1745# TYPE dynamo_component_testcounter counter
1746dynamo_component_testcounter{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345"} 123.456789
1747# HELP dynamo_component_testgauge A test gauge
1748# TYPE dynamo_component_testgauge gauge
1749dynamo_component_testgauge{dynamo_component="comp345",dynamo_namespace="ns345"} 50000"#,
1750 &wid,
1751 );
1752
1753 assert_eq!(
1754 component_output_raw.trim_end_matches('\n'),
1755 expected_component_output.trim_end_matches('\n'),
1756 "\n=== COMPONENT COMPARISON FAILED ===\n\
1757 Actual:\n{}\n\
1758 Expected:\n{}\n\
1759 ==============================",
1760 component_output_raw,
1761 expected_component_output
1762 );
1763
1764 let intcounter = namespace
1765 .metrics()
1766 .create_intcounter("testintcounter", "A test int counter", &[])
1767 .unwrap();
1768 intcounter.inc_by(12345);
1769 assert_eq!(intcounter.get(), 12345);
1770
1771 let namespace_output_raw = namespace.metrics().prometheus_expfmt().unwrap();
1773 println!("Namespace output:");
1774 println!("{}", namespace_output_raw);
1775
1776 let expected_namespace_output = inject_worker_id(
1777 r#"# HELP dynamo_component_testcounter A test counter
1778# TYPE dynamo_component_testcounter counter
1779dynamo_component_testcounter{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345"} 123.456789
1780# HELP dynamo_component_testgauge A test gauge
1781# TYPE dynamo_component_testgauge gauge
1782dynamo_component_testgauge{dynamo_component="comp345",dynamo_namespace="ns345"} 50000
1783# HELP dynamo_component_testintcounter A test int counter
1784# TYPE dynamo_component_testintcounter counter
1785dynamo_component_testintcounter{dynamo_namespace="ns345"} 12345"#,
1786 &wid,
1787 );
1788
1789 assert_eq!(
1790 namespace_output_raw.trim_end_matches('\n'),
1791 expected_namespace_output.trim_end_matches('\n'),
1792 "\n=== NAMESPACE COMPARISON FAILED ===\n\
1793 Actual:\n{}\n\
1794 Expected:\n{}\n\
1795 ==============================",
1796 namespace_output_raw,
1797 expected_namespace_output
1798 );
1799
1800 let intgauge = namespace
1802 .metrics()
1803 .create_intgauge("testintgauge", "A test int gauge", &[])
1804 .unwrap();
1805 intgauge.set(42);
1806 assert_eq!(intgauge.get(), 42);
1807
1808 let intgaugevec = namespace
1810 .metrics()
1811 .create_intgaugevec(
1812 "testintgaugevec",
1813 "A test int gauge vector",
1814 &["instance", "status"],
1815 &[("service", "api")],
1816 )
1817 .unwrap();
1818 intgaugevec
1819 .with_label_values(&["server1", "active"])
1820 .set(10);
1821 intgaugevec
1822 .with_label_values(&["server2", "inactive"])
1823 .set(0);
1824
1825 let countervec = endpoint
1827 .metrics()
1828 .create_countervec(
1829 "testcountervec",
1830 "A test counter vector",
1831 &["method", "status"],
1832 &[("service", "api")],
1833 )
1834 .unwrap();
1835 countervec.with_label_values(&["GET", "200"]).inc_by(10.0);
1836 countervec.with_label_values(&["POST", "201"]).inc_by(5.0);
1837
1838 let histogram = component
1840 .metrics()
1841 .create_histogram("testhistogram", "A test histogram", &[], None)
1842 .unwrap();
1843 histogram.observe(1.0);
1844 histogram.observe(2.5);
1845 histogram.observe(4.0);
1846
1847 let drt_output_raw = drt.metrics().prometheus_expfmt().unwrap();
1849 println!("DRT output:");
1850 println!("{}", drt_output_raw);
1851
1852 let expected_drt_output_without_uptime = inject_worker_id(
1855 r#"# HELP dynamo_component_testcounter A test counter
1856# TYPE dynamo_component_testcounter counter
1857dynamo_component_testcounter{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345"} 123.456789
1858# HELP dynamo_component_testcountervec A test counter vector
1859# TYPE dynamo_component_testcountervec counter
1860dynamo_component_testcountervec{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345",method="GET",service="api",status="200"} 10
1861dynamo_component_testcountervec{dynamo_component="comp345",dynamo_endpoint="ep345",dynamo_namespace="ns345",method="POST",service="api",status="201"} 5
1862# HELP dynamo_component_testgauge A test gauge
1863# TYPE dynamo_component_testgauge gauge
1864dynamo_component_testgauge{dynamo_component="comp345",dynamo_namespace="ns345"} 50000
1865# HELP dynamo_component_testhistogram A test histogram
1866# TYPE dynamo_component_testhistogram histogram
1867dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.005"} 0
1868dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.01"} 0
1869dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.025"} 0
1870dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.05"} 0
1871dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.1"} 0
1872dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.25"} 0
1873dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="0.5"} 0
1874dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="1"} 1
1875dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="2.5"} 2
1876dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="5"} 3
1877dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="10"} 3
1878dynamo_component_testhistogram_bucket{dynamo_component="comp345",dynamo_namespace="ns345",le="+Inf"} 3
1879dynamo_component_testhistogram_sum{dynamo_component="comp345",dynamo_namespace="ns345"} 7.5
1880dynamo_component_testhistogram_count{dynamo_component="comp345",dynamo_namespace="ns345"} 3
1881# HELP dynamo_component_testintcounter A test int counter
1882# TYPE dynamo_component_testintcounter counter
1883dynamo_component_testintcounter{dynamo_namespace="ns345"} 12345
1884# HELP dynamo_component_testintgauge A test int gauge
1885# TYPE dynamo_component_testintgauge gauge
1886dynamo_component_testintgauge{dynamo_namespace="ns345"} 42
1887# HELP dynamo_component_testintgaugevec A test int gauge vector
1888# TYPE dynamo_component_testintgaugevec gauge
1889dynamo_component_testintgaugevec{dynamo_namespace="ns345",instance="server1",service="api",status="active"} 10
1890dynamo_component_testintgaugevec{dynamo_namespace="ns345",instance="server2",service="api",status="inactive"} 0"#,
1891 &wid,
1892 );
1893
1894 let mut non_uptime_lines = Vec::new();
1898 let mut saw_uptime_value = false;
1899 for line in drt_output_raw.trim_end_matches('\n').lines() {
1900 if line.starts_with("dynamo_component_uptime_seconds") && !line.starts_with('#') {
1901 let val_str = line.split_whitespace().last().unwrap();
1902 val_str.parse::<f64>().expect("uptime should be a float");
1903 saw_uptime_value = true;
1904 } else if line.starts_with("# HELP dynamo_component_uptime_seconds")
1905 || line.starts_with("# TYPE dynamo_component_uptime_seconds")
1906 {
1907 } else {
1909 non_uptime_lines.push(line);
1910 }
1911 }
1912 assert!(
1913 saw_uptime_value,
1914 "uptime_seconds metric should be present in initial scrape"
1915 );
1916
1917 let actual_without_uptime = non_uptime_lines.join("\n");
1918 assert_eq!(
1919 actual_without_uptime,
1920 expected_drt_output_without_uptime.trim_end_matches('\n'),
1921 "\n=== DRT COMPARISON FAILED (excluding uptime) ===\n\
1922 Expected:\n{}\n\
1923 Actual:\n{}\n\
1924 ==============================",
1925 expected_drt_output_without_uptime,
1926 actual_without_uptime
1927 );
1928
1929 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1931 let drt_output_after = drt.metrics().prometheus_expfmt().unwrap();
1932 let uptime_line = drt_output_after
1933 .lines()
1934 .find(|l| l.starts_with("dynamo_component_uptime_seconds") && !l.starts_with('#'))
1935 .expect("uptime_seconds metric should be present after sleep");
1936 let uptime_after: f64 = uptime_line
1937 .split_whitespace()
1938 .last()
1939 .unwrap()
1940 .parse()
1941 .expect("uptime should be a float");
1942 assert!(
1943 uptime_after > 0.0,
1944 "uptime_seconds should be > 0 after 10ms sleep, got {}",
1945 uptime_after
1946 );
1947
1948 println!("✓ All Prometheus format outputs verified successfully!");
1949 }
1950
1951 #[test]
1952 fn test_refactored_filter_functions() {
1953 let test_input = r#"# HELP dynamo_component_requests Total requests
1955# TYPE dynamo_component_requests counter
1956dynamo_component_requests 42
1957# HELP dynamo_component_latency Response latency
1958# TYPE dynamo_component_latency histogram
1959dynamo_component_latency_bucket{le="0.1"} 10
1960dynamo_component_latency_bucket{le="0.5"} 25
1961dynamo_component_errors_total 5"#;
1962
1963 let metrics_only = super::test_helpers::extract_metrics(test_input);
1965 assert_eq!(metrics_only.len(), 4); assert!(
1967 metrics_only
1968 .iter()
1969 .all(|line| line.starts_with("dynamo_component") && !line.starts_with("#"))
1970 );
1971
1972 println!("✓ All refactored filter functions work correctly!");
1973 }
1974
1975 #[tokio::test]
1976 async fn test_same_metric_name_different_endpoints() {
1977 let drt = create_test_drt_async().await;
1981 let namespace = drt.namespace("ns_test").unwrap();
1982 let component = namespace.component("comp_test").unwrap();
1983
1984 let ep1 = component.endpoint("ep1");
1986 let ep2 = component.endpoint("ep2");
1987
1988 let counter1 = ep1
1989 .metrics()
1990 .create_counter("requests_total", "Total requests", &[])
1991 .unwrap();
1992 counter1.inc_by(100.0);
1993
1994 let counter2 = ep2
1995 .metrics()
1996 .create_counter("requests_total", "Total requests", &[])
1997 .unwrap();
1998 counter2.inc_by(200.0);
1999
2000 let output = component.metrics().prometheus_expfmt().unwrap();
2002
2003 let wid = format!("{:x}", drt.connection_id());
2004 use super::test_helpers::inject_worker_id;
2005
2006 let expected_output = inject_worker_id(
2007 r#"# HELP dynamo_component_requests_total Total requests
2008# TYPE dynamo_component_requests_total counter
2009dynamo_component_requests_total{dynamo_component="comp_test",dynamo_endpoint="ep1",dynamo_namespace="ns_test"} 100
2010dynamo_component_requests_total{dynamo_component="comp_test",dynamo_endpoint="ep2",dynamo_namespace="ns_test"} 200"#,
2011 &wid,
2012 );
2013
2014 assert_eq!(
2015 output.trim_end_matches('\n'),
2016 expected_output.trim_end_matches('\n'),
2017 "\n=== MULTI-REGISTRY COMPARISON FAILED ===\n\
2018 Actual:\n{}\n\
2019 Expected:\n{}\n\
2020 ==============================",
2021 output,
2022 expected_output
2023 );
2024
2025 println!("✓ Multi-registry prevents Prometheus collisions!");
2026 }
2027
2028 #[tokio::test]
2029 async fn test_duplicate_series_warning() {
2030 let drt = create_test_drt_async().await;
2033 let namespace = drt.namespace("ns_dup").unwrap();
2034 let component = namespace.component("comp_dup").unwrap();
2035
2036 let ep1 = component.endpoint("ep_same");
2038 let ep2 = component.endpoint("ep_same"); let counter1 = ep1
2041 .metrics()
2042 .create_counter("dup_metric", "Duplicate metric test", &[])
2043 .unwrap();
2044 counter1.inc_by(50.0);
2045
2046 let counter2 = ep2
2047 .metrics()
2048 .create_counter("dup_metric", "Duplicate metric test", &[])
2049 .unwrap();
2050 counter2.inc_by(75.0);
2051
2052 let output = component.metrics().prometheus_expfmt().unwrap();
2054
2055 let wid = format!("{:x}", drt.connection_id());
2056 use super::test_helpers::inject_worker_id;
2057
2058 let expected_output = inject_worker_id(
2059 r#"# HELP dynamo_component_dup_metric Duplicate metric test
2060# TYPE dynamo_component_dup_metric counter
2061dynamo_component_dup_metric{dynamo_component="comp_dup",dynamo_endpoint="ep_same",dynamo_namespace="ns_dup"} 50"#,
2062 &wid,
2063 );
2064
2065 assert_eq!(
2066 output.trim_end_matches('\n'),
2067 expected_output.trim_end_matches('\n'),
2068 "\n=== DEDUPLICATION COMPARISON FAILED ===\n\
2069 Actual:\n{}\n\
2070 Expected:\n{}\n\
2071 ==============================",
2072 output,
2073 expected_output
2074 );
2075
2076 println!("✓ Duplicate series detection and deduplication works!");
2077 }
2078}
2079
2080#[cfg(test)]
2081mod test_metric_families_combined {
2082 use super::*;
2083 use std::sync::Arc as StdArc;
2084
2085 fn gauge_family(name: &str, help: &str, value: f64) -> prometheus::proto::MetricFamily {
2086 let mut family = prometheus::proto::MetricFamily::new();
2087 family.set_name(name.to_string());
2088 family.set_help(help.to_string());
2089 family.set_field_type(prometheus::proto::MetricType::GAUGE);
2090 let mut metric = prometheus::proto::Metric::new();
2091 let mut gauge = prometheus::proto::Gauge::new();
2092 gauge.set_value(value);
2093 metric.set_gauge(gauge);
2094 family.mut_metric().push(metric);
2095 family
2096 }
2097
2098 fn registry() -> MetricsRegistry {
2099 let registry = MetricsRegistry::new();
2100 let counter = prometheus::IntCounter::new("dynamo_native_total", "Native counter").unwrap();
2101 counter.inc_by(7);
2102 registry
2103 .get_prometheus_registry()
2104 .register(Box::new(counter))
2105 .unwrap();
2106 registry.add_typed_callback(StdArc::new(|| {
2107 Ok(vec![crate::metrics::prom_typed::BuiltFamily {
2108 unit: String::new(),
2109 prom_type: "gauge".to_string(),
2110 family: gauge_family("vllm:num_requests_running", "Running requests", 4.0),
2111 }])
2112 }));
2113 registry
2114 }
2115
2116 fn names(fs: &[prometheus::proto::MetricFamily]) -> Vec<&str> {
2117 fs.iter().map(|f| f.name()).collect()
2118 }
2119
2120 #[test]
2121 fn typed_and_native_families_share_one_collection() {
2122 let collected = registry().metric_families_combined().expect("combined");
2123 assert!(names(&collected.families).contains(&"vllm:num_requests_running"));
2124 assert!(names(&collected.families).contains(&"dynamo_native_total"));
2125 }
2126
2127 #[test]
2139 fn metadata_from_a_source_that_contributed_no_samples_does_not_win() {
2140 let registry = MetricsRegistry::new();
2141 registry.add_typed_callback(StdArc::new(|| {
2142 Ok(vec![
2143 crate::metrics::prom_typed::BuiltFamily {
2144 unit: "bytes".to_string(),
2145 prom_type: "gauge".to_string(),
2146 family: gauge_family("vllm:probe", "Same help", 1.0),
2147 },
2148 crate::metrics::prom_typed::BuiltFamily {
2151 unit: "seconds".to_string(),
2152 prom_type: "gauge".to_string(),
2153 family: gauge_family("vllm:probe", "Same help", 2.0),
2154 },
2155 ])
2156 }));
2157
2158 let collected = registry.metric_families_combined().expect("combined");
2159
2160 let family = collected
2161 .families
2162 .iter()
2163 .find(|f| f.name() == "vllm:probe")
2164 .expect("family");
2165 assert_eq!(
2166 family.get_metric()[0].get_gauge().value(),
2167 1.0,
2168 "the surviving sample should be the first source's"
2169 );
2170 assert_eq!(
2171 collected.units.get("vllm:probe").map(String::as_str),
2172 Some("bytes"),
2173 "unit came from a source whose samples were all dropped"
2174 );
2175 }
2176
2177 #[test]
2178 fn rejected_family_leaves_no_metadata_behind() {
2179 let registry = MetricsRegistry::new();
2180 registry.add_typed_callback(StdArc::new(|| {
2181 Ok(vec![
2182 crate::metrics::prom_typed::BuiltFamily {
2183 unit: String::new(),
2184 prom_type: "gauge".to_string(),
2185 family: gauge_family("vllm:cache_usage", "Cache usage", 0.5),
2186 },
2187 crate::metrics::prom_typed::BuiltFamily {
2189 unit: "seconds".to_string(),
2190 prom_type: "info".to_string(),
2191 family: gauge_family("vllm:cache_usage", "A different help", 1.0),
2192 },
2193 ])
2194 }));
2195
2196 let collected = registry.metric_families_combined().expect("combined");
2197
2198 assert_eq!(
2199 collected
2200 .prom_types
2201 .get("vllm:cache_usage")
2202 .map(String::as_str),
2203 Some("gauge"),
2204 "the rejected family's type overwrote the accepted family's"
2205 );
2206 assert!(
2207 !collected.units.contains_key("vllm:cache_usage"),
2208 "the rejected family's unit was published for a family that has none"
2209 );
2210 }
2211
2212 #[test]
2217 fn typed_callbacks_feed_otlp_and_not_the_scrape() {
2218 let registry = registry();
2219
2220 let text = registry.prometheus_expfmt_combined().expect("text");
2221 assert!(
2222 text.contains("dynamo_native_total"),
2223 "native metrics must still render on /metrics"
2224 );
2225 assert!(
2226 !text.contains("vllm:num_requests_running"),
2227 "typed callbacks feed OTLP only; /metrics is fed by expfmt callbacks"
2228 );
2229
2230 registry.add_expfmt_callback(StdArc::new(|| {
2231 Ok("# HELP vllm:num_requests_running Running requests\n\
2232 # TYPE vllm:num_requests_running gauge\n\
2233 vllm:num_requests_running 4\n"
2234 .to_string())
2235 }));
2236 let text = registry.prometheus_expfmt_combined().expect("text");
2237 assert!(
2238 text.contains("vllm:num_requests_running"),
2239 "engine exposition text must be appended verbatim"
2240 );
2241 }
2242
2243 #[test]
2254 fn collection_does_not_hold_the_callback_lock() {
2255 for (name, collect) in [
2256 (
2257 "typed",
2258 Box::new(|r: &MetricsRegistry| {
2259 r.execute_typed_callbacks();
2260 }) as Box<dyn Fn(&MetricsRegistry) + Send>,
2261 ),
2262 (
2263 "expfmt",
2264 Box::new(|r: &MetricsRegistry| {
2265 r.execute_expfmt_callbacks();
2266 }),
2267 ),
2268 ] {
2269 let (tx, rx) = std::sync::mpsc::channel();
2270 std::thread::spawn(move || {
2271 let registry = MetricsRegistry::new();
2272 let reentrant = registry.clone();
2273 registry.add_typed_callback(StdArc::new(move || {
2274 reentrant.add_typed_callback(StdArc::new(|| Ok(Vec::new())));
2275 Ok(Vec::new())
2276 }));
2277 let reentrant = registry.clone();
2278 registry.add_expfmt_callback(StdArc::new(move || {
2279 reentrant.add_expfmt_callback(StdArc::new(|| Ok(String::new())));
2280 Ok(String::new())
2281 }));
2282 collect(®istry);
2283 let _ = tx.send(());
2284 });
2285
2286 assert!(
2287 rx.recv_timeout(std::time::Duration::from_secs(10)).is_ok(),
2288 "{name} collection deadlocked: the callback lock was held while a \
2289 callback registered another"
2290 );
2291 }
2292 }
2293
2294 #[test]
2297 fn failing_typed_callback_is_skipped() {
2298 let registry = MetricsRegistry::new();
2299 registry.add_typed_callback(StdArc::new(|| anyhow::bail!("engine exploded")));
2300 assert!(
2301 registry
2302 .metric_families_combined()
2303 .expect("collection survives")
2304 .families
2305 .is_empty()
2306 );
2307 }
2308}