From 13ca870d202399968433e0c0323f7405740a31bb Mon Sep 17 00:00:00 2001 From: Alessandro Zanin Date: Thu, 30 Jun 2022 15:54:54 +0200 Subject: [PATCH] Allows adding properties to the span (#43) Provides reqwest-tracing with a trait ReqwestOtelSpanBackend, allowing custom fields/behaviour to be defined. This is a breaking change as the TracingMiddleware signature now requires you to define a ReqwestOtelSpanBackend to provide it with the desired span behaviour. Co-authored-by: Alessandro Zanin Co-authored-by: tl-flavio-barinas Co-authored-by: Marco Tormento --- README.md | 8 +- reqwest-tracing/CHANGELOG.md | 4 + reqwest-tracing/Cargo.toml | 2 +- reqwest-tracing/README.md | 43 +++-- reqwest-tracing/src/lib.rs | 45 ++++- reqwest-tracing/src/middleware.rs | 137 ++++---------- reqwest-tracing/src/otel.rs | 2 +- .../src/reqwest_otel_span_builder.rs | 138 ++++++++++++++ .../src/reqwest_otel_span_macro.rs | 169 ++++++++++++++++++ 9 files changed, 428 insertions(+), 120 deletions(-) create mode 100644 reqwest-tracing/src/reqwest_otel_span_builder.rs create mode 100644 reqwest-tracing/src/reqwest_otel_span_macro.rs diff --git a/README.md b/README.md index 2e49ebd..cd12dec 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ The `reqwest-middleware` client exposes the same interface as a plain `reqwest` # ... [dependencies] reqwest = "0.11" -reqwest-middleware = "0.1.1" -reqwest-retry = "0.1.1" -reqwest-tracing = "0.2.0" +reqwest-middleware = "0.1.6" +reqwest-retry = "0.1.5" +reqwest-tracing = "0.2.3" tokio = { version = "1.12.0", features = ["macros", "rt-multi-thread"] } ``` @@ -42,7 +42,7 @@ async fn main() { let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); let client = ClientBuilder::new(reqwest::Client::new()) // Trace HTTP requests. See the tracing crate to make use of these traces. - .with(TracingMiddleware) + .with(TracingMiddleware::default()) // Retry failed requests. .with(RetryTransientMiddleware::new_with_policy(retry_policy)) .build(); diff --git a/reqwest-tracing/CHANGELOG.md b/reqwest-tracing/CHANGELOG.md index 30b9c02..9c0bc72 100644 --- a/reqwest-tracing/CHANGELOG.md +++ b/reqwest-tracing/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +# [0.3.0] - 2022-06-10 +### Breaking +- Created `ReqwestOtelSpanBackend` trait with `reqwest_otel_span` macro to provide extendable default request otel fields + ## [0.2.3] - 2022-06-23 ### Fixed - Fix how we set the OpenTelemetry span status, based on the HTTP response status. diff --git a/reqwest-tracing/Cargo.toml b/reqwest-tracing/Cargo.toml index 0495873..bff1998 100644 --- a/reqwest-tracing/Cargo.toml +++ b/reqwest-tracing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reqwest-tracing" -version = "0.2.3" +version = "0.3.0-alpha.0" authors = ["Rodrigo Gryzinski "] edition = "2018" description = "Opentracing middleware for reqwest." diff --git a/reqwest-tracing/README.md b/reqwest-tracing/README.md index d512fc3..57a7edf 100644 --- a/reqwest-tracing/README.md +++ b/reqwest-tracing/README.md @@ -20,7 +20,7 @@ opentelemetry = "0.17" reqwest = "0.11" reqwest-middleware = "0.1.1" reqwest-retry = "0.1.1" -reqwest-tracing = { version = "0.2.1", features = ["opentelemetry_0_17"] } +reqwest-tracing = { version = "0.3.0", features = ["opentelemetry_0_17"] } tokio = { version = "1.12.0", features = ["macros", "rt-multi-thread"] } tracing = "0.1" tracing-opentelemetry = "0.17" @@ -28,28 +28,47 @@ tracing-subscriber = "0.3" ``` ```rust,skip +use reqwest_tracing::{default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend, TracingMiddleware}; use opentelemetry::sdk::export::trace::stdout; -use reqwest_middleware::ClientBuilder; -use reqwest_tracing::TracingMiddleware; +use reqwest::{Request, Response}; +use reqwest_middleware::{ClientBuilder, Result}; +use std::time::Instant; +use task_local_extensions::Extensions; +use tracing::Span; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::Registry; +pub struct TimeTrace; + +impl ReqwestOtelSpanBackend for TimeTrace { + fn on_request_start(req: &Request, extension: &mut Extensions) -> Span { + extension.insert(Instant::now()); + reqwest_otel_span!(req, time_elapsed = tracing::field::Empty) + } + + fn on_request_end(span: &Span, outcome: &Result, extension: &mut Extensions) { + let time_elapsed = extension.get::().unwrap().elapsed().as_millis() as i64; + default_on_request_end(span, outcome); + span.record("time_elapsed", &time_elapsed); + } +} + #[tokio::main] async fn main() { - let tracer = stdout::new_pipeline().install_simple(); - let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); - let subscriber = Registry::default().with(telemetry); - tracing::subscriber::set_global_default(subscriber).unwrap(); + let tracer = stdout::new_pipeline().install_simple(); + let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); + let subscriber = Registry::default().with(telemetry); + tracing::subscriber::set_global_default(subscriber).unwrap(); - run().await; + run().await; } async fn run() { - let client = ClientBuilder::new(reqwest::Client::new()) - .with(TracingMiddleware) - .build(); + let client = ClientBuilder::new(reqwest::Client::new()) + .with(TracingMiddleware::::new()) + .build(); - client.get("https://truelayer.com").send().await.unwrap(); + client.get("https://truelayer.com").send().await.unwrap(); } ``` diff --git a/reqwest-tracing/src/lib.rs b/reqwest-tracing/src/lib.rs index e9367ed..0b36cb9 100644 --- a/reqwest-tracing/src/lib.rs +++ b/reqwest-tracing/src/lib.rs @@ -1,6 +1,38 @@ -//! Opentracing middleware implementation for [`reqwest-middleware`]. +//! Opentracing middleware implementation for [`reqwest_middleware`]. //! //! Attach [`TracingMiddleware`] to your client to automatically trace HTTP requests. +//! +//! In this example we define a custom span builder to calculate the request time elapsed and we register the [`TracingMiddleware`]. +//! ```rust +//! use reqwest_middleware::Result; +//! use task_local_extensions::Extensions; +//! use reqwest::{Request, Response}; +//! use reqwest_middleware::ClientBuilder; +//! use reqwest_tracing::{ +//! default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend, TracingMiddleware +//! }; +//! use tracing::Span; +//! use std::time::{Duration, Instant}; +//! +//! pub struct TimeTrace; +//! +//! impl ReqwestOtelSpanBackend for TimeTrace { +//! fn on_request_start(req: &Request, extension: &mut Extensions) -> Span { +//! extension.insert(Instant::now()); +//! reqwest_otel_span!(req, time_elapsed = tracing::field::Empty) +//! } +//! +//! fn on_request_end(span: &Span, outcome: &Result, extension: &mut Extensions) { +//! let time_elapsed = extension.get::().unwrap().elapsed().as_millis() as i64; +//! default_on_request_end(span, outcome); +//! span.record("time_elapsed", &time_elapsed); +//! } +//! } +//! +//! let http = ClientBuilder::new(reqwest::Client::new()) +//! .with(TracingMiddleware::::new()) +//! .build(); +//! ``` mod middleware; #[cfg(any( @@ -11,5 +43,14 @@ mod middleware; feature = "opentelemetry_0_17", ))] mod otel; - +mod reqwest_otel_span_builder; pub use middleware::TracingMiddleware; +pub use reqwest_otel_span_builder::{ + default_on_request_end, default_on_request_failure, default_on_request_success, + DefaultSpanBackend, ReqwestOtelSpanBackend, ERROR_CAUSE_CHAIN, ERROR_MESSAGE, HTTP_HOST, + HTTP_METHOD, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_USER_AGENT, NET_HOST_PORT, OTEL_KIND, + OTEL_NAME, OTEL_STATUS_CODE, +}; + +#[doc(hidden)] +pub mod reqwest_otel_span_macro; diff --git a/reqwest-tracing/src/middleware.rs b/reqwest-tracing/src/middleware.rs index f7314ba..1231bc6 100644 --- a/reqwest-tracing/src/middleware.rs +++ b/reqwest-tracing/src/middleware.rs @@ -1,45 +1,49 @@ -use reqwest::header::{HeaderMap, HeaderValue}; -use reqwest::{Request, Response, StatusCode as RequestStatusCode}; -use reqwest_middleware::{Error, Middleware, Next, Result}; +use reqwest::{Request, Response}; +use reqwest_middleware::{Middleware, Next, Result}; use task_local_extensions::Extensions; use tracing::Instrument; +use crate::{DefaultSpanBackend, ReqwestOtelSpanBackend}; + /// Middleware for tracing requests using the current Opentelemetry Context. -pub struct TracingMiddleware; +pub struct TracingMiddleware { + span_backend: std::marker::PhantomData, +} + +impl TracingMiddleware { + pub fn new() -> TracingMiddleware { + TracingMiddleware { + span_backend: Default::default(), + } + } +} + +impl Default for TracingMiddleware { + fn default() -> Self { + TracingMiddleware::new() + } +} + +impl Clone for TracingMiddleware { + fn clone(&self) -> Self { + Self::new() + } +} #[async_trait::async_trait] -impl Middleware for TracingMiddleware { +impl Middleware for TracingMiddleware +where + ReqwestOtelSpan: ReqwestOtelSpanBackend + Sync + Send + 'static, +{ async fn handle( &self, req: Request, extensions: &mut Extensions, next: Next<'_>, ) -> Result { - let request_span = { - let method = req.method(); - let scheme = req.url().scheme(); - let host = req.url().host_str().unwrap_or(""); - let host_port = req.url().port().unwrap_or(0) as i64; - let path = req.url().path(); - let otel_name = format!("{} {}", method, path); + let request_span = ReqwestOtelSpan::on_request_start(&req, extensions); - tracing::info_span!( - "HTTP request", - http.method = %method, - http.scheme = %scheme, - http.host = %host, - net.host.port = %host_port, - otel.kind = "client", - otel.name = %otel_name, - otel.status_code = tracing::field::Empty, - http.user_agent = tracing::field::Empty, - http.status_code = tracing::field::Empty, - error.message = tracing::field::Empty, - error.cause_chain = tracing::field::Empty, - ) - }; - - async { + let outcome_future = async { // Adds tracing headers to the given request to propagate the OpenTelemetry context to downstream revivers of the request. // Spans added by downstream consumers will be part of the same trace. #[cfg(any( @@ -53,77 +57,10 @@ impl Middleware for TracingMiddleware { // Run the request let outcome = next.run(req, extensions).await; - match &outcome { - Ok(response) => { - // The request ran successfully - let span_status = get_span_status(response.status()); - let status_code = response.status().as_u16() as i64; - let user_agent = get_header_value("user_agent", response.headers()); - if let Some(span_status) = span_status { - request_span.record("otel.status_code", &span_status); - } - request_span.record("http.status_code", &status_code); - request_span.record("http.user_agent", &user_agent.as_str()); - } - Err(e) => { - // The request didn't run successfully - let error_message = e.to_string(); - let error_cause_chain = format!("{:?}", e); - request_span.record("otel.status_code", &"ERROR"); - request_span.record("error.message", &error_message.as_str()); - request_span.record("error.cause_chain", &error_cause_chain.as_str()); - if let Error::Reqwest(e) = e { - request_span.record( - "http.status_code", - &e.status() - .map(|s| s.to_string()) - .unwrap_or_else(|| "".to_string()) - .as_str(), - ); - } - } - } + ReqwestOtelSpan::on_request_end(&request_span, &outcome, extensions); outcome - } - .instrument(request_span.clone()) - .await - } -} - -fn get_header_value(key: &str, headers: &HeaderMap) -> String { - let header_default = &HeaderValue::from_static(""); - format!("{:?}", headers.get(key).unwrap_or(header_default)).replace('"', "") -} - -/// HTTP Mapping -/// -/// Maps the the http status to an Opentelemetry span status following the the specified convention above. -fn get_span_status(request_status: RequestStatusCode) -> Option<&'static str> { - match request_status.as_u16() { - // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, unless there was - // another error (e.g., network error receiving the response body; or 3xx codes with max redirects exceeded), - // in which case status MUST be set to Error. - 100..=399 => None, - // For HTTP status codes in the 4xx range span status MUST be left unset in case of SpanKind.SERVER and MUST be - // set to Error in case of SpanKind.CLIENT. - 400..=499 => Some("ERROR"), - // For HTTP status codes in the 5xx range, as well as any other code the client failed to interpret, span - // status MUST be set to Error. - _ => Some("ERROR"), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn get_header_value_for_span_attribute() { - let expect = "IMPORTANT_HEADER"; - let mut header_map = HeaderMap::new(); - header_map.insert("test", expect.parse().unwrap()); - - let value = get_header_value("test", &header_map); - assert_eq!(value, expect); + }; + + outcome_future.instrument(request_span.clone()).await } } diff --git a/reqwest-tracing/src/otel.rs b/reqwest-tracing/src/otel.rs index e7c518c..0430041 100644 --- a/reqwest-tracing/src/otel.rs +++ b/reqwest-tracing/src/otel.rs @@ -119,7 +119,7 @@ mod test { .await; let client = ClientBuilder::new(reqwest::Client::new()) - .with(TracingMiddleware) + .with(TracingMiddleware::default()) .build(); let resp = client diff --git a/reqwest-tracing/src/reqwest_otel_span_builder.rs b/reqwest-tracing/src/reqwest_otel_span_builder.rs new file mode 100644 index 0000000..2d7cd6e --- /dev/null +++ b/reqwest-tracing/src/reqwest_otel_span_builder.rs @@ -0,0 +1,138 @@ +use reqwest::header::{HeaderMap, HeaderValue}; +use reqwest::{Request, Response, StatusCode as RequestStatusCode}; +use reqwest_middleware::{Error, Result}; +use task_local_extensions::Extensions; +use tracing::Span; + +use crate::reqwest_otel_span; + +/// The `http.method` field added to the span by [`reqwest_otel_span`] +pub const HTTP_METHOD: &str = "http.method"; +/// The `http.scheme` field added to the span by [`reqwest_otel_span`] +pub const HTTP_SCHEME: &str = "http.scheme"; +/// The `http.host` field added to the span by [`reqwest_otel_span`] +pub const HTTP_HOST: &str = "http.host"; +/// The `host.port` field added to the span by [`reqwest_otel_span`] +pub const NET_HOST_PORT: &str = "net.host.port"; +/// The `otel.kind` field added to the span by [`reqwest_otel_span`] +pub const OTEL_KIND: &str = "otel.kind"; +/// The `otel.name` field added to the span by [`reqwest_otel_span`] +pub const OTEL_NAME: &str = "otel.name"; +/// The `otel.status.code` field added to the span by [`reqwest_otel_span`] +pub const OTEL_STATUS_CODE: &str = "otel.status_code"; +/// The `error.message` field added to the span by [`reqwest_otel_span`] +pub const ERROR_MESSAGE: &str = "error.message"; +/// The `error.cause_chain` field added to the span by [`reqwest_otel_span`] +pub const ERROR_CAUSE_CHAIN: &str = "error.cause_chain"; +/// The `http.status_code` field added to the span by [`reqwest_otel_span`] +pub const HTTP_STATUS_CODE: &str = "http.status_code"; +/// The `http.user_agent` added to the span by [`reqwest_otel_span`] +pub const HTTP_USER_AGENT: &str = "http.user_agent"; + +/// [`ReqwestOtelSpanBackend`] allows you to customise the span attached by +/// [`TracingMiddleware`] to incoming requests. +/// +/// Check out [`reqwest_otel_span`] documentation for examples. +/// +/// [`TracingMiddleware`]: crate::middleware::TracingMiddleware. +pub trait ReqwestOtelSpanBackend { + /// Initalized a new span before the request is executed. + fn on_request_start(req: &Request, extension: &mut Extensions) -> Span; + + /// Runs after the request call has executed. + fn on_request_end(span: &Span, outcome: &Result, extension: &mut Extensions); +} + +/// Populates default success/failure fields for a given [`reqwest_otel_span!`] span. +#[inline] +pub fn default_on_request_end(span: &Span, outcome: &Result) { + match outcome { + Ok(res) => default_on_request_success(span, res), + Err(err) => default_on_request_failure(span, err), + } +} + +/// Populates default success fields for a given [`reqwest_otel_span!`] span. +#[inline] +pub fn default_on_request_success(span: &Span, response: &Response) { + let span_status = get_span_status(response.status()); + let status_code = response.status().as_u16() as i64; + let user_agent = get_header_value("user_agent", response.headers()); + if let Some(span_status) = span_status { + span.record(OTEL_STATUS_CODE, &span_status); + } + span.record(HTTP_STATUS_CODE, &status_code); + span.record(HTTP_USER_AGENT, &user_agent.as_str()); +} + +/// Populates default failure fields for a given [`reqwest_otel_span!`] span. +#[inline] +pub fn default_on_request_failure(span: &Span, e: &Error) { + let error_message = e.to_string(); + let error_cause_chain = format!("{:?}", e); + span.record(OTEL_STATUS_CODE, &"ERROR"); + span.record(ERROR_MESSAGE, &error_message.as_str()); + span.record(ERROR_CAUSE_CHAIN, &error_cause_chain.as_str()); + if let Error::Reqwest(e) = e { + span.record( + HTTP_STATUS_CODE, + &e.status() + .map(|s| s.to_string()) + .unwrap_or_else(|| "".to_string()) + .as_str(), + ); + } +} + +/// The default [`ReqwestOtelSpanBackend`] for [`TracingMiddleware`]. +/// +/// [`TracingMiddleware`]: crate::middleware::TracingMiddleware +pub struct DefaultSpanBackend; + +impl ReqwestOtelSpanBackend for DefaultSpanBackend { + fn on_request_start(req: &Request, _: &mut Extensions) -> Span { + reqwest_otel_span!(req) + } + + fn on_request_end(span: &Span, outcome: &Result, _: &mut Extensions) { + default_on_request_end(span, outcome) + } +} + +fn get_header_value(key: &str, headers: &HeaderMap) -> String { + let header_default = &HeaderValue::from_static(""); + format!("{:?}", headers.get(key).unwrap_or(header_default)).replace('"', "") +} + +/// HTTP Mapping +/// +/// Maps the the http status to an Opentelemetry span status following the the specified convention above. +fn get_span_status(request_status: RequestStatusCode) -> Option<&'static str> { + match request_status.as_u16() { + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, unless there was + // another error (e.g., network error receiving the response body; or 3xx codes with max redirects exceeded), + // in which case status MUST be set to Error. + 100..=399 => None, + // For HTTP status codes in the 4xx range span status MUST be left unset in case of SpanKind.SERVER and MUST be + // set to Error in case of SpanKind.CLIENT. + 400..=499 => Some("ERROR"), + // For HTTP status codes in the 5xx range, as well as any other code the client failed to interpret, span + // status MUST be set to Error. + _ => Some("ERROR"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_header_value_for_span_attribute() { + let expect = "IMPORTANT_HEADER"; + let mut header_map = HeaderMap::new(); + header_map.insert("test", expect.parse().unwrap()); + + let value = get_header_value("test", &header_map); + assert_eq!(value, expect); + } +} diff --git a/reqwest-tracing/src/reqwest_otel_span_macro.rs b/reqwest-tracing/src/reqwest_otel_span_macro.rs new file mode 100644 index 0000000..08a84d8 --- /dev/null +++ b/reqwest-tracing/src/reqwest_otel_span_macro.rs @@ -0,0 +1,169 @@ +#[macro_export] +/// [`reqwest_otel_span!`] creates a new [`tracing::Span`]. +/// It empowers you to add custom properties to the span on top of the default properties provided by the macro +/// +/// Default Fields: +/// - http.method +/// - http.scheme +/// - http.host +/// - net.host +/// - otel.kind +/// - otel.name +/// - otel.status_code +/// - http.user_agent +/// - http.status_code +/// - error.message +/// - error.cause_chain +/// +/// Here are some convenient functions to checkout [`default_on_request_success`], [`default_on_request_failure`], +/// and [`default_on_request_end`]. +/// +/// # Why a macro? +/// +/// [`tracing`] requires all the properties attached to a span to be declared upfront, when the span is created. +/// You cannot add new ones afterwards. +/// This makes it extremely fast, but it pushes us to reach for macros when we need some level of composition. +/// +/// # Macro syntax +/// +/// The first argument passed to [`reqwest_otel_span!`] is a reference to an [`reqwest::Request`]. +/// +/// ```rust +/// use reqwest_middleware::Result; +/// use task_local_extensions::Extensions; +/// use reqwest::{Request, Response}; +/// use reqwest_tracing::{ +/// default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend +/// }; +/// use tracing::Span; +/// +/// pub struct CustomReqwestOtelSpanBackend; +/// +/// impl ReqwestOtelSpanBackend for CustomReqwestOtelSpanBackend { +/// fn on_request_start(req: &Request, _extension: &mut Extensions) -> Span { +/// reqwest_otel_span!(req) +/// } +/// +/// fn on_request_end(span: &Span, outcome: &Result, _extension: &mut Extensions) { +/// default_on_request_end(span, outcome) +/// } +/// } +/// ``` +/// +/// If nothing else is specified, the span generated by `reqwest_otel_span!` is identical to the one you'd +/// get by using [`DefaultSpanBackend`]. +/// +/// You can define new fields following the same syntax of [`tracing::info_span!`] for fields: +/// +/// ```rust,should_panic +/// use reqwest_tracing::reqwest_otel_span; +/// # let request: &reqwest::Request = todo!(); +/// +/// // Define a `time_elapsed` field as empty. It might be populated later. +/// reqwest_otel_span!(request, time_elapsed = tracing::field::Empty); +/// +/// // Define a `name` field with a known value, `AppName`. +/// reqwest_otel_span!(request, name = "AppName"); +/// +/// // Define an `app_id` field using the variable with the same name as value. +/// let app_id = "XYZ"; +/// reqwest_otel_span!(request, app_id); +/// +/// // All together +/// reqwest_otel_span!(request, time_elapsed = tracing::field::Empty, name = "AppName", app_id); +/// ``` +/// +/// You can also choose to customise the level of the generated span: +/// +/// ```rust,should_panic +/// use reqwest_tracing::reqwest_otel_span; +/// use tracing::Level; +/// # let request: &reqwest::Request = todo!(); +/// +/// // Reduce the log level for service endpoints/probes +/// let level = if request.method().as_str() == "POST" { +/// Level::DEBUG +/// } else { +/// Level::INFO +/// }; +/// +/// // `level =` MUST be the first argument. +/// reqwest_otel_span!(level = level, request); +/// ``` +/// +/// +/// [`DefaultSpanBackend`]: crate::reqwest_otel_span_builder::DefaultSpanBackend +/// [`default_on_request_success`]: crate::reqwest_otel_span_builder::default_on_request_success +/// [`default_on_request_failure`]: crate::reqwest_otel_span_builder::default_on_request_failure +/// [`default_on_request_end`]: crate::reqwest_otel_span_builder::default_on_request_end +macro_rules! reqwest_otel_span { + // Vanilla root span at default INFO level, with no additional fields + ($request:ident) => { + reqwest_otel_span!($request,) + }; + // Vanilla root span, with no additional fields but custom level + (level=$level:expr, $request:ident) => { + reqwest_otel_span!(level=$level, $request,) + }; + // Root span with additional fields, default INFO level + ($request:ident, $($field:tt)*) => { + reqwest_otel_span!(level=$crate::reqwest_otel_span_macro::private::Level::INFO, $request, $($field)*) + }; + // Root span with additional fields and custom level + (level=$level:expr, $request:ident, $($field:tt)*) => { + { + let method = $request.method(); + let scheme = $request.url().scheme(); + let host = $request.url().host_str().unwrap_or(""); + let host_port = $request.url().port().unwrap_or(0) as i64; + let path = $request.url().path(); + let otel_name = format!("{} {}", method, path); + + macro_rules! request_span { + ($lvl:expr) => { + $crate::reqwest_otel_span_macro::private::span!( + $lvl, + "HTTP request", + http.method = %method, + http.scheme = %scheme, + http.host = %host, + net.host.port = %host_port, + otel.kind = "client", + otel.name = %otel_name, + otel.status_code = tracing::field::Empty, + http.user_agent = tracing::field::Empty, + http.status_code = tracing::field::Empty, + error.message = tracing::field::Empty, + error.cause_chain = tracing::field::Empty, + $($field)* + ) + } + } + + let span = match $level { + $crate::reqwest_otel_span_macro::private::Level::TRACE => { + request_span!($crate::reqwest_otel_span_macro::private::Level::TRACE) + }, + $crate::reqwest_otel_span_macro::private::Level::DEBUG => { + request_span!($crate::reqwest_otel_span_macro::private::Level::DEBUG) + }, + $crate::reqwest_otel_span_macro::private::Level::INFO => { + request_span!($crate::reqwest_otel_span_macro::private::Level::INFO) + }, + $crate::reqwest_otel_span_macro::private::Level::WARN => { + request_span!($crate::reqwest_otel_span_macro::private::Level::WARN) + }, + $crate::reqwest_otel_span_macro::private::Level::ERROR => { + request_span!($crate::reqwest_otel_span_macro::private::Level::ERROR) + }, + }; + span + } + } +} + +#[doc(hidden)] +pub mod private { + #[doc(hidden)] + pub use tracing::{span, Level}; +}