diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a060b7e60..34d979987d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,7 +148,7 @@ jobs: if: hashFiles('Cargo.lock') == '' run: cargo generate-lockfile - name: cargo llvm-cov - run: cargo llvm-cov --locked --all-features --workspace --lcov --output-path lcov.info + run: cargo llvm-cov --locked --all-features --workspace --lcov --lib --output-path lcov.info - name: Upload to codecov.io uses: codecov/codecov-action@v4 env: diff --git a/opentelemetry-otlp/tests/integration_test/.gitignore b/opentelemetry-otlp/tests/integration_test/.gitignore new file mode 100644 index 0000000000..059fd6dce2 --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/.gitignore @@ -0,0 +1,2 @@ +lcov.info +actual/*.json diff --git a/opentelemetry-otlp/tests/integration_test/Cargo.toml b/opentelemetry-otlp/tests/integration_test/Cargo.toml index 413673286e..0ce69526b1 100644 --- a/opentelemetry-otlp/tests/integration_test/Cargo.toml +++ b/opentelemetry-otlp/tests/integration_test/Cargo.toml @@ -4,15 +4,19 @@ version = "0.1.0" edition = "2021" publish = false - [dependencies] -opentelemetry = { path = "../../../opentelemetry", features = ["metrics", "logs"] } -opentelemetry_sdk = { path = "../../../opentelemetry-sdk", features = ["rt-tokio", "logs", "testing"] } -opentelemetry-proto = { path = "../../../opentelemetry-proto", features = ["gen-tonic-messages", "trace", "logs", "with-serde"] } +opentelemetry = { path = "../../../opentelemetry", features = [] } +opentelemetry_sdk = { path = "../../../opentelemetry-sdk", features = ["rt-tokio", "testing"] } +opentelemetry-proto = { path = "../../../opentelemetry-proto", features = ["gen-tonic-messages", "trace", "logs", "metrics", "with-serde"] } log = { workspace = true } tokio = { version = "1.0", features = ["full"] } serde_json = "1" -testcontainers = "0.15.0" +testcontainers = { version = "0.23.1", features = ["http_wait"]} +once_cell.workspace = true +anyhow = "1.0.94" +ctor = "0.2.9" +tracing-subscriber = "0.3.19" +tracing = "0.1.41" [target.'cfg(unix)'.dependencies] opentelemetry-appender-log = { path = "../../../opentelemetry-appender-log", default-features = false} @@ -20,11 +24,12 @@ opentelemetry-otlp = { path = "../../../opentelemetry-otlp", default-features = opentelemetry-semantic-conventions = { path = "../../../opentelemetry-semantic-conventions" } [features] -hyper-client = ["opentelemetry-otlp/hyper-client", "opentelemetry-otlp/http-proto", "opentelemetry-otlp/trace", "opentelemetry-otlp/logs", "opentelemetry-otlp/metrics"] -reqwest-client = ["opentelemetry-otlp/reqwest-client", "opentelemetry-otlp/http-proto", "opentelemetry-otlp/trace","opentelemetry-otlp/logs", "opentelemetry-otlp/metrics"] -reqwest-blocking-client = ["opentelemetry-otlp/reqwest-blocking-client", "opentelemetry-otlp/http-proto", "opentelemetry-otlp/trace","opentelemetry-otlp/logs", "opentelemetry-otlp/metrics"] -tonic-client = ["opentelemetry-otlp/grpc-tonic", "opentelemetry-otlp/trace", "opentelemetry-otlp/logs", "opentelemetry-otlp/metrics"] +hyper-client = ["opentelemetry-otlp/hyper-client", "opentelemetry-otlp/http-proto", "opentelemetry-otlp/trace", "opentelemetry-otlp/logs", "opentelemetry-otlp/metrics", "internal-logs"] +reqwest-client = ["opentelemetry-otlp/reqwest-client", "opentelemetry-otlp/http-proto", "opentelemetry-otlp/trace","opentelemetry-otlp/logs", "opentelemetry-otlp/metrics", "internal-logs"] +reqwest-blocking-client = ["opentelemetry-otlp/reqwest-blocking-client", "opentelemetry-otlp/http-proto", "opentelemetry-otlp/trace","opentelemetry-otlp/logs", "opentelemetry-otlp/metrics", "internal-logs"] +tonic-client = ["opentelemetry-otlp/grpc-tonic", "opentelemetry-otlp/trace", "opentelemetry-otlp/logs", "opentelemetry-otlp/metrics", "internal-logs"] +internal-logs = [] # Keep tonic as the default client -default = ["tonic-client"] +default = ["tonic-client", "internal-logs"] diff --git a/opentelemetry-otlp/tests/integration_test/README.md b/opentelemetry-otlp/tests/integration_test/README.md new file mode 100644 index 0000000000..0b251732dc --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/README.md @@ -0,0 +1,10 @@ +# OTLP - Integration Tests +This directory contains integration tests for `opentelemetry-otlp`. It uses +[testcontainers](https://testcontainers.com/) to start an instance of the OTEL collector using [otel-collector-config.yaml](otel-collector-config.yaml), which then uses a file exporter per signal to write the output it receives back to the host machine. + +The tests connect directly to the collector on `localhost:4317` and `localhost:4318`, push data through, and then check that what they expect +has popped back out into the files output by the collector. + +For this to work, you need a couple of things: +* Docker, for the test container +* TCP/4317 and TCP/4318 free on your local machine. If you are running another collector, you'll need to stop it for the tests to run \ No newline at end of file diff --git a/opentelemetry-otlp/tests/integration_test/actual/README.md b/opentelemetry-otlp/tests/integration_test/actual/README.md new file mode 100644 index 0000000000..9380bd7807 --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/actual/README.md @@ -0,0 +1 @@ +Output from the otel-collector goes here. diff --git a/opentelemetry-otlp/tests/integration_test/expected/different_metrics.json b/opentelemetry-otlp/tests/integration_test/expected/different_metrics.json new file mode 100644 index 0000000000..5b9bcdba0a --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/expected/different_metrics.json @@ -0,0 +1,133 @@ +{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "metrics-integration-test" + } + } + ] + }, + "scopeMetrics": [ + { + "scope": { + "name": "meter" + }, + "metrics": [ + { + "name": "counter_u64", + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "mykey1", + "value": { + "stringValue": "mydifferentval" + } + }, + { + "key": "mykey2", + "value": { + "stringValue": "myvalue2" + } + } + ], + "startTimeUnixNano": "1734094309366798000", + "timeUnixNano": "1734094317871514000", + "asInt": "15" + } + ], + "aggregationTemporality": 2, + "isMonotonic": true + } + }, + { + "name": "example_histogram", + "histogram": { + "dataPoints": [ + { + "attributes": [ + { + "key": "mykey3", + "value": { + "stringValue": "myvalue4" + } + } + ], + "startTimeUnixNano": "1734094309366875000", + "timeUnixNano": "1734094317871537000", + "count": "1", + "sum": 42, + "bucketCounts": [ + "0", + "0", + "0", + "0", + "1", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0" + ], + "explicitBounds": [ + 0, + 5, + 10, + 25, + 50, + 75, + 100, + 250, + 500, + 750, + 1000, + 2500, + 5000, + 7500, + 10000 + ], + "min": 42, + "max": 42 + } + ], + "aggregationTemporality": 2 + } + }, + { + "name": "example_up_down_counter", + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "mykey5", + "value": { + "stringValue": "myvalue5" + } + } + ], + "startTimeUnixNano": "1734094309366941000", + "timeUnixNano": "1734094317871548000", + "asInt": "-1" + } + ], + "aggregationTemporality": 2 + } + } + ] + } + ] + } + ] +} diff --git a/opentelemetry-otlp/tests/integration_test/expected/metrics.json b/opentelemetry-otlp/tests/integration_test/expected/metrics.json index fa713b8ea3..f1711d889e 100644 --- a/opentelemetry-otlp/tests/integration_test/expected/metrics.json +++ b/opentelemetry-otlp/tests/integration_test/expected/metrics.json @@ -6,7 +6,7 @@ { "key": "service.name", "value": { - "stringValue": "my.service" + "stringValue": "metrics-integration-test" } } ] @@ -14,106 +14,120 @@ "scopeMetrics": [ { "scope": { - "name": "my.library", - "version": "1.0.0", - "attributes": [ - { - "key": "my.scope.attribute", - "value": { - "stringValue": "some scope attribute" - } - } - ] + "name": "meter" }, "metrics": [ { - "name": "my.counter", - "unit": "1", - "description": "I am a Counter", - "metadata": [], + "name": "counter_u64", "sum": { - "aggregationTemporality": 1, - "isMonotonic": true, "dataPoints": [ { - "asDouble": 5, - "startTimeUnixNano": "1544712660300000000", - "timeUnixNano": "1544712660300000000", "attributes": [ { - "key": "my.counter.attr", + "key": "mykey1", "value": { - "stringValue": "some value" + "stringValue": "myvalue1" + } + }, + { + "key": "mykey2", + "value": { + "stringValue": "myvalue2" } } ], - "exemplars": [], - "flags": 0 + "startTimeUnixNano": "1734094309366798000", + "timeUnixNano": "1734094317871514000", + "asInt": "10" } - ] + ], + "aggregationTemporality": 2, + "isMonotonic": true } }, { - "name": "my.gauge", - "unit": "1", - "description": "I am a Gauge", - "metadata": [], - "gauge": { + "name": "example_histogram", + "histogram": { "dataPoints": [ { - "asDouble": 10, - "startTimeUnixNano": "1544712660300000000", - "timeUnixNano": "1544712660300000000", "attributes": [ { - "key": "my.gauge.attr", + "key": "mykey3", "value": { - "stringValue": "some value" + "stringValue": "myvalue4" } } ], - "exemplars": [], - "flags": 0 + "startTimeUnixNano": "1734094309366875000", + "timeUnixNano": "1734094317871537000", + "count": "1", + "sum": 42, + "bucketCounts": [ + "0", + "0", + "0", + "0", + "1", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0" + ], + "explicitBounds": [ + 0, + 5, + 10, + 25, + 50, + 75, + 100, + 250, + 500, + 750, + 1000, + 2500, + 5000, + 7500, + 10000 + ], + "min": 42, + "max": 42 } - ] + ], + "aggregationTemporality": 2 } }, { - "name": "my.histogram", - "unit": "1", - "description": "I am a Histogram", - "metadata": [], - "histogram": { - "aggregationTemporality": 1, + "name": "example_up_down_counter", + "sum": { "dataPoints": [ { - "startTimeUnixNano": "1544712660300000000", - "timeUnixNano": "1544712660300000000", - "count": 2, - "sum": 2, - "bucketCounts": [1,1], - "explicitBounds": [1], - "min": 0, - "max": 2, "attributes": [ { - "key": "my.histogram.attr", + "key": "mykey5", "value": { - "stringValue": "some value" + "stringValue": "myvalue5" } } ], - "exemplars": [], - "flags": 0 + "startTimeUnixNano": "1734094309366941000", + "timeUnixNano": "1734094317871548000", + "asInt": "-1" } - ] + ], + "aggregationTemporality": 2 } } - ], - "schemaUrl": "whatever" + ] } - ], - "schemaUrl": "whatever" + ] } ] -} \ No newline at end of file +} diff --git a/opentelemetry-otlp/tests/integration_test/expected/metrics/test_flush_on_shutdown.json b/opentelemetry-otlp/tests/integration_test/expected/metrics/test_flush_on_shutdown.json new file mode 100644 index 0000000000..c390a70664 --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/expected/metrics/test_flush_on_shutdown.json @@ -0,0 +1,39 @@ +{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "metrics-integration-test" + } + } + ] + }, + "scopeMetrics": [ + { + "scope": { + "name": "test_flush_on_shutdown" + }, + "metrics": [ + { + "name": "counter_", + "sum": { + "dataPoints": [ + { + "startTimeUnixNano": "1734370440803831000", + "timeUnixNano": "1734370440803905000", + "asInt": "123" + } + ], + "aggregationTemporality": 2, + "isMonotonic": true + } + } + ] + } + ] + } + ] +} diff --git a/opentelemetry-otlp/tests/integration_test/expected/metrics/test_histogram_meter.json b/opentelemetry-otlp/tests/integration_test/expected/metrics/test_histogram_meter.json new file mode 100644 index 0000000000..9ca8a5a49e --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/expected/metrics/test_histogram_meter.json @@ -0,0 +1,84 @@ +{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "metrics-integration-test" + } + } + ] + }, + "scopeMetrics": [ + { + "scope": { + "name": "test_histogram_meter" + }, + "metrics": [ + { + "name": "example_histogram", + "histogram": { + "dataPoints": [ + { + "attributes": [ + { + "key": "mykey3", + "value": { + "stringValue": "myvalue4" + } + } + ], + "startTimeUnixNano": "1734259947902842000", + "timeUnixNano": "1734259949551023000", + "count": "1", + "sum": 42, + "bucketCounts": [ + "0", + "0", + "0", + "0", + "1", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0" + ], + "explicitBounds": [ + 0, + 5, + 10, + 25, + 50, + 75, + 100, + 250, + 500, + 750, + 1000, + 2500, + 5000, + 7500, + 10000 + ], + "min": 42, + "max": 42 + } + ], + "aggregationTemporality": 2 + } + } + ] + } + ] + } + ] +} diff --git a/opentelemetry-otlp/tests/integration_test/expected/metrics/test_u64_counter_meter.json b/opentelemetry-otlp/tests/integration_test/expected/metrics/test_u64_counter_meter.json new file mode 100644 index 0000000000..aeb3da7b20 --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/expected/metrics/test_u64_counter_meter.json @@ -0,0 +1,53 @@ +{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "metrics-integration-test" + } + } + ] + }, + "scopeMetrics": [ + { + "scope": { + "name": "test_u64_counter_meter" + }, + "metrics": [ + { + "name": "counter_u64", + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "mykey1", + "value": { + "stringValue": "myvalue1" + } + }, + { + "key": "mykey2", + "value": { + "stringValue": "myvalue2" + } + } + ], + "startTimeUnixNano": "1734255506254812000", + "timeUnixNano": "1734255533415552000", + "asInt": "10" + } + ], + "aggregationTemporality": 2, + "isMonotonic": true + } + } + ] + } + ] + } + ] +} diff --git a/opentelemetry-otlp/tests/integration_test/expected/metrics/test_up_down_meter.json b/opentelemetry-otlp/tests/integration_test/expected/metrics/test_up_down_meter.json new file mode 100755 index 0000000000..a82cd63acf --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/expected/metrics/test_up_down_meter.json @@ -0,0 +1,46 @@ +{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "metrics-integration-test" + } + } + ] + }, + "scopeMetrics": [ + { + "scope": { + "name": "test_up_down_meter" + }, + "metrics": [ + { + "name": "example_up_down_counter", + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "mykey5", + "value": { + "stringValue": "myvalue5" + } + } + ], + "startTimeUnixNano": "1734259947902844000", + "timeUnixNano": "1734259952816822000", + "asInt": "-1" + } + ], + "aggregationTemporality": 2 + } + } + ] + } + ] + } + ] +} diff --git a/opentelemetry-otlp/tests/integration_test/expected/serialized_metrics.json b/opentelemetry-otlp/tests/integration_test/expected/serialized_metrics.json index 4910e128a2..de13fb3cbf 100644 --- a/opentelemetry-otlp/tests/integration_test/expected/serialized_metrics.json +++ b/opentelemetry-otlp/tests/integration_test/expected/serialized_metrics.json @@ -6,7 +6,7 @@ { "key": "service.name", "value": { - "stringValue": "my.service" + "stringValue": "metrics-integration-test" } } ], @@ -15,112 +15,81 @@ "scopeMetrics": [ { "scope": { - "name": "my.library", - "version": "1.0.0", - "attributes": [ - { - "key": "my.scope.attribute", - "value": { - "stringValue": "some scope attribute" - } - } - ], + "name": "meter", + "version": "", + "attributes": [], "droppedAttributesCount": 0 }, "metrics": [ { - "name": "my.counter", - "description": "I am a Counter", - "unit": "1", + "name": "counter_u64", + "description": "", + "unit": "", "metadata": [], "sum": { "dataPoints": [ { "attributes": [ { - "key": "my.counter.attr", + "key": "mykey1", "value": { - "stringValue": "some value" + "stringValue": "myvalue1" + } + }, + { + "key": "mykey2", + "value": { + "stringValue": "myvalue2" } } ], - "startTimeUnixNano": "1544712660300000000", - "timeUnixNano": "1544712660300000000", + "startTimeUnixNano": "1734094309366798000", + "timeUnixNano": "1734094317871514000", "exemplars": [], - "flags": 0, - "asDouble": 5.0 + "flags": 0 } ], - "aggregationTemporality": 1, + "aggregationTemporality": 2, "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": "example_histogram", + "description": "", + "unit": "", + "metadata": [] }, { - "name": "my.histogram", - "description": "I am a Histogram", - "unit": "1", + "name": "example_up_down_counter", + "description": "", + "unit": "", "metadata": [], - "histogram": { + "sum": { "dataPoints": [ { "attributes": [ { - "key": "my.histogram.attr", + "key": "mykey5", "value": { - "stringValue": "some value" + "stringValue": "myvalue5" } } ], - "startTimeUnixNano": "1544712660300000000", - "timeUnixNano": "1544712660300000000", - "count": 2, - "sum": 2.0, - "bucketCounts": [ - 1, - 1 - ], - "explicitBounds": [ - 1.0 - ], + "startTimeUnixNano": "1734094309366941000", + "timeUnixNano": "1734094317871548000", "exemplars": [], - "flags": 0, - "min": 0.0, - "max": 2.0 + "flags": 0 } ], - "aggregationTemporality": 1 + "aggregationTemporality": 2, + "isMonotonic": false } } ], - "schemaUrl": "whatever" + "schemaUrl": "" } ], - "schemaUrl": "whatever" + "schemaUrl": "" } ] } \ No newline at end of file diff --git a/opentelemetry-otlp/tests/integration_test/otel-collector-config.yaml b/opentelemetry-otlp/tests/integration_test/otel-collector-config.yaml index 7cd19bbfee..548d6fa44a 100644 --- a/opentelemetry-otlp/tests/integration_test/otel-collector-config.yaml +++ b/opentelemetry-otlp/tests/integration_test/otel-collector-config.yaml @@ -7,14 +7,23 @@ receivers: endpoint: 0.0.0.0:4318 exporters: - file: - path: /testresults/result.json + file/traces: + path: /testresults/traces.json + file/logs: + path: /testresults/logs.json + rotation: + file/metrics: + path: /testresults/metrics.json service: pipelines: traces: receivers: [otlp] - exporters: [file] + exporters: [file/traces] logs: receivers: [otlp] - exporters: [file] + exporters: [file/logs] + metrics: + receivers: [otlp] + exporters: [file/metrics] + diff --git a/opentelemetry-otlp/tests/integration_test/src/images.rs b/opentelemetry-otlp/tests/integration_test/src/images.rs deleted file mode 100644 index 37a9c1b38b..0000000000 --- a/opentelemetry-otlp/tests/integration_test/src/images.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::collections::HashMap; -use testcontainers::core::WaitFor; -use testcontainers::Image; - -pub struct Collector { - volumes: HashMap, -} - -impl Image for Collector { - type Args = (); - - fn name(&self) -> String { - "otel/opentelemetry-collector".to_string() - } - - fn tag(&self) -> String { - "latest".to_string() - } - - fn ready_conditions(&self) -> Vec { - vec![WaitFor::Nothing] - } - - fn volumes(&self) -> Box + '_> { - Box::new(self.volumes.iter()) - } - - fn expose_ports(&self) -> Vec { - vec![ - // 4317, // gRPC port, defined in Dockerfile - // 4318, // HTTP port, defined in Dockerfile - ] - } -} - -impl Default for Collector { - fn default() -> Self { - Collector { - volumes: HashMap::from([( - "./otel-collector-config.yaml".into(), - "/etc/otelcol/config.yaml".into(), - )]), - } - } -} - -impl Collector { - pub fn with_volume(mut self, src: &str, dst: &str) -> Self { - self.volumes.insert(src.into(), dst.into()); - self - } -} diff --git a/opentelemetry-otlp/tests/integration_test/src/lib.rs b/opentelemetry-otlp/tests/integration_test/src/lib.rs index e6bc88c742..65faf81bf4 100644 --- a/opentelemetry-otlp/tests/integration_test/src/lib.rs +++ b/opentelemetry-otlp/tests/integration_test/src/lib.rs @@ -1,4 +1,4 @@ -pub mod images; pub mod logs_asserter; pub mod metrics_asserter; +pub mod test_utils; 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 index 4845270999..f370df8a62 100644 --- a/opentelemetry-otlp/tests/integration_test/src/metrics_asserter.rs +++ b/opentelemetry-otlp/tests/integration_test/src/metrics_asserter.rs @@ -1,40 +1,64 @@ +use anyhow::Result; +use serde_json::Value; use std::fs::File; +use std::io::{BufReader, Read}; -use opentelemetry_proto::tonic::metrics::v1::{MetricsData, ResourceMetrics}; +pub fn read_metrics_from_json(file: File) -> Result { + // Create a buffered reader for the file + let mut reader = BufReader::new(file); + let mut contents = String::new(); + + // Read the file contents into a string + reader + .read_to_string(&mut contents) + .expect("Failed to read json file"); + + // Parse the contents into a JSON Value + let metrics_data: Value = serde_json::from_str(&contents)?; + Ok(metrics_data) +} pub struct MetricsAsserter { - results: Vec, - expected: Vec, + results: Value, + expected: Value, } impl MetricsAsserter { - pub fn new(results: Vec, expected: Vec) -> Self { + pub fn new(results: Value, expected: Value) -> Self { MetricsAsserter { results, expected } } - pub fn assert(self) { - self.assert_resource_metrics_eq(&self.results, &self.expected); + pub fn assert(mut self) { + // Normalize JSON by cleaning out timestamps + Self::zero_out_timestamps(&mut self.results); + Self::zero_out_timestamps(&mut self.expected); + + // Perform the assertion + assert_eq!( + self.results, self.expected, + "Metrics did not match. Results: {:#?}, Expected: {:#?}", + 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); + /// Recursively removes or zeros out timestamp fields in the JSON + fn zero_out_timestamps(value: &mut Value) { + match value { + Value::Object(map) => { + for (key, val) in map.iter_mut() { + if key == "startTimeUnixNano" || key == "timeUnixNano" { + *val = Value::String("0".to_string()); + } else { + Self::zero_out_timestamps(val); + } + } + } + Value::Array(array) => { + for item in array.iter_mut() { + Self::zero_out_timestamps(item); + } + } + _ => {} } } } - -// 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/src/test_utils.rs b/opentelemetry-otlp/tests/integration_test/src/test_utils.rs new file mode 100644 index 0000000000..7456569cce --- /dev/null +++ b/opentelemetry-otlp/tests/integration_test/src/test_utils.rs @@ -0,0 +1,149 @@ +//! Supporting infrastructure for OTLP integration tests. +//! +//! This module provides the pieces needed to work with an actual opentelemetry-collector +//! instance, which is started in Docker and has its output plumbed back into the host filesystem. +//! This lets us write tests that push data over OTLP (HTTP or gRPC) to the collector, and then read +//! that data back from the filesystem to ensure everything worked out as expected. +//! +//! To use this module, all you need to do is call `start_collector_container()` from each +//! of your tests, and use a single `#[dtor]` at the end of your test file to call +//! `stop_collector_container`. Note that as cargo integration tests run a process-per-test-file, +//! each test will get its own fresh instance of the container. +//! +//! Only a single test suite can run at once, as each container has statically mapped ports, but +//! this works nicely with the way cargo executes the suite. +//! +//! To skip integration tests with cargo, you can run `cargo test --mod`, which will run unit tests +//! only. +//! +#![cfg(unix)] + +use anyhow::Result; +use opentelemetry::{otel_debug, otel_info}; +use std::fs; +use std::fs::File; +use std::os::unix::fs::PermissionsExt; +use std::sync::{Arc, Mutex, Once, OnceLock}; +use testcontainers::core::wait::HttpWaitStrategy; +use testcontainers::core::{ContainerPort, Mount}; +use testcontainers::{ + core::WaitFor, + runners::AsyncRunner, + ContainerAsync, GenericImage, ImageExt, +}; +use tracing_subscriber::FmtSubscriber; + +// Static references for container management +static COLLECTOR_ARC: OnceLock>>>> = OnceLock::new(); + +pub static METRICS_FILE: &str = "./actual/metrics.json"; +pub static LOGS_FILE: &str = "./actual/logs.json"; +pub static TRACES_FILE: &str = "./actual/traces.json"; + +static INIT_TRACING: Once = Once::new(); + +fn init_tracing() { + INIT_TRACING.call_once(|| { + let subscriber = FmtSubscriber::builder() + .with_max_level(tracing::Level::DEBUG) + .finish(); + + tracing::subscriber::set_global_default(subscriber) + .expect("Failed to set tracing subscriber"); + otel_info!(name: "init_tracing"); + }); +} + +pub async fn start_collector_container() -> Result<()> { + init_tracing(); + + let mut arc_guard = COLLECTOR_ARC + .get_or_init(|| Mutex::new(None)) + .lock() + .unwrap(); + + // If the container isn't running, start it. + if arc_guard.is_none() { + // Make sure all our test data is mounted + upsert_empty_file(METRICS_FILE); + upsert_empty_file(TRACES_FILE); + upsert_empty_file(LOGS_FILE); + + // Start a new container + let container_instance = GenericImage::new("otel/opentelemetry-collector", "latest") + .with_wait_for(WaitFor::http( + HttpWaitStrategy::new("/") + .with_expected_status_code(404u16) + .with_port(ContainerPort::Tcp(4318)), + )) + .with_mapped_port(4317, ContainerPort::Tcp(4317)) + .with_mapped_port(4318, ContainerPort::Tcp(4318)) + .with_mount(Mount::bind_mount( + fs::canonicalize("./otel-collector-config.yaml")?.to_string_lossy(), + "/etc/otelcol/config.yaml", + )) + .with_mount(Mount::bind_mount( + fs::canonicalize("./actual/logs.json")?.to_string_lossy(), + "/testresults/logs.json", + )) + .with_mount(Mount::bind_mount( + fs::canonicalize("./actual/metrics.json")?.to_string_lossy(), + "/testresults/metrics.json", + )) + .with_mount(Mount::bind_mount( + fs::canonicalize("./actual/traces.json")?.to_string_lossy(), + "/testresults/traces.json", + )) + .start() + .await?; + + let container = Arc::new(container_instance); + otel_debug!( + name: "Container started", + ports = format!("{:?}", container.ports().await)); + + // Give the container a second to stabilize + //tokio::time::sleep(std::time::Duration::from_secs(5)).await; + + // Store the container in COLLECTOR_ARC + *arc_guard = Some(Arc::clone(&container)); + } + + Ok(()) +} + +/// +/// Creates an empty file with permissions that make it usable both within docker +/// and on the host. +/// +fn upsert_empty_file(path: &str) -> File { + let file = File::create(path).unwrap(); + file.set_permissions(std::fs::Permissions::from_mode(0o666)) + .unwrap(); + file +} + +/// +/// Shuts down our collector container. This should be run as part of each test +/// suite shutting down! +/// +pub fn stop_collector_container() { + // This is a bit heinous. We don't have an async runtime left when + // we hit this call, so we can't use the async methods on the testcontainers + // interface to shutdown. + // We _need_ to do this here, because otherwise we have no "all the tests in the module + // were complete" hook. + // + // https://github.com/testcontainers/testcontainers-rs/issues/707 + otel_debug!(name: "stop_collector_container"); + + if let Some(mutex_option_arc) = COLLECTOR_ARC.get() { + let guard = mutex_option_arc.lock().unwrap(); + if let Some(container_arc) = &*guard { + std::process::Command::new("docker") + .args(["container", "rm", "-f", container_arc.id()]) + .output() + .expect("failed to stop testcontainer"); + } + } +} diff --git a/opentelemetry-otlp/tests/integration_test/src/trace_asserter.rs b/opentelemetry-otlp/tests/integration_test/src/trace_asserter.rs index 00c7c2300d..ce7eec928a 100644 --- a/opentelemetry-otlp/tests/integration_test/src/trace_asserter.rs +++ b/opentelemetry-otlp/tests/integration_test/src/trace_asserter.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, Span, TracesData}; use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Formatter}; @@ -213,9 +214,9 @@ fn span_eq(left: &Span, right: &Span) -> bool { } // read a file contains ResourceSpans in json format -pub fn read_spans_from_json(file: File) -> Vec { +pub fn read_spans_from_json(file: File) -> Result> { let reader = std::io::BufReader::new(file); - let trace_data: TracesData = serde_json::from_reader(reader).expect("Failed to read json file"); - trace_data.resource_spans + let trace_data: TracesData = serde_json::from_reader(reader)?; + Ok(trace_data.resource_spans) } diff --git a/opentelemetry-otlp/tests/integration_test/tests/integration_tests.rs b/opentelemetry-otlp/tests/integration_test/tests/integration_tests.rs deleted file mode 100644 index 5f5468d0dc..0000000000 --- a/opentelemetry-otlp/tests/integration_test/tests/integration_tests.rs +++ /dev/null @@ -1,142 +0,0 @@ -#![cfg(unix)] - -use integration_test_runner::images::Collector; -use std::fs::File; -use std::os::unix::fs::PermissionsExt; -use std::time::Duration; -use testcontainers::clients::Cli; -use testcontainers::core::Port; -use testcontainers::RunnableImage; - -mod logs; -mod metrics; -mod traces; - -const COLLECTOR_CONTAINER_NAME: &str = "otel-collector"; -const TEST_RESULT_DIR_IN_CONTAINER: &str = "testresults"; -const EXPECTED_DIR: &str = "./expected"; -const RESULT_FILE_PATH: &str = "./result.json"; - -struct TestSuite { - expected_file_path: &'static str, -} - -impl TestSuite { - fn new(expected_file_path: &'static str) -> Self { - Self { expected_file_path } - } - - pub fn expected_file_path(&self) -> String { - format!("{}/{}", EXPECTED_DIR, self.expected_file_path) - } - - pub fn result_file_path_in_container(&self) -> String { - format!("/{}/{}", TEST_RESULT_DIR_IN_CONTAINER, RESULT_FILE_PATH) - } - - pub fn result_file_path(&self) -> String { - format!("./{}", RESULT_FILE_PATH) - } - - /// Create a empty file on localhost and copy it to container with proper permissions - /// we have to create the file for the container otherwise we will encounter a permission denied error. - /// see https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/3159 - pub fn create_temporary_result_file(&self) -> File { - let file = File::create(self.result_file_path()).unwrap(); - file.set_permissions(std::fs::Permissions::from_mode(0o666)) - .unwrap(); - file - } -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -#[ignore] // skip when running unit test -async fn integration_tests() { - trace_integration_tests().await; - logs_integration_tests().await; -} - -async fn trace_integration_tests() { - let test_suites = [TestSuite::new("traces.json")]; - let mut collector_image = Collector::default(); - for test in test_suites.as_ref() { - let _ = test.create_temporary_result_file(); - collector_image = collector_image.with_volume( - test.result_file_path().as_str(), - test.result_file_path_in_container().as_str(), - ); - } - - let docker = Cli::default(); - let mut image = - RunnableImage::from(collector_image).with_container_name(COLLECTOR_CONTAINER_NAME); - - for port in [ - 4317, // gRPC port - 4318, // HTTP port - ] { - image = image.with_mapped_port(Port { - local: port, - internal: port, - }) - } - - let collector_container = docker.run(image); - - tokio::time::sleep(Duration::from_secs(5)).await; - traces::traces().await.unwrap(); - - // wait for file to flush to disks - // ideally we should use volume mount but otel collector file exporter doesn't handle permission too well - // bind mount mitigate the issue by set up the permission correctly on host system - tokio::time::sleep(Duration::from_secs(5)).await; - traces::assert_traces_results( - test_suites[0].result_file_path().as_str(), - test_suites[0].expected_file_path().as_str(), - ); - - collector_container.stop(); -} - -async fn logs_integration_tests() { - let test_suites = [TestSuite::new("logs.json")]; - - let mut collector_image = Collector::default(); - for test in test_suites.as_ref() { - let _ = test.create_temporary_result_file(); - collector_image = collector_image.with_volume( - test.result_file_path().as_str(), - test.result_file_path_in_container().as_str(), - ); - } - - let docker = Cli::default(); - let mut image = - RunnableImage::from(collector_image).with_container_name(COLLECTOR_CONTAINER_NAME); - - for port in [ - 4317, // gRPC port - 4318, // HTTP port - ] { - image = image.with_mapped_port(Port { - local: port, - internal: port, - }) - } - - let collector_container = docker.run(image); - - tokio::time::sleep(Duration::from_secs(5)).await; - logs::logs().await.unwrap(); - - // wait for file to flush to disks - // ideally we should use volume mount but otel collector file exporter doesn't handle permission too well - // bind mount mitigate the issue by set up the permission correctly on host system - tokio::time::sleep(Duration::from_secs(5)).await; - logs::assert_logs_results( - test_suites[0].result_file_path().as_str(), - test_suites[0].expected_file_path().as_str(), - ); - - collector_container.stop(); -} diff --git a/opentelemetry-otlp/tests/integration_test/tests/logs.rs b/opentelemetry-otlp/tests/integration_test/tests/logs.rs index 4ff7b67eb6..8498a24913 100644 --- a/opentelemetry-otlp/tests/integration_test/tests/logs.rs +++ b/opentelemetry-otlp/tests/integration_test/tests/logs.rs @@ -1,16 +1,19 @@ #![cfg(unix)] +use anyhow::Result; +use ctor::dtor; use integration_test_runner::logs_asserter::{read_logs_from_json, LogsAsserter}; +use integration_test_runner::test_utils; use log::{info, Level}; use opentelemetry_appender_log::OpenTelemetryLogBridge; use opentelemetry_otlp::LogExporter; -use opentelemetry_sdk::logs::{LogError, LoggerProvider}; +use opentelemetry_sdk::logs::LoggerProvider; use opentelemetry_sdk::{logs as sdklogs, runtime, Resource}; -use std::error::Error; use std::fs::File; use std::os::unix::fs::MetadataExt; +use std::time::Duration; -fn init_logs() -> Result { +fn init_logs() -> Result { let exporter_builder = LogExporter::builder(); #[cfg(feature = "tonic-client")] let exporter_builder = exporter_builder.with_tonic(); @@ -34,7 +37,11 @@ fn init_logs() -> Result { .build()) } -pub async fn logs() -> Result<(), Box> { +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +pub async fn test_logs() -> Result<()> { + // Make sure the container is running + test_utils::start_collector_container().await?; + let logger_provider = init_logs().unwrap(); let otel_log_appender = OpenTelemetryLogBridge::new(&logger_provider); log::set_boxed_logger(Box::new(otel_log_appender))?; @@ -42,6 +49,11 @@ pub async fn logs() -> Result<(), Box> { info!(target: "my-target", "hello from {}. My price is {}.", "banana", 2.99); let _ = logger_provider.shutdown(); + + tokio::time::sleep(Duration::from_secs(10)).await; + + assert_logs_results(test_utils::LOGS_FILE, "expected/logs.json"); + Ok(()) } @@ -67,3 +79,12 @@ pub fn test_assert_logs_eq() { let logs = read_logs_from_json(File::open("./expected/logs.json").unwrap()); LogsAsserter::new(logs.clone(), logs).assert(); } + +/// +/// Make sure we stop the collector container, otherwise it will sit around hogging our +/// ports and subsequent test runs will fail. +/// +#[dtor] +fn shutdown() { + test_utils::stop_collector_container(); +} diff --git a/opentelemetry-otlp/tests/integration_test/tests/metrics.rs b/opentelemetry-otlp/tests/integration_test/tests/metrics.rs index 5395c67d58..b1b6a5ebdd 100644 --- a/opentelemetry-otlp/tests/integration_test/tests/metrics.rs +++ b/opentelemetry-otlp/tests/integration_test/tests/metrics.rs @@ -1,23 +1,332 @@ -use std::{fs::File, io::Write}; +//! OTLP integration tests for metrics +//! Note: these are all expressed using Serde types for the deserialized metrics records. +//! We might consider changing this once we have fixed the issue identified in the #[ignore]d test +//! `test_roundtrip_example_data` - as the roundtripping is currently broken for metrics. +//! +#![cfg(unix)] +use anyhow::{Context, Result}; +use ctor::dtor; use integration_test_runner::metrics_asserter::{read_metrics_from_json, MetricsAsserter}; +use integration_test_runner::test_utils; +use integration_test_runner::test_utils::start_collector_container; +use opentelemetry::KeyValue; +use opentelemetry_otlp::MetricExporter; use opentelemetry_proto::tonic::metrics::v1::MetricsData; +use opentelemetry_sdk::metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider}; +use opentelemetry_sdk::Resource; +use serde_json::Value; +use std::fs; +use std::fs::File; +use std::sync::Mutex; +use std::time::Duration; -#[test] -fn test_serde() { - let metrics = read_metrics_from_json(File::open("./expected/metrics.json").unwrap()); +static SETUP_DONE: Mutex = Mutex::new(false); - let json = serde_json::to_string_pretty(&MetricsData { - resource_metrics: metrics, - }) - .expect("Failed to serialize metrics"); +static RESULT_PATH: &str = "actual/metrics.json"; - // Write to file. - let mut file = File::create("./expected/serialized_metrics.json").unwrap(); - file.write_all(json.as_bytes()).unwrap(); +/// Initializes the OpenTelemetry metrics pipeline +async fn init_metrics() -> SdkMeterProvider { + let exporter = create_exporter(); - 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()); + let reader = PeriodicReader::builder(exporter) + .with_interval(Duration::from_millis(100)) + .with_timeout(Duration::from_secs(1)) + .build(); - MetricsAsserter::new(left, right).assert(); + let resource = Resource::builder_empty() + .with_service_name("metrics-integration-test") + .build(); + + let meter_provider = MeterProviderBuilder::default() + .with_resource(resource) + .with_reader(reader) + .build(); + + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + meter_provider +} + +/// +/// Creates an exporter using the appropriate HTTP or gRPC client based on +/// the configured features. +/// +fn create_exporter() -> MetricExporter { + let exporter_builder = MetricExporter::builder(); + + #[cfg(feature = "tonic-client")] + let exporter_builder = exporter_builder.with_tonic(); + #[cfg(not(feature = "tonic-client"))] + #[cfg(any( + feature = "hyper-client", + feature = "reqwest-client", + feature = "reqwest-blocking-client" + ))] + let exporter_builder = exporter_builder.with_http(); + + exporter_builder + .build() + .expect("Failed to build MetricExporter") +} + +/// +/// Retrieves the latest metrics for the given scope. Each test should use +/// its own scope, so that we can easily pull the data for it out from the rest +/// of the data. +/// +/// This will also retrieve the resource attached to the scope. +/// +pub fn fetch_latest_metrics_for_scope(scope_name: &str) -> Result { + // Open the file and fetch the contents + let contents = fs::read_to_string(test_utils::METRICS_FILE)?; + + // Find the last parseable metrics line that contains the desired scope + let json_line = contents + .lines() + .rev() + .find_map(|line| { + // Attempt to parse the line as JSON + serde_json::from_str::(line) + .ok() + .and_then(|mut json_line| { + // Check if it contains the specified scope + if let Some(resource_metrics) = json_line + .get_mut("resourceMetrics") + .and_then(|v| v.as_array_mut()) + { + resource_metrics.retain_mut(|resource| { + if let Some(scope_metrics) = resource + .get_mut("scopeMetrics") + .and_then(|v| v.as_array_mut()) + { + scope_metrics.retain(|scope| { + scope + .get("scope") + .and_then(|s| s.get("name")) + .and_then(|name| name.as_str()) + .map_or(false, |n| n == scope_name) + }); + + // Keep the resource only if it has any matching `ScopeMetrics` + !scope_metrics.is_empty() + } else { + false + } + }); + + // If any resource metrics remain, return this line + if !resource_metrics.is_empty() { + return Some(json_line); + } + } + + None + }) + }) + .with_context(|| { + format!( + "No valid JSON line containing scope `{}` found.", + scope_name + ) + })?; + + Ok(json_line) +} + +/// +/// Performs setup for metrics tests +/// +async fn setup_metrics_test() -> Result<()> { + // Make sure the collector container is running + start_collector_container().await?; + + let mut done = SETUP_DONE.lock().unwrap(); + if !*done { + println!("Running setup before any tests..."); + *done = true; // Mark setup as done + + // Initialise the metrics subsystem + _ = init_metrics().await; + } + + // Truncate results + _ = File::create(RESULT_PATH).expect("it's good"); + + Ok(()) +} + +/// +/// Check that the metrics for the given scope match what we expect. This +/// includes zeroing out timestamps, which we reasonably expect not to match. +/// +pub fn validate_metrics_against_results(scope_name: &str) -> Result<()> { + // Define the results file path + let results_file_path = format!("./expected/metrics/{}.json", scope_name); + + // Fetch the actual metrics for the given scope + let actual_metrics = fetch_latest_metrics_for_scope(scope_name) + .context(format!("Failed to fetch metrics for scope: {}", scope_name))?; + + // Read the expected metrics from the results file + let expected_metrics = { + let file = File::open(&results_file_path).context(format!( + "Failed to open results file: {}", + results_file_path + ))?; + read_metrics_from_json(file) + }?; + + // Compare the actual metrics with the expected metrics + MetricsAsserter::new(actual_metrics, expected_metrics).assert(); + + Ok(()) +} + +/// +/// TODO - the HTTP metrics exporters do not seem to flush at the moment. +/// TODO - fix this asynchronously. +/// +#[cfg(test)] +#[cfg(not(feature = "hyper-client"))] +#[cfg(not(feature = "reqwest-client"))] +#[cfg(not(feature = "reqwest-blocking-client"))] +mod tests { + + use super::*; + use opentelemetry::metrics::MeterProvider; + + /// + /// Validate JSON/Protobuf models roundtrip correctly. + /// + /// TODO - this test fails currently. Fields disappear, such as the actual value of a given metric. + /// This appears to be on the _deserialization_ side. + /// Issue: https://github.com/open-telemetry/opentelemetry-rust/issues/2434 + /// + #[tokio::test] + #[ignore] + async fn test_roundtrip_example_data() -> Result<()> { + let metrics_in = include_str!("../expected/metrics/test_u64_counter_meter.json"); + let metrics: MetricsData = serde_json::from_str(metrics_in)?; + let metrics_out = serde_json::to_string(&metrics)?; + + println!("{:}", metrics_out); + + let metrics_in_json: Value = serde_json::from_str(metrics_in)?; + let metrics_out_json: Value = serde_json::from_str(&metrics_out)?; + + assert_eq!(metrics_in_json, metrics_out_json); + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + async fn test_u64_counter() -> Result<()> { + let _result_path = setup_metrics_test().await; + const METER_NAME: &str = "test_u64_counter_meter"; + + // Add data to u64_counter + let meter = opentelemetry::global::meter_provider().meter(METER_NAME); + + let counter = meter.u64_counter("counter_u64").build(); + counter.add( + 10, + &[ + KeyValue::new("mykey1", "myvalue1"), + KeyValue::new("mykey2", "myvalue2"), + ], + ); + + tokio::time::sleep(Duration::from_secs(2)).await; + + // Validate metrics against results file + validate_metrics_against_results(METER_NAME)?; + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + // #[ignore] // skip when running unit test + async fn test_histogram() -> Result<()> { + _ = setup_metrics_test().await; + const METER_NAME: &str = "test_histogram_meter"; + + // Add data to histogram + let meter = opentelemetry::global::meter_provider().meter(METER_NAME); + let histogram = meter.u64_histogram("example_histogram").build(); + histogram.record(42, &[KeyValue::new("mykey3", "myvalue4")]); + tokio::time::sleep(Duration::from_secs(5)).await; + + validate_metrics_against_results(METER_NAME)?; + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + // #[ignore] // skip when running unit test + async fn test_up_down_counter() -> Result<()> { + _ = setup_metrics_test().await; + const METER_NAME: &str = "test_up_down_meter"; + + // Add data to up_down_counter + let meter = opentelemetry::global::meter_provider().meter(METER_NAME); + let up_down_counter = meter.i64_up_down_counter("example_up_down_counter").build(); + up_down_counter.add(-1, &[KeyValue::new("mykey5", "myvalue5")]); + tokio::time::sleep(Duration::from_secs(5)).await; + + validate_metrics_against_results(METER_NAME)?; + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + #[ignore] + async fn test_flush_on_shutdown() -> Result<()> { + const METER_NAME: &str = "test_flush_on_shutdown"; + + // Set everything up by hand, so that we can shutdown() the exporter + // and make sure our data is flushed through. + + // Make sure the collector is running + start_collector_container().await?; + + // Set up the exporter + let exporter = create_exporter(); + let reader = PeriodicReader::builder(exporter) + .with_interval(Duration::from_millis(100)) + .with_timeout(Duration::from_secs(1)) + .build(); + let resource = Resource::builder_empty() + .with_service_name("metrics-integration-test") + .build(); + let meter_provider = MeterProviderBuilder::default() + .with_resource(resource) + .with_reader(reader) + .build(); + + // Send something + let meter = meter_provider.meter(METER_NAME); + let counter = meter.u64_counter("counter_").build(); + counter.add(123, &[]); + + // Shutdown + meter_provider.shutdown()?; + + // We still need to sleep, to give otel-collector a chance to flush to disk + tokio::time::sleep(Duration::from_secs(2)).await; + + validate_metrics_against_results(METER_NAME)?; + + Ok(()) + } +} + +/// +/// Make sure we stop the collector container, otherwise it will sit around hogging our +/// ports and subsequent test runs will fail. +/// +#[dtor] +fn shutdown() { + println!("metrics::shutdown"); + test_utils::stop_collector_container(); } diff --git a/opentelemetry-otlp/tests/integration_test/tests/traces.rs b/opentelemetry-otlp/tests/integration_test/tests/traces.rs index 20a2bb15a5..1601e04132 100644 --- a/opentelemetry-otlp/tests/integration_test/tests/traces.rs +++ b/opentelemetry-otlp/tests/integration_test/tests/traces.rs @@ -9,12 +9,16 @@ use opentelemetry::{ }; use opentelemetry_otlp::SpanExporter; +use anyhow::Result; +use ctor::dtor; +use integration_test_runner::test_utils; use opentelemetry_proto::tonic::trace::v1::TracesData; use opentelemetry_sdk::{runtime, trace as sdktrace, Resource}; -use std::error::Error; use std::fs::File; use std::io::Write; use std::os::unix::fs::MetadataExt; +use std::time::Duration; +use tokio::time::sleep; fn init_tracer_provider() -> Result { let exporter_builder = SpanExporter::builder(); @@ -43,7 +47,10 @@ fn init_tracer_provider() -> Result { const LEMONS_KEY: Key = Key::from_static_str("lemons"); const ANOTHER_KEY: Key = Key::from_static_str("ex.com/another"); -pub async fn traces() -> Result<(), Box> { +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +pub async fn traces() -> Result<()> { + test_utils::start_collector_container().await?; + let tracer_provider = init_tracer_provider().expect("Failed to initialize tracer provider."); global::set_tracer_provider(tracer_provider.clone()); @@ -67,42 +74,51 @@ pub async fn traces() -> Result<(), Box> { tracer_provider.shutdown()?; + // Give it a second to flush + sleep(Duration::from_secs(2)).await; + + // Validate results + assert_traces_results(test_utils::TRACES_FILE, "./expected/traces.json")?; + Ok(()) } -pub fn assert_traces_results(result: &str, expected: &str) { - let left = read_spans_from_json(File::open(expected).unwrap()); - let right = read_spans_from_json(File::open(result).unwrap()); +pub fn assert_traces_results(result: &str, expected: &str) -> Result<()> { + let left = read_spans_from_json(File::open(expected)?)?; + let right = read_spans_from_json(File::open(result)?)?; TraceAsserter::new(left, right).assert(); // we cannot read result json file because the timestamp was represents as string instead of u64. // need to fix it on json file exporter + assert!(File::open(result)?.metadata()?.size() > 0); - assert!(File::open(result).unwrap().metadata().unwrap().size() > 0) + Ok(()) } #[test] #[should_panic(expected = "left: \"Sub operation...\"")] // we swap the parent spans with child spans in failed_traces.json pub fn test_assert_span_eq_failure() { - let left = read_spans_from_json(File::open("./expected/traces.json").unwrap()); - let right = read_spans_from_json(File::open("./expected/failed_traces.json").unwrap()); + let left = read_spans_from_json(File::open("./expected/traces.json").unwrap()).unwrap(); + let right = read_spans_from_json(File::open("./expected/failed_traces.json").unwrap()).unwrap(); TraceAsserter::new(right, left).assert(); } #[test] -pub fn test_assert_span_eq() { - let spans = read_spans_from_json(File::open("./expected/traces.json").unwrap()); +pub fn test_assert_span_eq() -> Result<()> { + let spans = read_spans_from_json(File::open("./expected/traces.json")?)?; TraceAsserter::new(spans.clone(), spans).assert(); + + Ok(()) } #[test] -pub fn test_serde() { +pub fn test_serde() -> Result<()> { let spans = read_spans_from_json( File::open("./expected/traces.json").expect("Failed to read traces.json"), - ); + )?; let json = serde_json::to_string_pretty(&TracesData { resource_spans: spans, }) @@ -114,11 +130,22 @@ pub fn test_serde() { let left = read_spans_from_json( File::open("./expected/traces.json").expect("Failed to read traces.json"), - ); + )?; let right = read_spans_from_json( File::open("./expected/serialized_traces.json") .expect("Failed to read serialized_traces.json"), - ); + )?; TraceAsserter::new(left, right).assert(); + + Ok(()) +} + +/// +/// Make sure we stop the collector container, otherwise it will sit around hogging our +/// ports and subsequent test runs will fail. +/// +#[dtor] +fn shutdown() { + test_utils::stop_collector_container(); } diff --git a/scripts/integration_tests.sh b/scripts/integration_tests.sh index 07b9d472ba..b984cc023f 100755 --- a/scripts/integration_tests.sh +++ b/scripts/integration_tests.sh @@ -1,19 +1,36 @@ set -e + TEST_DIR="./opentelemetry-otlp/tests/integration_test/tests" if [ -d "$TEST_DIR" ]; then cd "$TEST_DIR" + # Run tests with the grpc-tonic feature - cargo test --no-default-features --features "tonic-client" -- --ignored + echo + echo #### + echo Integration Tests: gRPC Tonic Client + echo #### + echo + cargo test --no-default-features --features "tonic-client","internal-logs" # Run tests with the reqwest-client feature - cargo test --no-default-features --features "reqwest-client" -- --ignored + echo + echo #### + echo Integration Tests: Reqwest Client + echo #### + echo + cargo test --no-default-features --features "reqwest-client","internal-logs" # TODO - Uncomment the following lines once the reqwest-blocking-client feature is working. - # cargo test --no-default-features --features "reqwest-blocking-client" -- --ignored + # cargo test --no-default-features --features "reqwest-blocking-client" # Run tests with the hyper-client feature - cargo test --no-default-features --features "hyper-client" -- --ignored + echo + echo #### + echo Integration Tests: Hyper Client + echo #### + echo + cargo test --no-default-features --features "hyper-client","internal-logs" else echo "Directory $TEST_DIR does not exist. Skipping tests." exit 1 diff --git a/scripts/test.sh b/scripts/test.sh index dfcb925659..467d5f7c4a 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -2,15 +2,19 @@ set -eu +# +# Using '--lib' skips integration tests +# + echo "Running tests for all packages in workspace with --all-features" -cargo test --workspace --all-features +cargo test --workspace --all-features --lib # See https://github.com/rust-lang/cargo/issues/5364 echo "Running tests for opentelemetry package with --no-default-features" -cargo test --manifest-path=opentelemetry/Cargo.toml --no-default-features +cargo test --manifest-path=opentelemetry/Cargo.toml --no-default-features --lib # Run global tracer provider test in single thread # //TODO: This tests were not running for a while. Need to find out how to run # run them. Using --ignored will run other tests as well, so that cannot be used. # echo "Running global tracer provider for opentelemetry-sdk package with single thread." -# cargo test --manifest-path=opentelemetry-sdk/Cargo.toml --all-features -- --test-threads=1 +# cargo test --manifest-path=opentelemetry-sdk/Cargo.toml --all-features -- --test-threads=1 --lib