This commit is contained in:
Lalit Kumar Bhasin 2025-06-12 23:41:31 -07:00 committed by GitHub
commit 7215220a59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 401 additions and 5 deletions

View File

@ -213,6 +213,82 @@ pub(crate) mod serializers {
let s: String = Deserialize::deserialize(deserializer)?;
s.parse::<i64>().map_err(de::Error::custom)
}
pub fn serialize_vec_u64_to_strings<S>(vec: &Vec<u64>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let str_vec: Vec<String> = vec.iter().map(|&num| num.to_string()).collect();
serializer.collect_seq(str_vec)
}
pub fn deserialize_strings_to_vec_u64<'de, D>(deserializer: D) -> Result<Vec<u64>, D::Error>
where
D: Deserializer<'de>,
{
let str_vec: Vec<String> = Deserialize::deserialize(deserializer)?;
str_vec
.into_iter()
.map(|s| s.parse::<u64>().map_err(de::Error::custom))
.collect()
}
// Special serializer and deserializer for NaN, Infinity, and -Infinity
pub fn serialize_f64_special<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if value.is_nan() {
serializer.serialize_str("NaN")
} else if value.is_infinite() {
if value.is_sign_positive() {
serializer.serialize_str("Infinity")
} else {
serializer.serialize_str("-Infinity")
}
} else {
serializer.serialize_f64(*value)
}
}
pub fn deserialize_f64_special<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: Deserializer<'de>,
{
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<E>(self, value: f64) -> Result<f64, E>
where
E: de::Error,
{
Ok(value)
}
fn visit_str<E>(self, value: &str) -> Result<f64, E>
where
E: de::Error,
{
match value {
"NaN" => Ok(f64::NAN),
"Infinity" => Ok(f64::INFINITY),
"-Infinity" => Ok(f64::NEG_INFINITY),
_ => Err(E::custom(format!(
"Invalid string for f64: expected NaN, Infinity, or -Infinity but got '{}'",
value
))),
}
}
}
deserializer.deserialize_any(F64Visitor)
}
}
#[cfg(feature = "gen-tonic-messages")]

View File

@ -431,6 +431,13 @@ pub struct HistogramDataPoint {
/// value must be equal to the sum of the "count" fields in buckets if a
/// histogram is provided.
#[prost(fixed64, tag = "4")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_u64_to_string",
deserialize_with = "crate::proto::serializers::deserialize_string_to_u64"
)
)]
pub count: u64,
/// sum of the values in the population. If count is zero then this field
/// must be zero.
@ -452,6 +459,13 @@ pub struct HistogramDataPoint {
/// is when the length of bucket_counts is 0, then the length of explicit_bounds
/// must also be 0.
#[prost(fixed64, repeated, tag = "6")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_vec_u64_to_strings",
deserialize_with = "crate::proto::serializers::deserialize_strings_to_vec_u64"
)
)]
pub bucket_counts: ::prost::alloc::vec::Vec<u64>,
/// explicit_bounds specifies buckets with explicitly defined bounds for values.
///
@ -508,17 +522,38 @@ pub struct ExponentialHistogramDataPoint {
/// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
/// 1970.
#[prost(fixed64, tag = "2")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_u64_to_string",
deserialize_with = "crate::proto::serializers::deserialize_string_to_u64"
)
)]
pub start_time_unix_nano: u64,
/// TimeUnixNano is required, see the detailed comments above Metric.
///
/// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
/// 1970.
#[prost(fixed64, tag = "3")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_u64_to_string",
deserialize_with = "crate::proto::serializers::deserialize_string_to_u64"
)
)]
pub time_unix_nano: u64,
/// count is the number of values in the population. Must be
/// non-negative. This value must be equal to the sum of the "bucket_counts"
/// values in the positive and negative Buckets plus the "zero_count" field.
#[prost(fixed64, tag = "4")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_u64_to_string",
deserialize_with = "crate::proto::serializers::deserialize_string_to_u64"
)
)]
pub count: u64,
/// sum of the values in the population. If count is zero then this field
/// must be zero.
@ -556,6 +591,13 @@ pub struct ExponentialHistogramDataPoint {
/// Implementations MAY consider the zero bucket to have probability
/// mass equal to (zero_count / count).
#[prost(fixed64, tag = "7")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_u64_to_string",
deserialize_with = "crate::proto::serializers::deserialize_string_to_u64"
)
)]
pub zero_count: u64,
/// positive carries the positive range of exponential bucket counts.
#[prost(message, optional, tag = "8")]
@ -633,15 +675,36 @@ pub struct SummaryDataPoint {
/// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
/// 1970.
#[prost(fixed64, tag = "2")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_u64_to_string",
deserialize_with = "crate::proto::serializers::deserialize_string_to_u64"
)
)]
pub start_time_unix_nano: u64,
/// TimeUnixNano is required, see the detailed comments above Metric.
///
/// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
/// 1970.
#[prost(fixed64, tag = "3")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_u64_to_string",
deserialize_with = "crate::proto::serializers::deserialize_string_to_u64"
)
)]
pub time_unix_nano: u64,
/// count is the number of values in the population. Must be non-negative.
#[prost(fixed64, tag = "4")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_u64_to_string",
deserialize_with = "crate::proto::serializers::deserialize_string_to_u64"
)
)]
pub count: u64,
/// sum of the values in the population. If count is zero then this field
/// must be zero.
@ -680,11 +743,25 @@ pub mod summary_data_point {
/// The quantile of a distribution. Must be in the interval
/// \[0.0, 1.0\].
#[prost(double, tag = "1")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_f64_special",
deserialize_with = "crate::proto::serializers::deserialize_f64_special"
)
)]
pub quantile: f64,
/// The value at the given quantile of a distribution.
///
/// Quantile values must NOT be negative.
#[prost(double, tag = "2")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_f64_special",
deserialize_with = "crate::proto::serializers::deserialize_f64_special"
)
)]
pub value: f64,
}
}
@ -709,6 +786,13 @@ pub struct Exemplar {
/// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January
/// 1970.
#[prost(fixed64, tag = "2")]
#[cfg_attr(
feature = "with-serde",
serde(
serialize_with = "crate::proto::serializers::serialize_u64_to_string",
deserialize_with = "crate::proto::serializers::deserialize_string_to_u64"
)
)]
pub time_unix_nano: u64,
/// (Optional) Span ID of the exemplar trace.
/// span_id may be missing if the measurement is not recorded inside a trace

