1use std::{borrow::Cow, collections::HashSet, error::Error, sync::Arc};
2
3#[cfg(feature = "experimental_metrics_bound_instruments")]
4use opentelemetry::metrics::BoundSyncInstrument;
5use opentelemetry::{
6 metrics::{AsyncInstrument, SyncInstrument},
7 InstrumentationScope, Key, KeyValue,
8};
9
10#[cfg(feature = "experimental_metrics_bound_instruments")]
11use crate::metrics::internal::BoundMeasure;
12use crate::metrics::{aggregation::Aggregation, internal::Measure};
13
14use super::meter::{INSTRUMENT_UNIT_INVALID_CHAR, INSTRUMENT_UNIT_LENGTH};
15
16use super::Temporality;
17
18#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
20pub enum InstrumentKind {
21 Counter,
24 UpDownCounter,
27 Histogram,
30 ObservableCounter,
33 ObservableUpDownCounter,
36
37 Gauge,
40 ObservableGauge,
43}
44
45impl InstrumentKind {
46 pub(crate) fn temporality_preference(&self, temporality: Temporality) -> Temporality {
50 match temporality {
51 Temporality::Cumulative => Temporality::Cumulative,
52 Temporality::Delta => match self {
53 Self::Counter
54 | Self::Histogram
55 | Self::ObservableCounter
56 | Self::Gauge
57 | Self::ObservableGauge => Temporality::Delta,
58 Self::UpDownCounter | InstrumentKind::ObservableUpDownCounter => {
59 Temporality::Cumulative
60 }
61 },
62 Temporality::LowMemory => match self {
63 Self::Counter | InstrumentKind::Histogram => Temporality::Delta,
64 Self::ObservableCounter
65 | Self::Gauge
66 | Self::ObservableGauge
67 | Self::UpDownCounter
68 | Self::ObservableUpDownCounter => Temporality::Cumulative,
69 },
70 }
71 }
72}
73#[derive(Clone, Debug, PartialEq)]
98pub struct Instrument {
99 pub(crate) name: Cow<'static, str>,
101 pub(crate) description: Cow<'static, str>,
103 pub(crate) kind: InstrumentKind,
105 pub(crate) unit: Cow<'static, str>,
107 pub(crate) scope: InstrumentationScope,
109}
110
111impl Instrument {
112 pub fn name(&self) -> &str {
114 self.name.as_ref()
115 }
116
117 pub fn kind(&self) -> InstrumentKind {
119 self.kind
120 }
121
122 pub fn unit(&self) -> &str {
124 self.unit.as_ref()
125 }
126
127 pub fn scope(&self) -> &InstrumentationScope {
129 &self.scope
130 }
131}
132
133#[derive(Default, Debug)]
149pub struct StreamBuilder {
150 name: Option<Cow<'static, str>>,
151 description: Option<Cow<'static, str>>,
152 unit: Option<Cow<'static, str>>,
153 aggregation: Option<Aggregation>,
154 allowed_attribute_keys: Option<Arc<HashSet<Key>>>,
155 cardinality_limit: Option<usize>,
156}
157
158impl StreamBuilder {
159 pub(crate) fn new() -> Self {
161 StreamBuilder::default()
162 }
163
164 pub fn with_name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
166 self.name = Some(name.into());
167 self
168 }
169
170 pub fn with_description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
172 self.description = Some(description.into());
173 self
174 }
175
176 pub fn with_unit(mut self, unit: impl Into<Cow<'static, str>>) -> Self {
178 self.unit = Some(unit.into());
179 self
180 }
181
182 pub fn with_aggregation(mut self, aggregation: Aggregation) -> Self {
185 self.aggregation = Some(aggregation);
186 self
187 }
188
189 #[cfg(feature = "spec_unstable_metrics_views")]
190 pub fn with_allowed_attribute_keys(
196 mut self,
197 attribute_keys: impl IntoIterator<Item = Key>,
198 ) -> Self {
199 self.allowed_attribute_keys = Some(Arc::new(attribute_keys.into_iter().collect()));
200 self
201 }
202
203 pub fn with_cardinality_limit(mut self, limit: usize) -> Self {
205 self.cardinality_limit = Some(limit);
206 self
207 }
208
209 pub fn build(self) -> Result<Stream, Box<dyn Error>> {
215 if let Some(unit) = &self.unit {
225 if unit.len() > super::meter::INSTRUMENT_UNIT_NAME_MAX_LENGTH {
226 return Err(INSTRUMENT_UNIT_LENGTH.into());
227 }
228
229 if unit.contains(|c: char| !c.is_ascii()) {
230 return Err(INSTRUMENT_UNIT_INVALID_CHAR.into());
231 }
232 }
233
234 if let Some(limit) = self.cardinality_limit {
236 if limit == 0 {
237 return Err("Cardinality limit must be greater than 0".into());
238 }
239 if limit == usize::MAX {
242 return Err("Cardinality limit must be less than usize::MAX".into());
243 }
244 }
245
246 if let Some(Aggregation::ExplicitBucketHistogram { boundaries, .. }) = &self.aggregation {
248 validate_bucket_boundaries(boundaries)?;
249 }
250
251 if let Some(Aggregation::Base2ExponentialHistogram {
253 max_size,
254 max_scale,
255 ..
256 }) = &self.aggregation
257 {
258 if *max_size == 0 {
259 return Err("max_size must be greater than 0".into());
260 }
261 if *max_scale < super::internal::EXPO_MIN_SCALE
262 || *max_scale > super::internal::EXPO_MAX_SCALE
263 {
264 return Err(format!(
265 "max_scale must be between {} and {}",
266 super::internal::EXPO_MIN_SCALE,
267 super::internal::EXPO_MAX_SCALE
268 )
269 .into());
270 }
271 }
272
273 Ok(Stream {
274 name: self.name,
275 description: self.description,
276 unit: self.unit,
277 aggregation: self.aggregation,
278 allowed_attribute_keys: self.allowed_attribute_keys,
279 cardinality_limit: self.cardinality_limit,
280 })
281 }
282}
283
284fn validate_bucket_boundaries(boundaries: &[f64]) -> Result<(), String> {
285 for boundary in boundaries {
287 if boundary.is_nan() || boundary.is_infinite() {
288 return Err(
289 "Bucket boundaries must not contain NaN, Infinity, or -Infinity".to_string(),
290 );
291 }
292 }
293
294 for i in 1..boundaries.len() {
296 if boundaries[i] <= boundaries[i - 1] {
297 return Err(
298 "Bucket boundaries must be sorted and not contain any duplicates".to_string(),
299 );
300 }
301 }
302
303 Ok(())
304}
305
306#[derive(Default, Debug)]
309pub struct Stream {
310 pub(crate) name: Option<Cow<'static, str>>,
312 pub(crate) description: Option<Cow<'static, str>>,
314 pub(crate) unit: Option<Cow<'static, str>>,
316 pub(crate) aggregation: Option<Aggregation>,
318 pub(crate) allowed_attribute_keys: Option<Arc<HashSet<Key>>>,
324
325 pub(crate) cardinality_limit: Option<usize>,
327}
328
329impl Stream {
330 pub fn builder() -> StreamBuilder {
332 StreamBuilder::new()
333 }
334}
335
336#[derive(Debug, PartialEq, Eq, Hash)]
338pub(crate) struct InstrumentId {
339 pub(crate) name: Cow<'static, str>,
341 pub(crate) description: Cow<'static, str>,
343 pub(crate) kind: InstrumentKind,
345 pub(crate) unit: Cow<'static, str>,
347 pub(crate) number: Cow<'static, str>,
349}
350
351impl InstrumentId {
352 pub(crate) fn normalize(&mut self) {
361 if self.name.chars().any(|c| c.is_ascii_uppercase()) {
362 self.name = self.name.to_ascii_lowercase().into();
363 }
364 }
365}
366
367pub(crate) struct ResolvedMeasures<T> {
368 pub(crate) measures: Vec<Arc<dyn Measure<T>>>,
369}
370
371impl<T: Copy + 'static> SyncInstrument<T> for ResolvedMeasures<T> {
372 fn measure(&self, val: T, attrs: &[KeyValue]) {
373 for measure in &self.measures {
374 measure.call(val, attrs)
375 }
376 }
377
378 #[cfg(feature = "experimental_metrics_bound_instruments")]
379 fn bind(&self, attrs: &[KeyValue]) -> Box<dyn BoundSyncInstrument<T> + Send + Sync> {
380 let bound_measures: Vec<Box<dyn BoundMeasure<T>>> =
381 self.measures.iter().map(|m| m.bind(attrs)).collect();
382 Box::new(ResolvedBoundMeasures {
383 measures: bound_measures,
384 })
385 }
386}
387
388#[cfg(feature = "experimental_metrics_bound_instruments")]
389pub(crate) struct ResolvedBoundMeasures<T> {
390 measures: Vec<Box<dyn BoundMeasure<T>>>,
391}
392
393#[cfg(feature = "experimental_metrics_bound_instruments")]
394impl<T: Copy + 'static> BoundSyncInstrument<T> for ResolvedBoundMeasures<T> {
395 fn measure(&self, val: T) {
396 for measure in &self.measures {
397 measure.call(val);
398 }
399 }
400}
401
402#[derive(Clone)]
403pub(crate) struct Observable<T> {
404 measures: Vec<Arc<dyn Measure<T>>>,
405}
406
407impl<T> Observable<T> {
408 pub(crate) fn new(measures: Vec<Arc<dyn Measure<T>>>) -> Self {
409 Self { measures }
410 }
411}
412
413impl<T: Copy + Send + Sync + 'static> AsyncInstrument<T> for Observable<T> {
414 fn observe(&self, measurement: T, attrs: &[KeyValue]) {
415 for measure in &self.measures {
416 measure.call(measurement, attrs)
417 }
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::StreamBuilder;
424 use crate::metrics::meter::{INSTRUMENT_UNIT_INVALID_CHAR, INSTRUMENT_UNIT_LENGTH};
425
426 #[test]
427 fn stream_name_no_validation() {
428 let stream_names_all_accepted = vec![
434 "validateName",
435 "_startWithNoneAlphabet",
436 "utf8char锈",
437 "a".repeat(255).leak(),
438 "a".repeat(256).leak(),
439 "invalid name",
440 "allow/slash",
441 "allow_under_score",
442 "allow.dots.ok",
443 "",
444 "\\allow\\slash /sec",
445 "\\allow\\$$slash /sec",
446 "Total $ Count",
447 "\\test\\UsagePercent(Total) > 80%",
448 "/not / allowed",
449 ];
450
451 for name in stream_names_all_accepted {
452 let result = StreamBuilder::new().with_name(name).build();
453 assert!(
454 result.is_ok(),
455 "Expected View-provided stream name '{}' to be accepted without validation, but got error: {:?}",
456 name,
457 result.err()
458 );
459 }
460 }
461
462 #[test]
463 fn stream_unit_validation() {
464 let stream_unit_test_cases = vec![
466 (
467 "0123456789012345678901234567890123456789012345678901234567890123",
468 INSTRUMENT_UNIT_LENGTH,
469 ),
470 ("utf8char锈", INSTRUMENT_UNIT_INVALID_CHAR),
471 ("kb", ""),
472 ("Kb/sec", ""),
473 ("%", ""),
474 ("", ""),
475 ];
476
477 for (unit, expected_error) in stream_unit_test_cases {
478 let builder = StreamBuilder::new().with_name("valid_name").with_unit(unit);
480
481 let result = builder.build();
482
483 if expected_error.is_empty() {
484 assert!(
485 result.is_ok(),
486 "Expected successful build for unit '{}', but got error: {:?}",
487 unit,
488 result.err()
489 );
490 } else {
491 let err = result.err().unwrap();
492 let err_str = err.to_string();
493 assert!(
494 err_str == expected_error,
495 "For unit '{unit}', expected error '{expected_error}', but got '{err_str}'"
496 );
497 }
498 }
499 }
500
501 #[test]
502 fn stream_cardinality_limit_validation() {
503 let builder = StreamBuilder::new()
505 .with_name("valid_name")
506 .with_cardinality_limit(0);
507
508 let result = builder.build();
509 assert!(result.is_err(), "Expected error for zero cardinality limit");
510 assert_eq!(
511 result.err().unwrap().to_string(),
512 "Cardinality limit must be greater than 0",
513 "Expected cardinality limit validation error message"
514 );
515
516 let builder = StreamBuilder::new()
518 .with_name("valid_name")
519 .with_cardinality_limit(usize::MAX);
520
521 let result = builder.build();
522 assert!(
523 result.is_err(),
524 "Expected error for usize::MAX cardinality limit"
525 );
526 assert_eq!(
527 result.err().unwrap().to_string(),
528 "Cardinality limit must be less than usize::MAX",
529 "Expected cardinality limit usize::MAX error message"
530 );
531
532 let valid_limits = vec![1, 10, 100, 1000, usize::MAX - 1];
534 for limit in valid_limits {
535 let builder = StreamBuilder::new()
536 .with_name("valid_name")
537 .with_cardinality_limit(limit);
538
539 let result = builder.build();
540 assert!(
541 result.is_ok(),
542 "Expected successful build for cardinality limit {}, but got error: {:?}",
543 limit,
544 result.err()
545 );
546 }
547 }
548
549 #[test]
550 fn stream_valid_build() {
551 let stream = StreamBuilder::new()
553 .with_name("valid_name")
554 .with_description("Valid description")
555 .with_unit("ms")
556 .with_cardinality_limit(100)
557 .build();
558
559 assert!(
560 stream.is_ok(),
561 "Expected valid Stream to be built successfully"
562 );
563 }
564
565 #[test]
566 fn stream_histogram_bucket_validation() {
567 use super::Aggregation;
568
569 let valid_boundaries = vec![1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0];
571 let builder = StreamBuilder::new()
572 .with_name("valid_histogram")
573 .with_aggregation(Aggregation::ExplicitBucketHistogram {
574 boundaries: valid_boundaries.clone(),
575 record_min_max: true,
576 });
577
578 let result = builder.build();
579 assert!(
580 result.is_ok(),
581 "Expected successful build with valid bucket boundaries"
582 );
583
584 let invalid_nan_boundaries = vec![1.0, 2.0, f64::NAN, 10.0];
588
589 let builder = StreamBuilder::new()
590 .with_name("invalid_histogram_nan")
591 .with_aggregation(Aggregation::ExplicitBucketHistogram {
592 boundaries: invalid_nan_boundaries,
593 record_min_max: true,
594 });
595
596 let result = builder.build();
597 assert!(
598 result.is_err(),
599 "Expected error for NaN in bucket boundaries"
600 );
601 assert_eq!(
602 result.err().unwrap().to_string(),
603 "Bucket boundaries must not contain NaN, Infinity, or -Infinity",
604 "Expected correct validation error for NaN"
605 );
606
607 let invalid_inf_boundaries = vec![1.0, 5.0, f64::INFINITY, 100.0];
609
610 let builder = StreamBuilder::new()
611 .with_name("invalid_histogram_inf")
612 .with_aggregation(Aggregation::ExplicitBucketHistogram {
613 boundaries: invalid_inf_boundaries,
614 record_min_max: true,
615 });
616
617 let result = builder.build();
618 assert!(
619 result.is_err(),
620 "Expected error for Infinity in bucket boundaries"
621 );
622 assert_eq!(
623 result.err().unwrap().to_string(),
624 "Bucket boundaries must not contain NaN, Infinity, or -Infinity",
625 "Expected correct validation error for Infinity"
626 );
627
628 let invalid_neg_inf_boundaries = vec![f64::NEG_INFINITY, 5.0, 10.0, 100.0];
630
631 let builder = StreamBuilder::new()
632 .with_name("invalid_histogram_neg_inf")
633 .with_aggregation(Aggregation::ExplicitBucketHistogram {
634 boundaries: invalid_neg_inf_boundaries,
635 record_min_max: true,
636 });
637
638 let result = builder.build();
639 assert!(
640 result.is_err(),
641 "Expected error for negative Infinity in bucket boundaries"
642 );
643 assert_eq!(
644 result.err().unwrap().to_string(),
645 "Bucket boundaries must not contain NaN, Infinity, or -Infinity",
646 "Expected correct validation error for negative Infinity"
647 );
648
649 let unsorted_boundaries = vec![1.0, 5.0, 2.0, 10.0]; let builder = StreamBuilder::new()
653 .with_name("unsorted_histogram")
654 .with_aggregation(Aggregation::ExplicitBucketHistogram {
655 boundaries: unsorted_boundaries,
656 record_min_max: true,
657 });
658
659 let result = builder.build();
660 assert!(
661 result.is_err(),
662 "Expected error for unsorted bucket boundaries"
663 );
664 assert_eq!(
665 result.err().unwrap().to_string(),
666 "Bucket boundaries must be sorted and not contain any duplicates",
667 "Expected correct validation error for unsorted boundaries"
668 );
669
670 let duplicate_boundaries = vec![1.0, 2.0, 5.0, 5.0, 10.0]; let builder = StreamBuilder::new()
674 .with_name("duplicate_histogram")
675 .with_aggregation(Aggregation::ExplicitBucketHistogram {
676 boundaries: duplicate_boundaries,
677 record_min_max: true,
678 });
679
680 let result = builder.build();
681 assert!(
682 result.is_err(),
683 "Expected error for duplicate bucket boundaries"
684 );
685 assert_eq!(
686 result.err().unwrap().to_string(),
687 "Bucket boundaries must be sorted and not contain any duplicates",
688 "Expected correct validation error for duplicate boundaries"
689 );
690 }
691
692 #[test]
693 fn stream_exponential_histogram_validation() {
694 use super::Aggregation;
695 use crate::metrics::internal::{EXPO_MAX_SCALE, EXPO_MIN_SCALE};
696
697 let builder = StreamBuilder::new()
699 .with_name("valid_expo_histogram")
700 .with_aggregation(Aggregation::Base2ExponentialHistogram {
701 max_size: 160,
702 max_scale: 10,
703 record_min_max: true,
704 });
705
706 let result = builder.build();
707 assert!(
708 result.is_ok(),
709 "Expected successful build with valid exponential histogram parameters"
710 );
711
712 let builder = StreamBuilder::new()
714 .with_name("invalid_expo_histogram_size")
715 .with_aggregation(Aggregation::Base2ExponentialHistogram {
716 max_size: 0,
717 max_scale: 10,
718 record_min_max: true,
719 });
720
721 let result = builder.build();
722 assert!(result.is_err(), "Expected error for max_size = 0");
723 assert_eq!(
724 result.err().unwrap().to_string(),
725 "max_size must be greater than 0",
726 "Expected correct validation error for max_size = 0"
727 );
728
729 let builder = StreamBuilder::new()
731 .with_name("invalid_expo_histogram_scale_high")
732 .with_aggregation(Aggregation::Base2ExponentialHistogram {
733 max_size: 160,
734 max_scale: EXPO_MAX_SCALE + 1,
735 record_min_max: true,
736 });
737
738 let result = builder.build();
739 assert!(
740 result.is_err(),
741 "Expected error for max_scale > EXPO_MAX_SCALE"
742 );
743 assert_eq!(
744 result.err().unwrap().to_string(),
745 format!(
746 "max_scale must be between {} and {}",
747 EXPO_MIN_SCALE, EXPO_MAX_SCALE
748 ),
749 "Expected correct validation error for max_scale too high"
750 );
751
752 let builder = StreamBuilder::new()
754 .with_name("invalid_expo_histogram_scale_low")
755 .with_aggregation(Aggregation::Base2ExponentialHistogram {
756 max_size: 160,
757 max_scale: EXPO_MIN_SCALE - 1,
758 record_min_max: true,
759 });
760
761 let result = builder.build();
762 assert!(
763 result.is_err(),
764 "Expected error for max_scale < EXPO_MIN_SCALE"
765 );
766 assert_eq!(
767 result.err().unwrap().to_string(),
768 format!(
769 "max_scale must be between {} and {}",
770 EXPO_MIN_SCALE, EXPO_MAX_SCALE
771 ),
772 "Expected correct validation error for max_scale too low"
773 );
774
775 let builder = StreamBuilder::new()
777 .with_name("valid_expo_histogram_min_scale")
778 .with_aggregation(Aggregation::Base2ExponentialHistogram {
779 max_size: 160,
780 max_scale: EXPO_MIN_SCALE,
781 record_min_max: true,
782 });
783
784 let result = builder.build();
785 assert!(
786 result.is_ok(),
787 "Expected successful build with max_scale = EXPO_MIN_SCALE"
788 );
789
790 let builder = StreamBuilder::new()
791 .with_name("valid_expo_histogram_max_scale")
792 .with_aggregation(Aggregation::Base2ExponentialHistogram {
793 max_size: 160,
794 max_scale: EXPO_MAX_SCALE,
795 record_min_max: true,
796 });
797
798 let result = builder.build();
799 assert!(
800 result.is_ok(),
801 "Expected successful build with max_scale = EXPO_MAX_SCALE"
802 );
803 }
804}