Files
jcoffey-dev 1e7e8dfd48
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 7m26s
Vendor upstream's opentelemetry-rust fork at the pinned commit
2026-10-27 22:55:35 -07:00

2244 lines
81 KiB
Rust

#[cfg(all(feature = "with-serde", feature = "gen-tonic-messages"))]
mod json_serde {
#[cfg(feature = "logs")]
use opentelemetry_proto::tonic::collector::logs::v1::{
ExportLogsServiceRequest, ExportLogsServiceResponse,
};
#[cfg(feature = "metrics")]
use opentelemetry_proto::tonic::collector::metrics::v1::{
ExportMetricsServiceRequest, ExportMetricsServiceResponse,
};
#[cfg(feature = "profiles")]
use opentelemetry_proto::tonic::collector::profiles::v1development::{
ExportProfilesServiceRequest, ExportProfilesServiceResponse,
};
#[cfg(feature = "trace")]
use opentelemetry_proto::tonic::collector::trace::v1::{
ExportTraceServiceRequest, ExportTraceServiceResponse,
};
use opentelemetry_proto::tonic::common::v1::any_value::Value;
use opentelemetry_proto::tonic::common::v1::{
AnyValue, ArrayValue, InstrumentationScope, KeyValue, KeyValueList,
};
#[cfg(feature = "logs")]
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,
};
use opentelemetry_proto::tonic::resource::v1::Resource;
#[cfg(feature = "trace")]
use opentelemetry_proto::tonic::trace::v1::{
span::{Event, Link},
ResourceSpans, ScopeSpans, Span, Status,
};
#[cfg(feature = "profiles")]
mod export_profiles_service_request {
use super::*;
#[test]
fn deserialize_omitted_resource_profiles() {
let empty: ExportProfilesServiceRequest = serde_json::from_str("{}").unwrap();
assert!(empty.resource_profiles.is_empty());
}
}
#[cfg(feature = "trace")]
mod export_trace_service_request {
use super::*;
#[test]
fn deserialize_omitted_resource_spans() {
let empty: ExportTraceServiceRequest = serde_json::from_str("{}").unwrap();
assert!(empty.resource_spans.is_empty());
}
// `ExportTraceServiceRequest` from the OpenTelemetry proto examples
// see <https://github.com/open-telemetry/opentelemetry-proto/blob/v1.3.2/examples/trace.json>
mod example {
use super::*;
fn value() -> ExportTraceServiceRequest {
ExportTraceServiceRequest {
resource_spans: vec![ResourceSpans {
resource: Some(Resource {
attributes: vec![KeyValue {
key: String::from("service.name"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from("my.service"))),
}),
key_strindex: 0,
}],
dropped_attributes_count: 0,
entity_refs: vec![],
}),
scope_spans: vec![ScopeSpans {
scope: Some(InstrumentationScope {
name: String::from("my.library"),
version: String::from("1.0.0"),
attributes: vec![KeyValue {
key: String::from("my.scope.attribute"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from(
"some scope attribute",
))),
}),
key_strindex: 0,
}],
dropped_attributes_count: 0,
}),
spans: vec![Span {
trace_id: const_hex::decode("5b8efff798038103d269b633813fc60c")
.unwrap(),
span_id: const_hex::decode("eee19b7ec3c1b174").unwrap(),
trace_state: String::new(),
parent_span_id: const_hex::decode("eee19b7ec3c1b173").unwrap(),
flags: 0,
name: String::from("I'm a server span"),
kind: 2,
start_time_unix_nano: 1544712660000000000,
end_time_unix_nano: 1544712661000000000,
attributes: vec![KeyValue {
key: String::from("my.span.attr"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from("some value"))),
}),
key_strindex: 0,
}],
dropped_attributes_count: 0,
events: vec![],
dropped_events_count: 0,
links: vec![],
dropped_links_count: 0,
status: None,
}],
schema_url: String::new(),
}],
schema_url: String::new(),
}],
}
}
// language=json
const CANONICAL: &str = r#"{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}
],
"droppedAttributesCount": 0,
"entityRefs": []
},
"scopeSpans": [
{
"scope": {
"name": "my.library",
"version": "1.0.0",
"attributes": [
{
"key": "my.scope.attribute",
"value": {
"stringValue": "some scope attribute"
}
}
],
"droppedAttributesCount": 0
},
"spans": [
{
"traceId": "5b8efff798038103d269b633813fc60c",
"spanId": "eee19b7ec3c1b174",
"traceState": "",
"parentSpanId": "eee19b7ec3c1b173",
"flags": 0,
"name": "I'm a server span",
"kind": 2,
"startTimeUnixNano": "1544712660000000000",
"endTimeUnixNano": "1544712661000000000",
"attributes": [
{
"key": "my.span.attr",
"value": {
"stringValue": "some value"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"links": [],
"droppedLinksCount": 0,
"status": null
}
],
"schemaUrl": ""
}
],
"schemaUrl": ""
}
]
}"#;
// copied from the example json file
// language=json
const ALTERNATIVE: &str = r#"{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}
]
},
"scopeSpans": [
{
"scope": {
"name": "my.library",
"version": "1.0.0",
"attributes": [
{
"key": "my.scope.attribute",
"value": {
"stringValue": "some scope attribute"
}
}
]
},
"spans": [
{
"traceId": "5B8EFFF798038103D269B633813FC60C",
"spanId": "EEE19B7EC3C1B174",
"parentSpanId": "EEE19B7EC3C1B173",
"name": "I'm a server span",
"startTimeUnixNano": "1544712660000000000",
"endTimeUnixNano": "1544712661000000000",
"kind": 2,
"attributes": [
{
"key": "my.span.attr",
"value": {
"stringValue": "some value"
}
}
]
}
]
}
]
}
]
}
"#;
#[test]
fn serialize() {
let input: ExportTraceServiceRequest = value();
let actual =
serde_json::to_string_pretty(&input).expect("serialization must succeed");
assert_eq!(actual, CANONICAL);
}
#[test]
fn deserialize_canonical() {
let actual: ExportTraceServiceRequest =
serde_json::from_str(CANONICAL).expect("deserialization must succeed");
let expected: ExportTraceServiceRequest = value();
assert_eq!(actual, expected);
}
#[test]
fn deserialize_alternative() {
let actual: ExportTraceServiceRequest =
serde_json::from_str(ALTERNATIVE).expect("deserialization must succeed");
let expected: ExportTraceServiceRequest = value();
assert_eq!(actual, expected);
}
}
// request where all fields are set
mod complete {
use super::*;
fn value() -> ExportTraceServiceRequest {
ExportTraceServiceRequest {
resource_spans: vec![ResourceSpans {
resource: Some(Resource {
attributes: vec![KeyValue {
key: String::from("service.name"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from("my.service"))),
}),
key_strindex: 0,
}],
dropped_attributes_count: 1,
entity_refs: vec![],
}),
scope_spans: vec![ScopeSpans {
scope: Some(InstrumentationScope {
name: String::from("my.library"),
version: String::from("1.0.0"),
attributes: vec![KeyValue {
key: String::from("my.scope.attribute"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from(
"some scope attribute",
))),
}),
key_strindex: 0,
}],
dropped_attributes_count: 1,
}),
spans: vec![Span {
trace_id: const_hex::decode("5b8efff798038103d269b633813fc60c")
.unwrap(),
span_id: const_hex::decode("eee19b7ec3c1b174").unwrap(),
trace_state: String::from("browser=firefox,os=linux"),
parent_span_id: const_hex::decode("eee19b7ec3c1b173").unwrap(),
flags: 1,
name: String::from("I'm a server span"),
kind: 2,
start_time_unix_nano: 1544712660000000000,
end_time_unix_nano: 1544712661000000000,
attributes: vec![
KeyValue {
key: String::from("my.span.attr"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from(
"some value",
))),
}),
key_strindex: 0,
},
KeyValue {
key: String::from("my.span.bytes.attr"),
value: Some(AnyValue {
value: Some(Value::BytesValue(vec![0x80, 0x80, 0x80])),
}),
key_strindex: 0,
},
],
dropped_attributes_count: 1,
events: vec![Event {
time_unix_nano: 1544712660500000000,
name: String::from("user.created"),
attributes: vec![KeyValue {
key: String::from("my.event.attr"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from(
"snowman",
))),
}),
key_strindex: 0,
}],
dropped_attributes_count: 1,
}],
dropped_events_count: 1,
links: vec![Link {
trace_id: const_hex::decode("5b8efff798038103d269b633813fc60b")
.unwrap(),
span_id: const_hex::decode("eee19b7ec3c1b172").unwrap(),
trace_state: String::from("food=pizza,color=red"),
attributes: vec![KeyValue {
key: String::from("my.link.attr"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from("rust"))),
}),
key_strindex: 0,
}],
dropped_attributes_count: 1,
flags: 0x0200,
}],
dropped_links_count: 1,
status: Some(Status {
message: String::from("service temporarily unavailable"),
code: 2,
}),
}],
schema_url: String::from("https://opentelemetry.io/schemas/1.24.0"),
}],
schema_url: String::from("https://opentelemetry.io/schemas/1.24.0"),
}],
}
}
// language=json
const CANONICAL: &str = r#"{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}
],
"droppedAttributesCount": 1,
"entityRefs": []
},
"scopeSpans": [
{
"scope": {
"name": "my.library",
"version": "1.0.0",
"attributes": [
{
"key": "my.scope.attribute",
"value": {
"stringValue": "some scope attribute"
}
}
],
"droppedAttributesCount": 1
},
"spans": [
{
"traceId": "5b8efff798038103d269b633813fc60c",
"spanId": "eee19b7ec3c1b174",
"traceState": "browser=firefox,os=linux",
"parentSpanId": "eee19b7ec3c1b173",
"flags": 1,
"name": "I'm a server span",
"kind": 2,
"startTimeUnixNano": "1544712660000000000",
"endTimeUnixNano": "1544712661000000000",
"attributes": [
{
"key": "my.span.attr",
"value": {
"stringValue": "some value"
}
},
{
"key": "my.span.bytes.attr",
"value": {
"bytesValue": "gICA"
}
}
],
"droppedAttributesCount": 1,
"events": [
{
"timeUnixNano": "1544712660500000000",
"name": "user.created",
"attributes": [
{
"key": "my.event.attr",
"value": {
"stringValue": "snowman"
}
}
],
"droppedAttributesCount": 1
}
],
"droppedEventsCount": 1,
"links": [
{
"traceId": "5b8efff798038103d269b633813fc60b",
"spanId": "eee19b7ec3c1b172",
"traceState": "food=pizza,color=red",
"attributes": [
{
"key": "my.link.attr",
"value": {
"stringValue": "rust"
}
}
],
"droppedAttributesCount": 1,
"flags": 512
}
],
"droppedLinksCount": 1,
"status": {
"message": "service temporarily unavailable",
"code": 2
}
}
],
"schemaUrl": "https://opentelemetry.io/schemas/1.24.0"
}
],
"schemaUrl": "https://opentelemetry.io/schemas/1.24.0"
}
]
}"#;
#[test]
fn serialize() {
let input: ExportTraceServiceRequest = value();
let actual =
serde_json::to_string_pretty(&input).expect("serialization must succeed");
assert_eq!(actual, CANONICAL);
}
#[test]
fn deserialize_canonical() {
let actual: ExportTraceServiceRequest =
serde_json::from_str(CANONICAL).expect("deserialization must succeed");
let expected: ExportTraceServiceRequest = value();
assert_eq!(actual, expected);
}
}
}
mod value {
use super::*;
#[test]
fn string() {
let value = Value::StringValue(String::from("my.service"));
// language=json
let json = r#"{"stringValue":"my.service"}"#;
assert_eq!(
serde_json::to_string(&value).expect("serialization succeeds"),
json
);
assert_eq!(
serde_json::from_str::<Value>(json).expect("deserialization succeeds"),
value
);
}
#[test]
fn bool() {
let value = Value::BoolValue(true);
// language=json
let json = r#"{"boolValue":true}"#;
assert_eq!(
serde_json::to_string(&value).expect("serialization succeeds"),
json
);
assert_eq!(
serde_json::from_str::<Value>(json).expect("deserialization succeeds"),
value
);
}
#[test]
fn int() {
let value = Value::IntValue(123);
// language=json
let json = r#"{"intValue":123}"#;
assert_eq!(
serde_json::to_string(&value).expect("serialization succeeds"),
json
);
assert_eq!(
serde_json::from_str::<Value>(json).expect("deserialization succeeds"),
value
);
}
#[test]
fn array_empty() {
let value = Value::ArrayValue(ArrayValue { values: vec![] });
// language=json
let json = r#"{"arrayValue":{"values":[]}}"#;
assert_eq!(
serde_json::to_string(&value).expect("serialization succeeds"),
json
);
assert_eq!(
serde_json::from_str::<Value>(json).expect("deserialization succeeds"),
value
);
}
#[test]
fn array_strings() {
let value = Value::ArrayValue(ArrayValue {
values: vec![
AnyValue {
value: Some(Value::StringValue(String::from("foo"))),
},
AnyValue {
value: Some(Value::StringValue(String::from("bar"))),
},
],
});
// language=json
let json = r#"{
"arrayValue": {
"values": [
{
"stringValue": "foo"
},
{
"stringValue": "bar"
}
]
}
}"#;
assert_eq!(
serde_json::to_string_pretty(&value).expect("serialization succeeds"),
json
);
assert_eq!(
serde_json::from_str::<Value>(json).expect("deserialization succeeds"),
value
);
}
#[test]
fn array_mixed() {
let value = Value::ArrayValue(ArrayValue {
values: vec![
AnyValue {
value: Some(Value::StringValue(String::from("foo"))),
},
AnyValue {
value: Some(Value::IntValue(1337)),
},
],
});
// language=json
let json = r#"{
"arrayValue": {
"values": [
{
"stringValue": "foo"
},
{
"intValue": "1337"
}
]
}
}"#;
assert_eq!(
serde_json::to_string_pretty(&value).expect("serialization succeeds"),
json
);
assert_eq!(
serde_json::from_str::<Value>(json).expect("deserialization succeeds"),
value
);
}
#[test]
fn map_single_string() {
let value = Value::KvlistValue(KeyValueList {
values: vec![KeyValue {
key: String::from("some.map.key"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from("some value"))),
}),
key_strindex: 0,
}],
});
// language=json
let json = r#"{
"kvlistValue": {
"values": [
{
"key": "some.map.key",
"value": {
"stringValue": "some value"
}
}
]
}
}"#;
assert_eq!(
serde_json::to_string_pretty(&value).expect("serialization succeeds"),
json
);
assert_eq!(
serde_json::from_str::<Value>(json).expect("deserialization succeeds"),
value
);
}
}
mod empty_any_value {
use super::*;
#[test]
fn deserialize_empty_and_unknown_only_values() {
for json in [r#"{}"#, r#"{"futureValue":"ignored"}"#] {
let actual: AnyValue =
serde_json::from_str(json).expect("empty AnyValue must deserialize");
assert_eq!(actual, AnyValue { value: None });
assert_eq!(
serde_json::to_string(&actual).expect("empty AnyValue must serialize"),
"{}"
);
}
}
#[test]
fn deserialize_null_values_as_unset() {
for field in [
"stringValue",
"boolValue",
"intValue",
"doubleValue",
"arrayValue",
"kvlistValue",
"bytesValue",
] {
let json = format!(r#"{{"{field}":null}}"#);
let actual: AnyValue =
serde_json::from_str(&json).expect("null AnyValue field must deserialize");
assert_eq!(actual.value, None, "field: {field}");
}
}
#[test]
fn deserialize_null_field_preserves_non_null_value() {
for field in [
"stringValue",
"boolValue",
"intValue",
"doubleValue",
"arrayValue",
"kvlistValue",
"bytesValue",
] {
let (non_null_field, expected) = if field == "stringValue" {
(r#""intValue":"42""#, Value::IntValue(42))
} else {
(r#""stringValue":"kept""#, Value::StringValue("kept".into()))
};
for json in [
format!(r#"{{"{field}":null,{non_null_field}}}"#),
format!(r#"{{{non_null_field},"{field}":null}}"#),
] {
let actual: AnyValue = serde_json::from_str(&json).unwrap();
assert_eq!(actual.value.as_ref(), Some(&expected), "input: {json}");
}
}
}
#[test]
fn deserialize_malformed_non_null_values_fails() {
for json in [
r#"{"stringValue":42}"#,
r#"{"boolValue":"true"}"#,
r#"{"intValue":"not-an-integer"}"#,
r#"{"intValue":"9223372036854775808"}"#,
r#"{"doubleValue":true}"#,
r#"{"arrayValue":42}"#,
r#"{"kvlistValue":42}"#,
r#"{"bytesValue":"!"}"#,
] {
assert!(
serde_json::from_str::<AnyValue>(json).is_err(),
"input: {json}"
);
}
}
#[cfg(feature = "trace")]
#[test]
fn deserialize_empty_span_attribute() {
let json = r#"{
"resourceSpans": [{
"scopeSpans": [{
"spans": [{
"traceId": "00000000000000000000000000000001",
"spanId": "0000000000000001",
"name": "cloudflare-span",
"attributes": [{"key": "empty", "value": {}}]
}]
}]
}]
}"#;
let request: ExportTraceServiceRequest =
serde_json::from_str(json).expect("trace request must deserialize");
let value = &request.resource_spans[0].scope_spans[0].spans[0].attributes[0]
.value
.as_ref()
.expect("attribute must remain present")
.value;
assert_eq!(value, &None);
}
#[cfg(feature = "logs")]
#[test]
fn deserialize_empty_log_field() {
let json = r#"{
"resourceLogs": [{
"scopeLogs": [{
"logRecords": [{
"body": {
"kvlistValue": {
"values": [{"key": "value", "value": {}}]
}
}
}]
}]
}]
}"#;
let request: ExportLogsServiceRequest =
serde_json::from_str(json).expect("logs request must deserialize");
let Some(Value::KvlistValue(body)) = request.resource_logs[0].scope_logs[0].log_records
[0]
.body
.as_ref()
.and_then(|body| body.value.as_ref()) else {
panic!("log body must remain a key-value list");
};
assert_eq!(body.values[0].value, Some(AnyValue { value: None }));
}
}
mod key_value {
use super::*;
mod string {
use super::*;
fn value() -> KeyValue {
KeyValue {
key: String::from("service.name"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from("my.service"))),
}),
key_strindex: 0,
}
}
// language=json
const CANONICAL: &str = r#"{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}"#;
#[test]
fn serialize() {
let input: KeyValue = value();
let actual =
serde_json::to_string_pretty(&input).expect("serialization must succeed");
assert_eq!(actual, CANONICAL);
}
#[test]
fn deserialize_canonical() {
let actual: KeyValue =
serde_json::from_str(CANONICAL).expect("deserialization must succeed");
let expected: KeyValue = value();
assert_eq!(actual, expected);
}
}
mod int {
use super::*;
fn value() -> KeyValue {
KeyValue {
key: String::from("service.id"),
value: Some(AnyValue {
value: Some(Value::IntValue(33)),
}),
key_strindex: 0,
}
}
// language=json
const CANONICAL: &str = r#"{
"key": "service.id",
"value": {
"intValue": "33"
}
}"#;
// unquoted int value
// language=json
const ALTERNATIVE: &str = r#"{
"key": "service.id",
"value": {
"intValue": 33
}
}"#;
#[test]
fn serialize() {
let input: KeyValue = value();
let actual =
serde_json::to_string_pretty(&input).expect("serialization must succeed");
assert_eq!(actual, CANONICAL);
}
#[test]
fn deserialize_canonical() {
let actual: KeyValue =
serde_json::from_str(CANONICAL).expect("deserialization must succeed");
let expected: KeyValue = value();
assert_eq!(actual, expected);
}
#[test]
fn deserialize_alternative() {
let actual: KeyValue =
serde_json::from_str(ALTERNATIVE).expect("deserialization must succeed");
let expected: KeyValue = value();
assert_eq!(actual, expected);
}
}
}
#[cfg(feature = "trace")]
mod event {
use super::*;
mod simple {
use super::*;
fn value() -> Event {
Event {
time_unix_nano: 1234567890,
name: String::from("my_event"),
attributes: vec![],
dropped_attributes_count: 0,
}
}
// language=json
const CANONICAL: &str = r#"{
"timeUnixNano": "1234567890",
"name": "my_event",
"attributes": [],
"droppedAttributesCount": 0
}"#;
// language=json
const ALTERNATIVE: &str = r#"{
"name": "my_event",
"timeUnixNano": "1234567890"
}"#;
#[test]
fn serialize() {
let input: Event = value();
let actual =
serde_json::to_string_pretty(&input).expect("serialization must succeed");
assert_eq!(actual, CANONICAL);
}
#[test]
fn deserialize_canonical() {
let actual: Event =
serde_json::from_str(CANONICAL).expect("deserialization must succeed");
let expected: Event = value();
assert_eq!(actual, expected);
}
#[test]
fn deserialize_alternative() {
let actual: Event =
serde_json::from_str(ALTERNATIVE).expect("deserialization must succeed");
let expected: Event = value();
assert_eq!(actual, expected);
}
}
}
#[cfg(feature = "metrics")]
mod export_metrics_service_request {
use super::*;
#[test]
fn deserialize_omitted_resource_metrics() {
let empty: ExportMetricsServiceRequest = serde_json::from_str("{}").unwrap();
assert!(empty.resource_metrics.is_empty());
}
// `ExportTraceServiceRequest` from the OpenTelemetry proto examples
// see <https://github.com/open-telemetry/opentelemetry-proto/blob/v1.3.2/examples/metrics.json>
mod example {
use super::*;
fn value() -> ExportMetricsServiceRequest {
ExportMetricsServiceRequest {
resource_metrics: vec![ResourceMetrics {
resource: Some(Resource {
attributes: vec![KeyValue {
key: String::from("service.name"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from("my.service"))),
}),
key_strindex: 0,
}],
dropped_attributes_count: 0,
entity_refs: vec![],
}),
scope_metrics: vec![ScopeMetrics {
scope: Some(InstrumentationScope {
name: String::from("my.library"),
version: String::from("1.0.0"),
attributes: vec![KeyValue {
key: String::from("my.scope.attribute"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from(
"some scope attribute",
))),
}),
key_strindex: 0,
}],
dropped_attributes_count: 0,
}),
metrics: vec![
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![],
flags: 0,
value: Some(MetricValue::AsDouble(5.0)),
}],
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![],
flags: 0,
value: Some(MetricValue::AsDouble(10.0)),
}],
})),
},
Metric {
name: String::from("my.histogram"),
description: String::from("I am a Histogram"),
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(2.0),
bucket_counts: vec![1, 1],
explicit_bounds: vec![1.0],
exemplars: vec![],
flags: 0,
min: Some(0.0),
max: Some(2.0),
}],
aggregation_temporality: 1,
})),
},
],
schema_url: String::new(),
}],
schema_url: String::new(),
}],
}
}
// language=json
const CANONICAL: &str = r#"{
"resourceMetrics": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}
],
"droppedAttributesCount": 0,
"entityRefs": []
},
"scopeMetrics": [
{
"scope": {
"name": "my.library",
"version": "1.0.0",
"attributes": [
{
"key": "my.scope.attribute",
"value": {
"stringValue": "some scope attribute"
}
}
],
"droppedAttributesCount": 0
},
"metrics": [
{
"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": [],
"flags": 0,
"asDouble": 5.0
}
],
"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": [],
"flags": 0,
"asDouble": 10.0
}
]
}
},
{
"name": "my.histogram",
"description": "I am a Histogram",
"unit": "1",
"metadata": [],
"histogram": {
"dataPoints": [
{
"attributes": [
{
"key": "my.histogram.attr",
"value": {
"stringValue": "some value"
}
}
],
"startTimeUnixNano": "1544712660300000000",
"timeUnixNano": "1544712660300000000",
"count": "2",
"sum": 2.0,
"bucketCounts": [
"1",
"1"
],
"explicitBounds": [
1.0
],
"exemplars": [],
"flags": 0,
"min": 0.0,
"max": 2.0
}
],
"aggregationTemporality": 1
}
}
],
"schemaUrl": ""
}
],
"schemaUrl": ""
}
]
}"#;
// copied from the example json file
// language=json
const ALTERNATIVE: &str = r#"{
"resourceMetrics": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}
]
},
"scopeMetrics": [
{
"scope": {
"name": "my.library",
"version": "1.0.0",
"attributes": [
{
"key": "my.scope.attribute",
"value": {
"stringValue": "some scope attribute"
}
}
]
},
"metrics": [
{
"name": "my.counter",
"unit": "1",
"description": "I am a Counter",
"sum": {
"aggregationTemporality": 1,
"isMonotonic": true,
"dataPoints": [
{
"asDouble": 5,
"startTimeUnixNano": "1544712660300000000",
"timeUnixNano": "1544712660300000000",
"attributes": [
{
"key": "my.counter.attr",
"value": {
"stringValue": "some value"
}
}
]
}
]
}
},
{
"name": "my.gauge",
"unit": "1",
"description": "I am a Gauge",
"gauge": {
"dataPoints": [
{
"asDouble": 10,
"timeUnixNano": "1544712660300000000",
"attributes": [
{
"key": "my.gauge.attr",
"value": {
"stringValue": "some value"
}
}
]
}
]
}
},
{
"name": "my.histogram",
"unit": "1",
"description": "I am a Histogram",
"histogram": {
"aggregationTemporality": 1,
"dataPoints": [
{
"startTimeUnixNano": "1544712660300000000",
"timeUnixNano": "1544712660300000000",
"count": "2",
"sum": 2,
"bucketCounts": ["1","1"],
"explicitBounds": [1],
"min": 0,
"max": 2,
"attributes": [
{
"key": "my.histogram.attr",
"value": {
"stringValue": "some value"
}
}
]
}
]
}
}
]
}
]
}
]
}
"#;
#[test]
fn serialize() {
let input: ExportMetricsServiceRequest = value();
let actual =
serde_json::to_string_pretty(&input).expect("serialization must succeed");
assert_eq!(actual, CANONICAL);
}
#[test]
fn deserialize_canonical() {
let actual: ExportMetricsServiceRequest =
serde_json::from_str(CANONICAL).expect("deserialization must succeed");
let expected: ExportMetricsServiceRequest = value();
assert_eq!(actual, expected);
}
#[test]
fn deserialize_alternative() {
let actual: ExportMetricsServiceRequest =
serde_json::from_str(ALTERNATIVE).expect("deserialization must succeed");
let expected: ExportMetricsServiceRequest = value();
assert_eq!(actual, expected);
}
}
}
#[cfg(feature = "logs")]
mod export_logs_service_response {
use super::*;
#[test]
fn deserialize_empty_partial_success() {
let response: ExportLogsServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_log_records, 0);
assert!(partial_success.error_message.is_empty());
}
#[test]
fn deserialize_partial_success_with_omitted_rejected_log_records() {
let response: ExportLogsServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{"errorMessage":"backend warning"}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_log_records, 0);
assert_eq!(partial_success.error_message, "backend warning");
}
#[test]
fn deserialize_partial_success_with_omitted_error_message() {
let response: ExportLogsServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{"rejectedLogRecords":1}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_log_records, 1);
assert!(partial_success.error_message.is_empty());
}
}
#[cfg(feature = "trace")]
mod export_trace_service_response {
use super::*;
#[test]
fn deserialize_empty_partial_success() {
let response: ExportTraceServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_spans, 0);
assert!(partial_success.error_message.is_empty());
}
#[test]
fn deserialize_partial_success_with_omitted_rejected_spans() {
let response: ExportTraceServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{"errorMessage":"backend warning"}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_spans, 0);
assert_eq!(partial_success.error_message, "backend warning");
}
#[test]
fn deserialize_partial_success_with_omitted_error_message() {
let response: ExportTraceServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{"rejectedSpans":1}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_spans, 1);
assert!(partial_success.error_message.is_empty());
}
}
#[cfg(feature = "metrics")]
mod export_metrics_service_response {
use super::*;
#[test]
fn deserialize_empty_partial_success() {
let response: ExportMetricsServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_data_points, 0);
assert!(partial_success.error_message.is_empty());
}
#[test]
fn deserialize_partial_success_with_omitted_rejected_data_points() {
let response: ExportMetricsServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{"errorMessage":"backend warning"}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_data_points, 0);
assert_eq!(partial_success.error_message, "backend warning");
}
#[test]
fn deserialize_partial_success_with_omitted_error_message() {
let response: ExportMetricsServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{"rejectedDataPoints":1}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_data_points, 1);
assert!(partial_success.error_message.is_empty());
}
}
#[cfg(feature = "profiles")]
mod export_profiles_service_response {
use super::*;
#[test]
fn deserialize_empty_partial_success() {
let response: ExportProfilesServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_profiles, 0);
assert!(partial_success.error_message.is_empty());
}
#[test]
fn deserialize_partial_success_with_omitted_rejected_profiles() {
let response: ExportProfilesServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{"errorMessage":"backend warning"}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_profiles, 0);
assert_eq!(partial_success.error_message, "backend warning");
}
#[test]
fn deserialize_partial_success_with_omitted_error_message() {
let response: ExportProfilesServiceResponse =
serde_json::from_str(r#"{"partialSuccess":{"rejectedProfiles":1}}"#)
.expect("deserialization must succeed");
let partial_success = response
.partial_success
.expect("partial success must be present");
assert_eq!(partial_success.rejected_profiles, 1);
assert!(partial_success.error_message.is_empty());
}
}
#[cfg(feature = "logs")]
mod export_logs_service_request {
use super::*;
#[test]
fn deserialize_omitted_resource_logs() {
let empty: ExportLogsServiceRequest = serde_json::from_str("{}").unwrap();
assert!(empty.resource_logs.is_empty());
}
// `ExportTraceServiceRequest` from the OpenTelemetry proto examples
// see <https://github.com/open-telemetry/opentelemetry-proto/blob/v1.3.2/examples/logs.json>
mod example {
use super::*;
fn value() -> ExportLogsServiceRequest {
ExportLogsServiceRequest {
resource_logs: vec![ResourceLogs {
resource: Some(Resource {
attributes: vec![KeyValue {
key: String::from("service.name"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from("my.service"))),
}),
key_strindex: 0,
}],
dropped_attributes_count: 0,
entity_refs: vec![],
}),
scope_logs: vec![ScopeLogs {
scope: Some(InstrumentationScope {
name: String::from("my.library"),
version: String::from("1.0.0"),
attributes: vec![KeyValue {
key: String::from("my.scope.attribute"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from(
"some scope attribute",
))),
}),
key_strindex: 0,
}],
dropped_attributes_count: 0,
}),
log_records: vec![LogRecord {
time_unix_nano: 1544712660300000000,
observed_time_unix_nano: 1544712660300000000,
severity_number: 10,
severity_text: String::from("Information"),
body: Some(AnyValue {
value: Some(Value::StringValue(String::from(
"Example log record",
))),
}),
event_name: "test_log_event".to_string(),
attributes: vec![
KeyValue {
key: String::from("string.attribute"),
value: Some(AnyValue {
value: Some(Value::StringValue(String::from(
"some string",
))),
}),
key_strindex: 0,
},
KeyValue {
key: String::from("boolean.attribute"),
value: Some(AnyValue {
value: Some(Value::BoolValue(true)),
}),
key_strindex: 0,
},
KeyValue {
key: String::from("int.attribute"),
value: Some(AnyValue {
value: Some(Value::IntValue(10)),
}),
key_strindex: 0,
},
KeyValue {
key: String::from("double.attribute"),
value: Some(AnyValue {
value: Some(Value::DoubleValue(637.704)),
}),
key_strindex: 0,
},
KeyValue {
key: String::from("array.attribute"),
value: Some(AnyValue {
value: Some(Value::ArrayValue(ArrayValue {
values: vec![
AnyValue {
value: Some(Value::StringValue(
String::from("many"),
)),
},
AnyValue {
value: Some(Value::StringValue(
String::from("values"),
)),
},
],
})),
}),
key_strindex: 0,
},
KeyValue {
key: String::from("map.attribute"),
value: Some(AnyValue {
value: Some(Value::KvlistValue(KeyValueList {
values: vec![KeyValue {
key: String::from("some.map.key"),
value: Some(AnyValue {
value: Some(Value::StringValue(
String::from("some value"),
)),
}),
key_strindex: 0,
}],
})),
}),
key_strindex: 0,
},
],
dropped_attributes_count: 0,
flags: 0,
trace_id: const_hex::decode("5b8efff798038103d269b633813fc60c")
.unwrap(),
span_id: const_hex::decode("eee19b7ec3c1b174").unwrap(),
}],
schema_url: String::new(),
}],
schema_url: String::new(),
}],
}
}
// language=json
const CANONICAL: &str = r#"{
"resourceLogs": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}
],
"droppedAttributesCount": 0,
"entityRefs": []
},
"scopeLogs": [
{
"scope": {
"name": "my.library",
"version": "1.0.0",
"attributes": [
{
"key": "my.scope.attribute",
"value": {
"stringValue": "some scope attribute"
}
}
],
"droppedAttributesCount": 0
},
"logRecords": [
{
"timeUnixNano": "1544712660300000000",
"observedTimeUnixNano": "1544712660300000000",
"severityNumber": 10,
"severityText": "Information",
"body": {
"stringValue": "Example log record"
},
"attributes": [
{
"key": "string.attribute",
"value": {
"stringValue": "some string"
}
},
{
"key": "boolean.attribute",
"value": {
"boolValue": true
}
},
{
"key": "int.attribute",
"value": {
"intValue": "10"
}
},
{
"key": "double.attribute",
"value": {
"doubleValue": 637.704
}
},
{
"key": "array.attribute",
"value": {
"arrayValue": {
"values": [
{
"stringValue": "many"
},
{
"stringValue": "values"
}
]
}
}
},
{
"key": "map.attribute",
"value": {
"kvlistValue": {
"values": [
{
"key": "some.map.key",
"value": {
"stringValue": "some value"
}
}
]
}
}
}
],
"droppedAttributesCount": 0,
"flags": 0,
"traceId": "5b8efff798038103d269b633813fc60c",
"spanId": "eee19b7ec3c1b174",
"eventName": "test_log_event"
}
],
"schemaUrl": ""
}
],
"schemaUrl": ""
}
]
}"#;
// copied from the example json file with fix for <https://github.com/open-telemetry/opentelemetry-proto/issues/579>
// language=json
const ALTERNATIVE: &str = r#"{
"resourceLogs": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "my.service"
}
}
]
},
"scopeLogs": [
{
"scope": {
"name": "my.library",
"version": "1.0.0",
"attributes": [
{
"key": "my.scope.attribute",
"value": {
"stringValue": "some scope attribute"
}
}
]
},
"logRecords": [
{
"timeUnixNano": "1544712660300000000",
"observedTimeUnixNano": "1544712660300000000",
"severityNumber": 10,
"severityText": "Information",
"traceId": "5B8EFFF798038103D269B633813FC60C",
"spanId": "EEE19B7EC3C1B174",
"body": {
"stringValue": "Example log record"
},
"attributes": [
{
"key": "string.attribute",
"value": {
"stringValue": "some string"
}
},
{
"key": "boolean.attribute",
"value": {
"boolValue": true
}
},
{
"key": "int.attribute",
"value": {
"intValue": "10"
}
},
{
"key": "double.attribute",
"value": {
"doubleValue": 637.704
}
},
{
"key": "array.attribute",
"value": {
"arrayValue": {
"values": [
{
"stringValue": "many"
},
{
"stringValue": "values"
}
]
}
}
},
{
"key": "map.attribute",
"value": {
"kvlistValue": {
"values": [
{
"key": "some.map.key",
"value": {
"stringValue": "some value"
}
}
]
}
}
}
],
"eventName": "test_log_event"
}
]
}
]
}
]
}
"#;
#[test]
fn serialize() {
let input: ExportLogsServiceRequest = value();
let actual =
serde_json::to_string_pretty(&input).expect("serialization must succeed");
assert_eq!(actual, CANONICAL);
}
#[test]
fn deserialize_canonical() {
let actual: ExportLogsServiceRequest =
serde_json::from_str(CANONICAL).expect("deserialization must succeed");
let expected: ExportLogsServiceRequest = value();
assert_eq!(actual, expected);
}
#[test]
fn deserialize_alternative() {
let actual: ExportLogsServiceRequest =
serde_json::from_str(ALTERNATIVE).expect("deserialization must succeed");
let expected: ExportLogsServiceRequest = value();
assert_eq!(actual, expected);
}
}
}
#[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,
key_strindex: 0,
}],
dropped_attributes_count: 0,
entity_refs: vec![],
}),
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,
"entityRefs": []
},
"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");
}
}
}
#[cfg(feature = "metrics")]
mod bare_number_deserialization {
use super::*;
#[test]
fn u64_bare_number() {
// parsers must accept both bare and quoted numbers
let json = r#"{
"resourceMetrics": [
{
"scopeMetrics": [
{
"metrics": [
{
"name": "test",
"gauge": {
"dataPoints": [
{
"startTimeUnixNano": 1544712660000000000,
"timeUnixNano": "1544712661000000000",
"asInt": "42"
}
]
}
}
]
}
]
}
]
}"#;
let result: ExportMetricsServiceRequest =
serde_json::from_str(json).expect("bare u64 numbers must deserialize");
let dp = &result.resource_metrics[0].scope_metrics[0].metrics[0];
if let Some(Data::Gauge(gauge)) = &dp.data {
assert_eq!(
gauge.data_points[0].start_time_unix_nano,
1544712660000000000
);
assert_eq!(gauge.data_points[0].time_unix_nano, 1544712661000000000);
} else {
panic!("expected gauge data");
}
}
#[test]
fn vec_u64_bare_numbers() {
// bucket_counts should accept both quoted and bare numbers in arrays
let json = r#"{
"resourceMetrics": [
{
"scopeMetrics": [
{
"metrics": [
{
"name": "test_histogram",
"histogram": {
"dataPoints": [
{
"startTimeUnixNano": "0",
"timeUnixNano": "0",
"count": 10,
"sum": 100.0,
"bucketCounts": [1, "2", 3],
"explicitBounds": [10.0, 20.0]
}
],
"aggregationTemporality": 2
}
}
]
}
]
}
]
}"#;
let result: ExportMetricsServiceRequest =
serde_json::from_str(json).expect("bare u64 vec numbers must deserialize");
let dp = &result.resource_metrics[0].scope_metrics[0].metrics[0];
if let Some(Data::Histogram(hist)) = &dp.data {
assert_eq!(hist.data_points[0].bucket_counts, vec![1, 2, 3]);
assert_eq!(hist.data_points[0].count, 10);
} else {
panic!("expected histogram data");
}
}
#[test]
fn f64_bare_number_in_summary_quantile() {
// deserialize_f64_special should accept both quoted and bare numeric strings
let json = r#"{
"resourceMetrics": [
{
"scopeMetrics": [
{
"metrics": [
{
"name": "test_summary",
"summary": {
"dataPoints": [
{
"count": "1",
"sum": 100.0,
"quantileValues": [
{
"quantile": "0.5",
"value": 99.0
}
]
}
]
}
}
]
}
]
}
]
}"#;
let result: ExportMetricsServiceRequest =
serde_json::from_str(json).expect("quoted f64 numbers must deserialize");
let dp = &result.resource_metrics[0].scope_metrics[0].metrics[0];
if let Some(opentelemetry_proto::tonic::metrics::v1::metric::Data::Summary(summary)) =
&dp.data
{
let qv = &summary.data_points[0].quantile_values[0];
// assert!((qv.quantile - 0.5).abs() < f64::EPSILON);
// assert!((qv.value - 99.0).abs() < f64::EPSILON);
assert_eq!(qv.quantile, 0.5);
assert_eq!(qv.value, 99.0);
} else {
panic!("expected summary data");
}
}
}
}