From ac7419447cfafd022cf78c479b243b625d655c28 Mon Sep 17 00:00:00 2001 From: LorenzoTettamanti Date: Sun, 3 May 2026 13:55:43 +0200 Subject: [PATCH 1/8] (opentelemetry-proto): added Serialization and Deserialization for Option type. Added tests in json_serde.rs for HistogramDataPoint.sum,HistogramDataPoint.min,HistogramDataPoint.max --- opentelemetry-proto/src/proto.rs | 81 ++++++ .../tonic/opentelemetry.proto.metrics.v1.rs | 46 ++- opentelemetry-proto/tests/grpc_build.rs | 12 + opentelemetry-proto/tests/json_serde.rs | 264 ++++++++++++++++-- 4 files changed, 370 insertions(+), 33 deletions(-) diff --git a/opentelemetry-proto/src/proto.rs b/opentelemetry-proto/src/proto.rs index 892d8f10c2..3907dbe4a5 100644 --- a/opentelemetry-proto/src/proto.rs +++ b/opentelemetry-proto/src/proto.rs @@ -8,6 +8,7 @@ pub(crate) mod serializers { use serde::de::{self, MapAccess, Visitor}; use serde::ser::{SerializeMap, SerializeSeq, SerializeStruct}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::borrow::Cow; use std::fmt; pub fn is_default(value: &T) -> bool @@ -416,6 +417,86 @@ pub(crate) mod serializers { deserializer.deserialize_any(F64Visitor) } + + // special serialize for option types for Nan, Infinity and -Infinity + + pub fn serialize_option_f64_special( + value: &Option, + serializer: S, + ) -> Result + where + S: Serializer, + { + match value { + Some(v) => { + if v.is_nan() { + serializer.serialize_str("NaN") + } else if v.is_infinite() { + if v.is_sign_positive() { + serializer.serialize_str("Infinity") + } else { + serializer.serialize_str("-Infinity") + } + } else { + serializer.serialize_some(v) + } + } + None => serializer.serialize_none(), + } + } + // deserializer method + pub fn deserialize_option_f64_special<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + struct OptionF64Visitor; + + impl<'de> de::Visitor<'de> for OptionF64Visitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a float or a string representing NaN, Infinity, or -Infinity") + } + + fn visit_f64(self, value: f64) -> Result, E> + where + E: de::Error, + { + Ok(Some((value as f64))) + } + + fn visit_u64(self, value: u64) -> Result, E> + where + E: de::Error, + { + Ok(Some(value as f64)) + } + + fn visit_i64(self, value: i64) -> Result, E> + where + E: de::Error, + { + Ok(Some(value as f64)) + } + + fn visit_str(self, value: &str) -> Result, E> + where + E: de::Error, + { + match value { + "NaN" => Ok(Some(f64::NAN)), + "Infinity" => Ok(Some(f64::INFINITY)), + "-Infinity" => Ok(Some(f64::NEG_INFINITY)), + _ => return Err(E::custom(format!( + "invalid string for f64: expected a number, NaN, Infinity, or -Infinity but got '{}'", + value + ))) + } + } + } + + deserializer.deserialize_any(OptionF64Visitor) + } } #[cfg(feature = "gen-tonic-messages")] diff --git a/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs b/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs index aeb68f94a2..6038bcd76a 100644 --- a/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs +++ b/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs @@ -466,6 +466,13 @@ pub struct HistogramDataPoint { /// doing so. This is specifically to enforce compatibility w/ OpenMetrics, /// see: #[prost(double, optional, tag = "5")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_option_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_option_f64_special" + ) + )] pub sum: ::core::option::Option, /// bucket_counts is an optional field contains the count values of histogram /// for each bucket. @@ -513,9 +520,23 @@ pub struct HistogramDataPoint { pub flags: u32, /// min is the minimum value over (start_time, end_time\]. #[prost(double, optional, tag = "11")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_option_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_option_f64_special" + ) + )] pub min: ::core::option::Option, /// max is the maximum value over (start_time, end_time\]. #[prost(double, optional, tag = "12")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_option_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_option_f64_special" + ) + )] pub max: ::core::option::Option, } /// ExponentialHistogramDataPoint is a single data point in a timeseries that describes the @@ -582,6 +603,13 @@ pub struct ExponentialHistogramDataPoint { /// doing so. This is specifically to enforce compatibility w/ OpenMetrics, /// see: #[prost(double, optional, tag = "5")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_option_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_option_f64_special" + ) + )] pub sum: ::core::option::Option, /// scale describes the resolution of the histogram. Boundaries are /// located at powers of the base, where: @@ -633,9 +661,23 @@ pub struct ExponentialHistogramDataPoint { pub exemplars: ::prost::alloc::vec::Vec, /// The minimum value over (start_time, end_time\]. #[prost(double, optional, tag = "12")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_option_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_option_f64_special" + ) + )] pub min: ::core::option::Option, /// The maximum value over (start_time, end_time\]. #[prost(double, optional, tag = "13")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_option_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_option_f64_special" + ) + )] pub max: ::core::option::Option, /// ZeroThreshold may be optionally set to convey the width of the zero /// region. Where the zero region is defined as the closed interval @@ -806,9 +848,7 @@ pub struct Exemplar { /// recorded alongside the original measurement. Only key/value pairs that were /// filtered out by the aggregator should be included #[prost(message, repeated, tag = "7")] - pub filtered_attributes: ::prost::alloc::vec::Vec< - super::super::common::v1::KeyValue, - >, + pub filtered_attributes: ::prost::alloc::vec::Vec, /// time_unix_nano is the exact time when this exemplar was recorded /// /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January diff --git a/opentelemetry-proto/tests/grpc_build.rs b/opentelemetry-proto/tests/grpc_build.rs index 102274b980..40db07d7f0 100644 --- a/opentelemetry-proto/tests/grpc_build.rs +++ b/opentelemetry-proto/tests/grpc_build.rs @@ -184,6 +184,18 @@ fn build_tonic() { ); } + //Special serializers and deserializers for Option floating point fields + for path in [ + "metrics.v1.HistogramDataPoint.sum", + "metrics.v1.HistogramDataPoint.min", + "metrics.v1.HistogramDataPoint.max", + "metrics.v1.ExponentialHistogramDataPoint.sum", + "metrics.v1.ExponentialHistogramDataPoint.min", + "metrics.v1.ExponentialHistogramDataPoint.max", + ] { + builder = builder.field_attribute(path, "#[cfg_attr(feature = \"with-serde\",serde(serialize_with = \"crate::proto::serializers::serialize_option_f64_special\", deserialize_with=\"crate::proto::serializers::deserialize_option_f64_special\"))]") + } + // special serializer and deserializer for value // The Value::value field must be hidden builder = builder diff --git a/opentelemetry-proto/tests/json_serde.rs b/opentelemetry-proto/tests/json_serde.rs index 49a9d93d3b..55ddd5d3c6 100644 --- a/opentelemetry-proto/tests/json_serde.rs +++ b/opentelemetry-proto/tests/json_serde.rs @@ -1577,36 +1577,68 @@ mod json_serde { }), scope_metrics: vec![ScopeMetrics { scope: None, - metrics: vec![Metric { - name: String::from("example_metric"), - description: String::from("A sample metric with NaN values"), - unit: String::from("1"), - metadata: vec![], - data: Some( - opentelemetry_proto::tonic::metrics::v1::metric::Data::Summary( - Summary { - data_points: vec![SummaryDataPoint { - attributes: vec![], - start_time_unix_nano: 0, - time_unix_nano: 0, - count: 100, - sum: 500.0, - quantile_values: vec![ - ValueAtQuantile { - quantile: 0.5, - value: f64::NAN, - }, - ValueAtQuantile { - quantile: 0.9, - value: f64::NAN, - }, - ], - flags: 0, - }], - }, + metrics: vec![ + Metric { + name: String::from("example_metric"), + description: String::from("A sample metric with NaN values"), + unit: String::from("1"), + metadata: vec![], + data: Some( + opentelemetry_proto::tonic::metrics::v1::metric::Data::Summary( + Summary { + data_points: vec![SummaryDataPoint { + attributes: vec![], + start_time_unix_nano: 0, + time_unix_nano: 0, + count: 100, + sum: 500.0, + quantile_values: vec![ + ValueAtQuantile { + quantile: 0.5, + value: f64::NAN, + }, + ValueAtQuantile { + quantile: 0.9, + value: f64::NAN, + }, + ], + flags: 0, + }], + }, + ), ), - ), - }], + }, + Metric { + name: String::from("my.histogram"), + description: String::from("I am an Histogram with NaN values"), + unit: String::from("1"), + metadata: vec![], + data: Some(Data::Histogram(Histogram { + data_points: vec![HistogramDataPoint { + attributes: vec![KeyValue { + key: String::from("my.histogram.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }], + start_time_unix_nano: 1544712660300000000, + time_unix_nano: 1544712660300000000, + count: 2, + sum: Some(f64::NAN), + bucket_counts: vec![1, 1], + explicit_bounds: vec![1.0], + exemplars: vec![], + flags: 0, + min: Some(f64::NAN), + max: Some(f64::NAN), + }], + aggregation_temporality: 1, + })), + }, + ], schema_url: String::new(), }], schema_url: String::new(), @@ -1659,7 +1691,43 @@ mod json_serde { } ] } + }, + { + "name": "my.histogram", + "description": "I am an Histogram with NaN values", + "unit": "1", + "metadata": [], + "histogram": { + "dataPoints": [ + { + "attributes": [ + { + "key": "my.histogram.attr", + "value": { + "stringValue": "some value" + } + } + ], + "startTimeUnixNano": "1544712660300000000", + "timeUnixNano": "1544712660300000000", + "count": "2", + "sum": "NaN", + "bucketCounts": [ + "1", + "1" + ], + "explicitBounds": [ + 1.0 + ], + "exemplars": [], + "flags": 0, + "min": "NaN", + "max": "NaN" + } + ], + "aggregationTemporality": 1 } + } ], "schemaUrl": "" } @@ -1711,7 +1779,7 @@ mod json_serde { let scope_metric = &resource_metric.scope_metrics[0]; assert!(scope_metric.scope.is_none()); - assert_eq!(scope_metric.metrics.len(), 1); + assert_eq!(scope_metric.metrics.len(), 2); let metric = &scope_metric.metrics[0]; assert_eq!(metric.name, "example_metric"); @@ -1740,6 +1808,26 @@ mod json_serde { } else { panic!("Expected metric data to be of type Summary"); } + let histogram_metric = &actual.resource_metrics[0].scope_metrics[0].metrics[1]; + assert_eq!(histogram_metric.name, "my.histogram"); + assert_eq!(histogram_metric.unit, "1"); + if let Some(opentelemetry_proto::tonic::metrics::v1::metric::Data::Histogram(hist)) = + &histogram_metric.data + { + assert_eq!(hist.data_points.len(), 1); + let data_point = &hist.data_points[0]; + assert_eq!(data_point.attributes.len(), 1); + assert_eq!(data_point.start_time_unix_nano, 1544712660300000000); + assert_eq!(data_point.time_unix_nano, 1544712660300000000); + assert_eq!(data_point.count, 2); + + // Checking special NaN quantile values + assert!(data_point.sum.unwrap().is_nan()); + assert!(data_point.min.unwrap().is_nan()); + assert!(data_point.max.unwrap().is_nan()); + } else { + panic!("Expected histogram data"); + } } } @@ -1878,4 +1966,120 @@ mod json_serde { } } } + + #[cfg(feature = "metrics")] + mod metrics_with_infinite { + use super::*; + use opentelemetry_proto::tonic::metrics::v1::Histogram; + use opentelemetry_proto::tonic::metrics::v1::HistogramDataPoint; + + fn value_with_infinite() -> ExportMetricsServiceRequest { + ExportMetricsServiceRequest { + resource_metrics: vec![ResourceMetrics { + resource: None, + scope_metrics: vec![ScopeMetrics { + scope: None, + metrics: vec![Metric { + name: String::from("infinite_metric"), + description: String::from("Metric with infinity values"), + unit: String::from("1"), + metadata: vec![], + data: Some( + opentelemetry_proto::tonic::metrics::v1::metric::Data::Histogram( + Histogram { + data_points: vec![HistogramDataPoint { + attributes: vec![], + start_time_unix_nano: 0, + time_unix_nano: 0, + count: 1, + sum: Some(f64::INFINITY), + bucket_counts: vec![1], + explicit_bounds: vec![], + exemplars: vec![], + flags: 0, + min: Some(f64::NEG_INFINITY), + max: Some(f64::INFINITY), + }], + aggregation_temporality: 1, + }, + ), + ), + }], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + } + } + + // language=json + const CANONICAL_WITH_INFINITE: &str = r#"{ + "resourceMetrics": [ + { + "resource": null, + "scopeMetrics": [ + { + "scope": null, + "metrics": [ + { + "name": "infinite_metric", + "description": "Metric with infinity values", + "unit": "1", + "metadata": [], + "histogram": { + "dataPoints": [ + { + "attributes": [], + "startTimeUnixNano": "0", + "timeUnixNano": "0", + "count": "1", + "sum": "Infinity", + "bucketCounts": ["1"], + "explicitBounds": [], + "exemplars": [], + "flags": 0, + "min": "-Infinity", + "max": "Infinity" + } + ], + "aggregationTemporality": 1 + } + } + ], + "schemaUrl": "" + } + ], + "schemaUrl": "" + } + ] + }"#; + + #[test] + fn serialize_with_infinite_values() { + let input = value_with_infinite(); + let actual = serde_json::to_string_pretty(&input).unwrap(); + let actual_value: serde_json::Value = serde_json::from_str(&actual).unwrap(); + let expected_value: serde_json::Value = + serde_json::from_str(CANONICAL_WITH_INFINITE).unwrap(); + assert_eq!(actual_value, expected_value); + } + + #[test] + fn deserialize_with_infinite_values() { + let actual: ExportMetricsServiceRequest = serde_json::from_str(CANONICAL_WITH_INFINITE) + .expect("deserialization must succeed"); + + let metric = &actual.resource_metrics[0].scope_metrics[0].metrics[0]; + if let Some(opentelemetry_proto::tonic::metrics::v1::metric::Data::Histogram(hist)) = + &metric.data + { + let dp = &hist.data_points[0]; + assert_eq!(dp.sum, Some(f64::INFINITY)); + assert_eq!(dp.min, Some(f64::NEG_INFINITY)); + assert_eq!(dp.max, Some(f64::INFINITY)); + } else { + panic!("Expected Histogram data"); + } + } + } } From 71bffa6fbd980039a522d77f0df53c4c1eb2798e Mon Sep 17 00:00:00 2001 From: LorenzoTettamanti Date: Thu, 7 May 2026 01:35:30 +0200 Subject: [PATCH 2/8] added cfg_attr to grpc_build.rs for metrics.v1.SummaryDataPoint.sum,metrics.v1.SummaryDataPoint.value,metrics.v1.Exemplar.value,metrics.v1.ExponentialHistogramDataPoint.zeroThreshold --- opentelemetry-proto/tests/grpc_build.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/opentelemetry-proto/tests/grpc_build.rs b/opentelemetry-proto/tests/grpc_build.rs index 40db07d7f0..99b1badf7f 100644 --- a/opentelemetry-proto/tests/grpc_build.rs +++ b/opentelemetry-proto/tests/grpc_build.rs @@ -177,6 +177,10 @@ fn build_tonic() { // metrics "metrics.v1.SummaryDataPoint.ValueAtQuantile.value", "metrics.v1.SummaryDataPoint.ValueAtQuantile.quantile", + "metrics.v1.ExponentialHistogramDataPoint.zeroThreshold", + "metrics.v1.SummaryDataPoint.sum", + "metrics.v1.NumberDataPoint.value", + "metrics.v1.Exemplar.value", ] { builder = builder.field_attribute( path, From 20e39252e1b8528cb66d45122656b356e6499212 Mon Sep 17 00:00:00 2001 From: LorenzoTettamanti Date: Thu, 7 May 2026 01:36:25 +0200 Subject: [PATCH 3/8] added cfg_attr for metrics.v1.SummaryDataPoint.sum,metrics.v1.SummaryDataPoint.value,metrics.v1.Exemplar.value,metrics.v1.ExponentialHistogramDataPoint.zeroThreshold --- .../tonic/opentelemetry.proto.metrics.v1.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs b/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs index 6038bcd76a..08e66b2d2c 100644 --- a/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs +++ b/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs @@ -390,6 +390,13 @@ pub mod number_data_point { #[derive(Clone, Copy, PartialEq, ::prost::Oneof)] pub enum Value { #[prost(double, tag = "4")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_f64_special" + ) + )] AsDouble(f64), #[prost(sfixed64, tag = "6")] AsInt(i64), @@ -686,6 +693,13 @@ pub struct ExponentialHistogramDataPoint { /// expressed using the standard exponential formula as well as values that /// have been rounded to zero. #[prost(double, tag = "14")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_f64_special" + ) + )] pub zero_threshold: f64, } /// Nested message and enum types in `ExponentialHistogramDataPoint`. @@ -784,6 +798,13 @@ pub struct SummaryDataPoint { /// doing so. This is specifically to enforce compatibility w/ OpenMetrics, /// see: #[prost(double, tag = "5")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_f64_special" + ) + )] pub sum: f64, /// (Optional) list of values at different quantiles of the distribution calculated /// from the current snapshot. The quantiles must be strictly increasing. @@ -903,6 +924,13 @@ pub mod exemplar { #[derive(Clone, Copy, PartialEq, ::prost::Oneof)] pub enum Value { #[prost(double, tag = "3")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_f64_special" + ) + )] AsDouble(f64), #[prost(sfixed64, tag = "6")] AsInt(i64), From f2f4fd9d4c46603a0de8c26cc8017a4daf279a85 Mon Sep 17 00:00:00 2001 From: LorenzoTettamanti Date: Thu, 7 May 2026 01:39:25 +0200 Subject: [PATCH 4/8] extended test coverage for Exemplar NaN metrics. Added exponential histogram NaN metrics test,Added gauge NaN metrics test --- opentelemetry-proto/tests/json_serde.rs | 434 +++++++++++++++++++++++- 1 file changed, 424 insertions(+), 10 deletions(-) diff --git a/opentelemetry-proto/tests/json_serde.rs b/opentelemetry-proto/tests/json_serde.rs index 55ddd5d3c6..4f320ff85a 100644 --- a/opentelemetry-proto/tests/json_serde.rs +++ b/opentelemetry-proto/tests/json_serde.rs @@ -14,8 +14,9 @@ mod json_serde { use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs}; #[cfg(feature = "metrics")] use opentelemetry_proto::tonic::metrics::v1::{ - metric::Data, number_data_point::Value as MetricValue, Gauge, Histogram, - HistogramDataPoint, Metric, NumberDataPoint, ResourceMetrics, ScopeMetrics, Sum, + exemplar::Value as ExemplarValue, metric::Data, number_data_point::Value as MetricValue, + ExponentialHistogramDataPoint, Gauge, Histogram, HistogramDataPoint, Metric, + NumberDataPoint, ResourceMetrics, ScopeMetrics, Sum, }; use opentelemetry_proto::tonic::resource::v1::Resource; #[cfg(feature = "trace")] @@ -1559,7 +1560,11 @@ mod json_serde { #[cfg(feature = "metrics")] mod metrics_with_nan { use super::*; + use opentelemetry_proto::tonic::common::v1::any_value::Value; + use opentelemetry_proto::tonic::metrics::v1::exponential_histogram_data_point::Buckets; use opentelemetry_proto::tonic::metrics::v1::summary_data_point::ValueAtQuantile; + use opentelemetry_proto::tonic::metrics::v1::Exemplar; + use opentelemetry_proto::tonic::metrics::v1::ExponentialHistogram; use opentelemetry_proto::tonic::metrics::v1::Summary; use opentelemetry_proto::tonic::metrics::v1::SummaryDataPoint; @@ -1591,7 +1596,7 @@ mod json_serde { start_time_unix_nano: 0, time_unix_nano: 0, count: 100, - sum: 500.0, + sum: f64::NAN, quantile_values: vec![ ValueAtQuantile { quantile: 0.5, @@ -1630,7 +1635,21 @@ mod json_serde { sum: Some(f64::NAN), bucket_counts: vec![1, 1], explicit_bounds: vec![1.0], - exemplars: vec![], + exemplars: vec![Exemplar { + filtered_attributes: vec![KeyValue { + key: String::from("my.histogram.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }], + time_unix_nano: 1544712660300000000, + span_id: vec![], + trace_id: vec![], + value: Some(ExemplarValue::AsDouble(f64::NAN)), + }], flags: 0, min: Some(f64::NAN), max: Some(f64::NAN), @@ -1638,6 +1657,139 @@ mod json_serde { aggregation_temporality: 1, })), }, + Metric { + name: String::from("my.exponential.histogram"), + description: String::from( + "I am an exponential Histogram with NaN values", + ), + unit: String::from("1"), + metadata: vec![], + data: Some(Data::ExponentialHistogram(ExponentialHistogram { + data_points: vec![ExponentialHistogramDataPoint { + attributes: vec![KeyValue { + key: String::from("my.exp.histogram.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }], + start_time_unix_nano: 1544712660300000000, + time_unix_nano: 1544712660300000000, + count: 2, + sum: Some(f64::NAN), + scale: 1, + zero_count: 0, + exemplars: vec![Exemplar { + filtered_attributes: vec![KeyValue { + key: String::from("my.histogram.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }], + time_unix_nano: 1544712660300000000, + span_id: vec![], + trace_id: vec![], + value: Some(ExemplarValue::AsDouble(f64::NAN)), + }], + flags: 0, + min: Some(f64::NAN), + max: Some(f64::NAN), + positive: Some(Buckets { + offset: 0, + bucket_counts: vec![], + }), + negative: Some(Buckets { + offset: 0, + bucket_counts: vec![], + }), + zero_threshold: f64::NAN, + }], + aggregation_temporality: 1, + })), + }, + Metric { + name: String::from("my.counter"), + description: String::from("I am a Counter"), + unit: String::from("1"), + metadata: vec![], + data: Some(Data::Sum(Sum { + data_points: vec![NumberDataPoint { + attributes: vec![KeyValue { + key: String::from("my.counter.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }], + start_time_unix_nano: 1544712660300000000, + time_unix_nano: 1544712660300000000, + exemplars: vec![Exemplar { + filtered_attributes: vec![KeyValue { + key: String::from("my.histogram.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }], + time_unix_nano: 1544712660300000000, + span_id: vec![], + trace_id: vec![], + value: Some(ExemplarValue::AsDouble(f64::NAN)), + }], + flags: 0, + value: Some(MetricValue::AsDouble(f64::NAN)), + }], + aggregation_temporality: 1, + is_monotonic: true, + })), + }, + Metric { + name: String::from("my.gauge"), + description: String::from("I am a Gauge"), + unit: String::from("1"), + metadata: vec![], + data: Some(Data::Gauge(Gauge { + data_points: vec![NumberDataPoint { + attributes: vec![KeyValue { + key: String::from("my.gauge.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }], + start_time_unix_nano: 0, + time_unix_nano: 1544712660300000000, + exemplars: vec![Exemplar { + filtered_attributes: vec![KeyValue { + key: String::from("my.histogram.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }], + time_unix_nano: 1544712660300000000, + span_id: vec![], + trace_id: vec![], + value: Some(ExemplarValue::AsDouble(f64::NAN)), + }], + flags: 0, + value: Some(MetricValue::AsDouble(f64::NAN)), + }], + })), + }, ], schema_url: String::new(), }], @@ -1676,7 +1828,7 @@ mod json_serde { "startTimeUnixNano": "0", "timeUnixNano": "0", "count": "100", - "sum": 500.0, + "sum": "NaN", "quantileValues": [ { "quantile": 0.5, @@ -1719,15 +1871,169 @@ mod json_serde { "explicitBounds": [ 1.0 ], - "exemplars": [], + "exemplars": [ + { + "filteredAttributes": [ + { + "key": "my.histogram.attr", + "value": { + "stringValue": "some value" + } + } + ], + "timeUnixNano": "1544712660300000000", + "traceId": "", + "spanId": "", + "value": { + "asDouble": "NaN" + } + } + ], "flags": 0, "min": "NaN", "max": "NaN" } ], - "aggregationTemporality": 1 + "aggregationTemporality": 1 + } + }, + { + "name": "my.exponential.histogram", + "description": "I am an exponential Histogram with NaN values", + "unit": "1", + "metadata": [], + "exponentialHistogram": { + "dataPoints": [ + { + "attributes": [ + { + "key": "my.exp.histogram.attr", + "value": { + "stringValue": "some value" + } + } + ], + "startTimeUnixNano": "1544712660300000000", + "timeUnixNano": "1544712660300000000", + "count": "2", + "sum": "NaN", + "scale": 1, + "zeroCount": "0", + "positive": {"offset":0,"bucketCounts":[]}, + "negative": {"offset":0,"bucketCounts":[]}, + "flags": 0, + "exemplars": [ + { + "filteredAttributes": [ + { + "key": "my.histogram.attr", + "value": { + "stringValue": "some value" + } + } + ], + "timeUnixNano": "1544712660300000000", + "traceId": "", + "spanId": "", + "value": { + "asDouble": "NaN" + } + } + ], + "min": "NaN", + "max": "NaN", + "zeroThreshold": "NaN" + } + ], + "aggregationTemporality": 1 + } + }, + { + "name": "my.counter", + "description": "I am a Counter", + "unit": "1", + "metadata": [], + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "my.counter.attr", + "value": { + "stringValue": "some value" + } + } + ], + "startTimeUnixNano": "1544712660300000000", + "timeUnixNano": "1544712660300000000", + "exemplars": [ + { + "filteredAttributes": [ + { + "key": "my.histogram.attr", + "value": { + "stringValue": "some value" + } + } + ], + "timeUnixNano": "1544712660300000000", + "traceId": "", + "spanId": "", + "value": { + "asDouble": "NaN" + } + } + ], + "flags": 0, + "asDouble": "NaN" + } + ], + "aggregationTemporality": 1, + "isMonotonic": true + } + }, + { + "name": "my.gauge", + "description": "I am a Gauge", + "unit": "1", + "metadata": [], + "gauge": { + "dataPoints": [ + { + "attributes": [ + { + "key": "my.gauge.attr", + "value": { + "stringValue": "some value" + } + } + ], + "startTimeUnixNano": "0", + "timeUnixNano": "1544712660300000000", + "exemplars": [ + { + "filteredAttributes": [ + { + "key": "my.histogram.attr", + "value": { + "stringValue": "some value" + } + } + ], + "timeUnixNano": "1544712660300000000", + "traceId": "", + "spanId": "", + "value": { + "asDouble": "NaN" + } + } + ], + "flags": 0, + "asDouble": "NaN" + } + ] + } } - } ], "schemaUrl": "" } @@ -1779,7 +2085,7 @@ mod json_serde { let scope_metric = &resource_metric.scope_metrics[0]; assert!(scope_metric.scope.is_none()); - assert_eq!(scope_metric.metrics.len(), 2); + assert_eq!(scope_metric.metrics.len(), 5); let metric = &scope_metric.metrics[0]; assert_eq!(metric.name, "example_metric"); @@ -1796,7 +2102,7 @@ mod json_serde { assert_eq!(data_point.start_time_unix_nano, 0); assert_eq!(data_point.time_unix_nano, 0); assert_eq!(data_point.count, 100); - assert_eq!(data_point.sum, 500.0); + assert!(data_point.sum.is_nan()); assert_eq!(data_point.quantile_values.len(), 2); @@ -1825,9 +2131,117 @@ mod json_serde { assert!(data_point.sum.unwrap().is_nan()); assert!(data_point.min.unwrap().is_nan()); assert!(data_point.max.unwrap().is_nan()); + + assert_eq!(data_point.exemplars.len(), 1); + let exemplar = &data_point.exemplars[0]; + match exemplar.value { + Some(ExemplarValue::AsDouble(val)) => assert!(val.is_nan()), + _ => panic!("Expected double value in exemplar"), + } } else { panic!("Expected histogram data"); } + + let exp_histogram_data_point_metric = + &actual.resource_metrics[0].scope_metrics[0].metrics[2]; + assert_eq!( + exp_histogram_data_point_metric.name, + "my.exponential.histogram" + ); + assert_eq!( + exp_histogram_data_point_metric.description, + "I am an exponential Histogram with NaN values" + ); + assert_eq!(exp_histogram_data_point_metric.unit, "1"); + assert_eq!(exp_histogram_data_point_metric.metadata.len(), 0); + + if let Some( + opentelemetry_proto::tonic::metrics::v1::metric::Data::ExponentialHistogram( + exp_hist, + ), + ) = &exp_histogram_data_point_metric.data + { + assert_eq!(exp_hist.data_points.len(), 1); + let data_point = &exp_hist.data_points[0]; + assert_eq!(data_point.attributes.len(), 1); + assert_eq!(data_point.start_time_unix_nano, 1544712660300000000); + assert_eq!(data_point.time_unix_nano, 1544712660300000000); + assert_eq!(data_point.count, 2); + assert!(data_point.sum.unwrap().is_nan()); + assert_eq!(data_point.scale, 1); + assert_eq!(data_point.zero_count, 0); + assert_eq!(data_point.positive.as_ref().unwrap().offset, 0); + assert_eq!(data_point.positive.as_ref().unwrap().bucket_counts.len(), 0); + assert_eq!(data_point.negative.as_ref().unwrap().bucket_counts.len(), 0); + assert_eq!(data_point.exemplars.len(), 1); + let exemplar = &data_point.exemplars[0]; + match exemplar.value { + Some(ExemplarValue::AsDouble(val)) => assert!(val.is_nan()), + _ => panic!("Expected double value in exemplar"), + } + assert_eq!(data_point.flags, 0); + assert!(data_point.min.unwrap().is_nan()); + assert!(data_point.max.unwrap().is_nan()); + assert!(data_point.zero_threshold.is_nan()); + } else { + panic!("Expected ExponentialHistogram data") + } + let counter_metrics = &actual.resource_metrics[0].scope_metrics[0].metrics[3]; + assert_eq!(counter_metrics.name, "my.counter"); + assert_eq!(counter_metrics.description, "I am a Counter"); + assert_eq!(counter_metrics.unit, "1"); + assert_eq!(counter_metrics.metadata.len(), 0); + if let Some(opentelemetry_proto::tonic::metrics::v1::metric::Data::Sum( + summary_data_point, + )) = &counter_metrics.data + { + let data_points = &summary_data_point.data_points[0]; + assert_eq!(data_points.attributes.len(), 1); + assert_eq!(data_points.start_time_unix_nano, 1544712660300000000); + assert_eq!(data_points.time_unix_nano, 1544712660300000000); + assert_eq!(data_points.exemplars.len(), 1); + let exemplar = &data_points.exemplars[0]; + match exemplar.value { + Some(ExemplarValue::AsDouble(val)) => assert!(val.is_nan()), + _ => panic!("Expected double value in exemplar"), + } + assert_eq!(data_points.flags, 0); + match data_points.value { + Some(MetricValue::AsDouble(val)) => assert!(val.is_nan()), + Some(MetricValue::AsInt(_val)) => (), + None => panic!("Expected double value in counter"), + } + } else { + panic!("Expected Sum data") + } + let gauge_metrics = &actual.resource_metrics[0].scope_metrics[0].metrics[4]; + assert_eq!(gauge_metrics.name, "my.gauge"); + assert_eq!(gauge_metrics.description, "I am a Gauge"); + assert_eq!(gauge_metrics.unit, "1"); + assert_eq!(gauge_metrics.metadata.len(), 0); + if let Some(opentelemetry_proto::tonic::metrics::v1::metric::Data::Gauge( + summary_data_point, + )) = &gauge_metrics.data + { + let data_points = &summary_data_point.data_points[0]; + assert_eq!(data_points.attributes.len(), 1); + assert_eq!(data_points.start_time_unix_nano, 0); + assert_eq!(data_points.time_unix_nano, 1544712660300000000); + assert_eq!(data_points.exemplars.len(), 1); + let exemplar = &data_points.exemplars[0]; + match exemplar.value { + Some(ExemplarValue::AsDouble(val)) => assert!(val.is_nan()), + _ => panic!("Expected double value in exemplar"), + } + assert_eq!(data_points.flags, 0); + match data_points.value { + Some(MetricValue::AsDouble(val)) => assert!(val.is_nan()), + Some(MetricValue::AsInt(_val)) => (), + None => panic!("Expected double value in counter"), + } + } else { + panic!("Expected gauge data") + } } } From c9c6ec8eee491fb75eac81482dc8aacfcc3c1dec Mon Sep 17 00:00:00 2001 From: LorenzoTettamanti Date: Wed, 20 May 2026 22:43:54 +0200 Subject: [PATCH 5/8] (feat): added serialize_vector_f64_special and deserialize_vector_f64_special --- opentelemetry-proto/src/proto.rs | 120 ++++++++++++++++++ .../tonic/opentelemetry.proto.metrics.v1.rs | 7 + 2 files changed, 127 insertions(+) diff --git a/opentelemetry-proto/src/proto.rs b/opentelemetry-proto/src/proto.rs index 3907dbe4a5..c02bdb506b 100644 --- a/opentelemetry-proto/src/proto.rs +++ b/opentelemetry-proto/src/proto.rs @@ -497,6 +497,126 @@ pub(crate) mod serializers { deserializer.deserialize_any(OptionF64Visitor) } + + // serialize vector f64 special entries + pub fn serialize_vector_f64_special( + value: &Vec, + serializer: S, + ) -> Result + where + S: Serializer, + { + let mut sequence = serializer.serialize_seq(Some(value.len()))?; + + for v in value { + if v.is_nan() { + sequence.serialize_element("NaN")?; + } else if v.is_infinite() { + if v.is_sign_positive() { + sequence.serialize_element("Infinity")? + } else { + sequence.serialize_element("-Infinity")? + } + } else { + sequence.serialize_element(v)? + } + } + sequence.end() + } + + // deserialize method for Vec entries + + pub fn deserialize_vector_f64_special<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + //element of a vecf64 + struct F64Visitor; + + impl<'de> de::Visitor<'de> for F64Visitor { + type Value = f64; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a float or a string representing NaN, Infinity, or -Infinity") + } + + fn visit_f64(self, value: f64) -> Result + where + E: de::Error, + { + Ok(value) + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + Ok(value as f64) + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + Ok(value as f64) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + "NaN" => Ok(f64::NAN), + "Infinity" => Ok(f64::INFINITY), + "-Infinity" => Ok(f64::NEG_INFINITY), + _ => value.parse::().map_err(|_| { + E::custom(format!( + "invalid string for f64: expected a number, NaN, Infinity, or -Infinity but got '{}'", + value + )) + }), + } + } + } + + // all vecf64 sequence + struct VecF64Visitor; + impl<'de> de::Visitor<'de> for VecF64Visitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "a vector containing a float or a str representing NaN, Infinity or -Infinity", + ) + } + fn visit_seq(self, mut sequence: E) -> Result, E::Error> + where + E: de::SeqAccess<'de>, + { + let mut v = Vec::with_capacity(sequence.size_hint().unwrap_or(0)); + + while let Some(value) = sequence.next_element_seed(F64ElemSeed)? { + v.push(value); + } + Ok(v) + } + } + + struct F64ElemSeed; + + impl<'de> de::DeserializeSeed<'de> for F64ElemSeed { + type Value = f64; + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(F64Visitor) + } + } + + deserializer.deserialize_seq(VecF64Visitor) + } } #[cfg(feature = "gen-tonic-messages")] diff --git a/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs b/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs index 08e66b2d2c..4ddf7514b4 100644 --- a/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs +++ b/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs @@ -516,6 +516,13 @@ pub struct HistogramDataPoint { /// If bucket_counts length is 0 then explicit_bounds length must also be 0, /// otherwise the data point is invalid. #[prost(double, repeated, tag = "7")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_vector_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_vector_f64_special" + ) + )] pub explicit_bounds: ::prost::alloc::vec::Vec, /// (Optional) List of exemplars collected from /// measurements that were used to form the data point From 10011b64c03f38d82ace1a595416ccdc24cf6a22 Mon Sep 17 00:00:00 2001 From: LorenzoTettamanti Date: Wed, 20 May 2026 22:47:22 +0200 Subject: [PATCH 6/8] (test): added tests for grpc_build.Added tests for json_serde with tests for -Infinity,+Infinity and NaN for Vec type --- opentelemetry-proto/tests/grpc_build.rs | 5 +++++ opentelemetry-proto/tests/json_serde.rs | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/opentelemetry-proto/tests/grpc_build.rs b/opentelemetry-proto/tests/grpc_build.rs index 99b1badf7f..07a6f05aa3 100644 --- a/opentelemetry-proto/tests/grpc_build.rs +++ b/opentelemetry-proto/tests/grpc_build.rs @@ -200,6 +200,11 @@ fn build_tonic() { builder = builder.field_attribute(path, "#[cfg_attr(feature = \"with-serde\",serde(serialize_with = \"crate::proto::serializers::serialize_option_f64_special\", deserialize_with=\"crate::proto::serializers::deserialize_option_f64_special\"))]") } + //Special serializers and deserializers for Vec floating point fields + for path in ["metric.v1.HistogramDataPoint.explicit_bounds"] { + builder = builder.field_attribute(path, "#[cfg_attr(feature = \"with-serde\",serde(serialize_with = \"crate::proto::serializers::serialize_vector_f64_special\", deserialize_with=\"crate::proto::serializers::deserialize_vector_f64_special\"))])") + } + // special serializer and deserializer for value // The Value::value field must be hidden builder = builder diff --git a/opentelemetry-proto/tests/json_serde.rs b/opentelemetry-proto/tests/json_serde.rs index 4f320ff85a..17cc3cbd49 100644 --- a/opentelemetry-proto/tests/json_serde.rs +++ b/opentelemetry-proto/tests/json_serde.rs @@ -1634,7 +1634,7 @@ mod json_serde { count: 2, sum: Some(f64::NAN), bucket_counts: vec![1, 1], - explicit_bounds: vec![1.0], + explicit_bounds: vec![f64::NAN], exemplars: vec![Exemplar { filtered_attributes: vec![KeyValue { key: String::from("my.histogram.attr"), @@ -1869,7 +1869,7 @@ mod json_serde { "1" ], "explicitBounds": [ - 1.0 + "NaN" ], "exemplars": [ { @@ -2408,7 +2408,7 @@ mod json_serde { count: 1, sum: Some(f64::INFINITY), bucket_counts: vec![1], - explicit_bounds: vec![], + explicit_bounds: vec![f64::NEG_INFINITY, f64::INFINITY], exemplars: vec![], flags: 0, min: Some(f64::NEG_INFINITY), @@ -2449,7 +2449,7 @@ mod json_serde { "count": "1", "sum": "Infinity", "bucketCounts": ["1"], - "explicitBounds": [], + "explicitBounds": ["-Infinity","Infinity"], "exemplars": [], "flags": 0, "min": "-Infinity", From 64b2d7fe97920135908bed113ddc5e0fa49ce3e2 Mon Sep 17 00:00:00 2001 From: LorenzoTettamanti Date: Tue, 26 May 2026 00:44:44 +0200 Subject: [PATCH 7/8] added serialization and deserializationfor double values. Updated json_serde test --- opentelemetry-proto/src/proto.rs | 44 +++++++++++++++++++++++-- opentelemetry-proto/tests/json_serde.rs | 20 ++++++++--- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/opentelemetry-proto/src/proto.rs b/opentelemetry-proto/src/proto.rs index c02bdb506b..f588a1920a 100644 --- a/opentelemetry-proto/src/proto.rs +++ b/opentelemetry-proto/src/proto.rs @@ -69,6 +69,22 @@ pub(crate) mod serializers { map.serialize_entry("bytesValue", &base64::encode(b)); map.end() } + Some(Value::DoubleValue(v)) => { + let mut map = serializer.serialize_map(Some(1))?; + if v.is_nan() { + map.serialize_entry("doubleValue", "NaN")?; + } else if v.is_infinite() { + if v.is_sign_positive() { + map.serialize_entry("doubleValue", "Infinity")?; + } else if v.is_sign_negative() { + map.serialize_entry("doubleValue", "-Infinity")?; + } + } else { + map.serialize_entry("doubleValue", &v)?; + } + + map.end() + } Some(value) => value.serialize(serializer), None => serializer.serialize_none(), } @@ -99,6 +115,30 @@ pub(crate) mod serializers { } } + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrFloat { + Float(f64), + String(String), + } + + impl StringOrFloat { + fn get_f64<'de, V>(&self) -> Result + where + V: de::MapAccess<'de>, + { + match self { + Self::Float(val) => Ok(*val), + Self::String(val) => match val.as_str() { + "NaN" => Ok(f64::NAN), + "Infinity" => Ok(f64::INFINITY), + "-Infinity" => Ok(f64::NEG_INFINITY), + _ => val.parse::().map_err(de::Error::custom), + }, + } + } + } + impl<'de> de::Visitor<'de> for ValueVisitor { type Value = Option; @@ -128,8 +168,8 @@ pub(crate) mod serializers { value = Some(any_value::Value::IntValue(int_value)); } "doubleValue" => { - let d = map.next_value()?; - value = Some(any_value::Value::DoubleValue(d)); + let double_value = map.next_value::()?.get_f64::()?; + value = Some(any_value::Value::DoubleValue(double_value)); } "arrayValue" => { let a = map.next_value()?; diff --git a/opentelemetry-proto/tests/json_serde.rs b/opentelemetry-proto/tests/json_serde.rs index 17cc3cbd49..b32e454751 100644 --- a/opentelemetry-proto/tests/json_serde.rs +++ b/opentelemetry-proto/tests/json_serde.rs @@ -1639,9 +1639,7 @@ mod json_serde { filtered_attributes: vec![KeyValue { key: String::from("my.histogram.attr"), value: Some(AnyValue { - value: Some(Value::StringValue(String::from( - "some value", - ))), + value: Some(Value::DoubleValue(f64::NAN)), }), key_strindex: 0, }], @@ -1877,7 +1875,7 @@ mod json_serde { { "key": "my.histogram.attr", "value": { - "stringValue": "some value" + "doubleValue": "NaN" } } ], @@ -2134,8 +2132,20 @@ mod json_serde { assert_eq!(data_point.exemplars.len(), 1); let exemplar = &data_point.exemplars[0]; + assert_eq!(exemplar.filtered_attributes.len(), 1); + let attr = &exemplar.filtered_attributes[0]; + assert_eq!(attr.key, "my.histogram.attr"); + match &attr.value { + Some(opentelemetry_proto::tonic::common::v1::AnyValue { + value: + Some(opentelemetry_proto::tonic::common::v1::any_value::Value::DoubleValue( + val, + )), + }) => assert!(val.is_nan()), + _ => panic!("Expected double value NaN in filtered_attributes"), + } match exemplar.value { - Some(ExemplarValue::AsDouble(val)) => assert!(val.is_nan()), + Some(opentelemetry_proto::tonic::metrics::v1::exemplar::Value::AsDouble(val)) => assert!(val.is_nan()), _ => panic!("Expected double value in exemplar"), } } else { From 35957514b3ccbe68c49526901f6f8a83f8d7f085 Mon Sep 17 00:00:00 2001 From: LorenzoTettamanti Date: Tue, 26 May 2026 00:48:37 +0200 Subject: [PATCH 8/8] (fix): fix formatting --- opentelemetry-proto/tests/json_serde.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opentelemetry-proto/tests/json_serde.rs b/opentelemetry-proto/tests/json_serde.rs index b32e454751..f20c583d56 100644 --- a/opentelemetry-proto/tests/json_serde.rs +++ b/opentelemetry-proto/tests/json_serde.rs @@ -2145,7 +2145,9 @@ mod json_serde { _ => panic!("Expected double value NaN in filtered_attributes"), } match exemplar.value { - Some(opentelemetry_proto::tonic::metrics::v1::exemplar::Value::AsDouble(val)) => assert!(val.is_nan()), + Some(opentelemetry_proto::tonic::metrics::v1::exemplar::Value::AsDouble( + val, + )) => assert!(val.is_nan()), _ => panic!("Expected double value in exemplar"), } } else {