diff --git a/opentelemetry-otlp/tests/integration_test/expected/metrics.json b/opentelemetry-otlp/tests/integration_test/expected/metrics.json new file mode 100644 index 0000000000..fa713b8ea3 --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/expected/metrics.json @@ -0,0 +1,119 @@ +{ + "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", + "metadata": [], + "sum": { + "aggregationTemporality": 1, + "isMonotonic": true, + "dataPoints": [ + { + "asDouble": 5, + "startTimeUnixNano": "1544712660300000000", + "timeUnixNano": "1544712660300000000", + "attributes": [ + { + "key": "my.counter.attr", + "value": { + "stringValue": "some value" + } + } + ], + "exemplars": [], + "flags": 0 + } + ] + } + }, + { + "name": "my.gauge", + "unit": "1", + "description": "I am a Gauge", + "metadata": [], + "gauge": { + "dataPoints": [ + { + "asDouble": 10, + "startTimeUnixNano": "1544712660300000000", + "timeUnixNano": "1544712660300000000", + "attributes": [ + { + "key": "my.gauge.attr", + "value": { + "stringValue": "some value" + } + } + ], + "exemplars": [], + "flags": 0 + } + ] + } + }, + { + "name": "my.histogram", + "unit": "1", + "description": "I am a Histogram", + "metadata": [], + "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" + } + } + ], + "exemplars": [], + "flags": 0 + } + ] + } + } + ], + "schemaUrl": "whatever" + } + ], + "schemaUrl": "whatever" + } + ] +} \ No newline at end of file diff --git a/opentelemetry-otlp/tests/integration_test/expected/serialized_metrics.json b/opentelemetry-otlp/tests/integration_test/expected/serialized_metrics.json new file mode 100644 index 0000000000..4910e128a2 --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/expected/serialized_metrics.json @@ -0,0 +1,126 @@ +{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "my.service" + } + } + ], + "droppedAttributesCount": 0 + }, + "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": "1544712660300000000", + "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": "whatever" + } + ], + "schemaUrl": "whatever" + } + ] +} \ No newline at end of file diff --git a/opentelemetry-otlp/tests/integration_test/expected/serialized_traces.json b/opentelemetry-otlp/tests/integration_test/expected/serialized_traces.json new file mode 100644 index 0000000000..849e66dd7b --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/expected/serialized_traces.json @@ -0,0 +1,135 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "basic-otlp-tracing-example" + } + } + ], + "droppedAttributesCount": 0 + }, + "scopeSpans": [ + { + "scope": { + "name": "ex.com/basic", + "version": "", + "attributes": [], + "droppedAttributesCount": 0 + }, + "spans": [ + { + "traceId": "9b458af7378cba65253d7042d34fc72e", + "spanId": "cd7cf7bf939930b7", + "traceState": "", + "parentSpanId": "d58cf2d702a061e0", + "flags": 0, + "name": "Sub operation...", + "kind": 1, + "startTimeUnixNano": "1703985537070566698", + "endTimeUnixNano": "1703985537070572718", + "attributes": [ + { + "key": "lemons", + "value": { + "stringValue": "five" + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "timeUnixNano": "1703985537070567697", + "name": "Sub span event", + "attributes": [], + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "links": [], + "droppedLinksCount": 0, + "status": { + "message": "", + "code": 0 + } + } + ], + "schemaUrl": "" + } + ], + "schemaUrl": "" + }, + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "basic-otlp-tracing-example" + } + } + ], + "droppedAttributesCount": 0 + }, + "scopeSpans": [ + { + "scope": { + "name": "ex.com/basic", + "version": "", + "attributes": [], + "droppedAttributesCount": 0 + }, + "spans": [ + { + "traceId": "9b458af7378cba65253d7042d34fc72e", + "spanId": "d58cf2d702a061e0", + "traceState": "", + "parentSpanId": "", + "flags": 0, + "name": "operation", + "kind": 1, + "startTimeUnixNano": "1703985537070558635", + "endTimeUnixNano": "1703985537070580454", + "attributes": [ + { + "key": "ex.com/another", + "value": { + "stringValue": "yes" + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "timeUnixNano": "1703985537070563326", + "name": "Nice operation!", + "attributes": [ + { + "key": "bogons", + "value": { + "intValue": "100" + } + } + ], + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "links": [], + "droppedLinksCount": 0, + "status": { + "message": "", + "code": 0 + } + } + ], + "schemaUrl": "" + } + ], + "schemaUrl": "" + } + ] +} \ No newline at end of file diff --git a/opentelemetry-otlp/tests/integration_test/src/lib.rs b/opentelemetry-otlp/tests/integration_test/src/lib.rs index d6b7196d84..e6bc88c742 100644 --- a/opentelemetry-otlp/tests/integration_test/src/lib.rs +++ b/opentelemetry-otlp/tests/integration_test/src/lib.rs @@ -1,3 +1,4 @@ pub mod images; pub mod logs_asserter; +pub mod metrics_asserter; pub mod trace_asserter; diff --git a/opentelemetry-otlp/tests/integration_test/src/metrics_asserter.rs b/opentelemetry-otlp/tests/integration_test/src/metrics_asserter.rs new file mode 100644 index 0000000000..4845270999 --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/src/metrics_asserter.rs @@ -0,0 +1,40 @@ +use std::fs::File; + +use opentelemetry_proto::tonic::metrics::v1::{MetricsData, ResourceMetrics}; + +pub struct MetricsAsserter { + results: Vec, + expected: Vec, +} + +impl MetricsAsserter { + pub fn new(results: Vec, expected: Vec) -> Self { + MetricsAsserter { results, expected } + } + + pub fn assert(self) { + self.assert_resource_metrics_eq(&self.results, &self.expected); + } + + fn assert_resource_metrics_eq( + &self, + results: &[ResourceMetrics], + expected: &[ResourceMetrics], + ) { + assert_eq!(results.len(), expected.len()); + for i in 0..results.len() { + let result_resource_metrics = &results[i]; + let expected_resource_metrics = &expected[i]; + assert_eq!(result_resource_metrics, expected_resource_metrics); + } + } +} + +// read a file contains ResourceMetrics in json format +pub fn read_metrics_from_json(file: File) -> Vec { + let reader = std::io::BufReader::new(file); + + let metrics_data: MetricsData = + serde_json::from_reader(reader).expect("Failed to read json file"); + metrics_data.resource_metrics +} diff --git a/opentelemetry-otlp/tests/integration_test/tests/integration_tests.rs b/opentelemetry-otlp/tests/integration_test/tests/integration_tests.rs index 01df65239a..5f5468d0dc 100644 --- a/opentelemetry-otlp/tests/integration_test/tests/integration_tests.rs +++ b/opentelemetry-otlp/tests/integration_test/tests/integration_tests.rs @@ -9,6 +9,7 @@ use testcontainers::core::Port; use testcontainers::RunnableImage; mod logs; +mod metrics; mod traces; const COLLECTOR_CONTAINER_NAME: &str = "otel-collector"; diff --git a/opentelemetry-otlp/tests/integration_test/tests/metrics.rs b/opentelemetry-otlp/tests/integration_test/tests/metrics.rs new file mode 100644 index 0000000000..5395c67d58 --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/tests/metrics.rs @@ -0,0 +1,23 @@ +use std::{fs::File, io::Write}; + +use integration_test_runner::metrics_asserter::{read_metrics_from_json, MetricsAsserter}; +use opentelemetry_proto::tonic::metrics::v1::MetricsData; + +#[test] +fn test_serde() { + let metrics = read_metrics_from_json(File::open("./expected/metrics.json").unwrap()); + + let json = serde_json::to_string_pretty(&MetricsData { + resource_metrics: metrics, + }) + .expect("Failed to serialize metrics"); + + // Write to file. + let mut file = File::create("./expected/serialized_metrics.json").unwrap(); + file.write_all(json.as_bytes()).unwrap(); + + let left = read_metrics_from_json(File::open("./expected/metrics.json").unwrap()); + let right = read_metrics_from_json(File::open("./expected/serialized_metrics.json").unwrap()); + + MetricsAsserter::new(left, right).assert(); +} diff --git a/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs b/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs index 1a69fcc5c5..11768cb742 100644 --- a/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs +++ b/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs @@ -181,6 +181,7 @@ pub struct Metric { /// reported value type for the data points, as well as the relatationship to /// the time interval over which they are reported. #[prost(oneof = "metric::Data", tags = "5, 7, 9, 10, 11")] + #[cfg_attr(feature = "with-serde", serde(flatten))] pub data: ::core::option::Option, } /// Nested message and enum types in `Metric`. @@ -307,12 +308,26 @@ pub struct NumberDataPoint { /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January /// 1970. #[prost(fixed64, tag = "2")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] pub start_time_unix_nano: u64, /// TimeUnixNano is required, see the detailed comments above Metric. /// /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January /// 1970. #[prost(fixed64, tag = "3")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] pub time_unix_nano: u64, /// (Optional) List of exemplars collected from /// measurements that were used to form the data point @@ -325,6 +340,7 @@ pub struct NumberDataPoint { /// The value itself. A point is considered invalid when one of the recognized /// value fields is not present inside this oneof. #[prost(oneof = "number_data_point::Value", tags = "4, 6")] + #[cfg_attr(feature = "with-serde", serde(flatten))] pub value: ::core::option::Option, } /// Nested message and enum types in `NumberDataPoint`. @@ -371,12 +387,26 @@ pub struct HistogramDataPoint { /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January /// 1970. #[prost(fixed64, tag = "2")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] pub start_time_unix_nano: u64, /// TimeUnixNano is required, see the detailed comments above Metric. /// /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January /// 1970. #[prost(fixed64, tag = "3")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] pub time_unix_nano: u64, /// count is the number of values in the population. Must be non-negative. This /// value must be equal to the sum of the "count" fields in buckets if a diff --git a/opentelemetry-proto/tests/grpc_build.rs b/opentelemetry-proto/tests/grpc_build.rs index 8477a4e626..836dd1af39 100644 --- a/opentelemetry-proto/tests/grpc_build.rs +++ b/opentelemetry-proto/tests/grpc_build.rs @@ -88,6 +88,10 @@ fn build_tonic() { "trace.v1.Span.Event.time_unix_nano", "logs.v1.LogRecord.time_unix_nano", "logs.v1.LogRecord.observed_time_unix_nano", + "metrics.v1.HistogramDataPoint.start_time_unix_nano", + "metrics.v1.HistogramDataPoint.time_unix_nano", + "metrics.v1.NumberDataPoint.start_time_unix_nano", + "metrics.v1.NumberDataPoint.time_unix_nano", ] { builder = builder .field_attribute(path, "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_string_to_u64\"))]") @@ -99,6 +103,12 @@ fn build_tonic() { .field_attribute(path, "#[cfg_attr(feature =\"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_to_value\", deserialize_with = \"crate::proto::serializers::deserialize_from_value\"))]"); } + // flatten + for path in ["metrics.v1.Metric.data", "metrics.v1.NumberDataPoint.value"] { + builder = + builder.field_attribute(path, "#[cfg_attr(feature =\"with-serde\", serde(flatten))]"); + } + builder .out_dir(out_dir.path()) .compile(TONIC_PROTO_FILES, TONIC_INCLUDES)