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    /// *Note*: Calls to this method are additive, each call merges the provided
247    /// resource with the previous one.
248    ///
249    /// [Meter]: opentelemetry::metrics::Meter
250    pub fn with_resource(mut self, resource: Resource) -> Self {
251        self.resource = match self.resource {
252            Some(existing) => Some(existing.merge(&resource)),
253            None => Some(resource),
254        };
255
256        self
257    }
258
259    /// Associates a [MetricReader] with a [MeterProvider].
260    /// [`MeterProviderBuilder::with_periodic_exporter()] can be used to add a PeriodicReader which is
261    /// the most common use case.
262    ///
263    /// A [MeterProvider] will export no metrics without [MetricReader]
264    /// added.
265    pub fn with_reader<T: MetricReader>(mut self, reader: T) -> Self {
266        self.readers.push(Box::new(reader));
267        self
268    }
269
270    /// Adds a [`PushMetricExporter`] to the [`MeterProvider`] and configures it
271    /// to export metrics at **fixed** intervals (60 seconds) using a
272    /// [`PeriodicReader`].
273    ///
274    /// To customize the export interval, set the
275    /// **"OTEL_METRIC_EXPORT_INTERVAL"** environment variable (in
276    /// milliseconds).
277    ///
278    /// Most users should use this method to attach an exporter. Advanced users
279    /// who need finer control over the export process can use
280    /// [`crate::metrics::PeriodicReaderBuilder`] to configure a custom reader and attach it
281    /// using [`MeterProviderBuilder::with_reader()`].
282    pub fn with_periodic_exporter<T>(mut self, exporter: T) -> Self
283    where
284        T: PushMetricExporter,
285    {
286        let reader = PeriodicReader::builder(exporter).build();
287        self.readers.push(Box::new(reader));
288        self
289    }
290
291    /// Adds a view to the [MeterProvider].
292    ///
293    /// Views allow you to customize how metrics are aggregated, renamed, or
294    /// otherwise transformed before export, without modifying instrument
295    /// definitions in your application or library code.
296    ///
297    /// You can pass any function or closure matching the signature
298    /// `Fn(&Instrument) -> Option<Stream> + Send + Sync + 'static`. The
299    /// function receives a reference to the [`Instrument`] and can return an
300    /// [`Option`] of [`Stream`] to specify how matching instruments should be
301    /// exported. Returning `None` means the view does not apply to the given
302    /// instrument, and the default behavior will be used.
303    ///
304    ///
305    /// # Examples
306    ///
307    /// Renaming a metric:
308    ///
309    /// ```
310    /// # use opentelemetry_sdk::metrics::{Stream, Instrument};
311    /// let view_rename = |i: &Instrument| {
312    ///     if i.name() == "my_counter" {
313    ///         Some(Stream::builder().with_name("my_counter_renamed").build().expect("Stream should be valid"))
314    ///     } else {
315    ///         None
316    ///     }
317    /// };
318    /// # let builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder();
319    /// # let _builder =
320    /// builder.with_view(view_rename);
321    /// ```
322    ///
323    /// Setting a cardinality limit to control resource usage:
324    ///
325    /// ```
326    /// # use opentelemetry_sdk::metrics::{Stream, Instrument};
327    /// let view_change_cardinality = |i: &Instrument| {
328    ///     if i.name() == "my_counter" {
329    ///         Some(
330    ///             Stream::builder()
331    ///                 .with_cardinality_limit(100).build().expect("Stream should be valid"),
332    ///         )
333    ///     } else {
334    ///         None
335    ///     }
336    /// };
337    /// # let builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder();
338    /// # let _builder =
339    /// builder.with_view(view_change_cardinality);
340    /// ```
341    ///
342    /// Silently ignoring Stream build errors:
343    ///
344    /// ```
345    /// # use opentelemetry_sdk::metrics::{Stream, Instrument};
346    /// let my_view_change_cardinality = |i: &Instrument| {
347    ///     if i.name() == "my_second_histogram" {
348    ///         // Note: If Stream is invalid, build() will return `Error` variant.
349    ///         // By calling `.ok()`, any such error is ignored and treated as if the view does not match
350    ///         // the instrument.
351    ///         // If this is not the desired behavior, consider handling the error explicitly.
352    ///         Stream::builder().with_cardinality_limit(0).build().ok()
353    ///     } else {
354    ///         None
355    ///     }
356    /// };
357    /// # let builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder();
358    /// # let _builder =
359    /// builder.with_view(my_view_change_cardinality);
360    /// ```
361    ///
362    /// If no views are added, the [MeterProvider] uses the default view.
363    ///
364    /// [`Instrument`]: crate::metrics::Instrument
365    /// [`Stream`]: crate::metrics::Stream
366    /// [`Option`]: core::option::Option
367    pub fn with_view<T>(mut self, view: T) -> Self
368    where
369        T: Fn(&Instrument) -> Option<Stream> + Send + Sync + 'static,
370    {
371        self.views.push(Arc::new(view));
372        self
373    }
374
375    /// Construct a new [MeterProvider] with this configuration.
376    pub fn build(self) -> SdkMeterProvider {
377        otel_debug!(
378            name: "MeterProvider.Building",
379            builder = format!("{:?}", &self),
380        );
381
382        let meter_provider = SdkMeterProvider {
383            inner: Arc::new(SdkMeterProviderInner {
384                pipes: Arc::new(Pipelines::new(
385                    self.resource.unwrap_or(Resource::builder().build()),
386                    self.readers,
387                    self.views,
388                )),
389                meters: Default::default(),
390                shutdown_invoked: AtomicBool::new(false),
391            }),
392        };
393
394        otel_debug!(
395            name: "MeterProvider.Built",
396        );
397        meter_provider
398    }
399}
400
401impl fmt::Debug for MeterProviderBuilder {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        f.debug_struct("MeterProviderBuilder")
404            .field("resource", &self.resource)
405            .field("readers", &self.readers)
406            .field("views", &self.views.len())
407            .finish()
408    }
409}
410#[cfg(all(test, feature = "testing"))]
411mod tests {
412    use crate::error::OTelSdkError;
413    use crate::metrics::SdkMeterProvider;
414    use crate::resource::{
415        SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, TELEMETRY_SDK_VERSION,
416    };
417    use crate::testing::metrics::metric_reader::TestMetricReader;
418    use crate::Resource;
419    use opentelemetry::metrics::MeterProvider;
420    use opentelemetry::{global, InstrumentationScope};
421    use opentelemetry::{Key, KeyValue, Value};
422    use std::env;
423
424    #[test]
425    fn test_meter_provider_resource() {
426        let assert_resource = |provider: &super::SdkMeterProvider,
427                               resource_key: &'static str,
428                               expect: Option<&'static str>| {
429            assert_eq!(
430                provider.inner.pipes.0[0]
431                    .resource
432                    .get(&Key::from_static_str(resource_key))
433                    .map(|v| v.to_string()),
434                expect.map(|s| s.to_string())
435            );
436        };
437        let assert_telemetry_resource = |provider: &super::SdkMeterProvider| {
438            assert_eq!(
439                provider.inner.pipes.0[0]
440                    .resource
441                    .get(&TELEMETRY_SDK_LANGUAGE.into()),
442                Some(Value::from("rust"))
443            );
444            assert_eq!(
445                provider.inner.pipes.0[0]
446                    .resource
447                    .get(&TELEMETRY_SDK_NAME.into()),
448                Some(Value::from("opentelemetry"))
449            );
450            assert_eq!(
451                provider.inner.pipes.0[0]
452                    .resource
453                    .get(&TELEMETRY_SDK_VERSION.into()),
454                Some(Value::from(env!("CARGO_PKG_VERSION")))
455            );
456        };
457
458        // If users didn't provide a resource and there isn't a env var set. Use default one.
459        temp_env::with_var_unset("OTEL_RESOURCE_ATTRIBUTES", || {
460            let reader = TestMetricReader::new();
461            let default_meter_provider = super::SdkMeterProvider::builder()
462                .with_reader(reader)
463                .build();
464            assert_resource(
465                &default_meter_provider,
466                SERVICE_NAME,
467                Some("unknown_service"),
468            );
469            assert_telemetry_resource(&default_meter_provider);
470        });
471
472        // If user provided a resource, use that.
473        let reader2 = TestMetricReader::new();
474        let custom_meter_provider = super::SdkMeterProvider::builder()
475            .with_reader(reader2)
476            .with_resource(
477                Resource::builder_empty()
478                    .with_service_name("test_service")
479                    .build(),
480            )
481            .build();
482        assert_resource(&custom_meter_provider, SERVICE_NAME, Some("test_service"));
483        assert_eq!(custom_meter_provider.inner.pipes.0[0].resource.len(), 1);
484
485        temp_env::with_var(
486            "OTEL_RESOURCE_ATTRIBUTES",
487            Some("key1=value1, k2, k3=value2"),
488            || {
489                // If `OTEL_RESOURCE_ATTRIBUTES` is set, read them automatically
490                let reader3 = TestMetricReader::new();
491                let env_resource_provider = super::SdkMeterProvider::builder()
492                    .with_reader(reader3)
493                    .build();
494                assert_resource(
495                    &env_resource_provider,
496                    SERVICE_NAME,
497                    Some("unknown_service"),
498                );
499                assert_resource(&env_resource_provider, "key1", Some("value1"));
500                assert_resource(&env_resource_provider, "k3", Some("value2"));
501                assert_telemetry_resource(&env_resource_provider);
502                assert_eq!(env_resource_provider.inner.pipes.0[0].resource.len(), 6);
503            },
504        );
505
506        // When `OTEL_RESOURCE_ATTRIBUTES` is set and also user provided config
507        temp_env::with_var(
508            "OTEL_RESOURCE_ATTRIBUTES",
509            Some("my-custom-key=env-val,k2=value2"),
510            || {
511                let reader4 = TestMetricReader::new();
512                let user_provided_resource_config_provider = super::SdkMeterProvider::builder()
513                    .with_reader(reader4)
514                    .with_resource(
515                        Resource::builder()
516                            .with_attributes([
517                                KeyValue::new("my-custom-key", "my-custom-value"),
518                                KeyValue::new("my-custom-key2", "my-custom-value2"),
519                            ])
520                            .build(),
521                    )
522                    .build();
523                assert_resource(
524                    &user_provided_resource_config_provider,
525                    SERVICE_NAME,
526                    Some("unknown_service"),
527                );
528                assert_resource(
529                    &user_provided_resource_config_provider,
530                    "my-custom-key",
531                    Some("my-custom-value"),
532                );
533                assert_resource(
534                    &user_provided_resource_config_provider,
535                    "my-custom-key2",
536                    Some("my-custom-value2"),
537                );
538                assert_resource(
539                    &user_provided_resource_config_provider,
540                    "k2",
541                    Some("value2"),
542                );
543                assert_telemetry_resource(&user_provided_resource_config_provider);
544                assert_eq!(
545                    user_provided_resource_config_provider.inner.pipes.0[0]
546                        .resource
547                        .len(),
548                    7
549                );
550            },
551        );
552
553        // If user provided a resource, it takes priority during collision.
554        let reader5 = TestMetricReader::new();
555        let no_service_name = super::SdkMeterProvider::builder()
556            .with_reader(reader5)
557            .with_resource(Resource::empty())
558            .build();
559
560        assert_eq!(no_service_name.inner.pipes.0[0].resource.len(), 0)
561    }
562
563    #[test]
564    fn test_meter_provider_shutdown() {
565        let reader = TestMetricReader::new();
566        let provider = super::SdkMeterProvider::builder()
567            .with_reader(reader.clone())
568            .build();
569        global::set_meter_provider(provider.clone());
570        assert!(!reader.is_shutdown());
571        // create a meter and an instrument
572        let meter = global::meter("test");
573        let counter = meter.u64_counter("test_counter").build();
574        // no need to drop a meter for meter_provider shutdown
575        let shutdown_res = provider.shutdown();
576        assert!(shutdown_res.is_ok());
577
578        // shutdown once more should return an error
579        let shutdown_res = provider.shutdown();
580        assert!(matches!(shutdown_res, Err(OTelSdkError::AlreadyShutdown)));
581
582        assert!(shutdown_res.is_err());
583        assert!(reader.is_shutdown());
584        // TODO Fix: the instrument is still available, and can be used.
585        // While the reader is shutdown, and no collect is happening
586        counter.add(1, &[]);
587    }
588    #[test]
589    fn test_shutdown_invoked_on_last_drop() {
590        let reader = TestMetricReader::new();
591        let provider = super::SdkMeterProvider::builder()
592            .with_reader(reader.clone())
593            .build();
594        let clone1 = provider.clone();
595        let clone2 = provider.clone();
596
597        // Initially, shutdown should not be called
598        assert!(!reader.is_shutdown());
599
600        // Drop the first clone
601        drop(clone1);
602        assert!(!reader.is_shutdown());
603
604        // Drop the second clone
605        drop(clone2);
606        assert!(!reader.is_shutdown());
607
608        // Drop the last original provider
609        drop(provider);
610        // Now the shutdown should be invoked
611        assert!(reader.is_shutdown());
612    }
613
614    #[test]
615    fn same_meter_reused_same_scope() {
616        let provider = super::SdkMeterProvider::builder().build();
617        let _meter1 = provider.meter("test");
618        let _meter2 = provider.meter("test");
619        assert_eq!(provider.inner.meters.lock().unwrap().len(), 1);
620
621        let scope = InstrumentationScope::builder("test")
622            .with_version("1.0.0")
623            .with_schema_url("http://example.com")
624            .build();
625
626        let _meter3 = provider.meter_with_scope(scope.clone());
627        let _meter4 = provider.meter_with_scope(scope.clone());
628        let _meter5 = provider.meter_with_scope(scope);
629        assert_eq!(provider.inner.meters.lock().unwrap().len(), 2);
630
631        // these are different meters because meter names are case sensitive
632        let make_scope = |name| {
633            InstrumentationScope::builder(name)
634                .with_version("1.0.0")
635                .with_schema_url("http://example.com")
636                .build()
637        };
638
639        let _meter6 = provider.meter_with_scope(make_scope("ABC"));
640        let _meter7 = provider.meter_with_scope(make_scope("Abc"));
641        let _meter8 = provider.meter_with_scope(make_scope("abc"));
642
643        assert_eq!(provider.inner.meters.lock().unwrap().len(), 5);
644    }
645
646    #[test]
647    fn same_meter_reused_same_scope_attributes() {
648        let meter_provider = super::SdkMeterProvider::builder().build();
649        let make_scope = |attributes| {
650            InstrumentationScope::builder("test.meter")
651                .with_version("v0.1.0")
652                .with_schema_url("http://example.com")
653                .with_attributes(attributes)
654                .build()
655        };
656
657        let _meter1 =
658            meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")]));
659        let _meter2 =
660            meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")]));
661
662        assert_eq!(meter_provider.inner.meters.lock().unwrap().len(), 1);
663
664        // these are identical because InstrumentScope ignores the order of attributes
665        let _meter3 = meter_provider.meter_with_scope(make_scope(vec![
666            KeyValue::new("key1", "value1"),
667            KeyValue::new("key2", "value2"),
668        ]));
669        let _meter4 = meter_provider.meter_with_scope(make_scope(vec![
670            KeyValue::new("key2", "value2"),
671            KeyValue::new("key1", "value1"),
672        ]));
673
674        assert_eq!(meter_provider.inner.meters.lock().unwrap().len(), 2);
675    }
676
677    #[test]
678    fn different_meter_different_attributes() {
679        let meter_provider = super::SdkMeterProvider::builder().build();
680        let make_scope = |attributes| {
681            InstrumentationScope::builder("test.meter")
682                .with_version("v0.1.0")
683                .with_schema_url("http://example.com")
684                .with_attributes(attributes)
685                .build()
686        };
687
688        let _meter1 = meter_provider.meter_with_scope(make_scope(vec![]));
689        // _meter2 and _meter3, and _meter4 are different because attribute is case sensitive
690        let _meter2 =
691            meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key1", "value1")]));
692        let _meter3 =
693            meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("Key1", "value1")]));
694        let _meter4 =
695            meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key1", "Value1")]));
696        let _meter5 = meter_provider.meter_with_scope(make_scope(vec![
697            KeyValue::new("key1", "value1"),
698            KeyValue::new("key2", "value2"),
699        ]));
700
701        assert_eq!(meter_provider.inner.meters.lock().unwrap().len(), 5);
702    }
703
704    #[test]
705    fn with_resource_multiple_calls_ensure_additive() {
706        let builder = SdkMeterProvider::builder()
707            .with_resource(Resource::new(vec![KeyValue::new("key1", "value1")]))
708            .with_resource(Resource::new(vec![KeyValue::new("key2", "value2")]))
709            .with_resource(
710                Resource::builder_empty()
711                    .with_schema_url(vec![], "http://example.com")
712                    .build(),
713            )
714            .with_resource(Resource::new(vec![KeyValue::new("key3", "value3")]));
715
716        let resource = builder.resource.unwrap();
717
718        assert_eq!(
719            resource.get(&Key::from_static_str("key1")),
720            Some(Value::from("value1"))
721        );
722        assert_eq!(
723            resource.get(&Key::from_static_str("key2")),
724            Some(Value::from("value2"))
725        );
726        assert_eq!(
727            resource.get(&Key::from_static_str("key3")),
728            Some(Value::from("value3"))
729        );
730        assert_eq!(resource.schema_url(), Some("http://example.com"));
731    }
732}