apollo-opentelemetry 0.8.0

OpenTelemetry configuration types for Apollo platform
Documentation
//! Grafana Cloud OTLP exporter configuration.
//!
//! See [Grafana Cloud OTLP](https://grafana.com/docs/grafana-cloud/send-data/otlp/)
//! for details.

use apollo_configuration::configuration;
use apollo_configuration::types::Url;
use apollo_redaction::Redacted;

use super::otlp::{Compression, TlsConfig};

/// Grafana Cloud OTLP exporter configuration.
///
/// Uses Basic authentication with instance ID and API key.
#[configuration]
pub(crate) struct GrafanaCloudHttpExporterConfig {
    /// Grafana Cloud instance ID.
    #[config(required)]
    pub(crate) instance_id: String,

    /// Grafana Cloud API key (typically starts with `glc_...`).
    #[config(required)]
    pub(crate) api_key: Redacted<String>,

    /// Grafana Cloud OTLP endpoint URL.
    ///
    /// See [Grafana Cloud OTLP endpoint](https://grafana.com/docs/grafana-cloud/send-data/otlp/send-data-otlp/#manual-opentelemetry-setup-for-advanced-users)
    /// for endpoint URL format: `https://otlp-gateway-{zone}.grafana.net/otlp`
    ///
    /// Common zones: `prod-us-central-0`, `prod-eu-west-0`, `prod-ap-southeast-0`
    #[config(required)]
    pub(crate) endpoint: Url,

    /// Compression algorithm.
    pub(crate) compression: Compression,

    /// Export timeout in milliseconds.
    #[config(default = 10000)]
    pub(crate) timeout: u64,

    /// TLS configuration.
    pub(crate) tls: Option<TlsConfig>,
}

impl GrafanaCloudHttpExporterConfig {
    /// Returns the Basic auth header value.
    ///
    /// See [Grafana Cloud authentication](https://grafana.com/docs/grafana-cloud/send-data/otlp/send-data-otlp/#manual-opentelemetry-setup-for-advanced-users).
    pub(crate) fn auth_header(&self) -> String {
        use base64::Engine;
        let credentials = format!("{}:{}", self.instance_id, self.api_key.unredact());
        format!(
            "Basic {}",
            base64::engine::general_purpose::STANDARD.encode(credentials)
        )
    }
}

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

    use super::*;
    use crate::config::OpenTelemetryConfig;
    use crate::config::SpanExporter;
    use crate::config::traces::SpanProcessor;

    fn get_grafana_config(config: &OpenTelemetryConfig) -> &GrafanaCloudHttpExporterConfig {
        match &config.tracer_provider.processors[0] {
            SpanProcessor::Batch(batch) => match &batch.exporter {
                SpanExporter::GrafanaCloud(gc) => gc,
                _ => panic!("Expected GrafanaCloud exporter"),
            },
            _ => panic!("Expected Batch processor"),
        }
    }

    #[test]
    fn parse_grafana_cloud_exporter() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                tracer_provider:
                  processors:
                    - batch:
                        exporter:
                          grafana_cloud:
                            instance_id: '123456'
                            api_key: glc_test_key
                            endpoint: https://otlp-gateway-prod-us-central-0.grafana.net/otlp
            "},
            &Default::default(),
        )
        .unwrap();

        let gc = get_grafana_config(&config);
        assert_eq!(
            gc.endpoint.as_str(),
            "https://otlp-gateway-prod-us-central-0.grafana.net/otlp"
        );
    }

    #[test]
    fn auth_header_basic_encoding() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                tracer_provider:
                  processors:
                    - batch:
                        exporter:
                          grafana_cloud:
                            instance_id: '123456'
                            api_key: glc_test_key
                            endpoint: https://otlp-gateway-prod-us-central-0.grafana.net/otlp
            "},
            &Default::default(),
        )
        .unwrap();

        let gc = get_grafana_config(&config);
        let auth = gc.auth_header();

        assert!(auth.starts_with("Basic "));
        // Decode and verify: "123456:glc_test_key" -> base64
        use base64::Engine;
        let encoded = auth.strip_prefix("Basic ").unwrap();
        let decoded = base64::engine::general_purpose::STANDARD
            .decode(encoded)
            .unwrap();
        let credentials = String::from_utf8(decoded).unwrap();
        assert_eq!(credentials, "123456:glc_test_key");
    }
}