apollo-opentelemetry 0.8.0

OpenTelemetry configuration types for Apollo platform
Documentation
//! Resource attributes configuration.
//!
//! See the [resource specification](https://opentelemetry.io/docs/specs/otel/resource/sdk/)
//! for details.

use apollo_configuration::Validate;
use apollo_configuration::configuration;

/// Configuration for resource attributes that identify the service.
///
/// See the [resource specification](https://opentelemetry.io/docs/specs/otel/resource/sdk/)
/// for details.
#[configuration]
pub(crate) struct ResourceConfig {
    /// Key-value pairs for resource attributes.
    ///
    /// Common attributes include `service.name`, `service.version`, `deployment.environment`.
    pub(crate) attributes: Vec<ResourceAttribute>,

    /// Resource detectors to use.
    pub(crate) detectors: ResourceDetectors,

    /// Schema URL for the resource.
    pub(crate) schema_url: Option<String>,
}

/// A resource attribute key-value pair.
#[configuration]
pub(crate) struct ResourceAttribute {
    /// Attribute name.
    #[config(required)]
    pub(crate) name: String,

    /// Attribute value.
    #[config(required)]
    pub(crate) value: AttributeValue,
}

/// Possible attribute value types.
///
/// Supports all OTel attribute value types: strings, integers, floats, booleans,
/// and arrays of each.
#[allow(dead_code)]
#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub(crate) enum AttributeValue {
    String(String),
    Int(i64),
    Float(f64),
    Bool(bool),
    StringArray(Vec<String>),
    IntArray(Vec<i64>),
    FloatArray(Vec<f64>),
    BoolArray(Vec<bool>),
}

impl Validate for AttributeValue {}

/// Configuration for automatic resource detection.
#[configuration]
pub(crate) struct ResourceDetectors {
    /// Resource attributes provided by the environment.
    pub(crate) attributes: EnvironmentResourceDetector,
}

/// Environment-based resource detector configuration.
#[configuration]
pub(crate) struct EnvironmentResourceDetector {
    /// Included resource attribute keys.
    pub(crate) included: Vec<String>,

    /// Excluded resource attribute keys.
    pub(crate) excluded: Vec<String>,
}

// --- SDK Conversions ---

use opentelemetry::{Key, KeyValue, StringValue, Value};
use opentelemetry_sdk::Resource;

impl From<&ResourceConfig> for Resource {
    fn from(config: &ResourceConfig) -> Self {
        let attributes: Vec<KeyValue> = config
            .attributes
            .iter()
            .map(|attr| KeyValue::new(attr.name.clone(), Value::from(&attr.value)))
            .collect();

        if let Some(ref schema_url) = config.schema_url {
            Resource::builder()
                .with_schema_url(attributes, schema_url.clone())
                .build()
        } else {
            Resource::builder().with_attributes(attributes).build()
        }
    }
}

impl From<&AttributeValue> for Value {
    fn from(value: &AttributeValue) -> Self {
        match value {
            AttributeValue::String(s) => Value::String(s.clone().into()),
            AttributeValue::Int(i) => Value::I64(*i),
            AttributeValue::Float(f) => Value::F64(*f),
            AttributeValue::Bool(b) => Value::Bool(*b),
            AttributeValue::StringArray(arr) => {
                let strings: Vec<StringValue> = arr.iter().map(|s| s.clone().into()).collect();
                Value::Array(strings.into())
            }
            AttributeValue::IntArray(arr) => Value::Array(arr.clone().into()),
            AttributeValue::FloatArray(arr) => Value::Array(arr.clone().into()),
            AttributeValue::BoolArray(arr) => Value::Array(arr.clone().into()),
        }
    }
}

impl From<&ResourceAttribute> for KeyValue {
    fn from(attr: &ResourceAttribute) -> Self {
        KeyValue::new(Key::new(attr.name.clone()), Value::from(&attr.value))
    }
}

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

    use super::*;
    use crate::config::OpenTelemetryConfig;

    #[test]
    fn from_config() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                resource:
                  attributes:
                    - name: service.name
                      value: test-service
                    - name: service.version
                      value: 1.0.0
                    - name: deployment.environment
                      value: production
            "},
            &Default::default(),
        )
        .unwrap();

        let resource: Resource = (&config.resource).into();

        assert!(!resource.is_empty());
    }

    #[test]
    fn with_schema_url() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                resource:
                  schema_url: https://opentelemetry.io/schemas/1.21.0
                  attributes:
                    - name: service.name
                      value: test-service
            "},
            &Default::default(),
        )
        .unwrap();

        let resource: Resource = (&config.resource).into();
        assert!(!resource.is_empty());
    }

    #[test]
    fn with_various_attribute_types() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                resource:
                  attributes:
                    - name: string_attr
                      value: hello
                    - name: int_attr
                      value: 42
                    - name: float_attr
                      value: 3.14
                    - name: bool_attr
                      value: true
            "},
            &Default::default(),
        )
        .unwrap();

        let resource: Resource = (&config.resource).into();
        assert!(resource.len() >= 4);
    }

    #[test]
    fn parse_resource() {
        let config: OpenTelemetryConfig = parse_yaml(
            indoc::indoc! {"
                resource:
                  attributes:
                    - name: service.name
                      value: my-service
                    - name: service.version
                      value: 1.0.0
            "},
            &Default::default(),
        )
        .unwrap();

        assert_eq!(config.resource.attributes.len(), 2);
    }
}