diff --git a/opentelemetry-proto/src/proto.rs b/opentelemetry-proto/src/proto.rs index 892d8f10c2..f588a1920a 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 @@ -68,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(), } @@ -98,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; @@ -127,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()?; @@ -416,6 +457,206 @@ 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) + } + + // 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 aeb68f94a2..4ddf7514b4 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), @@ -466,6 +473,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. @@ -502,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 @@ -513,9 +534,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 +617,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 +675,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 @@ -644,6 +700,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`. @@ -742,6 +805,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. @@ -806,9 +876,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 @@ -863,6 +931,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), diff --git a/opentelemetry-proto/tests/grpc_build.rs b/opentelemetry-proto/tests/grpc_build.rs index 102274b980..07a6f05aa3 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, @@ -184,6 +188,23 @@ 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 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 49a9d93d3b..f20c583d56 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; @@ -1577,36 +1582,213 @@ 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: f64::NAN, + 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![f64::NAN], + exemplars: vec![Exemplar { + filtered_attributes: vec![KeyValue { + key: String::from("my.histogram.attr"), + value: Some(AnyValue { + value: Some(Value::DoubleValue(f64::NAN)), + }), + 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), + }], + 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(), }], schema_url: String::new(), @@ -1644,7 +1826,7 @@ mod json_serde { "startTimeUnixNano": "0", "timeUnixNano": "0", "count": "100", - "sum": 500.0, + "sum": "NaN", "quantileValues": [ { "quantile": 0.5, @@ -1659,6 +1841,196 @@ 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": [ + "NaN" + ], + "exemplars": [ + { + "filteredAttributes": [ + { + "key": "my.histogram.attr", + "value": { + "doubleValue": "NaN" + } + } + ], + "timeUnixNano": "1544712660300000000", + "traceId": "", + "spanId": "", + "value": { + "asDouble": "NaN" + } + } + ], + "flags": 0, + "min": "NaN", + "max": "NaN" + } + ], + "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": "" @@ -1711,7 +2083,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(), 5); let metric = &scope_metric.metrics[0]; assert_eq!(metric.name, "example_metric"); @@ -1728,7 +2100,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); @@ -1740,6 +2112,148 @@ 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()); + + 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(opentelemetry_proto::tonic::metrics::v1::exemplar::Value::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") + } } } @@ -1878,4 +2392,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![f64::NEG_INFINITY, f64::INFINITY], + 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": ["-Infinity","Infinity"], + "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"); + } + } + } }