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
pull/60/head^2
Robert Collins 2022-11-07 14:07:23 +01:00 committed by GitHub
parent 920cb5ac16
commit 07d154cadf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 117 additions and 29 deletions

View File

@ -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`.

View File

@ -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<Response>, 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)]

View File

@ -18,18 +18,18 @@ impl<S: ReqwestOtelSpanBackend> TracingMiddleware<S> {
}
}
impl Default for TracingMiddleware<DefaultSpanBackend> {
fn default() -> Self {
TracingMiddleware::new()
}
}
impl<S: ReqwestOtelSpanBackend> Clone for TracingMiddleware<S> {
fn clone(&self) -> Self {
Self::new()
}
}
impl Default for TracingMiddleware<DefaultSpanBackend> {
fn default() -> Self {
TracingMiddleware::new()
}
}
#[async_trait::async_trait]
impl<ReqwestOtelSpan> Middleware for TracingMiddleware<ReqwestOtelSpan>
where

View File

@ -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::<OtelName>()
.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<Response>, _: &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::*;

View File

@ -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<Response>, _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) => {