Skip to main content

opentelemetry_sdk/metrics/
periodic_reader.rs

1use std::{
2    env, fmt,
3    sync::{
4        mpsc::{self, Receiver, Sender},
5        Arc, Mutex, Weak,
6    },
7    thread,
8    time::{Duration, Instant},
9};
10
11use opentelemetry::{otel_debug, otel_error, otel_info, otel_warn, Context};
12
13use crate::{
14    error::{OTelSdkError, OTelSdkResult},
15    metrics::{exporter::PushMetricExporter, reader::SdkProducer},
16    Resource,
17};
18
19use super::{
20    data::ResourceMetrics, instrument::InstrumentKind, pipeline::Pipeline, reader::MetricReader,
21    Temporality,
22};
23
24const DEFAULT_INTERVAL: Duration = Duration::from_secs(60);
25
26const METRIC_EXPORT_INTERVAL_NAME: &str = "OTEL_METRIC_EXPORT_INTERVAL";
27
28/// Configuration options for [PeriodicReader].
29#[derive(Debug)]
30pub struct PeriodicReaderBuilder<E> {
31    interval: Duration,
32    exporter: E,
33}
34
35impl<E> PeriodicReaderBuilder<E>
36where
37    E: PushMetricExporter,
38{
39    fn new(exporter: E) -> Self {
40        let interval = env::var(METRIC_EXPORT_INTERVAL_NAME)
41            .ok()
42            .and_then(|v| v.parse().map(Duration::from_millis).ok())
43            .unwrap_or(DEFAULT_INTERVAL);
44
45        PeriodicReaderBuilder { interval, exporter }
46    }
47
48    /// Configures the intervening time between exports for a [PeriodicReader].
49    ///
50    /// This option overrides any value set for the `OTEL_METRIC_EXPORT_INTERVAL`
51    /// environment variable.
52    ///
53    /// If this option is not used or `interval` is equal to zero, 60 seconds is
54    /// used as the default.
55    pub fn with_interval(mut self, interval: Duration) -> Self {
56        if !interval.is_zero() {
57            self.interval = interval;
58        }
59        self
60    }
61
62    /// Create a [PeriodicReader] with the given config.
63    pub fn build(self) -> PeriodicReader<E> {
64        PeriodicReader::new(self.exporter, self.interval)
65    }
66}
67
68/// A `MetricReader` that periodically collects and exports metrics at a configurable interval.
69///
70/// By default, [`PeriodicReader`] collects and exports metrics every **60 seconds**.
71/// The time taken for export is **not** included in the interval. Use [`PeriodicReaderBuilder`]
72/// to customize the interval.
73///
74/// [`PeriodicReader`] spawns a background thread to handle metric collection and export.
75/// This thread remains active until [`shutdown()`] is called.
76///
77/// ## Collection Process
78/// "Collection" refers to gathering aggregated metrics from the SDK's internal storage.
79/// During this phase, callbacks from observable instruments are also triggered.
80///
81/// [`PeriodicReader`] does **not** enforce a timeout for collection. If an
82/// observable callback takes too long, it may delay the next collection cycle.
83/// If a callback never returns, it **will stall** all metric collection (and exports)
84/// indefinitely.
85///
86/// ## Exporter Compatibility
87/// When used with the [`OTLP Exporter`](https://docs.rs/opentelemetry-otlp), the following
88/// transport options are supported:
89///
90/// - **`grpc-tonic`**: Requires [`MeterProvider`] to be initialized within a `tokio` runtime.
91/// - **`reqwest-blocking-client`**: Works with both a standard (`main`) function and `tokio::main`.
92///
93/// Async HTTP clients such as `reqwest-client` and `hyper-client` are not
94/// supported by this default reader. The OTLP HTTP exporter chooses its default
95/// HTTP client from enabled crate features and cannot tell which reader will
96/// drive it. If your dependency graph enables async HTTP client features, either
97/// pass an explicit blocking client for this reader or use the experimental
98/// async-runtime periodic reader.
99///
100/// [`PeriodicReader`] does **not** enforce a timeout for exports either. Instead,
101/// the configured exporter is responsible for enforcing timeouts. If an export operation
102/// never returns, [`PeriodicReader`] will **stop exporting new metrics**, stalling
103/// metric collection.
104///
105/// ## Manual Export & Shutdown
106/// Users can manually trigger an export via [`force_flush()`]. Calling [`shutdown()`]
107/// exports any remaining metrics and should be done before application exit to ensure
108/// all data is sent.
109///
110/// **Warning**: If using **tokio’s current-thread runtime**, calling [`shutdown()`]
111/// from the main thread may cause a deadlock. To prevent this, call [`shutdown()`]
112/// from a separate thread or use tokio's `spawn_blocking`.
113///
114/// [`PeriodicReader`]: crate::metrics::PeriodicReader
115/// [`PeriodicReaderBuilder`]: crate::metrics::PeriodicReaderBuilder
116/// [`MeterProvider`]: crate::metrics::SdkMeterProvider
117/// [`shutdown()`]: crate::metrics::SdkMeterProvider::shutdown
118/// [`force_flush()`]: crate::metrics::SdkMeterProvider::force_flush
119///
120/// # Example
121///
122/// ```no_run
123/// use opentelemetry_sdk::metrics::PeriodicReader;
124/// # fn example<E>(get_exporter: impl Fn() -> E)
125/// # where
126/// #     E: opentelemetry_sdk::metrics::exporter::PushMetricExporter,
127/// # {
128///
129/// let exporter = get_exporter(); // set up a push exporter
130///
131/// let reader = PeriodicReader::builder(exporter).build();
132/// # drop(reader);
133/// # }
134/// ```
135pub struct PeriodicReader<E: PushMetricExporter> {
136    inner: Arc<PeriodicReaderInner<E>>,
137}
138
139impl<E: PushMetricExporter> Clone for PeriodicReader<E> {
140    fn clone(&self) -> Self {
141        Self {
142            inner: Arc::clone(&self.inner),
143        }
144    }
145}
146
147impl<E: PushMetricExporter> PeriodicReader<E> {
148    /// Configuration options for a periodic reader with own thread
149    pub fn builder(exporter: E) -> PeriodicReaderBuilder<E> {
150        PeriodicReaderBuilder::new(exporter)
151    }
152
153    fn new(exporter: E, interval: Duration) -> Self {
154        let (message_sender, message_receiver): (Sender<Message>, Receiver<Message>) =
155            mpsc::channel();
156        let exporter_arc = Arc::new(exporter);
157        let reader = PeriodicReader {
158            inner: Arc::new(PeriodicReaderInner {
159                message_sender,
160                producer: Mutex::new(None),
161                exporter: exporter_arc.clone(),
162            }),
163        };
164        let cloned_reader = reader.clone();
165
166        let mut rm = ResourceMetrics {
167            resource: Resource::empty(),
168            scope_metrics: Vec::new(),
169        };
170
171        let result_thread_creation = thread::Builder::new()
172            .name("OpenTelemetry.Metrics.PeriodicReader".to_string())
173            .spawn(move || {
174                let _suppress_guard = Context::enter_telemetry_suppressed_scope();
175                let mut interval_start = Instant::now();
176                let mut remaining_interval = interval;
177                otel_debug!(
178                    name: "PeriodReaderThreadStarted",
179                    interval_in_millisecs = interval.as_millis(),
180                );
181                loop {
182                    otel_debug!(
183                        name: "PeriodReaderThreadLoopAlive", message = "Next export will happen after interval, unless flush or shutdown is triggered.", interval_in_millisecs = remaining_interval.as_millis()
184                    );
185                    match message_receiver.recv_timeout(remaining_interval) {
186                        Ok(Message::Flush(response_sender)) => {
187                            otel_debug!(
188                                name: "PeriodReaderThreadExportingDueToFlush"
189                            );
190                            let export_result = cloned_reader.collect_and_export(&mut rm);
191                            otel_debug!(
192                                name: "PeriodReaderInvokedExport",
193                                export_result = format!("{:?}", export_result)
194                            );
195
196                            // If response_sender is disconnected, we can't send
197                            // the result back. This occurs when the thread that
198                            // initiated flush gave up due to timeout.
199                            // Gracefully handle that with internal logs. The
200                            // internal errors are of Info level, as this is
201                            // useful for user to know whether the flush was
202                            // successful or not, when flush() itself merely
203                            // tells that it timed out.
204
205                            if export_result.is_err() {
206                                if response_sender.send(false).is_err() {
207                                    otel_debug!(
208                                        name: "PeriodReader.Flush.ResponseSendError",
209                                        message = "PeriodicReader's flush has failed, but unable to send this info back to caller.
210                                        This occurs when the caller has timed out waiting for the response. If you see this occuring frequently, consider increasing the flush timeout."
211                                    );
212                                }
213                            } else if response_sender.send(true).is_err() {
214                                otel_debug!(
215                                    name: "PeriodReader.Flush.ResponseSendError",
216                                    message = "PeriodicReader's flush has completed successfully, but unable to send this info back to caller.
217                                    This occurs when the caller has timed out waiting for the response. If you see this occuring frequently, consider increasing the flush timeout."
218                                );
219                            }
220
221                            // Adjust the remaining interval after the flush
222                            let elapsed = interval_start.elapsed();
223                            if elapsed < interval {
224                                remaining_interval = interval - elapsed;
225                                otel_debug!(
226                                    name: "PeriodReaderThreadAdjustingRemainingIntervalAfterFlush",
227                                    remaining_interval = remaining_interval.as_secs()
228                                );
229                            } else {
230                                otel_debug!(
231                                    name: "PeriodReaderThreadAdjustingExportAfterFlush",
232                                );
233                                // Reset the interval if the flush finishes after the expected export time
234                                // effectively missing the normal export.
235                                // Should we attempt to do the missed export immediately?
236                                // Or do the next export at the next interval?
237                                // Currently this attempts the next export immediately.
238                                // i.e calling Flush can affect the regularity.
239                                interval_start = Instant::now();
240                                remaining_interval = Duration::ZERO;
241                            }
242                        }
243                        Ok(Message::Shutdown(response_sender)) => {
244                            // Perform final export and break out of loop and exit the thread
245                            otel_debug!(name: "PeriodReaderThreadExportingDueToShutdown");
246                            let export_result = cloned_reader.collect_and_export(&mut rm);
247                            otel_debug!(
248                                name: "PeriodReaderInvokedExport",
249                                export_result = format!("{:?}", export_result)
250                            );
251                            let shutdown_result = exporter_arc.shutdown();
252                            otel_debug!(
253                                name: "PeriodReaderInvokedExporterShutdown",
254                                shutdown_result = format!("{:?}", shutdown_result)
255                            );
256
257                            // If response_sender is disconnected, we can't send
258                            // the result back. This occurs when the thread that
259                            // initiated shutdown gave up due to timeout.
260                            // Gracefully handle that with internal logs and
261                            // continue with shutdown (i.e exit thread) The
262                            // internal errors are of Info level, as this is
263                            // useful for user to know whether the shutdown was
264                            // successful or not, when shutdown() itself merely
265                            // tells that it timed out.
266                            if export_result.is_err() || shutdown_result.is_err() {
267                                if response_sender.send(false).is_err() {
268                                    otel_info!(
269                                        name: "PeriodReaderThreadShutdown.ResponseSendError",
270                                        message = "PeriodicReader's shutdown has failed, but unable to send this info back to caller.
271                                        This occurs when the caller has timed out waiting for the response. If you see this occuring frequently, consider increasing the shutdown timeout."
272                                    );
273                                }
274                            } else if response_sender.send(true).is_err() {
275                                otel_debug!(
276                                    name: "PeriodReaderThreadShutdown.ResponseSendError",
277                                    message = "PeriodicReader completed its shutdown, but unable to send this info back to caller.
278                                    This occurs when the caller has timed out waiting for the response. If you see this occuring frequently, consider increasing the shutdown timeout."
279                                );
280                            }
281
282                            otel_debug!(
283                                name: "PeriodReaderThreadExiting",
284                                reason = "ShutdownRequested"
285                            );
286                            break;
287                        }
288                        Err(mpsc::RecvTimeoutError::Timeout) => {
289                            let export_start = Instant::now();
290                            otel_debug!(
291                                name: "PeriodReaderThreadExportingDueToTimer"
292                            );
293
294                            let export_result = cloned_reader.collect_and_export(&mut rm);
295                            otel_debug!(
296                                name: "PeriodReaderInvokedExport",
297                                export_result = format!("{:?}", export_result)
298                            );
299
300                            let time_taken_for_export = export_start.elapsed();
301                            if time_taken_for_export > interval {
302                                otel_debug!(
303                                    name: "PeriodReaderThreadExportTookLongerThanInterval"
304                                );
305                                // if export took longer than interval, do the
306                                // next export immediately.
307                                // Alternatively, we could skip the next export
308                                // and wait for the next interval.
309                                // Or enforce that export timeout is less than interval.
310                                // What is the desired behavior?
311                                interval_start = Instant::now();
312                                remaining_interval = Duration::ZERO;
313                            } else {
314                                remaining_interval = interval - time_taken_for_export;
315                                interval_start = Instant::now();
316                            }
317                        }
318                        Err(mpsc::RecvTimeoutError::Disconnected) => {
319                            // Channel disconnected, only thing to do is break
320                            // out (i.e exit the thread)
321                            otel_debug!(
322                                name: "PeriodReaderThreadExiting",
323                                reason = "MessageSenderDisconnected"
324                            );
325                            break;
326                        }
327                    }
328                }
329                otel_debug!(
330                    name: "PeriodReaderThreadStopped"
331                );
332            });
333
334        // TODO: Should we fail-fast here and bubble up the error to user?
335        #[allow(unused_variables)]
336        if let Err(e) = result_thread_creation {
337            otel_error!(
338                name: "PeriodReaderThreadStartError",
339                message = "Failed to start PeriodicReader thread. Metrics will not be exported.",
340                error = format!("{:?}", e)
341            );
342        }
343        reader
344    }
345
346    fn collect_and_export(&self, rm: &mut ResourceMetrics) -> OTelSdkResult {
347        self.inner.collect_and_export(rm)
348    }
349}
350
351impl<E: PushMetricExporter> fmt::Debug for PeriodicReader<E> {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        f.debug_struct("PeriodicReader").finish()
354    }
355}
356
357struct PeriodicReaderInner<E: PushMetricExporter> {
358    exporter: Arc<E>,
359    message_sender: mpsc::Sender<Message>,
360    producer: Mutex<Option<Weak<dyn SdkProducer>>>,
361}
362
363impl<E: PushMetricExporter> PeriodicReaderInner<E> {
364    fn register_pipeline(&self, producer: Weak<dyn SdkProducer>) {
365        let mut inner = self.producer.lock().expect("lock poisoned");
366        *inner = Some(producer);
367    }
368
369    fn temporality(&self, _kind: InstrumentKind) -> Temporality {
370        self.exporter.temporality()
371    }
372
373    fn collect(&self, rm: &mut ResourceMetrics) -> OTelSdkResult {
374        let producer = self.producer.lock().expect("lock poisoned");
375        if let Some(p) = producer.as_ref() {
376            p.upgrade()
377                .ok_or(OTelSdkError::AlreadyShutdown)?
378                .produce(rm)?;
379            Ok(())
380        } else {
381            otel_warn!(
382            name: "PeriodReader.MeterProviderNotRegistered",
383            message = "PeriodicReader is not registered with MeterProvider. Metrics will not be collected. \
384                   This occurs when a periodic reader is created but not associated with a MeterProvider \
385                   by calling `.with_reader(reader)` on MeterProviderBuilder."
386            );
387            Err(OTelSdkError::InternalFailure(
388                "MeterProvider is not registered".into(),
389            ))
390        }
391    }
392
393    fn collect_and_export(&self, rm: &mut ResourceMetrics) -> OTelSdkResult {
394        let current_time = Instant::now();
395        let collect_result = self.collect(rm);
396        let time_taken_for_collect = current_time.elapsed();
397
398        #[allow(clippy::question_mark)]
399        if let Err(e) = collect_result {
400            otel_warn!(
401                name: "PeriodReaderCollectError",
402                error = format!("{:?}", e)
403            );
404            return Err(OTelSdkError::InternalFailure(e.to_string()));
405        }
406
407        if rm.scope_metrics.is_empty() {
408            otel_debug!(name: "NoMetricsCollected");
409            return Ok(());
410        }
411
412        let metrics_count = rm.scope_metrics.iter().fold(0, |count, scope_metrics| {
413            count + scope_metrics.metrics.len()
414        });
415        otel_debug!(name: "PeriodicReaderMetricsCollected", count = metrics_count, time_taken_in_millis = time_taken_for_collect.as_millis());
416
417        // Relying on futures executor to execute async call.
418        // TODO: Pass timeout to exporter
419        futures_executor::block_on(self.exporter.export(rm))
420    }
421
422    fn force_flush(&self) -> OTelSdkResult {
423        // TODO: Better message for this scenario.
424        // Flush and Shutdown called from 2 threads Flush check shutdown
425        // flag before shutdown thread sets it. Both threads attempt to send
426        // message to the same channel. Case1: Flush thread sends message first,
427        // shutdown thread sends message next. Flush would succeed, as
428        // background thread won't process shutdown message until flush
429        // triggered export is done. Case2: Shutdown thread sends message first,
430        // flush thread sends message next. Shutdown would succeed, as
431        // background thread would process shutdown message first. The
432        // background exits so it won't receive the flush message. ForceFlush
433        // returns Failure, but we could indicate specifically that shutdown has
434        // completed. TODO is to see if this message can be improved.
435
436        let (response_tx, response_rx) = mpsc::channel();
437        self.message_sender
438            .send(Message::Flush(response_tx))
439            .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?;
440
441        if let Ok(response) = response_rx.recv() {
442            // TODO: call exporter's force_flush method.
443            if response {
444                Ok(())
445            } else {
446                Err(OTelSdkError::InternalFailure("Failed to flush".into()))
447            }
448        } else {
449            Err(OTelSdkError::InternalFailure("Failed to flush".into()))
450        }
451    }
452
453    fn shutdown(&self) -> OTelSdkResult {
454        // TODO: See if this is better to be created upfront.
455        let (response_tx, response_rx) = mpsc::channel();
456        self.message_sender
457            .send(Message::Shutdown(response_tx))
458            .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?;
459
460        // TODO: Make this timeout configurable.
461        match response_rx.recv_timeout(Duration::from_secs(5)) {
462            Ok(response) => {
463                if response {
464                    Ok(())
465                } else {
466                    Err(OTelSdkError::InternalFailure("Failed to shutdown".into()))
467                }
468            }
469            Err(mpsc::RecvTimeoutError::Timeout) => {
470                Err(OTelSdkError::Timeout(Duration::from_secs(5)))
471            }
472            Err(mpsc::RecvTimeoutError::Disconnected) => {
473                Err(OTelSdkError::InternalFailure("Failed to shutdown".into()))
474            }
475        }
476    }
477}
478
479#[derive(Debug)]
480enum Message {
481    Flush(Sender<bool>),
482    Shutdown(Sender<bool>),
483}
484
485impl<E: PushMetricExporter> MetricReader for PeriodicReader<E> {
486    fn register_pipeline(&self, pipeline: Weak<Pipeline>) {
487        self.inner.register_pipeline(pipeline);
488    }
489
490    fn collect(&self, rm: &mut ResourceMetrics) -> OTelSdkResult {
491        self.inner.collect(rm)
492    }
493
494    fn force_flush(&self) -> OTelSdkResult {
495        self.inner.force_flush()
496    }
497
498    // TODO: Offer an async version of shutdown so users can await the shutdown
499    // completion, and avoid blocking the thread. The default shutdown on drop
500    // can still use blocking call. If user already explicitly called shutdown,
501    // drop won't call shutdown again.
502    fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult {
503        self.inner.shutdown()
504    }
505
506    /// To construct a [MetricReader][metric-reader] when setting up an SDK,
507    /// The output temporality (optional), a function of instrument kind.
508    /// This function SHOULD be obtained from the exporter.
509    ///
510    /// If not configured, the Cumulative temporality SHOULD be used.
511    ///
512    /// [metric-reader]: https://github.com/open-telemetry/opentelemetry-specification/blob/0a78571045ca1dca48621c9648ec3c832c3c541c/specification/metrics/sdk.md#metricreader
513    fn temporality(&self, kind: InstrumentKind) -> Temporality {
514        kind.temporality_preference(self.inner.temporality(kind))
515    }
516}
517
518#[cfg(all(test, feature = "testing"))]
519mod tests {
520    use super::PeriodicReader;
521    use crate::{
522        error::{OTelSdkError, OTelSdkResult},
523        metrics::{
524            data::ResourceMetrics, exporter::PushMetricExporter, reader::MetricReader,
525            InMemoryMetricExporter, SdkMeterProvider, Temporality,
526        },
527        Resource,
528    };
529    use opentelemetry::metrics::MeterProvider;
530    use std::{
531        sync::{
532            atomic::{AtomicBool, AtomicUsize, Ordering},
533            mpsc, Arc,
534        },
535        time::Duration,
536    };
537
538    // use below command to run all tests
539    // cargo test metrics::periodic_reader::tests --features=testing,spec_unstable_metrics_views -- --nocapture
540
541    #[derive(Debug, Clone)]
542    struct MetricExporterThatFailsOnlyOnFirst {
543        count: Arc<AtomicUsize>,
544    }
545
546    impl Default for MetricExporterThatFailsOnlyOnFirst {
547        fn default() -> Self {
548            MetricExporterThatFailsOnlyOnFirst {
549                count: Arc::new(AtomicUsize::new(0)),
550            }
551        }
552    }
553
554    impl MetricExporterThatFailsOnlyOnFirst {
555        fn get_count(&self) -> usize {
556            self.count.load(Ordering::Relaxed)
557        }
558    }
559
560    impl PushMetricExporter for MetricExporterThatFailsOnlyOnFirst {
561        async fn export(&self, _metrics: &ResourceMetrics) -> OTelSdkResult {
562            if self.count.fetch_add(1, Ordering::Relaxed) == 0 {
563                Err(OTelSdkError::InternalFailure("export failed".into()))
564            } else {
565                Ok(())
566            }
567        }
568
569        fn force_flush(&self) -> OTelSdkResult {
570            Ok(())
571        }
572
573        fn shutdown(&self) -> OTelSdkResult {
574            Ok(())
575        }
576
577        fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult {
578            Ok(())
579        }
580
581        fn temporality(&self) -> Temporality {
582            Temporality::Cumulative
583        }
584    }
585
586    #[derive(Debug, Clone, Default)]
587    struct MockMetricExporter {
588        is_shutdown: Arc<AtomicBool>,
589    }
590
591    impl PushMetricExporter for MockMetricExporter {
592        async fn export(&self, _metrics: &ResourceMetrics) -> OTelSdkResult {
593            Ok(())
594        }
595
596        fn force_flush(&self) -> OTelSdkResult {
597            Ok(())
598        }
599
600        fn shutdown(&self) -> OTelSdkResult {
601            self.shutdown_with_timeout(Duration::from_secs(5))
602        }
603
604        fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult {
605            self.is_shutdown.store(true, Ordering::Relaxed);
606            Ok(())
607        }
608
609        fn temporality(&self) -> Temporality {
610            Temporality::Cumulative
611        }
612    }
613
614    #[test]
615    fn collection_triggered_by_interval_multiple() {
616        // Arrange
617        let interval = std::time::Duration::from_millis(1);
618        let exporter = InMemoryMetricExporter::default();
619        let reader = PeriodicReader::builder(exporter.clone())
620            .with_interval(interval)
621            .build();
622        let i = Arc::new(AtomicUsize::new(0));
623        let i_clone = i.clone();
624
625        // Act
626        let meter_provider = SdkMeterProvider::builder().with_reader(reader).build();
627        let meter = meter_provider.meter("test");
628        let _counter = meter
629            .u64_observable_counter("testcounter")
630            .with_callback(move |_| {
631                i_clone.fetch_add(1, Ordering::Relaxed);
632            })
633            .build();
634
635        // Sleep for a duration 5X (plus liberal buffer to account for potential
636        // CI slowness) the interval to ensure multiple collection.
637        // Not a fan of such tests, but this seems to be the only way to test
638        // if periodic reader is doing its job.
639        // TODO: Decide if this should be ignored in CI
640        std::thread::sleep(interval * 5 * 20);
641
642        // Assert
643        assert!(i.load(Ordering::Relaxed) >= 5);
644    }
645
646    #[test]
647    fn shutdown_repeat() {
648        // Arrange
649        let exporter = InMemoryMetricExporter::default();
650        let reader = PeriodicReader::builder(exporter.clone()).build();
651
652        let meter_provider = SdkMeterProvider::builder().with_reader(reader).build();
653        let result = meter_provider.shutdown();
654        assert!(result.is_ok());
655
656        // calling shutdown again should return Err
657        let result = meter_provider.shutdown();
658        assert!(result.is_err());
659        assert!(matches!(result, Err(OTelSdkError::AlreadyShutdown)));
660
661        // calling shutdown again should return Err
662        let result = meter_provider.shutdown();
663        assert!(result.is_err());
664        assert!(matches!(result, Err(OTelSdkError::AlreadyShutdown)));
665    }
666
667    #[test]
668    fn flush_after_shutdown() {
669        // Arrange
670        let exporter = InMemoryMetricExporter::default();
671        let reader = PeriodicReader::builder(exporter.clone()).build();
672
673        let meter_provider = SdkMeterProvider::builder().with_reader(reader).build();
674        let result = meter_provider.force_flush();
675        assert!(result.is_ok());
676
677        let result = meter_provider.shutdown();
678        assert!(result.is_ok());
679
680        // calling force_flush after shutdown should return Err
681        let result = meter_provider.force_flush();
682        assert!(result.is_err());
683    }
684
685    #[test]
686    fn flush_repeat() {
687        // Arrange
688        let exporter = InMemoryMetricExporter::default();
689        let reader = PeriodicReader::builder(exporter.clone()).build();
690
691        let meter_provider = SdkMeterProvider::builder().with_reader(reader).build();
692        let result = meter_provider.force_flush();
693        assert!(result.is_ok());
694
695        // calling force_flush again should return Ok
696        let result = meter_provider.force_flush();
697        assert!(result.is_ok());
698    }
699
700    #[test]
701    fn periodic_reader_without_pipeline() {
702        // Arrange
703        let exporter = InMemoryMetricExporter::default();
704        let reader = PeriodicReader::builder(exporter.clone()).build();
705
706        let rm = &mut ResourceMetrics {
707            resource: Resource::empty(),
708            scope_metrics: Vec::new(),
709        };
710        // Pipeline is not registered, so collect should return an error
711        let result = reader.collect(rm);
712        assert!(result.is_err());
713
714        // Pipeline is not registered, so flush should return an error
715        let result = reader.force_flush();
716        assert!(result.is_err());
717
718        // Adding reader to meter provider should register the pipeline
719        // TODO: This part might benefit from a different design.
720        let meter_provider = SdkMeterProvider::builder()
721            .with_reader(reader.clone())
722            .build();
723
724        // Now collect and flush should succeed
725        let result = reader.collect(rm);
726        assert!(result.is_ok());
727
728        let result = meter_provider.force_flush();
729        assert!(result.is_ok());
730    }
731
732    #[test]
733    fn exporter_failures_are_handled() {
734        // create a mock exporter that fails 1st time and succeeds 2nd time
735        // Validate using this exporter that periodic reader can handle exporter failure
736        // and continue to export metrics.
737        // Arrange
738        let interval = std::time::Duration::from_millis(10);
739        let exporter = MetricExporterThatFailsOnlyOnFirst::default();
740        let reader = PeriodicReader::builder(exporter.clone())
741            .with_interval(interval)
742            .build();
743
744        let meter_provider = SdkMeterProvider::builder().with_reader(reader).build();
745        let meter = meter_provider.meter("test");
746        let counter = meter.u64_counter("sync_counter").build();
747        counter.add(1, &[]);
748        let _obs_counter = meter
749            .u64_observable_counter("testcounter")
750            .with_callback(move |observer| {
751                observer.observe(1, &[]);
752            })
753            .build();
754
755        // Sleep for a duration much longer than the interval to trigger
756        // multiple exports, including failures.
757        // Not a fan of such tests, but this seems to be the
758        // only way to test if periodic reader is doing its job. TODO: Decide if
759        // this should be ignored in CI
760        std::thread::sleep(Duration::from_millis(500));
761
762        // Assert that atleast 2 exports are attempted given the 1st one fails.
763        assert!(exporter.get_count() >= 2);
764    }
765
766    #[test]
767    fn shutdown_passed_to_exporter() {
768        // Arrange
769        let exporter = MockMetricExporter::default();
770        let reader = PeriodicReader::builder(exporter.clone()).build();
771
772        let meter_provider = SdkMeterProvider::builder().with_reader(reader).build();
773        let meter = meter_provider.meter("test");
774        let counter = meter.u64_counter("sync_counter").build();
775        counter.add(1, &[]);
776
777        // shutdown the provider, which should call shutdown on periodic reader
778        // which in turn should call shutdown on exporter.
779        let result = meter_provider.shutdown();
780        assert!(result.is_ok());
781        assert!(exporter.is_shutdown.load(Ordering::Relaxed));
782    }
783
784    #[test]
785    fn collection() {
786        collection_triggered_by_interval_helper();
787        collection_triggered_by_flush_helper();
788        collection_triggered_by_shutdown_helper();
789        collection_triggered_by_drop_helper();
790    }
791
792    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
793    async fn collection_from_tokio_multi_with_one_worker() {
794        collection_triggered_by_interval_helper();
795        collection_triggered_by_flush_helper();
796        collection_triggered_by_shutdown_helper();
797        collection_triggered_by_drop_helper();
798    }
799
800    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
801    async fn collection_from_tokio_with_two_worker() {
802        collection_triggered_by_interval_helper();
803        collection_triggered_by_flush_helper();
804        collection_triggered_by_shutdown_helper();
805        collection_triggered_by_drop_helper();
806    }
807
808    #[tokio::test(flavor = "current_thread")]
809    async fn collection_from_tokio_current() {
810        collection_triggered_by_interval_helper();
811        collection_triggered_by_flush_helper();
812        collection_triggered_by_shutdown_helper();
813        collection_triggered_by_drop_helper();
814    }
815
816    fn collection_triggered_by_interval_helper() {
817        collection_helper(|_| {
818            // Sleep for a duration longer than the interval to ensure at least one collection
819            // Not a fan of such tests, but this seems to be the only way to test
820            // if periodic reader is doing its job.
821            // TODO: Decide if this should be ignored in CI
822            std::thread::sleep(Duration::from_millis(500));
823        });
824    }
825
826    fn collection_triggered_by_flush_helper() {
827        collection_helper(|meter_provider| {
828            meter_provider.force_flush().expect("flush should succeed");
829        });
830    }
831
832    fn collection_triggered_by_shutdown_helper() {
833        collection_helper(|meter_provider| {
834            meter_provider.shutdown().expect("shutdown should succeed");
835        });
836    }
837
838    fn collection_triggered_by_drop_helper() {
839        collection_helper(|meter_provider| {
840            drop(meter_provider);
841        });
842    }
843
844    fn collection_helper(trigger: fn(SdkMeterProvider)) {
845        // Arrange
846        let exporter = InMemoryMetricExporter::default();
847        let reader = PeriodicReader::builder(exporter.clone()).build();
848        let (sender, receiver) = mpsc::channel();
849
850        let meter_provider = SdkMeterProvider::builder().with_reader(reader).build();
851        let meter = meter_provider.meter("test");
852        let _counter = meter
853            .u64_observable_counter("testcounter")
854            .with_callback(move |observer| {
855                observer.observe(1, &[]);
856                sender.send(()).expect("channel should still be open");
857            })
858            .build();
859
860        // Act
861        trigger(meter_provider);
862
863        // Assert
864        receiver
865            .recv_timeout(Duration::ZERO)
866            .expect("message should be available in channel, indicating a collection occurred, which should trigger observable callback");
867
868        let exported_metrics = exporter
869            .get_finished_metrics()
870            .expect("this should not fail");
871        assert!(
872            !exported_metrics.is_empty(),
873            "Metrics should be available in exporter."
874        );
875    }
876
877    async fn some_async_function() -> u64 {
878        // No dependency on any particular async runtime.
879        std::thread::sleep(std::time::Duration::from_millis(1));
880        1
881    }
882
883    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
884    async fn async_inside_observable_callback_from_tokio_multi_with_one_worker() {
885        async_inside_observable_callback_helper();
886    }
887
888    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
889    async fn async_inside_observable_callback_from_tokio_multi_with_two_worker() {
890        async_inside_observable_callback_helper();
891    }
892
893    #[tokio::test(flavor = "current_thread")]
894    async fn async_inside_observable_callback_from_tokio_current_thread() {
895        async_inside_observable_callback_helper();
896    }
897
898    #[test]
899    fn async_inside_observable_callback_from_regular_main() {
900        async_inside_observable_callback_helper();
901    }
902
903    fn async_inside_observable_callback_helper() {
904        let interval = std::time::Duration::from_millis(10);
905        let exporter = InMemoryMetricExporter::default();
906        let reader = PeriodicReader::builder(exporter.clone())
907            .with_interval(interval)
908            .build();
909
910        let meter_provider = SdkMeterProvider::builder().with_reader(reader).build();
911        let meter = meter_provider.meter("test");
912        let _gauge = meter
913            .u64_observable_gauge("my_observable_gauge")
914            .with_callback(|observer| {
915                // using futures_executor::block_on intentionally and avoiding
916                // any particular async runtime.
917                let value = futures_executor::block_on(some_async_function());
918                observer.observe(value, &[]);
919            })
920            .build();
921
922        meter_provider.force_flush().expect("flush should succeed");
923        let exported_metrics = exporter
924            .get_finished_metrics()
925            .expect("this should not fail");
926        assert!(
927            !exported_metrics.is_empty(),
928            "Metrics should be available in exporter."
929        );
930    }
931
932    async fn some_tokio_async_function() -> u64 {
933        // Tokio specific async function
934        tokio::time::sleep(Duration::from_millis(1)).await;
935        1
936    }
937
938    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
939
940    async fn tokio_async_inside_observable_callback_from_tokio_multi_with_one_worker() {
941        tokio_async_inside_observable_callback_helper(true);
942    }
943
944    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
945    async fn tokio_async_inside_observable_callback_from_tokio_multi_with_two_worker() {
946        tokio_async_inside_observable_callback_helper(true);
947    }
948
949    #[tokio::test(flavor = "current_thread")]
950    #[ignore] //TODO: Investigate if this can be fixed.
951    async fn tokio_async_inside_observable_callback_from_tokio_current_thread() {
952        tokio_async_inside_observable_callback_helper(true);
953    }
954
955    #[test]
956    fn tokio_async_inside_observable_callback_from_regular_main() {
957        tokio_async_inside_observable_callback_helper(false);
958    }
959
960    fn tokio_async_inside_observable_callback_helper(use_current_tokio_runtime: bool) {
961        let exporter = InMemoryMetricExporter::default();
962        let reader = PeriodicReader::builder(exporter.clone()).build();
963
964        let meter_provider = SdkMeterProvider::builder().with_reader(reader).build();
965        let meter = meter_provider.meter("test");
966
967        if use_current_tokio_runtime {
968            let rt = tokio::runtime::Handle::current().clone();
969            let _gauge = meter
970                .u64_observable_gauge("my_observable_gauge")
971                .with_callback(move |observer| {
972                    // call tokio specific async function from here
973                    let value = rt.block_on(some_tokio_async_function());
974                    observer.observe(value, &[]);
975                })
976                .build();
977            // rt here is a reference to the current tokio runtime.
978            // Dropping it occurs when the tokio::main itself ends.
979        } else {
980            let rt = tokio::runtime::Runtime::new().unwrap();
981            let _gauge = meter
982                .u64_observable_gauge("my_observable_gauge")
983                .with_callback(move |observer| {
984                    // call tokio specific async function from here
985                    let value = rt.block_on(some_tokio_async_function());
986                    observer.observe(value, &[]);
987                })
988                .build();
989            // rt is not dropped here as it is moved to the closure,
990            // and is dropped only when MeterProvider itself is dropped.
991            // This works when called from normal main.
992        };
993
994        meter_provider.force_flush().expect("flush should succeed");
995        let exported_metrics = exporter
996            .get_finished_metrics()
997            .expect("this should not fail");
998        assert!(
999            !exported_metrics.is_empty(),
1000            "Metrics should be available in exporter."
1001        );
1002    }
1003}