apollo-opentelemetry 0.8.0

OpenTelemetry configuration types for Apollo platform
Documentation
//! Meter provider builder.
//!
//! See the [OpenTelemetry Metrics SDK specification](https://opentelemetry.io/docs/specs/otel/metrics/sdk/)
//! for details.

use std::time::Duration;

use opentelemetry_sdk::Resource;
use opentelemetry_sdk::metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider};

use crate::InitError;
use crate::config::{MetricExporter, MetricReader, OpenTelemetryConfig};

/// Build a MeterProvider from configuration.
pub(crate) fn build_meter_provider(
    config: &OpenTelemetryConfig,
    resource: Resource,
    mut builder: MeterProviderBuilder,
) -> Result<SdkMeterProvider, InitError> {
    builder = builder.with_resource(resource);

    for reader_config in &config.meter_provider.readers {
        builder = add_metric_reader(builder, reader_config)?;
    }

    for view_config in &config.meter_provider.views {
        builder = builder.with_view(view_config.to_view());
    }

    Ok(builder.build())
}

fn add_metric_reader(
    builder: MeterProviderBuilder,
    config: &MetricReader,
) -> Result<MeterProviderBuilder, InitError> {
    match config {
        MetricReader::Periodic(periodic_config) => {
            add_periodic_reader(builder, &periodic_config.exporter, periodic_config.interval)
        }
        MetricReader::Pull(pull_config) => {
            // Pull readers aren't directly supported, use periodic with low interval
            add_periodic_reader(builder, &pull_config.exporter, 1000)
        }
    }
}

fn add_periodic_reader(
    #[allow(unused_variables)] builder: MeterProviderBuilder,
    exporter_config: &MetricExporter,
    #[allow(unused_variables)] interval_ms: u64,
) -> Result<MeterProviderBuilder, InitError> {
    let interval = Duration::from_millis(interval_ms);

    match exporter_config {
        #[cfg(feature = "otlp")]
        MetricExporter::OtlpHttp(config) => {
            let exporter = crate::exporters::otlp::build_otlp_metric_exporter(config)?;
            let reader = PeriodicReader::builder(exporter)
                .with_interval(interval)
                .build();
            Ok(builder.with_reader(reader))
        }
        #[cfg(feature = "otlp")]
        MetricExporter::OtlpGrpc(config) => {
            let exporter = crate::exporters::otlp::build_otlp_grpc_metric_exporter(config)?;
            let reader = PeriodicReader::builder(exporter)
                .with_interval(interval)
                .build();
            Ok(builder.with_reader(reader))
        }
        #[cfg(feature = "honeycomb")]
        MetricExporter::HoneycombHttp(config) => {
            let exporter = crate::exporters::honeycomb::build_http_metric_exporter(config)?;
            let reader = PeriodicReader::builder(exporter)
                .with_interval(interval)
                .build();
            Ok(builder.with_reader(reader))
        }
        #[cfg(feature = "honeycomb")]
        MetricExporter::HoneycombGrpc(config) => {
            let exporter = crate::exporters::honeycomb::build_grpc_metric_exporter(config)?;
            let reader = PeriodicReader::builder(exporter)
                .with_interval(interval)
                .build();
            Ok(builder.with_reader(reader))
        }
        #[cfg(feature = "new-relic")]
        MetricExporter::NewRelicHttp(config) => {
            let exporter = crate::exporters::new_relic::build_http_metric_exporter(config)?;
            let reader = PeriodicReader::builder(exporter)
                .with_interval(interval)
                .build();
            Ok(builder.with_reader(reader))
        }
        #[cfg(feature = "new-relic")]
        MetricExporter::NewRelicGrpc(config) => {
            let exporter = crate::exporters::new_relic::build_grpc_metric_exporter(config)?;
            let reader = PeriodicReader::builder(exporter)
                .with_interval(interval)
                .build();
            Ok(builder.with_reader(reader))
        }
        #[cfg(feature = "grafana-cloud")]
        MetricExporter::GrafanaCloud(config) => {
            let exporter = crate::exporters::grafana_cloud::build_http_metric_exporter(config)?;
            let reader = PeriodicReader::builder(exporter)
                .with_interval(interval)
                .build();
            Ok(builder.with_reader(reader))
        }
        #[cfg(feature = "datadog-otlp")]
        MetricExporter::DatadogOtlpHttp(config) => {
            let exporter = crate::exporters::datadog_otlp::build_http_metric_exporter(config)?;
            let reader = PeriodicReader::builder(exporter)
                .with_interval(interval)
                .build();
            Ok(builder.with_reader(reader))
        }
        #[cfg(feature = "datadog-otlp")]
        MetricExporter::DatadogOtlpGrpc(config) => {
            let exporter = crate::exporters::datadog_otlp::build_grpc_metric_exporter(config)?;
            let reader = PeriodicReader::builder(exporter)
                .with_interval(interval)
                .build();
            Ok(builder.with_reader(reader))
        }

        MetricExporter::Console(config) => {
            let exporter =
                apollo_opentelemetry_stdout::__private::StdoutMetricExporter::with_writer(
                    config.format.into(),
                    config.target.into(),
                    config.use_color(),
                );
            let reader = PeriodicReader::builder(exporter)
                .with_interval(interval)
                .build();
            Ok(builder.with_reader(reader))
        }
    }
}

