diff --git a/opentelemetry-instrumentation-tower/CHANGELOG.md b/opentelemetry-instrumentation-tower/CHANGELOG.md index 314bf8e85..a6de97f72 100644 --- a/opentelemetry-instrumentation-tower/CHANGELOG.md +++ b/opentelemetry-instrumentation-tower/CHANGELOG.md @@ -4,6 +4,13 @@ ### Changed +* **BREAKING**: Removed `with_meter()` method. The middleware now uses global meter and tracer providers by default via `opentelemetry::global::meter()` and `opentelemetry::global::tracer()`, with optional overrides via `with_tracer_provider()` and `with_meter_provider()` methods. +* **BREAKING**: Renamed types. Use the new names: + - `HTTPMetricsLayer` → `HTTPLayer` + - `HTTPMetricsService` → `HTTPService` + - `HTTPMetricsResponseFuture` → `HTTPResponseFuture` + - `HTTPMetricsLayerBuilder` → `HTTPLayerBuilder` +* Added OpenTelemetry trace support * Migrate to use `opentelemetry-semantic-conventions` package for metric names and attribute keys instead of hardcoded strings * Add dependency on otel semantic conventions crate and use constants from it instead of hardcoded attribute names. The values are unchanged - `HTTP_SERVER_ACTIVE_REQUESTS_METRIC` now uses `semconv::metric::HTTP_SERVER_ACTIVE_REQUESTS` @@ -20,6 +27,37 @@ * Add comprehensive test coverage for all HTTP server metrics with attribute validation +### Migration Guide + +#### API Changes +Before: +```rust +use opentelemetry_instrumentation_tower::HTTPMetricsLayerBuilder; + +let layer = HTTPMetricsLayerBuilder::builder() + .with_meter(meter) + .build() + .unwrap(); +``` + +After: +```rust +use opentelemetry_instrumentation_tower::HTTPLayer; + +// Set global providers first +global::set_meter_provider(meter_provider); +global::set_tracer_provider(tracer_provider); // for tracing support + +// Then create the layer - simple API using global providers +let layer = HTTPLayer::new(); +``` + +#### Type Name Changes +- Replace `HTTPMetricsLayerBuilder` with `HTTPLayerBuilder` +- Replace `HTTPMetricsLayer` with `HTTPLayer` +- Replace `HTTPMetricsService` with `HTTPService` +- Replace `HTTPMetricsResponseFuture` with `HTTPResponseFuture` + ## v0.16.0 Initial release of OpenTelemetry Tower instrumentation middleware for HTTP metrics collection. diff --git a/opentelemetry-instrumentation-tower/Cargo.toml b/opentelemetry-instrumentation-tower/Cargo.toml index ffe0ceb8c..4258d49dd 100644 --- a/opentelemetry-instrumentation-tower/Cargo.toml +++ b/opentelemetry-instrumentation-tower/Cargo.toml @@ -5,7 +5,7 @@ rust-version = "1.75.0" version = "0.16.0" license = "Apache-2.0" -description = "OpenTelemetry Metrics Middleware for Tower-compatible Rust HTTP servers" +description = "OpenTelemetry Metrics and Tracing Middleware for Tower-compatible Rust HTTP servers" homepage = "https://github.com/open-telemetry/opentelemetry-rust-contrib" repository = "https://github.com/open-telemetry/opentelemetry-rust-contrib" documentation = "https://docs.rs/tower-otel-http-metrics" @@ -21,7 +21,8 @@ axum = { features = ["matched-path", "macros"], version = "0.8", default-feature futures-util = { version = "0.3", default-features = false } http = { version = "1", features = ["std"], default-features = false } http-body = { version = "1", default-features = false } -opentelemetry = { workspace = true, features = ["futures", "metrics"]} +opentelemetry = { workspace = true, features = ["futures", "metrics", "trace"] } +opentelemetry_sdk = { workspace = true, features = ["trace"] } opentelemetry-semantic-conventions = { workspace = true, features = ["semconv_experimental"] } pin-project-lite = { version = "0.2", default-features = false } tower-service = { version = "0.3", default-features = false } @@ -31,6 +32,7 @@ tower-layer = { version = "0.3", default-features = false } opentelemetry_sdk = { workspace = true, features = ["metrics", "testing"] } tokio = { version = "1.0", features = ["macros", "rt"] } tower = { version = "0.5", features = ["util"] } +tower-test = { version = "0.4" } [lints] workspace = true diff --git a/opentelemetry-instrumentation-tower/README.md b/opentelemetry-instrumentation-tower/README.md index 60f2ea709..ea04d9b99 100644 --- a/opentelemetry-instrumentation-tower/README.md +++ b/opentelemetry-instrumentation-tower/README.md @@ -1,6 +1,38 @@ -# Tower OTEL Metrics Middleware +# Tower OTEL HTTP Instrumentation Middleware -OpenTelemetry Metrics Middleware for Tower-compatible Rust HTTP servers. +OpenTelemetry HTTP Metrics and Tracing Middleware for Tower-compatible Rust HTTP servers. + +This middleware provides both metrics and distributed tracing for HTTP requests, following OpenTelemetry semantic conventions. + +## Features + +- **HTTP Metrics**: Request duration, active requests, request/response body sizes +- **Distributed Tracing**: HTTP spans with semantic attributes +- **Semantic Conventions**: Uses OpenTelemetry semantic conventions for consistent attribute naming +- **Flexible Configuration**: Support for custom attribute extractors and tracer configuration +- **Framework Support**: Works with any Tower-compatible HTTP framework (Axum, Hyper, Tonic etc.) + +## Usage + +## Metrics + +The middleware exports the following metrics: + +- `http.server.request.duration` - Duration of HTTP requests +- `http.server.active_requests` - Number of active HTTP requests +- `http.server.request.body.size` - Size of HTTP request bodies +- `http.server.response.body.size` - Size of HTTP response bodies + +## Tracing + +HTTP spans are created with the following attributes (following OpenTelemetry semantic conventions): + +- `http.request.method` - HTTP method +- `url.scheme` - URL scheme (http/https) +- `url.path` - Request path +- `url.full` - Full URL +- `user_agent.original` - User agent string +- `http.response.status_code` - HTTP response status code ## Examples diff --git a/opentelemetry-instrumentation-tower/examples/axum-http-service/src/main.rs b/opentelemetry-instrumentation-tower/examples/axum-http-service/src/main.rs index 559c74c2e..42521cc5c 100644 --- a/opentelemetry-instrumentation-tower/examples/axum-http-service/src/main.rs +++ b/opentelemetry-instrumentation-tower/examples/axum-http-service/src/main.rs @@ -1,7 +1,9 @@ use axum::routing::{get, post, put, Router}; use bytes::Bytes; use opentelemetry::global; -use opentelemetry_instrumentation_tower as otel_tower_metrics; +use opentelemetry_instrumentation_tower::HTTPLayer; +use opentelemetry_otlp::MetricExporter; +use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider}; use std::time::Duration; const SERVICE_NAME: &str = "example-axum-http-service"; @@ -40,28 +42,24 @@ async fn handle() -> Bytes { #[tokio::main] async fn main() { - let exporter = opentelemetry_otlp::MetricExporter::builder() + let exporter = MetricExporter::builder() .with_tonic() // .with_endpoint("http://localhost:4317") // default; leave out in favor of env var OTEL_EXPORTER_OTLP_ENDPOINT .build() .unwrap(); - let reader = opentelemetry_sdk::metrics::PeriodicReader::builder(exporter) + let reader = PeriodicReader::builder(exporter) .with_interval(_OTEL_METRIC_EXPORT_INTERVAL) .build(); - let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder() + let meter_provider = SdkMeterProvider::builder() .with_reader(reader) .with_resource(init_otel_resource()) .build(); global::set_meter_provider(meter_provider); - // init our otel metrics middleware - let global_meter = global::meter(SERVICE_NAME); - let otel_metrics_service_layer = otel_tower_metrics::HTTPMetricsLayerBuilder::builder() - .with_meter(global_meter) - .build() - .unwrap(); + + let otel_metrics_service_layer = HTTPLayer::new(); let app = Router::new() .route("/", get(handle)) diff --git a/opentelemetry-instrumentation-tower/examples/hyper-http-service/src/main.rs b/opentelemetry-instrumentation-tower/examples/hyper-http-service/src/main.rs index ccd31bd5b..0f4e195dc 100644 --- a/opentelemetry-instrumentation-tower/examples/hyper-http-service/src/main.rs +++ b/opentelemetry-instrumentation-tower/examples/hyper-http-service/src/main.rs @@ -2,7 +2,9 @@ use http_body_util::Full; use hyper::body::Bytes; use hyper::{Request, Response}; use opentelemetry::global; -use opentelemetry_instrumentation_tower as otel_tower_metrics; +use opentelemetry_instrumentation_tower::HTTPLayer; +use opentelemetry_otlp::MetricExporter; +use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider}; use std::convert::Infallible; use std::net::SocketAddr; use std::time::Duration; @@ -45,28 +47,24 @@ async fn handle(_req: Request) -> Result, pub server_active_requests: UpDownCounter, pub server_request_body_size: Histogram, pub server_response_body_size: Histogram, + pub tracer_provider: Option, } #[derive(Clone)] -/// [`Service`] used by [`HTTPMetricsLayer`] -pub struct HTTPMetricsService { - pub(crate) state: Arc, +/// [`Service`] used by [`HTTPLayer`] +pub struct HTTPService { + pub(crate) state: Arc, request_extractor: ReqExt, response_extractor: ResExt, inner_service: S, } #[derive(Clone)] -/// [`Layer`] which applies the OTEL HTTP server metrics middleware -pub struct HTTPMetricsLayer { - state: Arc, +/// [`Layer`] which applies the OTEL HTTP server metrics and tracing middleware +pub struct HTTPLayer { + state: Arc, request_extractor: ReqExt, response_extractor: ResExt, } -pub struct HTTPMetricsLayerBuilder { - meter: Option, +impl HTTPLayer { + /// Create a new HTTP layer with default configuration using global providers + pub fn new() -> Self { + HTTPLayerBuilder::builder().build().unwrap() + } +} + +impl Default for HTTPLayer { + fn default() -> Self { + Self::new() + } +} + +pub struct HTTPLayerBuilder { + tracer_provider: Option, + meter_provider: Option>, req_dur_bounds: Option>, request_extractor: ReqExt, response_extractor: ResExt, @@ -190,10 +207,11 @@ impl fmt::Debug for Error { } } -impl HTTPMetricsLayerBuilder { +impl HTTPLayerBuilder { pub fn builder() -> Self { - HTTPMetricsLayerBuilder { - meter: None, + HTTPLayerBuilder { + tracer_provider: None, + meter_provider: None, req_dur_bounds: Some(LIBRARY_DEFAULT_HTTP_SERVER_DURATION_BOUNDARIES.to_vec()), request_extractor: NoOpExtractor, response_extractor: NoOpExtractor, @@ -201,17 +219,18 @@ impl HTTPMetricsLayerBuilder { } } -impl HTTPMetricsLayerBuilder { +impl HTTPLayerBuilder { /// Set a request attribute extractor pub fn with_request_extractor( self, extractor: NewReqExt, - ) -> HTTPMetricsLayerBuilder + ) -> HTTPLayerBuilder where NewReqExt: RequestAttributeExtractor, { - HTTPMetricsLayerBuilder { - meter: self.meter, + HTTPLayerBuilder { + meter_provider: self.meter_provider, + tracer_provider: self.tracer_provider, req_dur_bounds: self.req_dur_bounds, request_extractor: extractor, response_extractor: self.response_extractor, @@ -222,12 +241,13 @@ impl HTTPMetricsLayerBuilder { pub fn with_response_extractor( self, extractor: NewResExt, - ) -> HTTPMetricsLayerBuilder + ) -> HTTPLayerBuilder where NewResExt: ResponseAttributeExtractor, { - HTTPMetricsLayerBuilder { - meter: self.meter, + HTTPLayerBuilder { + meter_provider: self.meter_provider, + tracer_provider: self.tracer_provider, req_dur_bounds: self.req_dur_bounds, request_extractor: self.request_extractor, response_extractor: extractor, @@ -238,7 +258,7 @@ impl HTTPMetricsLayerBuilder { pub fn with_request_extractor_fn( self, f: F, - ) -> HTTPMetricsLayerBuilder, ResExt> + ) -> HTTPLayerBuilder, ResExt> where F: Fn(&http::Request) -> Vec + Clone + Send + Sync + 'static, { @@ -249,32 +269,27 @@ impl HTTPMetricsLayerBuilder { pub fn with_response_extractor_fn( self, f: F, - ) -> HTTPMetricsLayerBuilder> + ) -> HTTPLayerBuilder> where F: Fn(&http::Response) -> Vec + Clone + Send + Sync + 'static, { self.with_response_extractor(FnResponseExtractor::new(f)) } - pub fn build(self) -> Result> { + pub fn build(self) -> Result> { let req_dur_bounds = self .req_dur_bounds .unwrap_or_else(|| LIBRARY_DEFAULT_HTTP_SERVER_DURATION_BOUNDARIES.to_vec()); - match self.meter { - Some(meter) => Ok(HTTPMetricsLayer { - state: Arc::from(Self::make_state(meter, req_dur_bounds)), - request_extractor: self.request_extractor, - response_extractor: self.response_extractor, - }), - None => Err(Error { - inner: ErrorKind::Config(String::from("no meter provided")), - }), - } - } - pub fn with_meter(mut self, meter: Meter) -> Self { - self.meter = Some(meter); - self + Ok(HTTPLayer { + state: Arc::from(Self::make_state( + self.meter_provider, + self.tracer_provider, + req_dur_bounds, + )), + request_extractor: self.request_extractor, + response_extractor: self.response_extractor, + }) } pub fn with_request_duration_bounds(mut self, bounds: Vec) -> Self { @@ -282,8 +297,33 @@ impl HTTPMetricsLayerBuilder { self } - fn make_state(meter: Meter, req_dur_bounds: Vec) -> HTTPMetricsLayerState { - HTTPMetricsLayerState { + /// Set a meter provider to use for creating a meter. + /// If none is specified, the global provider is used. + pub fn with_meter_provider(mut self, provider: M) -> Self + where + M: MeterProvider + Send + Sync + 'static, + { + self.meter_provider = Some(Box::new(provider)); + self + } + + /// Set a meter provider to use for creating a meter. + /// If none is specified, the global provider is used. + pub fn with_tracer_provider(mut self, provider: SdkTracerProvider) -> Self { + self.tracer_provider = Some(provider); + self + } + + fn make_state( + meter_provider: Option>, + tracer_provider: Option, + req_dur_bounds: Vec, + ) -> HTTPLayerState { + let meter = match meter_provider { + Some(provider) => provider.meter("opentelemetry-instrumentation-tower"), + None => global::meter("opentelemetry-instrumentation-tower"), + }; + HTTPLayerState { server_request_duration: meter .f64_histogram(Cow::from(HTTP_SERVER_DURATION_METRIC)) .with_description("Duration of HTTP server requests.") @@ -305,19 +345,20 @@ impl HTTPMetricsLayerBuilder { .with_description("Size of HTTP server response bodies.") .with_unit(HTTP_SERVER_RESPONSE_BODY_SIZE_UNIT) .build(), + tracer_provider, } } } -impl Layer for HTTPMetricsLayer +impl Layer for HTTPLayer where ReqExt: Clone, ResExt: Clone, { - type Service = HTTPMetricsService; + type Service = HTTPService; fn layer(&self, service: S) -> Self::Service { - HTTPMetricsService { + HTTPService { state: self.state.clone(), request_extractor: self.request_extractor.clone(), response_extractor: self.response_extractor.clone(), @@ -326,14 +367,13 @@ where } } -/// ResponseFutureMetricsState holds request-scoped data for metrics and their attributes. +/// ResponseFutureState holds request-scoped data for metrics, tracing and their attributes. /// -/// ResponseFutureMetricsState lives inside the response future, as it needs to hold data +/// ResponseFutureState lives inside the response future, as it needs to hold data /// initialized or extracted from the request before it is forwarded to the inner Service. /// The rest of the data (e.g. status code, error) can be extracted from the response /// or calculated with respect to the data held here (e.g., duration = now - duration start). -#[derive(Clone)] -struct ResponseFutureMetricsState { +struct ResponseFutureState { // fields for the metric values // https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration duration_start: Instant, @@ -349,21 +389,24 @@ struct ResponseFutureMetricsState { // Custom attributes from request custom_request_attributes: Vec, + + // Tracing fields + otel_context: OtelContext, } pin_project! { - /// Response [`Future`] for [`HTTPMetricsService`]. - pub struct HTTPMetricsResponseFuture { + /// Response [`Future`] for [`HTTPService`]. + pub struct HTTPResponseFuture { #[pin] inner_response_future: F, - layer_state: Arc, - metrics_state: ResponseFutureMetricsState, + layer_state: Arc, + future_state: ResponseFutureState, response_extractor: ResExt, } } impl Service> - for HTTPMetricsService + for HTTPService where S: Service, Response = http::Response>, ResBody: http_body::Body, @@ -372,7 +415,7 @@ where { type Response = S::Response; type Error = S::Error; - type Future = HTTPMetricsResponseFuture; + type Future = HTTPResponseFuture; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner_service.poll_ready(cx) @@ -394,29 +437,65 @@ where let url_scheme_kv = KeyValue::new(URL_SCHEME_LABEL, scheme); let method = req.method().as_str().to_owned(); - let method_kv = KeyValue::new(HTTP_REQUEST_METHOD_LABEL, method); + let method_kv = KeyValue::new(HTTP_REQUEST_METHOD_LABEL, method.clone()); #[allow(unused_mut)] let mut route_kv_opt = None; #[cfg(feature = "axum")] if let Some(matched_path) = req.extensions().get::() { - route_kv_opt = Some(KeyValue::new( - HTTP_ROUTE_LABEL, - matched_path.as_str().to_owned(), - )); + let route = matched_path.as_str().to_owned(); + route_kv_opt = Some(KeyValue::new(HTTP_ROUTE_LABEL, route.clone())); }; // Extract custom request attributes let custom_request_attributes = self.request_extractor.extract_attributes(&req); + // Start tracing span + let mut span_attributes = vec![ + KeyValue::new(semconv::trace::HTTP_REQUEST_METHOD, method.clone()), + url_scheme_kv.clone(), + KeyValue::new(semconv::attribute::URL_PATH, req.uri().path().to_string()), + KeyValue::new(semconv::trace::URL_FULL, req.uri().to_string()), + ]; + + if let Some(user_agent) = req + .headers() + .get("user-agent") + .and_then(|v| v.to_str().ok()) + { + span_attributes.push(KeyValue::new( + semconv::trace::USER_AGENT_ORIGINAL, + user_agent.to_string(), + )); + } + + span_attributes.extend(custom_request_attributes.clone()); + + let span_name = format!("{} {}", method, req.uri().path()); + + let tracer = match &self.state.tracer_provider { + Some(tp) => { + BoxedTracer::new(Box::new(tp.tracer("opentelemetry-instrumentation-tower"))) + } + None => global::tracer("opentelemetry-instrumentation-tower"), + }; + + let span = tracer + .span_builder(span_name) + .with_kind(SpanKind::Server) + .with_attributes(span_attributes) + .start(&tracer); + + let cx = OtelContext::current_with_span(span); + self.state .server_active_requests .add(1, &[url_scheme_kv.clone(), method_kv.clone()]); - HTTPMetricsResponseFuture { + HTTPResponseFuture { inner_response_future: self.inner_service.call(req), layer_state: self.state.clone(), - metrics_state: ResponseFutureMetricsState { + future_state: ResponseFutureState { duration_start, req_body_size: content_length, @@ -426,13 +505,15 @@ where method_kv, route_kv_opt, custom_request_attributes, + + otel_context: cx, }, response_extractor: self.response_extractor.clone(), } } } -impl Future for HTTPMetricsResponseFuture +impl Future for HTTPResponseFuture where F: Future, E>>, ResBody: http_body::Body, @@ -447,30 +528,56 @@ where // Build base label set let mut label_superset = vec![ - this.metrics_state.protocol_name_kv.clone(), - this.metrics_state.protocol_version_kv.clone(), - this.metrics_state.url_scheme_kv.clone(), - this.metrics_state.method_kv.clone(), + this.future_state.protocol_name_kv.clone(), + this.future_state.protocol_version_kv.clone(), + this.future_state.url_scheme_kv.clone(), + this.future_state.method_kv.clone(), KeyValue::new(HTTP_RESPONSE_STATUS_CODE_LABEL, i64::from(status.as_u16())), ]; - if let Some(route_kv) = this.metrics_state.route_kv_opt.clone() { + if let Some(route_kv) = this.future_state.route_kv_opt.clone() { label_superset.push(route_kv); } // Add custom request attributes - label_superset.extend(this.metrics_state.custom_request_attributes.clone()); + label_superset.extend(this.future_state.custom_request_attributes.clone()); // Extract and add custom response attributes let custom_response_attributes = this.response_extractor.extract_attributes(&response); - label_superset.extend(custom_response_attributes); + label_superset.extend(custom_response_attributes.clone()); + + // Update span + let span = this.future_state.otel_context.span(); + span.set_attribute(KeyValue::new( + semconv::trace::HTTP_RESPONSE_STATUS_CODE, + status.as_u16() as i64, + )); + + // Add custom response attributes to span + for attr in &custom_response_attributes { + span.set_attribute(attr.clone()); + } + + // Set span status based on HTTP status code + // Following server-side semantic conventions: + // - 5xx server errors indicate server failure and should be marked as span errors + // - 4xx client errors indicate client mistakes, not server failures + if status.is_server_error() { + span.set_status(Status::Error { + description: format!("HTTP {}", status.as_u16()).into(), + }); + } else { + span.set_status(Status::Ok); + } + + span.end(); this.layer_state.server_request_duration.record( - this.metrics_state.duration_start.elapsed().as_secs_f64(), + this.future_state.duration_start.elapsed().as_secs_f64(), &label_superset, ); - if let Some(req_content_length) = this.metrics_state.req_body_size { + if let Some(req_content_length) = this.future_state.req_body_size { this.layer_state .server_request_body_size .record(req_content_length, &label_superset); @@ -486,8 +593,8 @@ where this.layer_state.server_active_requests.add( -1, &[ - this.metrics_state.url_scheme_kv.clone(), - this.metrics_state.method_kv.clone(), + this.future_state.url_scheme_kv.clone(), + this.future_state.method_kv.clone(), ], ); @@ -509,27 +616,129 @@ fn split_and_format_protocol_version(http_version: http::Version) -> (String, St #[cfg(test)] mod tests { + // Tests use optional provider overrides instead of global providers to avoid interference. use super::*; + use http::{Request, Response, StatusCode}; - use opentelemetry::metrics::MeterProvider; + use opentelemetry::trace::{FutureExt, TraceContextExt, Tracer}; + use opentelemetry_sdk::metrics::SdkMeterProvider; use opentelemetry_sdk::metrics::{ data::{AggregatedMetrics, MetricData}, - InMemoryMetricExporter, PeriodicReader, SdkMeterProvider, + InMemoryMetricExporter, PeriodicReader, }; + use opentelemetry_sdk::trace::{InMemorySpanExporterBuilder, SdkTracerProvider}; + use std::result::Result; use std::time::Duration; - use tower::Service; + use tower::{Service, ServiceBuilder, ServiceExt}; + + #[tokio::test(flavor = "current_thread")] + async fn test_tracing_with_in_memory_tracer() { + let trace_exporter = InMemorySpanExporterBuilder::new().build(); + let tracer_provider = SdkTracerProvider::builder() + .with_simple_exporter(trace_exporter.clone()) + .build(); + + let tracer = tracer_provider.tracer("test_tracer"); + + let layer = HTTPLayerBuilder::builder() + .with_tracer_provider(tracer_provider.clone()) + .build() + .unwrap(); + + let mut service = ServiceBuilder::new() + .layer(layer) + .service(tower::service_fn(echo)); + + // Create a parent span and set it as the current context + let parent_span = tracer.start("parent_operation"); + let cx = OtelContext::current_with_span(parent_span); + + let request_body = "test".to_string(); + let request = http::Request::builder() + .uri("http://example.com/api/users/123") + .header("Content-Length", request_body.len().to_string()) + .header("User-Agent", "tower-test-client/1.0") + .body(request_body) + .unwrap(); + + // Execute the service call within the parent span context + let _response = async { service.ready().await.unwrap().call(request).await.unwrap() } + .with_context(cx) + .await; + + tracer_provider.force_flush().unwrap(); + + let spans = trace_exporter.get_finished_spans().unwrap(); + assert_eq!( + spans.len(), + 2, + "Expected exactly two spans to be recorded (parent + HTTP)" + ); + + // Find the HTTP span (should be the child) + let http_span = spans + .iter() + .find(|span| span.name == "GET /api/users/123") + .expect("Should find HTTP span"); + + // Find the parent span + let parent_span = spans + .iter() + .find(|span| span.name == "parent_operation") + .expect("Should find parent span"); + + // Verify the HTTP span has the correct parent + assert_eq!( + http_span.parent_span_id, + parent_span.span_context.span_id(), + "HTTP span should have parent span as parent" + ); + + // Verify they share the same trace ID + assert_eq!( + http_span.span_context.trace_id(), + parent_span.span_context.trace_id(), + "Parent and child spans should share the same trace ID" + ); - #[tokio::test] + assert_eq!( + http_span.name, "GET /api/users/123", + "Span name should match the request" + ); + // Build expected attributes + let expected_attributes = vec![ + KeyValue::new(semconv::trace::HTTP_REQUEST_METHOD, "GET".to_string()), + KeyValue::new(semconv::trace::URL_SCHEME, "http".to_string()), + KeyValue::new(semconv::trace::URL_PATH, "/api/users/123".to_string()), + KeyValue::new( + semconv::trace::URL_FULL, + "http://example.com/api/users/123".to_string(), + ), + KeyValue::new( + semconv::trace::USER_AGENT_ORIGINAL, + "tower-test-client/1.0".to_string(), + ), + KeyValue::new(semconv::trace::HTTP_RESPONSE_STATUS_CODE, 200), + ]; + + assert_eq!(http_span.attributes, expected_attributes); + } + + async fn echo(req: http::Request) -> Result, Error> { + Ok(http::Response::new(req.into_body())) + } + + #[tokio::test(flavor = "current_thread")] async fn test_metrics_labels() { let exporter = InMemoryMetricExporter::default(); let reader = PeriodicReader::builder(exporter.clone()) .with_interval(Duration::from_millis(100)) .build(); let meter_provider = SdkMeterProvider::builder().with_reader(reader).build(); - let meter = meter_provider.meter("test"); - let layer = HTTPMetricsLayerBuilder::builder() - .with_meter(meter) + // Use the new API with optional provider override instead of global providers + let layer = HTTPLayerBuilder::builder() + .with_meter_provider(meter_provider.clone()) .build() .unwrap(); @@ -642,7 +851,7 @@ mod tests { .iter() .find(|kv| kv.key.as_str() == NETWORK_PROTOCOL_NAME_LABEL) .expect("Protocol name should be present in request body size"); - assert_eq!(protocol_name.value.as_str(), "http"); + assert_eq!(protocol_name.value.as_str(), "https"); let protocol_version = attributes .iter()