2244 lines
81 KiB
Rust
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");
|
|
}
|
|
}
|
|
}
|
|
}
|