apollo-opentelemetry 0.8.0

OpenTelemetry configuration types for Apollo platform
Documentation
//! Trait for converting values into OpenTelemetry attribute values.
//!
//! This module provides the [`ToValue`] trait which allows types to be converted
//! into OpenTelemetry [`Value`]s for use as span or event attributes.
//!
//! The trait returns `Option<Value>` to support optional attributes - if `None` is
//! returned, the attribute is skipped rather than set to a null value.

use opentelemetry::Value;

/// Converts a value into an OpenTelemetry [`Value`] for use as an attribute.
///
/// Returns `Option<Value>` to support optional attributes. When the result is
/// `None`, the OTel attribute will be skipped entirely.
///
/// # Implementing for Custom Types
///
/// ```ignore
/// use apollo_opentelemetry::ToValue;
/// use opentelemetry::Value;
///
/// struct UserId(u64);
///
/// impl ToValue for UserId {
///     fn to_value(&self) -> Option<Value> {
///         Some(Value::I64(self.0 as i64))
///     }
/// }
/// ```
pub trait ToValue {
    /// Converts this value into an OpenTelemetry [`Value`].
    ///
    /// Returns `None` if the attribute should be skipped.
    fn to_value(&self) -> Option<Value>;
}

// ============ Blanket impl for references ============

impl<T: ToValue + ?Sized> ToValue for &T {
    fn to_value(&self) -> Option<Value> {
        (*self).to_value()
    }
}

// ============ Option implementation ============

impl<T: ToValue> ToValue for Option<T> {
    fn to_value(&self) -> Option<Value> {
        self.as_ref().and_then(|val| val.to_value())
    }
}

// ============ Primitive implementations ============

macro_rules! impl_to_value_copy {
    ($ty:ty => $variant:ident) => {
        impl ToValue for $ty {
            fn to_value(&self) -> Option<Value> {
                Some(Value::$variant(*self))
            }
        }
    };
    ($ty:ty => $variant:ident as $cast:ty) => {
        impl ToValue for $ty {
            fn to_value(&self) -> Option<Value> {
                Some(Value::$variant(*self as $cast))
            }
        }
    };
}

impl ToValue for str {
    fn to_value(&self) -> Option<Value> {
        Some(Value::String(self.to_string().into()))
    }
}

impl ToValue for String {
    fn to_value(&self) -> Option<Value> {
        Some(Value::String(self.clone().into()))
    }
}

impl_to_value_copy!(i64 => I64);
impl_to_value_copy!(i32 => I64 as i64);
impl_to_value_copy!(i16 => I64 as i64);
impl_to_value_copy!(i8 => I64 as i64);
impl_to_value_copy!(u64 => I64 as i64);
impl_to_value_copy!(u32 => I64 as i64);
impl_to_value_copy!(u16 => I64 as i64);
impl_to_value_copy!(u8 => I64 as i64);
impl_to_value_copy!(usize => I64 as i64);
impl_to_value_copy!(isize => I64 as i64);

impl_to_value_copy!(f64 => F64);
impl_to_value_copy!(f32 => F64 as f64);

impl_to_value_copy!(bool => Bool);

// ============ Value pass-through ============

impl ToValue for Value {
    fn to_value(&self) -> Option<Value> {
        Some(self.clone())
    }
}

// ============ HTTP type implementations ============

#[cfg(feature = "http")]
mod http_impl {
    use super::{ToValue, Value};

    impl ToValue for http::Method {
        fn to_value(&self) -> Option<Value> {
            Some(Value::String(self.to_string().into()))
        }
    }

    impl ToValue for http::StatusCode {
        fn to_value(&self) -> Option<Value> {
            Some(Value::I64(self.as_u16() as i64))
        }
    }

    impl ToValue for http::Version {
        fn to_value(&self) -> Option<Value> {
            Some(Value::String(format!("{self:?}").into()))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_string_types() {
        let hello: &str = "hello";
        assert_eq!(
            ToValue::to_value(&hello),
            Some(Value::String("hello".into()))
        );
        assert_eq!(
            ToValue::to_value(&String::from("world")),
            Some(Value::String("world".into()))
        );
    }

    #[test]
    fn test_integer_types() {
        assert_eq!(ToValue::to_value(&42i64), Some(Value::I64(42)));
        assert_eq!(ToValue::to_value(&42i32), Some(Value::I64(42)));
        assert_eq!(ToValue::to_value(&42i16), Some(Value::I64(42)));
        assert_eq!(ToValue::to_value(&42i8), Some(Value::I64(42)));
        assert_eq!(ToValue::to_value(&42u64), Some(Value::I64(42)));
        assert_eq!(ToValue::to_value(&42u32), Some(Value::I64(42)));
        assert_eq!(ToValue::to_value(&42u16), Some(Value::I64(42)));
        assert_eq!(ToValue::to_value(&42u8), Some(Value::I64(42)));
    }

    #[test]
    fn test_float_types() {
        assert_eq!(ToValue::to_value(&2.5f64), Some(Value::F64(2.5)));
        assert_eq!(ToValue::to_value(&2.5f32), Some(Value::F64(2.5f32 as f64)));
    }

    #[test]
    fn test_bool() {
        assert_eq!(ToValue::to_value(&true), Some(Value::Bool(true)));
        assert_eq!(ToValue::to_value(&false), Some(Value::Bool(false)));
    }

    #[test]
    fn test_option_some() {
        let value: Option<i64> = Some(42);
        assert_eq!(ToValue::to_value(&value), Some(Value::I64(42)));
    }

    #[test]
    fn test_option_none() {
        let value: Option<i64> = None;
        assert_eq!(ToValue::to_value(&value), None);
    }

    #[test]
    fn test_option_some_string() {
        let value: Option<String> = Some("hello".to_string());
        assert_eq!(
            ToValue::to_value(&value),
            Some(Value::String("hello".into()))
        );
    }

    #[test]
    fn test_double_reference() {
        let value = 42i64;
        let ref1 = &value;
        let ref2 = &ref1;
        assert_eq!(ToValue::to_value(ref2), Some(Value::I64(42)));
    }

    #[test]
    fn test_value_passthrough() {
        let value = Value::String("test".into());
        assert_eq!(
            ToValue::to_value(&value),
            Some(Value::String("test".into()))
        );
    }
}

// ============ Tonic/gRPC type implementations ============

#[cfg(feature = "grpc")]
mod tonic_impl {
    use super::{ToValue, Value};

