From 07d154cadf9b389212e5f40e007348d38b05dcf0 Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Mon, 7 Nov 2022 14:07:23 +0100 Subject: [PATCH] Custom otel names (#65) * Breaking change(macros): require explicit name for tracing middleware Closes: #52 This is suggested by the Opentelemetry spec, which requires "Therefore, HTTP client spans SHOULD be using conservative, low cardinality names formed from the available parameters of an HTTP request, such as "HTTP {METHOD_NAME}". Instrumentation MUST NOT default to using URI path as span name, but MAY provide hooks to allow custom logic to override the default span name. " * Permit customisation of otel span names via OtelName --- reqwest-tracing/CHANGELOG.md | 3 ++ reqwest-tracing/src/lib.rs | 54 +++++++++++++++++-- reqwest-tracing/src/middleware.rs | 12 ++--- .../src/reqwest_otel_span_builder.rs | 43 ++++++++++++++- .../src/reqwest_otel_span_macro.rs | 34 ++++++------ 5 files changed, 117 insertions(+), 29 deletions(-) diff --git a/reqwest-tracing/CHANGELOG.md b/reqwest-tracing/CHANGELOG.md index 1f616b3..659676c 100644 --- a/reqwest-tracing/CHANGELOG.md +++ b/reqwest-tracing/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - HTTP URL is captured in traces as the `http.url` attribute. + - Require an explicit otel name in the macros. Reduces cardinality and complies + with otel specification for HTTP bindings. + - Permit customisation of the otel name from the non-macro layer. ## [0.3.1] - 2022-09-21 - Added support for `opentelemetry` version `0.18`. diff --git a/reqwest-tracing/src/lib.rs b/reqwest-tracing/src/lib.rs index 527461c..a24ad55 100644 --- a/reqwest-tracing/src/lib.rs +++ b/reqwest-tracing/src/lib.rs @@ -2,6 +2,52 @@ //! //! Attach [`TracingMiddleware`] to your client to automatically trace HTTP requests. //! +//! The simplest possible usage: +//! ```no_run +//! # use reqwest_middleware::Result; +//! use reqwest_middleware::{ClientBuilder}; +//! use reqwest_tracing::TracingMiddleware; +//! +//! # async fn example() -> Result<()> { +//! let reqwest_client = reqwest::Client::builder().build().unwrap(); +//! let client = ClientBuilder::new(reqwest_client) +//! // Insert the tracing middleware +//! .with(TracingMiddleware::default()) +//! .build(); +//! +//! let resp = client.get("https://truelayer.com").send().await.unwrap(); +//! # Ok(()) +//! # } +//! ``` +//! +//! To customise the span names use [`OtelName`]. +//! ```no_run +//! # use reqwest_middleware::Result; +//! use reqwest_middleware::{ClientBuilder, Extension}; +//! use reqwest_tracing::{ +//! TracingMiddleware, OtelName +//! }; +//! # async fn example() -> Result<()> { +//! let reqwest_client = reqwest::Client::builder().build().unwrap(); +//! let client = ClientBuilder::new(reqwest_client) +//! // Inserts the extension before the request is started +//! .with_init(Extension(OtelName("my-client".into()))) +//! // Makes use of that extension to specify the otel name +//! .with(TracingMiddleware::default()) +//! .build(); +//! +//! let resp = client.get("https://truelayer.com").send().await.unwrap(); +//! +//! // Or specify it on the individual request (will take priority) +//! let resp = client.post("https://api.truelayer.com/payment") +//! .with_extension(OtelName("POST /payment".into())) +//! .send() +//! .await +//! .unwrap(); +//! # Ok(()) +//! # } +//! ``` +//! //! In this example we define a custom span builder to calculate the request time elapsed and we register the [`TracingMiddleware`]. //! //! Note that Opentelemetry tracks start and stop already, there is no need to have a custom builder like this. @@ -21,7 +67,7 @@ //! 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) +//! reqwest_otel_span!(name="example-request", req, time_elapsed = tracing::field::Empty) //! } //! //! fn on_request_end(span: &Span, outcome: &Result, extension: &mut Extensions) { @@ -50,9 +96,9 @@ 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_URL, HTTP_USER_AGENT, NET_HOST_PORT, - OTEL_KIND, OTEL_NAME, OTEL_STATUS_CODE, + DefaultSpanBackend, OtelName, ReqwestOtelSpanBackend, ERROR_CAUSE_CHAIN, ERROR_MESSAGE, + HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_URL, HTTP_USER_AGENT, + NET_HOST_PORT, OTEL_KIND, OTEL_NAME, OTEL_STATUS_CODE, }; #[doc(hidden)] diff --git a/reqwest-tracing/src/middleware.rs b/reqwest-tracing/src/middleware.rs index 6342f61..a3c37d7 100644 --- a/reqwest-tracing/src/middleware.rs +++ b/reqwest-tracing/src/middleware.rs @@ -18,18 +18,18 @@ impl TracingMiddleware { } } -impl Default for TracingMiddleware { - fn default() -> Self { - TracingMiddleware::new() - } -} - impl Clone for TracingMiddleware { fn clone(&self) -> Self { Self::new() } } +impl Default for TracingMiddleware { + fn default() -> Self { + TracingMiddleware::new() + } +} + #[async_trait::async_trait] impl Middleware for TracingMiddleware where diff --git a/reqwest-tracing/src/reqwest_otel_span_builder.rs b/reqwest-tracing/src/reqwest_otel_span_builder.rs index 48901d9..9c3dbdc 100644 --- a/reqwest-tracing/src/reqwest_otel_span_builder.rs +++ b/reqwest-tracing/src/reqwest_otel_span_builder.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::{Request, Response, StatusCode as RequestStatusCode}; use reqwest_middleware::{Error, Result}; @@ -92,8 +94,12 @@ pub fn default_on_request_failure(span: &Span, e: &Error) { pub struct DefaultSpanBackend; impl ReqwestOtelSpanBackend for DefaultSpanBackend { - fn on_request_start(req: &Request, _: &mut Extensions) -> Span { - reqwest_otel_span!(req) + fn on_request_start(req: &Request, ext: &mut Extensions) -> Span { + let name = ext + .get::() + .map(|on| on.0.as_ref()) + .unwrap_or("reqwest-http-client"); + reqwest_otel_span!(name = name, req) } fn on_request_end(span: &Span, outcome: &Result, _: &mut Extensions) { @@ -124,6 +130,39 @@ fn get_span_status(request_status: RequestStatusCode) -> Option<&'static str> { } } +/// [`OtelName`] allows customisation of the name of the spans created by +/// DefaultSpanBackend. +/// +/// Usage: +/// ```no_run +/// # use reqwest_middleware::Result; +/// use reqwest_middleware::{ClientBuilder, Extension}; +/// use reqwest_tracing::{ +/// TracingMiddleware, OtelName +/// }; +/// # async fn example() -> Result<()> { +/// let reqwest_client = reqwest::Client::builder().build().unwrap(); +/// let client = ClientBuilder::new(reqwest_client) +/// // Inserts the extension before the request is started +/// .with_init(Extension(OtelName("my-client".into()))) +/// // Makes use of that extension to specify the otel name +/// .with(TracingMiddleware::default()) +/// .build(); +/// +/// let resp = client.get("https://truelayer.com").send().await.unwrap(); +/// +/// // Or specify it on the individual request (will take priority) +/// let resp = client.post("https://api.truelayer.com/payment") +/// .with_extension(OtelName("POST /payment".into())) +/// .send() +/// .await +/// .unwrap(); +/// # Ok(()) +/// # } +/// ``` +#[derive(Clone)] +pub struct OtelName(pub Cow<'static, str>); + #[cfg(test)] mod tests { use super::*; diff --git a/reqwest-tracing/src/reqwest_otel_span_macro.rs b/reqwest-tracing/src/reqwest_otel_span_macro.rs index ced64b2..8925238 100644 --- a/reqwest-tracing/src/reqwest_otel_span_macro.rs +++ b/reqwest-tracing/src/reqwest_otel_span_macro.rs @@ -26,7 +26,8 @@ /// /// # Macro syntax /// -/// The first argument passed to [`reqwest_otel_span!`](crate::reqwest_otel_span) is a reference to an [`reqwest::Request`]. +/// The first argument is a [span name](https://opentelemetry.io/docs/reference/specification/trace/api/#span). +/// The second argument passed to [`reqwest_otel_span!`](crate::reqwest_otel_span) is a reference to an [`reqwest::Request`]. /// /// ```rust /// use reqwest_middleware::Result; @@ -41,7 +42,7 @@ /// /// impl ReqwestOtelSpanBackend for CustomReqwestOtelSpanBackend { /// fn on_request_start(req: &Request, _extension: &mut Extensions) -> Span { -/// reqwest_otel_span!(req) +/// reqwest_otel_span!(name = "reqwest-http-request", req) /// } /// /// fn on_request_end(span: &Span, outcome: &Result, _extension: &mut Extensions) { @@ -61,17 +62,17 @@ /// /// // Define a `time_elapsed` field as empty. It might be populated later. /// // (This example is just to show how to inject data - otel already tracks durations) -/// reqwest_otel_span!(request, time_elapsed = tracing::field::Empty); +/// reqwest_otel_span!(name = "reqwest-http-request", request, time_elapsed = tracing::field::Empty); /// /// // Define a `name` field with a known value, `AppName`. -/// reqwest_otel_span!(request, name = "AppName"); +/// reqwest_otel_span!(name = "reqwest-http-request", 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); +/// reqwest_otel_span!(name = "reqwest-http-request", request, app_id); /// /// // All together -/// reqwest_otel_span!(request, time_elapsed = tracing::field::Empty, name = "AppName", app_id); +/// reqwest_otel_span!(name = "reqwest-http-request", request, time_elapsed = tracing::field::Empty, name = "AppName", app_id); /// ``` /// /// You can also choose to customise the level of the generated span: @@ -88,8 +89,8 @@ /// Level::INFO /// }; /// -/// // `level =` MUST be the first argument. -/// reqwest_otel_span!(level = level, request); +/// // `level =` and name MUST come before the request, in this order +/// reqwest_otel_span!(level = level, name = "reqwest-http-request", request); /// ``` /// /// @@ -99,27 +100,26 @@ /// [`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,) + (name=$name:expr, $request:ident) => { + reqwest_otel_span!(name=$name, $request,) }; // Vanilla root span, with no additional fields but custom level - (level=$level:expr, $request:ident) => { - reqwest_otel_span!(level=$level, $request,) + (level=$level:expr, name=$name:expr, $request:ident) => { + reqwest_otel_span!(level=$level, name=$name, $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)*) + (name=$name:expr, $request:ident, $($field:tt)*) => { + reqwest_otel_span!(level=$crate::reqwest_otel_span_macro::private::Level::INFO, name=$name, $request, $($field)*) }; // Root span with additional fields and custom level - (level=$level:expr, $request:ident, $($field:tt)*) => { + (level=$level:expr, name=$name:expr, $request:ident, $($field:tt)*) => { { let method = $request.method(); let url = $request.url(); let scheme = url.scheme(); let host = url.host_str().unwrap_or(""); let host_port = url.port().unwrap_or(0) as i64; - let path = url.path(); - let otel_name = format!("{} {}", method, path); + let otel_name = $name.to_string(); macro_rules! request_span { ($lvl:expr) => {