View File

@ -100,15 +100,23 @@ fn build_tonic() {
// the proto file uses u64 for timestamp
// Thus, special serializer and deserializer are needed
for path in [
//trace
"trace.v1.Span.start_time_unix_nano",
"trace.v1.Span.end_time_unix_nano",
"trace.v1.Span.Event.time_unix_nano",
//logs
"logs.v1.LogRecord.time_unix_nano",
"logs.v1.LogRecord.observed_time_unix_nano",
//metrics
"metrics.v1.HistogramDataPoint.start_time_unix_nano",
"metrics.v1.HistogramDataPoint.time_unix_nano",
"metrics.v1.NumberDataPoint.start_time_unix_nano",
"metrics.v1.NumberDataPoint.time_unix_nano",
"metrics.v1.ExponentialHistogramDataPoint.start_time_unix_nano",
"metrics.v1.ExponentialHistogramDataPoint.time_unix_nano",
"metrics.v1.SummaryDataPoint.start_time_unix_nano",
"metrics.v1.SummaryDataPoint.time_unix_nano",
"metrics.v1.Exemplar.time_unix_nano",
] {
builder = builder
.field_attribute(path, "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_string_to_u64\"))]")
@ -122,6 +130,51 @@ fn build_tonic() {
.field_attribute(path, "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_vec_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_vec_string_to_vec_u64\"))]")
}
// special serializer and deserializer for metrics count
// OTLP/JSON format may uses string for count
// the proto file uses u64 for count
// Thus, special serializer and deserializer are needed
for path in [
// metrics count and bucket fields
"metrics.v1.HistogramDataPoint.count",
"metrics.v1.ExponentialHistogramDataPoint.count",
"metrics.v1.ExponentialHistogramDataPoint.zero_count",
"metrics.v1.SummaryDataPoint.count",
] {
builder = builder.field_attribute(
path,
"#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_string_to_u64\"))]",
);
}
// special serializer and deserializer for metrics bucket counts
// OTLP/JSON format may uses string for bucket counts
// the proto file uses u64 for bucket counts
// Thus, special serializer and deserializer are needed
for path in [
"metrics.v1.HistogramDataPoint.bucket_counts",
"metrics.v1.ExponentialHistogramDataPoint.positive.bucket_counts",
"metrics.v1.ExponentialHistogramDataPoint.negative.bucket_counts",
] {
builder = builder.field_attribute(
path,
"#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_vec_u64_to_strings\", deserialize_with = \"crate::proto::serializers::deserialize_strings_to_vec_u64\"))]",
);
}
// Special handling for floating-point fields that might contain NaN, Infinity, or -Infinity
// TODO: More needs to be added here as we find more fields that need this special handling
for path in [
// metrics
"metrics.v1.SummaryDataPoint.ValueAtQuantile.value",
"metrics.v1.SummaryDataPoint.ValueAtQuantile.quantile",
] {
builder = builder.field_attribute(
path,
"#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_f64_special\", deserialize_with = \"crate::proto::serializers::deserialize_f64_special\"))]",
);
}
// special serializer and deserializer for value
// The Value::value field must be hidden
builder = builder

View File

@ -997,11 +997,11 @@ mod json_serde {
],
"startTimeUnixNano": "1544712660300000000",
"timeUnixNano": "1544712660300000000",
"count": 2,
"count": "2",
"sum": 2.0,
"bucketCounts": [
1,
1
"1",
"1"
],
"explicitBounds": [
1.0
@ -1109,9 +1109,9 @@ mod json_serde {
{
"startTimeUnixNano": "1544712660300000000",
"timeUnixNano": "1544712660300000000",
"count": 2,
"count": "2",
"sum": 2,
"bucketCounts": [1,1],
"bucketCounts": ["1","1"],
"explicitBounds": [1],
"min": 0,
"max": 2,
@ -1523,4 +1523,187 @@ mod json_serde {
}
}
}
#[cfg(feature = "metrics")]
mod metrics_with_nan {
use super::*;
use opentelemetry_proto::tonic::metrics::v1::summary_data_point::ValueAtQuantile;
use opentelemetry_proto::tonic::metrics::v1::Summary;
use opentelemetry_proto::tonic::metrics::v1::SummaryDataPoint;
fn value_with_nan() -> ExportMetricsServiceRequest {
ExportMetricsServiceRequest {
resource_metrics: vec![ResourceMetrics {
resource: Some(Resource {
attributes: vec![KeyValue {
key: String::from("service.name"),
value: None,
}],
dropped_attributes_count: 0,
}),
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,
}],
},
),
),
}],
schema_url: String::new(),
}],
schema_url: String::new(),
}],
}
}
// language=json
const CANONICAL_WITH_NAN: &str = r#"{
"resourceMetrics": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": null
}
],
"droppedAttributesCount": 0
},
"scopeMetrics": [
{
"scope": null,
"metrics": [
{
"name": "example_metric",
"description": "A sample metric with NaN values",
"unit": "1",
"metadata": [],
"summary": {
"dataPoints": [
{
"attributes": [],
"startTimeUnixNano": "0",
"timeUnixNano": "0",
"count": "100",
"sum": 500.0,
"quantileValues": [
{
"quantile": 0.5,
"value": "NaN"
},
{
"quantile": 0.9,
"value": "NaN"
}
],
"flags": 0
}
]
}
}
],
"schemaUrl": ""
}
],
"schemaUrl": ""
}
]
}"#;
#[test]
fn serialize_with_nan() {
let input: ExportMetricsServiceRequest = value_with_nan();
// Serialize the structure to JSON
let actual = serde_json::to_string_pretty(&input).expect("serialization must succeed");
// Normalize both the actual and expected JSON for comparison
let actual_value: serde_json::Value =
serde_json::from_str(&actual).expect("valid JSON");
let expected_value: serde_json::Value =
serde_json::from_str(CANONICAL_WITH_NAN).expect("valid JSON");
// Compare the normalized JSON values
assert_eq!(actual_value, expected_value);
}
#[test]
fn deserialize_with_nan() {
let actual: ExportMetricsServiceRequest =
serde_json::from_str(CANONICAL_WITH_NAN).expect("deserialization must succeed");
// Ensure the deserialized structure matches the expected values
assert_eq!(actual.resource_metrics.len(), 1);
let resource_metric = &actual.resource_metrics[0];
assert_eq!(
resource_metric.resource.as_ref().unwrap().attributes.len(),
1
);
assert_eq!(
resource_metric.resource.as_ref().unwrap().attributes[0].key,
"service.name"
);
assert!(resource_metric.resource.as_ref().unwrap().attributes[0]
.value
.is_none());
assert_eq!(resource_metric.scope_metrics.len(), 1);
let scope_metric = &resource_metric.scope_metrics[0];
assert!(scope_metric.scope.is_none());
assert_eq!(scope_metric.metrics.len(), 1);
let metric = &scope_metric.metrics[0];
assert_eq!(metric.name, "example_metric");
assert_eq!(metric.description, "A sample metric with NaN values");
assert_eq!(metric.unit, "1");
if let Some(opentelemetry_proto::tonic::metrics::v1::metric::Data::Summary(summary)) =
&metric.data
{
assert_eq!(summary.data_points.len(), 1);
let data_point = &summary.data_points[0];
assert_eq!(data_point.attributes.len(), 0);
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_eq!(data_point.quantile_values.len(), 2);
// Verify that quantile values are NaN
assert!(data_point.quantile_values[0].value.is_nan());
assert!(data_point.quantile_values[0].quantile == 0.5);
assert!(data_point.quantile_values[1].value.is_nan());
assert!(data_point.quantile_values[1].quantile == 0.9);
} else {
panic!("Expected metric data to be of type Summary");
}
}
}
}