Skip to main content

opentelemetry_sdk/metrics/
meter_provider.rs

1use core::fmt;
2use opentelemetry::{
3    metrics::{Meter, MeterProvider},
4    otel_debug, otel_error, otel_info, InstrumentationScope,
5};
6use std::time::Duration;
7use std::{
8    collections::HashMap,
9    sync::{
10        atomic::{AtomicBool, Ordering},
11        Arc, Mutex,
12    },
13};
14
15use crate::error::OTelSdkResult;
16use crate::Resource;
17
18use super::{
19    exporter::PushMetricExporter, meter::SdkMeter, noop::NoopMeter,
20    periodic_reader::PeriodicReader, pipeline::Pipelines, reader::MetricReader, view::View,
21    Instrument, Stream,
22};
23
24/// Handles the creation and coordination of [Meter]s.
25///
26/// All `Meter`s created by a `MeterProvider` will be associated with the same
27/// [Resource], have the same views applied to them, and have their produced
28/// metric telemetry passed to the configured `MetricReader`s. This is a
29/// clonable handle to the MeterProvider implementation itself, and cloning it
30/// will create a new reference, not a new instance of a MeterProvider. Dropping
31/// the last reference to it will trigger shutdown of the provider. Shutdown can
32/// also be triggered manually by calling the `shutdown` method.
33/// [Meter]: opentelemetry::metrics::Meter
34#[derive(Clone, Debug)]
35pub struct SdkMeterProvider {
36    inner: Arc<SdkMeterProviderInner>,
37}
38
39#[derive(Debug)]
40struct SdkMeterProviderInner {
41    pipes: Arc<Pipelines>,
42    meters: Mutex<HashMap<InstrumentationScope, Arc<SdkMeter>>>,
43    shutdown_invoked: AtomicBool,
44}
45
46impl Default for SdkMeterProvider {
47    fn default() -> Self {
48        SdkMeterProvider::builder().build()
49    }
50}
51
52impl SdkMeterProvider {
53    /// Return default [MeterProviderBuilder]
54    pub fn builder() -> MeterProviderBuilder {
55        MeterProviderBuilder::default()
56    }
57
58    /// Flushes all pending telemetry.
59    ///
60    /// There is no guaranteed that all telemetry be flushed or all resources have
61    /// been released on error.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use opentelemetry::{global, Context};
67    /// use opentelemetry_sdk::metrics::SdkMeterProvider;
68    ///
69    /// fn init_metrics() -> SdkMeterProvider {
70    ///     // Setup metric pipelines with readers + views, default has no
71    ///     // readers so nothing is exported.
72    ///     let provider = SdkMeterProvider::default();
73    ///
74    ///     // Set provider to be used as global meter provider
75    ///     let _ = global::set_meter_provider(provider.clone());
76    ///
77    ///     provider
78    /// }
79    ///
80    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
81    ///     let provider = init_metrics();
82    ///
83    ///     // create instruments + record measurements
84    ///
85    ///     // force all instruments to flush
86    ///     provider.force_flush()?;
87    ///
88    ///     // record more measurements..
89    ///
90    ///     // shutdown ensures any cleanup required by the provider is done,
91    ///     // and also invokes shutdown on the readers.
92    ///     provider.shutdown()?;
93    ///
94    ///     Ok(())
95    /// }
96    /// ```
97    pub fn force_flush(&self) -> OTelSdkResult {
98        self.inner.force_flush()
99    }
100
101    /// Shuts down the meter provider flushing all pending telemetry and releasing
102    /// any held computational resources.
103    ///
104    /// This call is idempotent. The first call will perform all flush and releasing
105    /// operations. Subsequent calls will perform no action and will return an error
106    /// stating this.
107    ///
108    /// Measurements made by instruments from meters this MeterProvider created will
109    /// not be exported after Shutdown is called.
110    ///
111    /// There is no guaranteed that all telemetry be flushed or all resources have
112    /// been released on error.
113    pub fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult {
114        otel_debug!(
115            name: "MeterProvider.Shutdown",
116            message = "User initiated shutdown of MeterProvider."
117        );
118        self.inner.shutdown()
119    }
120
121    /// shutdown with default timeout
122    pub fn shutdown(&self) -> OTelSdkResult {
123        self.shutdown_with_timeout(Duration::from_secs(5))
124    }
125}
126
127impl SdkMeterProviderInner {
128    fn force_flush(&self) -> OTelSdkResult {
129        if self
130            .shutdown_invoked
131            .load(std::sync::atomic::Ordering::Relaxed)
132        {
133            Err(crate::error::OTelSdkError::AlreadyShutdown)
134        } else {
135            self.pipes.force_flush()
136        }
137    }
138
139    fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult {
140        if self
141            .shutdown_invoked
142            .swap(true, std::sync::atomic::Ordering::SeqCst)
143        {
144            // If the previous value was true, shutdown was already invoked.
145            Err(crate::error::OTelSdkError::AlreadyShutdown)
146        } else {
147            self.pipes.shutdown()
148        }
149    }
150
151    fn shutdown(&self) -> OTelSdkResult {
152        self.shutdown_with_timeout(Duration::from_secs(5))
153    }
154}
155
156impl Drop for SdkMeterProviderInner {
157    fn drop(&mut self) {
158        // If user has already shutdown the provider manually by calling
159        // shutdown(), then we don't need to call shutdown again.
160        if self.shutdown_invoked.load(Ordering::Relaxed) {
161            otel_debug!(
162                name: "MeterProvider.Drop.AlreadyShutdown",
163                message = "MeterProvider was already shut down; drop will not attempt shutdown again."
164            );
165        } else {
166            otel_info!(
167                name: "MeterProvider.Drop",
168                message = "Last reference of MeterProvider dropped, initiating shutdown."
169            );
170            if let Err(err) = self.shutdown() {
171                otel_error!(
172                    name: "MeterProvider.Drop.ShutdownFailed",
173                    message = "Shutdown attempt failed during drop of MeterProvider.",
174                    reason = format!("{}", err)
175                );
176            } else {
177                otel_debug!(
178                    name: "MeterProvider.Drop.ShutdownCompleted",
179                );
180            }
181        }
182    }
183}
184
185impl MeterProvider for SdkMeterProvider {
186    fn meter(&self, name: &'static str) -> Meter {
187        let scope = InstrumentationScope::builder(name).build();
188        self.meter_with_scope(scope)
189    }
190
191    fn meter_with_scope(&self, scope: InstrumentationScope) -> Meter {
192        if self.inner.shutdown_invoked.load(Ordering::Relaxed) {
193            otel_debug!(
194                name: "MeterProvider.NoOpMeterReturned",
195                meter_name = scope.name(),
196            );
197            return Meter::new(Arc::new(NoopMeter::new()));
198        }
199
200        if scope.name().is_empty() {
201            otel_info!(name: "MeterNameEmpty", message = "Meter name is empty; consider providing a meaningful name. Meter will function normally and the provided name will be used as-is.");
202        };
203
204        if let Ok(mut meters) = self.inner.meters.lock() {
205            if let Some(existing_meter) = meters.get(&scope) {
206                otel_debug!(
207                    name: "MeterProvider.ExistingMeterReturned",
208                    meter_name = scope.name(),
209                );
210                Meter::new(existing_meter.clone())
211            } else {
212                let new_meter = Arc::new(SdkMeter::new(scope.clone(), self.inner.pipes.clone()));
213                meters.insert(scope.clone(), new_meter.clone());
214                otel_debug!(
215                    name: "MeterProvider.NewMeterCreated",
216                    meter_name = scope.name(),
217                );
218                Meter::new(new_meter)
219            }
220        } else {
221            otel_debug!(
222                name: "MeterProvider.NoOpMeterReturned",
223                meter_name = scope.name(),
224            );
225            Meter::new(Arc::new(NoopMeter::new()))
226        }
227    }
228}
229
230/// Configuration options for a [MeterProvider].
231#[derive(Default)]
232pub struct MeterProviderBuilder {
233    resource: Option<Resource>,
234    readers: Vec<Box<dyn MetricReader>>,
235    views: Vec<Arc<dyn View>>,
236}
237
238impl MeterProviderBuilder {
239    /// Associates a [Resource] with a [MeterProvider].
240    ///
241    /// This [Resource] represents the entity producing telemetry and is associated
242    /// with all [Meter]s the [MeterProvider] will create.
243    ///
244    /// By default, if this option is not used, the default [Resource] will be used.
245    ///
246    /// When constructing a [Resource], use [`Resource::builder()`] to preserve
247    /// SDK-provided defaults such as `telemetry.sdk.*` and `service.name`.
248    /// Using [`Resource::builder_empty()`] will **not** include these attributes.
249    ///
250    /// # Example
251    ///
252    /// ```
253    /// use opentelemetry_sdk::{Resource, metrics::SdkMeterProvider};
254    /// use opentelemetry::KeyValue;
255    ///
256    /// let provider = SdkMeterProvider::builder()
257    ///     .with_resource(
258    ///         Resource::builder()
259    ///             .with_service_name("my-service")
260    ///             .with_attributes([KeyValue::new("deployment.environment.name", "production")])
261    ///             .build(),
262    ///     )
263    ///     .build();
264    /// ```
265    ///
266    /// *Note*: Calls to this method are additive, each call merges the provided
267    /// resource with the previous one.
268    ///
269    /// [Meter]: opentelemetry::metrics::Meter
270    /// [`Resource::builder()`]: Resource::builder
271    /// [`Resource::builder_empty()`]: Resource::builder_empty
272    pub fn with_resource(mut self, resource: Resource) -> Self {
273        self.resource = match self.resource {
274            Some(existing) => Some(existing.merge(&resource)),
275            None => Some(resource),
276        };
277
278        self
279    }
280
281    /// Associates a `MetricReader` with a [MeterProvider].
282    /// [`MeterProviderBuilder::with_periodic_exporter()`] can be used to add a PeriodicReader which is
283    /// the most common use case.
284    ///
285    /// A [MeterProvider] will export no metrics without a `MetricReader`
286    /// added.
287    pub fn with_reader<T: MetricReader>(mut self, reader: T) -> Self {
288        self.readers.push(Box::new(reader));
289        self
290    }
291
292    /// Adds a [`PushMetricExporter`] to the [`MeterProvider`] and configures it
293    /// to export metrics at **fixed** intervals (60 seconds) using a
294    /// [`PeriodicReader`].
295    ///
296    /// To customize the export interval, set the
297    /// **"OTEL_METRIC_EXPORT_INTERVAL"** environment variable (in
298    /// milliseconds).
299    ///
300    /// Most users should use this method to attach an exporter. Advanced users
301    /// who need finer control over the export process can use
302    /// [`crate::metrics::PeriodicReaderBuilder`] to configure a custom reader and attach it
303    /// using [`MeterProviderBuilder::with_reader()`].
304    pub fn with_periodic_exporter<T>(mut self, exporter: T) -> Self
305    where
306        T: PushMetricExporter,
307    {
308        let reader = PeriodicReader::builder(exporter).build();
309        self.readers.push(Box::new(reader));
310        self
311    }
312
313    /// Adds a view to the [MeterProvider].
314    ///
315    /// Views allow you to customize how metrics are aggregated, renamed, or
316    /// otherwise transformed before export, without modifying instrument
317    /// definitions in your application or library code.
318    ///
319    /// You can pass any function or closure matching the signature
320    /// `Fn(&Instrument) -> Option<Stream> + Send + Sync + 'static`. The
321    /// function receives a reference to the [`Instrument`] and can return an
322    /// [`Option`] of [`Stream`] to specify how matching instruments should be
323    /// exported. Returning `None` means the view does not apply to the given
324    /// instrument, and the default behavior will be used.
325    ///
326    ///
327    /// # Examples
328    ///
329    /// Renaming a metric:
330    ///
331    /// ```
332    /// # use opentelemetry_sdk::metrics::{Stream, Instrument};
333    /// let view_rename = |i: &Instrument| {
334    ///     if i.name() == "my_counter" {
335    ///         Some(Stream::builder().with_name("my_counter_renamed").build().expect("Stream should be valid"))
336    ///     } else {
337    ///         None
338    ///     }
339    /// };
340    /// # let builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder();
341    /// # let _builder =
342    /// builder.with_view(view_rename);
343    /// ```
344    ///
345    /// Setting a cardinality limit to control resource usage:
346    ///
347    /// ```
348    /// # use opentelemetry_sdk::metrics::{Stream, Instrument};
349    /// let view_change_cardinality = |i: &Instrument| {
350    ///     if i.name() == "my_counter" {
351    ///         Some(
352    ///             Stream::builder()
353    ///                 .with_cardinality_limit(100).build().expect("Stream should be valid"),
354    ///         )
355    ///     } else {
356    ///         None
357    ///     }
358    /// };
359    /// # let builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder();
360    /// # let _builder =
361    /// builder.with_view(view_change_cardinality);
362    /// ```
363    ///
364    /// Silently ignoring Stream build errors:
365    ///
366    /// ```
367    /// # use opentelemetry_sdk::metrics::{Stream, Instrument};
368    /// let my_view_change_cardinality = |i: &Instrument| {
369    ///     if i.name() == "my_second_histogram" {
370    ///         // Note: If Stream is invalid, build() will return `Error` variant.
371    ///         // By calling `.ok()`, any such error is ignored and treated as if the view does not match
372    ///         // the instrument.
373    ///         // If this is not the desired behavior, consider handling the error explicitly.
374    ///         Stream::builder().with_cardinality_limit(0).build().ok()
375    ///     } else {
376    ///         None
377    ///     }
378    /// };
379    /// # let builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder();
380    /// # let _builder =
381    /// builder.with_view(my_view_change_cardinality);
382    /// ```
383    ///
384    /// If no views are added, the [MeterProvider] uses the default view.
385    ///
386    /// [`Instrument`]: crate::metrics::Instrument
387    /// [`Stream`]: crate::metrics::Stream
388    /// [`Option`]: core::option::Option
389    pub fn with_view<T>(mut self, view: T) -> Self
390    where
391        T: Fn(&Instrument) -> Option<Stream> + Send + Sync + 'static,
392    {
393        self.views.push(Arc::new(view));
394        self
395    }
396
397    /// Construct a new [MeterProvider] with this configuration.
398    pub fn build(self) -> SdkMeterProvider {
399        otel_debug!(
400            name: "MeterProvider.Building",
401            builder = format!("{:?}", &self),
402        );
403
404        let meter_provider = SdkMeterProvider {
405            inner: Arc::new(SdkMeterProviderInner {
406                pipes: Arc::new(Pipelines::new(
407                    self.resource.unwrap_or(Resource::builder().build()),
408                    self.readers,
409                    self.views,
410                )),
411                meters: Default::default(),
412                shutdown_invoked: AtomicBool::new(false),
413            }),
414        };
415
416        otel_debug!(
417            name: "MeterProvider.Built",
418        );
419        meter_provider
420    }
421}
422
423impl fmt::Debug for MeterProviderBuilder {
424    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425        f.debug_struct("MeterProviderBuilder")
426            .field("resource", &self.resource)
427            .field("readers", &self.readers)
428            .field("views", &self.views.len())
429            .finish()
430    }
431}
432#[cfg(all(test, feature = "testing"))]
433mod tests {
434    use crate::error::OTelSdkError;
435    use crate::metrics::SdkMeterProvider;
436    use crate::resource::{
437        SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, TELEMETRY_SDK_VERSION,
438    };
439    use crate::testing::metrics::metric_reader::TestMetricReader;
440    use crate::Resource;
441    use opentelemetry::metrics::MeterProvider;
442    use opentelemetry::{global, InstrumentationScope};
443    use opentelemetry::{Key, KeyValue, Value};
444    use std::env;
445
446    #[test]
447    fn test_meter_provider_resource() {
448        let assert_resource = |provider: &super::SdkMeterProvider,
449                               resource_key: &'static str,
450                               expect: Option<&'static str>| {
451            assert_eq!(
452                provider.inner.pipes.0[0]
453                    .resource
454                    .get(&Key::from_static_str(resource_key))
455                    .map(|v| v.to_string()),
456                expect.map(|s| s.to_string())
457            );
458        };
459        let assert_telemetry_resource = |provider: &super::SdkMeterProvider| {
460            assert_eq!(
461                provider.inner.pipes.0[0]
462                    .resource
463                    .get(&TELEMETRY_SDK_LANGUAGE.into()),
464                Some(Value::from("rust"))
465            );
466            assert_eq!(
467                provider.inner.pipes.0[0]
468                    .resource
469                    .get(&TELEMETRY_SDK_NAME.into()),
470                Some(Value::from("opentelemetry"))
471            );
472            assert_eq!(
473                provider.inner.pipes.0[0]
474                    .resource
475                    .get(&TELEMETRY_SDK_VERSION.into()),
476                Some(Value::from(env!("CARGO_PKG_VERSION")))
477            );
478        };
479
480        // If users didn't provide a resource and there isn't a env var set. Use default one.
481        temp_env::with_var_unset("OTEL_RESOURCE_ATTRIBUTES", || {
482            let reader = TestMetricReader::new();
483            let default_meter_provider = super::SdkMeterProvider::builder()
484                .with_reader(reader)
485                .build();
486            let service_name = default_meter_provider.inner.pipes.0[0]
487                .resource
488                .get(&Key::from_static_str(SERVICE_NAME))
489                .map(|v| v.to_string())
490                .unwrap();
491            assert!(
492                service_name.starts_with("unknown_service:opentelemetry_sdk-"),
493                "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}",
494                service_name
495            );
496            assert_telemetry_resource(&default_meter_provider);
497        });
498
499        // If user provided a resource, use that.
500        let reader2 = TestMetricReader::new();
501        let custom_meter_provider = super::SdkMeterProvider::builder()
502            .with_reader(reader2)
503            .with_resource(
504                Resource::builder_empty()
505                    .with_service_name("test_service")
506                    .build(),
507            )
508            .build();
509        assert_resource(&custom_meter_provider, SERVICE_NAME, Some("test_service"));
510        assert_eq!(custom_meter_provider.inner.pipes.0[0].resource.len(), 1);
511
512        temp_env::with_var(
513            "OTEL_RESOURCE_ATTRIBUTES",
514            Some("key1=value1, k2, k3=value2"),
515            || {
516                // If `OTEL_RESOURCE_ATTRIBUTES` is set, read them automatically
517                let reader3 = TestMetricReader::new();
518                let env_resource_provider = super::SdkMeterProvider::builder()
519                    .with_reader(reader3)
520                    .build();
521                let service_name = env_resource_provider.inner.pipes.0[0]
522                    .resource
523                    .get(&Key::from_static_str(SERVICE_NAME))
524                    .map(|v| v.to_string())
525                    .unwrap();
526                assert!(
527                    service_name.starts_with("unknown_service:opentelemetry_sdk-"),
528                    "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}",
529                    service_name
530                );
531                assert_resource(&env_resource_provider, "key1", Some("value1"));
532                assert_resource(&env_resource_provider, "k3", Some("value2"));
533                assert_telemetry_resource(&env_resource_provider);
534                assert_eq!(env_resource_provider.inner.pipes.0[0].resource.len(), 6);
535            },
536        );
537
538        // When `OTEL_RESOURCE_ATTRIBUTES` is set and also user provided config
539        temp_env::with_var(
540            "OTEL_RESOURCE_ATTRIBUTES",
541            Some("my-custom-key=env-val,k2=value2"),
542            || {
543                let reader4 = TestMetricReader::new();
544                let user_provided_resource_config_provider = super::SdkMeterProvider::builder()
545                    .with_reader(reader4)
546                    .with_resource(
547                        Resource::builder()
548                            .with_attributes([
549                                KeyValue::new("my-custom-key", "my-custom-value"),
550                                KeyValue::new("my-custom-key2", "my-custom-value2"),
551                            ])
552                            .build(),
553                    )
554                    .build();
555                let service_name = user_provided_resource_config_provider.inner.pipes.0[0]
556                    .resource
557                    .get(&Key::from_static_str(SERVICE_NAME))
558                    .map(|v| v.to_string())
559                    .unwrap();
560                assert!(
561                    service_name.starts_with("unknown_service:opentelemetry_sdk-"),
562                    "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}",
563                    service_name
564                );
565                assert_resource(
566                    &user_provided_resource_config_provider,
567                    "my-custom-key",
568                    Some("my-custom-value"),
569                );
570                assert_resource(
571                    &user_provided_resource_config_provider,
572                    "my-custom-key2",
573                    Some("my-custom-value2"),
574                );
575                assert_resource(
576                    &user_provided_resource_config_provider,
577                    "k2",
578                    Some("value2"),
579                );
580                assert_telemetry_resource(&user_provided_resource_config_provider);
581                assert_eq!(
582                    user_provided_resource_config_provider.inner.pipes.0[0]
583                        .resource
584                        .len(),
585                    7
586                );
587            },
588        );
589
590        // If user provided a resource, it takes priority during collision.
591        let reader5 = TestMetricReader::new();
592        let no_service_name = super::SdkMeterProvider::builder()
593            .with_reader(reader5)
594            .with_resource(Resource::empty())
595            .build();
596
597        assert_eq!(no_service_name.inner.pipes.0[0].resource.len(), 0)
598    }
599
600    #[test]
601    fn test_meter_provider_shutdown() {
602        let reader = TestMetricReader::new();
603        let provider = super::SdkMeterProvider::builder()
604            .with_reader(reader.clone())
605            .build();
606        global::set_meter_provider(provider.clone());
607        assert!(!reader.is_shutdown());
608        // create a meter and an instrument
609        let meter = global::meter("test");
610        let counter = meter.u64_counter("test_counter").build();
611        // no need to drop a meter for meter_provider shutdown
612        let shutdown_res = provider.shutdown();
613        assert!(shutdown_res.is_ok());
614
615        // shutdown once more should return an error
616        let shutdown_res = provider.shutdown();
617        assert!(matches!(shutdown_res, Err(OTelSdkError::AlreadyShutdown)));
618
619        assert!(shutdown_res.is_err());
620        assert!(reader.is_shutdown());
621        // TODO Fix: the instrument is still available, and can be used.
622        // While the reader is shutdown, and no collect is happening
623        counter.add(1, &[]);
624    }
625    #[test]
626    fn test_shutdown_invoked_on_last_drop() {
627        let reader = TestMetricReader::new();
628        let provider = super::SdkMeterProvider::builder()
629            .with_reader(reader.clone())
630            .build();
631        let clone1 = provider.clone();
632        let clone2 = provider.clone();
633
634        // Initially, shutdown should not be called
635        assert!(!reader.is_shutdown());
636
637        // Drop the first clone
638        drop(clone1);
639        assert!(!reader.is_shutdown());
640
641        // Drop the second clone
642        drop(clone2);
643        assert!(!reader.is_shutdown());
644
645        // Drop the last original provider
646        drop(provider);
647        // Now the shutdown should be invoked
648        assert!(reader.is_shutdown());
649    }
650
651    #[test]
652    fn same_meter_reused_same_scope() {
653        let provider = super::SdkMeterProvider::builder().build();
654        let _meter1 = provider.meter("test");
655        let _meter2 = provider.meter("test");
656        assert_eq!(provider.inner.meters.lock().unwrap().len(), 1);
657
658        let scope = InstrumentationScope::builder("test")
659            .with_version("1.0.0")
660            .with_schema_url("http://example.com")
661            .build();
662
663        let _meter3 = provider.meter_with_scope(scope.clone());
664        let _meter4 = provider.meter_with_scope(scope.clone());
665        let _meter5 = provider.meter_with_scope(scope);
666        assert_eq!(provider.inner.meters.lock().unwrap().len(), 2);
667
668        // these are different meters because meter names are case sensitive
669        let make_scope = |name| {
670            InstrumentationScope::builder(name)
671                .with_version("1.0.0")
672                .with_schema_url("http://example.com")
673                .build()
674        };
675
676        let _meter6 = provider.meter_with_scope(make_scope("ABC"));
677        let _meter7 = provider.meter_with_scope(make_scope("Abc"));
678        let _meter8 = provider.meter_with_scope(make_scope("abc"));
679
680        assert_eq!(provider.inner.meters.lock().unwrap().len(), 5);
681    }
682
683    #[test]
684    fn same_meter_reused_same_scope_attributes() {
685        let meter_provider = super::SdkMeterProvider::builder().build();
686        let make_scope = |attributes| {
687            InstrumentationScope::builder("test.meter")
688                .with_version("v0.1.0")
689                .with_schema_url("http://example.com")
690                .with_attributes(attributes)
691                .build()
692        };
693
694        let _meter1 =
695            meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")]));
696        let _meter2 =
697            meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")]));
698
699        assert_eq!(meter_provider.inner.meters.lock().unwrap().len(), 1);
700
701        // these are identical because InstrumentScope ignores the order of attributes
702        let _meter3 = meter_provider.meter_with_scope(make_scope(vec![
703            KeyValue::new("key1", "value1"),
704            KeyValue::new("key2", "value2"),
705        ]));
706        let _meter4 = meter_provider.meter_with_scope(make_scope(vec![
707            KeyValue::new("key2", "value2"),
708            KeyValue::new("key1", "value1"),
709        ]));
710
711        assert_eq!(meter_provider.inner.meters.lock().unwrap().len(), 2);
712    }
713
714    #[test]
715    fn different_meter_different_attributes() {
716        let meter_provider = super::SdkMeterProvider::builder().build();
717        let make_scope = |attributes| {
718            InstrumentationScope::builder("test.meter")
719                .with_version("v0.1.0")
720                .with_schema_url("http://example.com")
721                .with_attributes(attributes)
722                .build()
723        };
724
725        let _meter1 = meter_provider.meter_with_scope(make_scope(vec![]));
726        // _meter2 and _meter3, and _meter4 are different because attribute is case sensitive
727        let _meter2 =
728            meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key1", "value1")]));
729        let _meter3 =
730            meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("Key1", "value1")]));
731        let _meter4 =
732            meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key1", "Value1")]));
733        let _meter5 = meter_provider.meter_with_scope(make_scope(vec![
734            KeyValue::new("key1", "value1"),
735            KeyValue::new("key2", "value2"),
736        ]));
737
738        assert_eq!(meter_provider.inner.meters.lock().unwrap().len(), 5);
739    }
740
741    #[test]
742    fn with_resource_multiple_calls_ensure_additive() {
743        let builder = SdkMeterProvider::builder()
744            .with_resource(
745                Resource::builder_empty()
746                    .with_attributes(vec![KeyValue::new("key1", "value1")])
747                    .build(),
748            )
749            .with_resource(
750                Resource::builder_empty()
751                    .with_attributes(vec![KeyValue::new("key2", "value2")])
752                    .build(),
753            )
754            .with_resource(
755                Resource::builder_empty()
756                    .with_schema_url(vec![], "http://example.com")
757                    .build(),
758            )
759            .with_resource(
760                Resource::builder_empty()
761                    .with_attributes(vec![KeyValue::new("key3", "value3")])
762                    .build(),
763            );
764
765        let resource = builder.resource.unwrap();
766
767        assert_eq!(
768            resource.get(&Key::from_static_str("key1")),
769            Some(Value::from("value1"))
770        );
771        assert_eq!(
772            resource.get(&Key::from_static_str("key2")),
773            Some(Value::from("value2"))
774        );
775        assert_eq!(
776            resource.get(&Key::from_static_str("key3")),
777            Some(Value::from("value3"))
778        );
779        assert_eq!(resource.schema_url(), Some("http://example.com"));
780    }
781}