    impl ToValue for tonic::Code {
        fn to_value(&self) -> Option<Value> {
            // OTel semantic convention: rpc.grpc.status_code is an integer
            Some(Value::I64(i32::from(*self) as i64))
        }
    }

    impl ToValue for tonic::Status {
        fn to_value(&self) -> Option<Value> {
            self.code().to_value()
        }
    }

    // MetadataKey for ASCII metadata
    impl ToValue for tonic::metadata::MetadataKey<tonic::metadata::Ascii> {
        fn to_value(&self) -> Option<Value> {
            Some(Value::String(self.as_str().to_string().into()))
        }
    }

    // MetadataKey for binary metadata
    impl ToValue for tonic::metadata::MetadataKey<tonic::metadata::Binary> {
        fn to_value(&self) -> Option<Value> {
            Some(Value::String(self.as_str().to_string().into()))
        }
    }

    // MetadataValue for ASCII - convert to owned string
    impl ToValue for tonic::metadata::MetadataValue<tonic::metadata::Ascii> {
        fn to_value(&self) -> Option<Value> {
            self.to_str()
                .ok()
                .map(|s| Value::String(s.to_string().into()))
        }
    }

    // Binary metadata values are converted to base64
    impl ToValue for tonic::metadata::MetadataValue<tonic::metadata::Binary> {
        fn to_value(&self) -> Option<Value> {
            use base64::Engine;
            let bytes = self.as_encoded_bytes();
            let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
            Some(Value::String(encoded.into()))
        }
    }
}

#[cfg(all(test, feature = "http"))]
mod http_tests {
    use super::*;

    #[test]
    fn test_http_method() {
        assert_eq!(
            ToValue::to_value(&http::Method::GET),
            Some(Value::String("GET".into()))
        );
        assert_eq!(
            ToValue::to_value(&http::Method::POST),
            Some(Value::String("POST".into()))
        );
    }

    #[test]
    fn test_http_status_code() {
        assert_eq!(
            ToValue::to_value(&http::StatusCode::OK),
            Some(Value::I64(200))
        );
        assert_eq!(
            ToValue::to_value(&http::StatusCode::NOT_FOUND),
            Some(Value::I64(404))
        );
    }

    #[test]
    fn test_http_version() {
        assert_eq!(
            ToValue::to_value(&http::Version::HTTP_11),
            Some(Value::String("HTTP/1.1".into()))
        );
        assert_eq!(
            ToValue::to_value(&http::Version::HTTP_2),
            Some(Value::String("HTTP/2.0".into()))
        );
    }

    #[test]
    fn test_http_option() {
        let method: Option<http::Method> = Some(http::Method::GET);
        assert_eq!(
            ToValue::to_value(&method),
            Some(Value::String("GET".into()))
        );

        let no_method: Option<http::Method> = None;
        assert_eq!(ToValue::to_value(&no_method), None);
    }
}

#[cfg(all(test, feature = "grpc"))]
mod tonic_tests {
    use super::*;

    #[test]
    fn test_tonic_code() {
        // OTel semantic convention: rpc.grpc.status_code is an integer
        assert_eq!(ToValue::to_value(&tonic::Code::Ok), Some(Value::I64(0)));
        assert_eq!(
            ToValue::to_value(&tonic::Code::InvalidArgument),
            Some(Value::I64(3))
        );
        assert_eq!(
            ToValue::to_value(&tonic::Code::NotFound),
            Some(Value::I64(5))
        );
    }

    #[test]
    fn test_tonic_status() {
        let status = tonic::Status::not_found("resource missing");
        assert_eq!(
            ToValue::to_value(&status),
            Some(Value::I64(5)) // NOT_FOUND = 5
        );
    }

    #[test]
    fn test_tonic_metadata_key() {
        let key: tonic::metadata::MetadataKey<tonic::metadata::Ascii> =
            "x-request-id".parse().unwrap();
        assert_eq!(
            ToValue::to_value(&key),
            Some(Value::String("x-request-id".into()))
        );
    }

    #[test]
    fn test_tonic_metadata_value_ascii() {
        let value: tonic::metadata::MetadataValue<tonic::metadata::Ascii> =
            "test-value".parse().unwrap();
        assert_eq!(
            ToValue::to_value(&value),
            Some(Value::String("test-value".into()))
        );
    }

    #[test]
    fn test_tonic_code_option() {
        let code: Option<tonic::Code> = Some(tonic::Code::Ok);
        assert_eq!(ToValue::to_value(&code), Some(Value::I64(0)));

        let no_code: Option<tonic::Code> = None;
        assert_eq!(ToValue::to_value(&no_code), None);
    }
}