From 3f8d7365d57a92eb278afc72b30e19fcdc561b69 Mon Sep 17 00:00:00 2001 From: peng-huang-ch Date: Tue, 6 Jan 2026 23:21:13 +0800 Subject: [PATCH 1/3] test: add integration tests and test dependencies - Add integration test for axum-otel middleware - Add unit tests for resource creation - Add dev-dependencies (tokio, reqwest, tower) to axum-otel for testing --- Cargo.lock | 3 ++ crates/axum-otel/Cargo.toml | 5 +++ crates/axum-otel/tests/integration.rs | 39 ++++++++++++++++++++ crates/tracing-opentelemetry/src/resource.rs | 27 ++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 crates/axum-otel/tests/integration.rs diff --git a/Cargo.lock b/Cargo.lock index dd6a891..77090cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,6 +119,9 @@ version = "0.31.5" dependencies = [ "axum", "opentelemetry 0.31.0", + "reqwest", + "tokio", + "tower", "tower-http", "tracing", "tracing-otel-extra", diff --git a/crates/axum-otel/Cargo.toml b/crates/axum-otel/Cargo.toml index 65ca360..ff0be52 100644 --- a/crates/axum-otel/Cargo.toml +++ b/crates/axum-otel/Cargo.toml @@ -17,3 +17,8 @@ opentelemetry = { workspace = true } tower-http = { workspace = true } tracing = { workspace = true } tracing-otel-extra = { workspace = true, features = ["macros"] } + +[dev-dependencies] +tokio = { workspace = true } +reqwest = { workspace = true } +tower = { workspace = true } diff --git a/crates/axum-otel/tests/integration.rs b/crates/axum-otel/tests/integration.rs new file mode 100644 index 0000000..3a8da76 --- /dev/null +++ b/crates/axum-otel/tests/integration.rs @@ -0,0 +1,39 @@ +use axum::{Router, routing::get}; +use axum_otel::{AxumOtelOnFailure, AxumOtelOnResponse, AxumOtelSpanCreator, Level}; + +use tower_http::trace::TraceLayer; + +#[tokio::test] +async fn test_axum_otel_middleware() { + // Basic test to ensure the middleware compiles and runs without panicking + // Ideally we would verify spans are exported, but that requires setting up a full in-memory tracer + // For now, we verify the app handles requests correctly with the middleware active. + + async fn handler() -> &'static str { + "Hello, world!" + } + + let app = Router::new().route("/", get(handler)).layer( + TraceLayer::new_for_http() + .make_span_with(AxumOtelSpanCreator::new().level(Level::INFO)) + .on_response(AxumOtelOnResponse::new().level(Level::INFO)) + .on_failure(AxumOtelOnFailure::new()), + ); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + let client = reqwest::Client::new(); + let resp = client + .get(format!("http://{}/", addr)) + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); + assert_eq!(resp.text().await.unwrap(), "Hello, world!"); +} diff --git a/crates/tracing-opentelemetry/src/resource.rs b/crates/tracing-opentelemetry/src/resource.rs index 89461e5..9f04dba 100644 --- a/crates/tracing-opentelemetry/src/resource.rs +++ b/crates/tracing-opentelemetry/src/resource.rs @@ -28,3 +28,30 @@ pub fn get_resource(service_name: &str, attributes: &[KeyValue]) -> Resource { .with_attributes(attributes.to_vec()) .build() } + +#[cfg(test)] +mod tests { + use super::get_resource; + use opentelemetry::KeyValue; + + #[test] + fn test_get_resource() { + let service_name = "test-service"; + let attributes = vec![ + KeyValue::new("env", "test"), + KeyValue::new("version", "1.0.0"), + ]; + + let resource = get_resource(service_name, &attributes); + + assert_eq!( + resource.get(&opentelemetry::Key::new("service.name")), + Some(opentelemetry::Value::String(service_name.into())) + ); + + assert_eq!( + resource.get(&opentelemetry::Key::new("env")), + Some(opentelemetry::Value::String("test".into())) + ); + } +} From cc2b5db5d0f903af10c9f8e8186fab7ecff1479c Mon Sep 17 00:00:00 2001 From: peng-huang-ch Date: Tue, 6 Jan 2026 23:28:02 +0800 Subject: [PATCH 2/3] test: add assertion for resource version in integration tests --- Cargo.lock | 6 + Cargo.toml | 1 + crates/axum-otel/Cargo.toml | 6 +- crates/axum-otel/tests/integration.rs | 132 +++++++++++++++---- crates/tracing-opentelemetry/src/resource.rs | 5 + 5 files changed, 124 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77090cb..f8817cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,13 +118,17 @@ name = "axum-otel" version = "0.31.5" dependencies = [ "axum", + "http-body-util", "opentelemetry 0.31.0", + "opentelemetry_sdk 0.31.0", "reqwest", "tokio", "tower", "tower-http", "tracing", + "tracing-opentelemetry 0.32.0", "tracing-otel-extra", + "tracing-subscriber", ] [[package]] @@ -1110,6 +1114,8 @@ dependencies = [ "percent-encoding", "rand", "thiserror 2.0.17", + "tokio", + "tokio-stream", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e0eccb1..c37d9c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ axum-otel = { path = "crates/axum-otel", version = "0.31.5" } config = { version = "0.14.0", default-features = false } dotenvy = { version = "0.15.7" } http = { version = "1.3.1" } +http-body-util = { version = "0.1" } opentelemetry = { version = "0.31.0", default-features = false } opentelemetry-appender-tracing = { version = "0.31.0" } opentelemetry-http = { version = "0.31.0", default-features = false } diff --git a/crates/axum-otel/Cargo.toml b/crates/axum-otel/Cargo.toml index ff0be52..a41345f 100644 --- a/crates/axum-otel/Cargo.toml +++ b/crates/axum-otel/Cargo.toml @@ -19,6 +19,10 @@ tracing = { workspace = true } tracing-otel-extra = { workspace = true, features = ["macros"] } [dev-dependencies] -tokio = { workspace = true } +http-body-util = { workspace = true } +opentelemetry_sdk = { workspace = true, features = ["testing"] } reqwest = { workspace = true } +tokio = { workspace = true } tower = { workspace = true } +tracing-opentelemetry = { workspace = true } +tracing-subscriber = { workspace = true, features = ["registry"] } diff --git a/crates/axum-otel/tests/integration.rs b/crates/axum-otel/tests/integration.rs index 3a8da76..064a622 100644 --- a/crates/axum-otel/tests/integration.rs +++ b/crates/axum-otel/tests/integration.rs @@ -1,39 +1,121 @@ -use axum::{Router, routing::get}; +use axum::{ + Router, + body::Body, + http::{Method, Request, StatusCode}, + routing::get, +}; use axum_otel::{AxumOtelOnFailure, AxumOtelOnResponse, AxumOtelSpanCreator, Level}; - +use http_body_util::BodyExt; +use opentelemetry::{global, trace::TracerProvider}; +use opentelemetry_sdk::{ + Resource, + trace::{InMemorySpanExporter, RandomIdGenerator, Sampler, SdkTracerProvider}, +}; +use tower::ServiceExt; use tower_http::trace::TraceLayer; +use tracing::instrument; +use tracing_subscriber::{Registry, layer::SubscriberExt, util::SubscriberInitExt}; -#[tokio::test] -async fn test_axum_otel_middleware() { - // Basic test to ensure the middleware compiles and runs without panicking - // Ideally we would verify spans are exported, but that requires setting up a full in-memory tracer - // For now, we verify the app handles requests correctly with the middleware active. - - async fn handler() -> &'static str { - "Hello, world!" - } +#[instrument] +async fn hello() -> &'static str { + "Hello, world!" +} - let app = Router::new().route("/", get(handler)).layer( +fn app() -> Router<()> { + Router::new().route("/", get(hello)).layer( TraceLayer::new_for_http() .make_span_with(AxumOtelSpanCreator::new().level(Level::INFO)) .on_response(AxumOtelOnResponse::new().level(Level::INFO)) .on_failure(AxumOtelOnFailure::new()), - ); + ) +} - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); +#[tokio::test] +async fn test_axum_otel_middleware() { + // Set up in-memory exporter for testing + let exporter = InMemorySpanExporter::default(); + let provider: SdkTracerProvider = SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOn) + .with_id_generator(RandomIdGenerator::default()) + .with_simple_exporter(exporter.clone()) + .with_resource(Resource::builder().build()) + .build(); + + global::set_tracer_provider(provider.clone()); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); + // Set up tracing subscriber with OpenTelemetry layer + let tracer = provider.tracer("axum-otel-test".to_string()); + let otel_layer = tracing_opentelemetry::OpenTelemetryLayer::new(tracer); + Registry::default() + .with(otel_layer) + .try_init() + .expect("Failed to initialize tracing subscriber"); - let client = reqwest::Client::new(); - let resp = client - .get(format!("http://{}/", addr)) - .send() + let app = app(); + + // Send request using oneshot + let response = app + .oneshot( + Request::builder() + .uri("/") + .method(Method::GET) + .body(Body::empty()) + .expect("Failed to build request"), + ) .await - .unwrap(); + .expect("Failed to send request"); + + assert_eq!(response.status(), StatusCode::OK); - assert_eq!(resp.status(), 200); - assert_eq!(resp.text().await.unwrap(), "Hello, world!"); + let body = response + .into_body() + .collect() + .await + .expect("Failed to read body"); + + assert_eq!(body.to_bytes(), "Hello, world!".as_bytes()); + + // Force flush to ensure spans are exported + let _ = provider.force_flush(); + + // Verify spans were created + let spans = exporter + .get_finished_spans() + .expect("Failed to get finished spans"); + + assert!( + !spans.is_empty(), + "Expected at least one span to be created" + ); + + // With oneshot(), there's no MatchedPath, so span name is just "GET" + // When using a real server, it would be "GET /" + let request_span = spans + .iter() + .find(|s| s.name == "GET" || s.name == "GET /") + .expect("Request span not found"); + + let hello_span = spans + .iter() + .find(|s| s.name == "hello") + .expect("Handler span not found"); + + assert_eq!( + hello_span.parent_span_id, + request_span.span_context.span_id(), + "Handler span should be a child of the request span" + ); + + // Check http.status_code attribute + let status_code_attr = request_span + .attributes + .iter() + .find(|kv| kv.key.as_str() == "http.status_code") + .map(|kv| kv.value.to_string()); + + assert_eq!( + status_code_attr, + Some("200".to_string()), + "Expected http.status_code to be 200" + ); } diff --git a/crates/tracing-opentelemetry/src/resource.rs b/crates/tracing-opentelemetry/src/resource.rs index 9f04dba..5f7747f 100644 --- a/crates/tracing-opentelemetry/src/resource.rs +++ b/crates/tracing-opentelemetry/src/resource.rs @@ -53,5 +53,10 @@ mod tests { resource.get(&opentelemetry::Key::new("env")), Some(opentelemetry::Value::String("test".into())) ); + + assert_eq!( + resource.get(&opentelemetry::Key::new("version")), + Some(opentelemetry::Value::String("1.0.0".into())) + ); } } From a6f31b3ea8445c796439d433c8bc2c69525a0e05 Mon Sep 17 00:00:00 2001 From: peng-huang-ch Date: Wed, 7 Jan 2026 22:01:41 +0800 Subject: [PATCH 3/3] test: add shutdown assertion for tracer provider in integration test --- crates/axum-otel/tests/integration.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/axum-otel/tests/integration.rs b/crates/axum-otel/tests/integration.rs index 064a622..9d0786a 100644 --- a/crates/axum-otel/tests/integration.rs +++ b/crates/axum-otel/tests/integration.rs @@ -118,4 +118,8 @@ async fn test_axum_otel_middleware() { Some("200".to_string()), "Expected http.status_code to be 200" ); + + provider + .shutdown() + .expect("Failed to shutdown tracer provider"); }