#[cfg(test)]
mod tests {
    use apollo_configuration::parse_yaml;
    use opentelemetry_sdk::metrics::SdkMeterProvider;

    use super::*;

    #[cfg(feature = "otlp")]
    #[test]
    fn build_meter_provider_with_otlp_http() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                meter_provider:
                  readers:
                    - periodic:
                        exporter:
                          otlp_http:
                            endpoint: http://localhost:4318
                        interval: 60000
            "},
            &Default::default(),
        )
        .unwrap();

        let builder = SdkMeterProvider::builder();
        let result = build_meter_provider(&config, (&config.resource).into(), builder);
        assert!(result.is_ok());
    }

    #[test]
    fn build_meter_provider_with_resource() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                resource:
                  attributes:
                    - name: service.name
                      value: test-service
                meter_provider:
                  readers:
                    - periodic:
                        exporter:
                          console: {}
                        interval: 60000
            "},
            &Default::default(),
        )
        .unwrap();

        let builder = SdkMeterProvider::builder();
        let result = build_meter_provider(&config, (&config.resource).into(), builder);
        assert!(result.is_ok());
    }

    #[test]
    fn build_meter_provider_with_pull_reader() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                meter_provider:
                  readers:
                    - pull:
                        exporter:
                          console: {}
            "},
            &Default::default(),
        )
        .unwrap();

        let builder = SdkMeterProvider::builder();
        let result = build_meter_provider(&config, (&config.resource).into(), builder);
        assert!(result.is_ok());
    }

    #[test]
    fn build_meter_provider_with_view_drop() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                meter_provider:
                  readers:
                    - periodic:
                        exporter:
                          console: {}
                        interval: 60000
                  views:
                    - selector:
                        instrument_name: 'internal.*'
                      stream:
                        aggregation: drop
            "},
            &Default::default(),
        )
        .unwrap();

        let builder = SdkMeterProvider::builder();
        let result = build_meter_provider(&config, (&config.resource).into(), builder);
        assert!(result.is_ok());
    }

    #[test]
    fn build_meter_provider_with_view_rename() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                meter_provider:
                  readers:
                    - periodic:
                        exporter:
                          console: {}
                        interval: 60000
                  views:
                    - selector:
                        instrument_name: 'old.metric.name'
                      stream:
                        name: 'new.metric.name'
                        description: 'Updated description'
            "},
            &Default::default(),
        )
        .unwrap();

        let builder = SdkMeterProvider::builder();
        let result = build_meter_provider(&config, (&config.resource).into(), builder);
        assert!(result.is_ok());
    }

    #[test]
    fn build_meter_provider_with_view_histogram_buckets() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                meter_provider:
                  readers:
                    - periodic:
                        exporter:
                          console: {}
                        interval: 60000
                  views:
                    - selector:
                        instrument_name: 'http.request.duration'
                        instrument_type: histogram
                      stream:
                        aggregation:
                          explicit_bucket_histogram:
                            boundaries: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0]
                            record_min_max: true
            "},
            &Default::default(),
        )
        .unwrap();

        let builder = SdkMeterProvider::builder();
        let result = build_meter_provider(&config, (&config.resource).into(), builder);
        assert!(result.is_ok());
    }

    #[test]
    fn build_meter_provider_with_multiple_views() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                meter_provider:
                  readers:
                    - periodic:
                        exporter:
                          console: {}
                        interval: 60000
                  views:
                    - selector:
                        instrument_name: 'debug.*'
                      stream:
                        aggregation: drop
                    - selector:
                        instrument_name: 'http.*'
                      stream:
                        attribute_keys:
                          included:
                            - method
                            - status_code
                    - selector:
                        meter_name: 'my.library'
                      stream:
                        aggregation: sum
            "},
            &Default::default(),
        )
        .unwrap();

        let builder = SdkMeterProvider::builder();
        let result = build_meter_provider(&config, (&config.resource).into(), builder);
        assert!(result.is_ok());
    }
}