diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..2f7896d1d1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +target/ diff --git a/.github/codecov.yaml b/.github/codecov.yaml index 807624140a..8c86f46582 100644 --- a/.github/codecov.yaml +++ b/.github/codecov.yaml @@ -10,3 +10,6 @@ coverage: default: target: auto threshold: 0.5% + +ignore: + - "opentelemetry-jaeger/src/testing" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e43d20bd1..5b5d791054 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test - args: -p opentelemetry -p opentelemetry-aws -p opentelemetry-jaeger -p opentelemetry-datadog -p opentelemetry-dynatrace -p opentelemetry-zipkin --all-features --no-fail-fast + args: -p opentelemetry -p opentelemetry-api -p opentelemetry-sdk -p opentelemetry-aws -p opentelemetry-jaeger -p opentelemetry-datadog -p opentelemetry-dynatrace -p opentelemetry-zipkin --all-features --no-fail-fast env: CARGO_INCREMENTAL: '0' RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' diff --git a/opentelemetry-jaeger/Cargo.toml b/opentelemetry-jaeger/Cargo.toml index 9a0fccaaab..f020cb87ca 100644 --- a/opentelemetry-jaeger/Cargo.toml +++ b/opentelemetry-jaeger/Cargo.toml @@ -40,6 +40,10 @@ tokio = { version = "1.0", features = ["net", "sync"], optional = true } wasm-bindgen = { version = "0.2", optional = true } wasm-bindgen-futures = { version = "0.4.18", optional = true } +tonic = { version = "0.6.2", optional = true } +prost = { version = "0.9.0", optional = true } +prost-types = { version = "0.9.0", optional = true } + [dev-dependencies] bytes = "1" futures-executor = "0.3" @@ -77,4 +81,5 @@ wasm_collector_client = [ ] rt-tokio = ["tokio", "opentelemetry/rt-tokio"] rt-tokio-current-thread = ["tokio", "opentelemetry/rt-tokio-current-thread"] -rt-async-std = ["async-std","opentelemetry/rt-async-std"] +rt-async-std = ["async-std", "opentelemetry/rt-async-std"] +integration_test = ["tonic", "prost", "prost-types", "rt-tokio", "collector_client"] diff --git a/opentelemetry-jaeger/src/lib.rs b/opentelemetry-jaeger/src/lib.rs index e03daaaa07..5efe2bc7c4 100644 --- a/opentelemetry-jaeger/src/lib.rs +++ b/opentelemetry-jaeger/src/lib.rs @@ -234,6 +234,11 @@ #![cfg_attr(test, deny(warnings))] mod exporter; + +#[cfg(feature = "integration_test")] +#[doc(hidden)] +pub mod testing; + mod propagator { //! # Jaeger Propagator //! diff --git a/opentelemetry-jaeger/src/testing/jaeger_api_v2.rs b/opentelemetry-jaeger/src/testing/jaeger_api_v2.rs new file mode 100644 index 0000000000..9eb37a3c98 --- /dev/null +++ b/opentelemetry-jaeger/src/testing/jaeger_api_v2.rs @@ -0,0 +1,375 @@ +// generated files from jaeger proto(https://github.com/jaegertracing/jaeger-idl/tree/main/proto/api_v2) +// DO NOT EDIT + +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct KeyValue { + #[prost(string, tag = "1")] + pub key: ::prost::alloc::string::String, + #[prost(enumeration = "ValueType", tag = "2")] + pub v_type: i32, + #[prost(string, tag = "3")] + pub v_str: ::prost::alloc::string::String, + #[prost(bool, tag = "4")] + pub v_bool: bool, + #[prost(int64, tag = "5")] + pub v_int64: i64, + #[prost(double, tag = "6")] + pub v_float64: f64, + #[prost(bytes = "vec", tag = "7")] + pub v_binary: std::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Log { + #[prost(message, optional, tag = "1")] + pub timestamp: std::option::Option<::prost_types::Timestamp>, + #[prost(message, repeated, tag = "2")] + pub fields: std::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SpanRef { + #[prost(bytes = "vec", tag = "1")] + pub trace_id: std::vec::Vec, + #[prost(bytes = "vec", tag = "2")] + pub span_id: std::vec::Vec, + #[prost(enumeration = "SpanRefType", tag = "3")] + pub ref_type: i32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Process { + #[prost(string, tag = "1")] + pub service_name: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "2")] + pub tags: std::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Span { + #[prost(bytes = "vec", tag = "1")] + pub trace_id: std::vec::Vec, + #[prost(bytes = "vec", tag = "2")] + pub span_id: std::vec::Vec, + #[prost(string, tag = "3")] + pub operation_name: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "4")] + pub references: std::vec::Vec, + #[prost(uint32, tag = "5")] + pub flags: u32, + #[prost(message, optional, tag = "6")] + pub start_time: std::option::Option<::prost_types::Timestamp>, + #[prost(message, optional, tag = "7")] + pub duration: std::option::Option<::prost_types::Duration>, + #[prost(message, repeated, tag = "8")] + pub tags: std::vec::Vec, + #[prost(message, repeated, tag = "9")] + pub logs: std::vec::Vec, + #[prost(message, optional, tag = "10")] + pub process: std::option::Option, + #[prost(string, tag = "11")] + pub process_id: ::prost::alloc::string::String, + #[prost(string, repeated, tag = "12")] + pub warnings: std::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Trace { + #[prost(message, repeated, tag = "1")] + pub spans: std::vec::Vec, + #[prost(message, repeated, tag = "2")] + pub process_map: std::vec::Vec, + #[prost(string, repeated, tag = "3")] + pub warnings: std::vec::Vec<::prost::alloc::string::String>, +} +/// Nested message and enum types in `Trace`. +pub mod trace { + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct ProcessMapping { + #[prost(string, tag = "1")] + pub process_id: ::prost::alloc::string::String, + #[prost(message, optional, tag = "2")] + pub process: std::option::Option, + } +} +/// Note that both Span and Batch may contain a Process. +/// This is different from the Thrift model which was only used +/// for transport, because Proto model is also used by the backend +/// as the domain model, where once a batch is received it is split +/// into individual spans which are all processed independently, +/// and therefore they all need a Process. As far as on-the-wire +/// semantics, both Batch and Spans in the same message may contain +/// their own instances of Process, with span.Process taking priority +/// over batch.Process. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Batch { + #[prost(message, repeated, tag = "1")] + pub spans: std::vec::Vec, + #[prost(message, optional, tag = "2")] + pub process: std::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DependencyLink { + #[prost(string, tag = "1")] + pub parent: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub child: ::prost::alloc::string::String, + #[prost(uint64, tag = "3")] + pub call_count: u64, + #[prost(string, tag = "4")] + pub source: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ValueType { + String = 0, + Bool = 1, + Int64 = 2, + Float64 = 3, + Binary = 4, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum SpanRefType { + ChildOf = 0, + FollowsFrom = 1, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetTraceRequest { + #[prost(bytes = "vec", tag = "1")] + pub trace_id: std::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SpansResponseChunk { + #[prost(message, repeated, tag = "1")] + pub spans: std::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ArchiveTraceRequest { + #[prost(bytes = "vec", tag = "1")] + pub trace_id: std::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ArchiveTraceResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TraceQueryParameters { + #[prost(string, tag = "1")] + pub service_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub operation_name: ::prost::alloc::string::String, + #[prost(map = "string, string", tag = "3")] + pub tags: + std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, + #[prost(message, optional, tag = "4")] + pub start_time_min: std::option::Option<::prost_types::Timestamp>, + #[prost(message, optional, tag = "5")] + pub start_time_max: std::option::Option<::prost_types::Timestamp>, + #[prost(message, optional, tag = "6")] + pub duration_min: std::option::Option<::prost_types::Duration>, + #[prost(message, optional, tag = "7")] + pub duration_max: std::option::Option<::prost_types::Duration>, + #[prost(int32, tag = "8")] + pub search_depth: i32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FindTracesRequest { + #[prost(message, optional, tag = "1")] + pub query: std::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetServicesRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetServicesResponse { + #[prost(string, repeated, tag = "1")] + pub services: std::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetOperationsRequest { + #[prost(string, tag = "1")] + pub service: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub span_kind: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Operation { + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub span_kind: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetOperationsResponse { + ///deprecated + #[prost(string, repeated, tag = "1")] + pub operation_names: std::vec::Vec<::prost::alloc::string::String>, + #[prost(message, repeated, tag = "2")] + pub operations: std::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetDependenciesRequest { + #[prost(message, optional, tag = "1")] + pub start_time: std::option::Option<::prost_types::Timestamp>, + #[prost(message, optional, tag = "2")] + pub end_time: std::option::Option<::prost_types::Timestamp>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetDependenciesResponse { + #[prost(message, repeated, tag = "1")] + pub dependencies: std::vec::Vec, +} +#[doc = r" Generated client implementations."] +pub mod query_service_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + #[derive(Debug, Clone)] + pub struct QueryServiceClient { + inner: tonic::client::Grpc, + } + impl QueryServiceClient { + #[doc = r" Attempt to create a new client by connecting to a given endpoint."] + pub async fn connect(dst: D) -> Result + where + D: std::convert::TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl QueryServiceClient + where + T: tonic::client::GrpcService, + T::ResponseBody: Body + Send + 'static, + T::Error: Into, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> QueryServiceClient> + where + F: tonic::service::Interceptor, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + >>::Error: + Into + Send + Sync, + { + QueryServiceClient::new(InterceptedService::new(inner, interceptor)) + } + #[doc = r" Compress requests with `gzip`."] + #[doc = r""] + #[doc = r" This requires the server to support it otherwise it might respond with an"] + #[doc = r" error."] + pub fn send_gzip(mut self) -> Self { + self.inner = self.inner.send_gzip(); + self + } + #[doc = r" Enable decompressing responses with `gzip`."] + pub fn accept_gzip(mut self) -> Self { + self.inner = self.inner.accept_gzip(); + self + } + pub async fn get_trace( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result< + tonic::Response>, + tonic::Status, + > { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/jaeger.api_v2.QueryService/GetTrace"); + self.inner + .server_streaming(request.into_request(), path, codec) + .await + } + pub async fn archive_trace( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = + http::uri::PathAndQuery::from_static("/jaeger.api_v2.QueryService/ArchiveTrace"); + self.inner.unary(request.into_request(), path, codec).await + } + pub async fn find_traces( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result< + tonic::Response>, + tonic::Status, + > { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = + http::uri::PathAndQuery::from_static("/jaeger.api_v2.QueryService/FindTraces"); + self.inner + .server_streaming(request.into_request(), path, codec) + .await + } + pub async fn get_services( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = + http::uri::PathAndQuery::from_static("/jaeger.api_v2.QueryService/GetServices"); + self.inner.unary(request.into_request(), path, codec).await + } + pub async fn get_operations( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = + http::uri::PathAndQuery::from_static("/jaeger.api_v2.QueryService/GetOperations"); + self.inner.unary(request.into_request(), path, codec).await + } + pub async fn get_dependencies( + &mut self, + request: impl tonic::IntoRequest, + ) -> Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = + http::uri::PathAndQuery::from_static("/jaeger.api_v2.QueryService/GetDependencies"); + self.inner.unary(request.into_request(), path, codec).await + } + } +} diff --git a/opentelemetry-jaeger/src/testing/mod.rs b/opentelemetry-jaeger/src/testing/mod.rs new file mode 100644 index 0000000000..9a3c495e8d --- /dev/null +++ b/opentelemetry-jaeger/src/testing/mod.rs @@ -0,0 +1,87 @@ +#[allow(unused, missing_docs)] +pub mod jaeger_api_v2; + +#[allow(missing_docs)] +pub mod jaeger_client { + use crate::testing::jaeger_api_v2::query_service_client::QueryServiceClient; + use crate::testing::jaeger_api_v2::{ + FindTracesRequest, GetServicesRequest, GetTraceRequest, Span as JaegerSpan, + TraceQueryParameters, + }; + use tonic::transport::Channel; + + #[derive(Debug)] + pub struct JaegerTestClient { + query_service_client: QueryServiceClient, + } + + impl JaegerTestClient { + pub fn new(jaeger_url: &'static str) -> JaegerTestClient { + let channel = Channel::from_static(jaeger_url).connect_lazy(); + + JaegerTestClient { + query_service_client: QueryServiceClient::new(channel), + } + } + + /// Check if the jaeger contains the service + pub async fn contain_service(&mut self, service_name: &'static str) -> bool { + self.query_service_client + .get_services(GetServicesRequest {}) + .await + .unwrap() + .get_ref() + .services + .iter() + .any(|svc_name| svc_name == service_name) + } + + /// Find trace by trace id. + /// Note that `trace_id` should be a u128 in hex. + pub async fn get_trace(&mut self, trace_id: String) -> Vec { + let trace_id = u128::from_str_radix(trace_id.as_ref(), 16).expect("invalid trace id"); + let mut resp = self + .query_service_client + .get_trace(GetTraceRequest { + trace_id: trace_id.to_be_bytes().into(), + }) + .await + .unwrap(); + + return if let Some(spans) = resp + .get_mut() + .message() + .await + .expect("jaeger returns error") + { + spans.spans + } else { + vec![] + }; + } + + /// Find traces belongs the service. + /// It assumes the service exists. + pub async fn find_traces_from_services( + &mut self, + service_name: &'static str, + ) -> Vec { + let request = FindTracesRequest { + query: Some(TraceQueryParameters { + service_name: service_name.into(), + ..Default::default() + }), + }; + self.query_service_client + .find_traces(request) + .await + .unwrap() + .get_mut() + .message() + .await + .expect("jaeger returns error") + .unwrap_or_default() + .spans + } + } +} diff --git a/opentelemetry-jaeger/tests/Dockerfile b/opentelemetry-jaeger/tests/Dockerfile new file mode 100644 index 0000000000..0fcabd9b54 --- /dev/null +++ b/opentelemetry-jaeger/tests/Dockerfile @@ -0,0 +1,4 @@ +FROM rust + +WORKDIR /usr/src/opentelemetry +COPY . . diff --git a/opentelemetry-jaeger/tests/docker-compose.yaml b/opentelemetry-jaeger/tests/docker-compose.yaml new file mode 100644 index 0000000000..0e77a04f8b --- /dev/null +++ b/opentelemetry-jaeger/tests/docker-compose.yaml @@ -0,0 +1,20 @@ +version: "3" +services: + jaeger: + image: jaegertracing/all-in-one:1 + container_name: opentelemetry-jaeger-integration-test-jaeger + ports: + - "6831:6831/udp" + - "16685:16685" + opentelemetry-jaeger: + build: + context: ../.. + dockerfile: ./opentelemetry-jaeger/tests/Dockerfile + container_name: opentelemetry-jaeger-integration-test-exporter + environment: + OTEL_TEST_JAEGER_AGENT_ENDPOINT: "jaeger:6831" + OTEL_TEST_JAEGER_ENDPOINT: "http://jaeger:16685" + command: [ "cargo", "test", "--package", "opentelemetry-jaeger", "--test", "integration_test", + "--features=integration_test", "tests::integration_test", "--", "--exact" ] + depends_on: + - jaeger diff --git a/opentelemetry-jaeger/tests/integration_test.rs b/opentelemetry-jaeger/tests/integration_test.rs new file mode 100644 index 0000000000..251833c93e --- /dev/null +++ b/opentelemetry-jaeger/tests/integration_test.rs @@ -0,0 +1,190 @@ +#[cfg(feature = "integration_test")] +mod tests { + use opentelemetry::sdk::trace::Tracer as SdkTracer; + use opentelemetry::trace::{StatusCode, TraceContextExt, Tracer, TracerProvider}; + use opentelemetry::KeyValue; + use opentelemetry_jaeger::testing::{ + jaeger_api_v2 as jaeger_api, jaeger_client::JaegerTestClient, + }; + use std::collections::HashMap; + + // the sample application that will be traced. + // Expect the following span relationship: + // ┌─────────┐ + // │ Step-1 │────────────┐ + // └───┬─────┘ │ + // │ │ + // ┌───┴─────┐ ┌────┴────┐ + // │ Step-2-1│ │ Step-2-2├───────────┐ + // └─────────┘ └────┬────┘ │ + // │ │ + // ┌────┴─────┐ ┌───┴─────┐ + // │ Step-3-1 │ │ Step-3-2│ + // └──────────┘ └─────────┘ + async fn sample_application(tracer: &SdkTracer) { + { + tracer.in_span("step-1", |cx| { + tracer.in_span("step-2-1", |_cx| {}); + tracer.in_span("step-2-2", |_cx| { + tracer.in_span("step-3-1", |cx| { + let span = cx.span(); + span.set_status(StatusCode::Error, "") + }); + tracer.in_span("step-3-2", |cx| { + cx.span() + .set_attribute(KeyValue::new("tag-3-2-1", "tag-value-3-2-1")) + }) + }); + cx.span() + .add_event("something happened", vec![KeyValue::new("key1", "value1")]); + }); + } + } + + // This tests requires a jaeger agent running on the localhost. + // You can override the agent end point using OTEL_TEST_JAEGER_AGENT_ENDPOINT env var + // You can override the query API endpoint using OTEL_TEST_JAEGER_ENDPOINT env var + // Alternative you can run scripts/integration-test.sh from project root path. + // + #[test] + #[ignore] + fn integration_test() { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("cannot start runtime"); + + let agent_endpoint = + option_env!("OTEL_TEST_JAEGER_AGENT_ENDPOINT").unwrap_or("localhost:6831"); + let query_api_endpoint = + option_env!("OTEL_TEST_JAEGER_ENDPOINT").unwrap_or("http://localhost:16685"); + const SERVICE_NAME: &str = "opentelemetry_jaeger_integration_test"; + const CRATE_VERSION: &str = env!("CARGO_PKG_VERSION"); + const CRATE_NAME: &str = env!("CARGO_PKG_NAME"); + + println!("{}, {}", agent_endpoint, query_api_endpoint); + + runtime.block_on(async { + let tracer = opentelemetry_jaeger::new_pipeline() + .with_agent_endpoint(agent_endpoint) + .with_service_name(SERVICE_NAME) + .install_batch(opentelemetry::runtime::Tokio) + .expect("cannot create tracer using default configuration"); + + sample_application(&tracer).await; + + tracer.provider().unwrap().force_flush(); + }); + + runtime.block_on(async { + // build client + let mut client = JaegerTestClient::new(query_api_endpoint); + assert!( + client.contain_service(SERVICE_NAME).await, + "jaeger cannot find service" + ); + let spans = client.find_traces_from_services(SERVICE_NAME).await; + assert_eq!(spans.len(), 5); + + for span in spans.iter() { + assert_common_attributes(span, SERVICE_NAME, CRATE_NAME, CRATE_VERSION) + } + + // convert to span name/operation name -> span map + let span_map: HashMap = spans + .into_iter() + .map(|spans| (spans.operation_name.clone(), spans)) + .collect(); + + let step_1 = span_map.get("step-1").expect("cannot find step-1 span"); + assert_parent(step_1, None); + assert_eq!(step_1.logs.len(), 1); + + let step_2_1 = span_map.get("step-2-1").expect("cannot find step-2-1 span"); + assert_parent(step_2_1, Some(step_1)); + + let step_2_2 = span_map.get("step-2-2").expect("cannot find step-2-2 span"); + assert_parent(step_2_2, Some(step_1)); + + let step_3_1 = span_map.get("step-3-1").expect("cannot find step-3-1 span"); + assert_parent(step_3_1, Some(step_2_2)); + assert_tags_contains(step_3_1, "otel.status_code", "ERROR"); + assert_tags_contains(step_3_1, "error", "true"); + assert_eq!(step_3_1.flags, 1); + + let step_3_2 = span_map + .get("step-3-2") + .expect("cannot find step 3-2 spans"); + assert_parent(step_3_2, Some(step_2_2)); + assert_tags_contains(step_3_2, "tag-3-2-1", "tag-value-3-2-1"); + }); + } + + fn assert_parent(span: &jaeger_api::Span, parent_span: Option<&jaeger_api::Span>) { + let parent = span + .references + .iter() + .filter(|span_ref| span_ref.ref_type == jaeger_api::SpanRefType::ChildOf as i32) + .collect::>(); + if let Some(parent_span) = parent_span { + assert_eq!(parent.len(), 1); + let parent = parent.get(0).unwrap(); + assert_eq!(parent.span_id, parent_span.span_id); + assert_eq!(parent.trace_id, parent_span.trace_id); + } else { + assert!(parent.is_empty()); + } + } + + fn assert_common_attributes( + span: &jaeger_api::Span, + service_name: T, + library_name: T, + library_version: T, + ) where + T: Into, + { + assert_eq!( + span.process.as_ref().unwrap().service_name, + service_name.into() + ); + let mut library_metadata = span + .tags + .iter() + .filter(|kvs| kvs.key == "otel.library.name" || kvs.key == "otel.library.version") + .collect::>(); + assert_eq!(library_metadata.len(), 2); + if library_metadata.get(0).unwrap().key != "otel.library.name" { + library_metadata.swap(0, 1) + } + assert_eq!(library_metadata.get(0).unwrap().v_str, library_name.into()); + assert_eq!( + library_metadata.get(1).unwrap().v_str, + library_version.into() + ); + } + + fn assert_tags_contains(span: &jaeger_api::Span, key: T, value: T) + where + T: Into, + { + let key = key.into(); + let value = value.into(); + assert!(span + .tags + .iter() + .map(|tag| { + (tag.key.clone(), { + match tag.v_type { + 0 => tag.v_str.to_string(), + 1 => tag.v_bool.to_string(), + 2 => tag.v_int64.to_string(), + 3 => tag.v_float64.to_string(), + 4 => std::str::from_utf8(&tag.v_binary).unwrap_or("").into(), + _ => "".to_string(), + } + }) + }) + .any(|(tag_key, tag_value)| tag_key == key.clone() && tag_value == value.clone())); + } +} diff --git a/scripts/integration-test.sh b/scripts/integration-test.sh new file mode 100755 index 0000000000..649f99ee3c --- /dev/null +++ b/scripts/integration-test.sh @@ -0,0 +1,3 @@ +COMPOSE_FILE=./opentelemetry-jaeger/tests/docker-compose.yaml +docker-compose -f $COMPOSE_FILE down -v && +docker-compose -f $COMPOSE_FILE up --build --exit-code-from opentelemetry-jaeger