Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
9 changes: 9 additions & 0 deletions crates/axum-otel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ opentelemetry = { workspace = true }
tower-http = { workspace = true }
tracing = { workspace = true }
tracing-otel-extra = { workspace = true, features = ["macros"] }

[dev-dependencies]
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"] }
125 changes: 125 additions & 0 deletions crates/axum-otel/tests/integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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};

#[instrument]
async fn hello() -> &'static str {
"Hello, world!"
}

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()),
)
}

#[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());

// 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 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
.expect("Failed to send request");

assert_eq!(response.status(), StatusCode::OK);

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"
);

provider
.shutdown()
.expect("Failed to shutdown tracer provider");
}
32 changes: 32 additions & 0 deletions crates/tracing-opentelemetry/src/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,35 @@ 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()))
);

assert_eq!(
resource.get(&opentelemetry::Key::new("version")),
Some(opentelemetry::Value::String("1.0.0".into()))
);
}
}
Loading