Add `OtelPathNames` for span names (#89)
* feat: add `OtelPathNames` for span names If this extension is provided span names will be `<method> <path name>`. These path names will include parameter names rather than IDs or other elements that would increase the cardinality. * doc: update changelog * refactor: update `known_paths` to return error * refactor: return `anyhow::Error` instead This is as `reqwest_middleware::Error` is more focused on handling a request.pull/90/head
parent
f8ff599f50
commit
3457bf5702
|
@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `OtelPathNames` extension to provide known parameterized paths that will be used in span names
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `DefaultSpanBackend` and `SpanBackendWithUrl` default span name to HTTP method name instead of `reqwest-http-client`
|
||||||
|
|
||||||
## [0.2.1] - 2023-03-09
|
## [0.2.1] - 2023-03-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -21,7 +21,9 @@ opentelemetry_0_18 = ["opentelemetry_0_18_pkg", "tracing-opentelemetry_0_18_pkg"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest-middleware = { version = "0.2.0", path = "../reqwest-middleware" }
|
reqwest-middleware = { version = "0.2.0", path = "../reqwest-middleware" }
|
||||||
|
|
||||||
|
anyhow = "1.0.70"
|
||||||
async-trait = "0.1.51"
|
async-trait = "0.1.51"
|
||||||
|
matchit = "0.7.0"
|
||||||
reqwest = { version = "0.11", default-features = false }
|
reqwest = { version = "0.11", default-features = false }
|
||||||
task-local-extensions = "0.1.4"
|
task-local-extensions = "0.1.4"
|
||||||
tracing = "0.1.26"
|
tracing = "0.1.26"
|
||||||
|
|
|
@ -96,9 +96,9 @@ mod reqwest_otel_span_builder;
|
||||||
pub use middleware::TracingMiddleware;
|
pub use middleware::TracingMiddleware;
|
||||||
pub use reqwest_otel_span_builder::{
|
pub use reqwest_otel_span_builder::{
|
||||||
default_on_request_end, default_on_request_failure, default_on_request_success,
|
default_on_request_end, default_on_request_failure, default_on_request_success,
|
||||||
DefaultSpanBackend, OtelName, ReqwestOtelSpanBackend, SpanBackendWithUrl, ERROR_CAUSE_CHAIN,
|
DefaultSpanBackend, OtelName, OtelPathNames, ReqwestOtelSpanBackend, SpanBackendWithUrl,
|
||||||
ERROR_MESSAGE, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_URL,
|
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,
|
HTTP_URL, HTTP_USER_AGENT, NET_HOST_PORT, OTEL_KIND, OTEL_NAME, OTEL_STATUS_CODE,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use matchit::Router;
|
||||||
use reqwest::header::{HeaderMap, HeaderValue};
|
use reqwest::header::{HeaderMap, HeaderValue};
|
||||||
use reqwest::{Request, Response, StatusCode as RequestStatusCode, Url};
|
use reqwest::{Request, Response, StatusCode as RequestStatusCode, Url};
|
||||||
use reqwest_middleware::{Error, Result};
|
use reqwest_middleware::{Error, Result};
|
||||||
use task_local_extensions::Extensions;
|
use task_local_extensions::Extensions;
|
||||||
use tracing::Span;
|
use tracing::{warn, Span};
|
||||||
|
|
||||||
use crate::reqwest_otel_span;
|
use crate::reqwest_otel_span;
|
||||||
|
|
||||||
|
@ -96,10 +97,20 @@ pub struct DefaultSpanBackend;
|
||||||
|
|
||||||
impl ReqwestOtelSpanBackend for DefaultSpanBackend {
|
impl ReqwestOtelSpanBackend for DefaultSpanBackend {
|
||||||
fn on_request_start(req: &Request, ext: &mut Extensions) -> Span {
|
fn on_request_start(req: &Request, ext: &mut Extensions) -> Span {
|
||||||
let name = ext
|
let name = if let Some(name) = ext.get::<OtelName>() {
|
||||||
.get::<OtelName>()
|
Cow::Borrowed(name.0.as_ref())
|
||||||
.map(|on| on.0.as_ref())
|
} else if let Some(path_names) = ext.get::<OtelPathNames>() {
|
||||||
.unwrap_or("reqwest-http-client");
|
path_names
|
||||||
|
.find(req.url().path())
|
||||||
|
.map(|path| Cow::Owned(format!("{} {}", req.method(), path)))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
warn!("no OTEL path name found");
|
||||||
|
Cow::Owned(format!("{} UNKNOWN", req.method().as_str()))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(req.method().as_str())
|
||||||
|
};
|
||||||
|
|
||||||
reqwest_otel_span!(name = name, req)
|
reqwest_otel_span!(name = name, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,10 +131,19 @@ pub struct SpanBackendWithUrl;
|
||||||
|
|
||||||
impl ReqwestOtelSpanBackend for SpanBackendWithUrl {
|
impl ReqwestOtelSpanBackend for SpanBackendWithUrl {
|
||||||
fn on_request_start(req: &Request, ext: &mut Extensions) -> Span {
|
fn on_request_start(req: &Request, ext: &mut Extensions) -> Span {
|
||||||
let name = ext
|
let name = if let Some(name) = ext.get::<OtelName>() {
|
||||||
.get::<OtelName>()
|
Cow::Borrowed(name.0.as_ref())
|
||||||
.map(|on| on.0.as_ref())
|
} else if let Some(path_names) = ext.get::<OtelPathNames>() {
|
||||||
.unwrap_or("reqwest-http-client");
|
path_names
|
||||||
|
.find(req.url().path())
|
||||||
|
.map(|path| Cow::Owned(format!("{} {}", req.method(), path)))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
warn!("no OTEL path name found");
|
||||||
|
Cow::Owned(format!("{} UNKNOWN", req.method().as_str()))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(req.method().as_str())
|
||||||
|
};
|
||||||
|
|
||||||
reqwest_otel_span!(name = name, req, http.url = %remove_credentials(req.url()))
|
reqwest_otel_span!(name = name, req, http.url = %remove_credentials(req.url()))
|
||||||
}
|
}
|
||||||
|
@ -152,7 +172,7 @@ fn get_span_status(request_status: RequestStatusCode) -> Option<&'static str> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`OtelName`] allows customisation of the name of the spans created by
|
/// [`OtelName`] allows customisation of the name of the spans created by
|
||||||
/// DefaultSpanBackend.
|
/// [`DefaultSpanBackend`] and [`SpanBackendWithUrl`].
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
|
@ -184,6 +204,88 @@ fn get_span_status(request_status: RequestStatusCode) -> Option<&'static str> {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct OtelName(pub Cow<'static, str>);
|
pub struct OtelName(pub Cow<'static, str>);
|
||||||
|
|
||||||
|
/// [`OtelPathNames`] allows including templated paths in the spans created by
|
||||||
|
/// [`DefaultSpanBackend`] and [`SpanBackendWithUrl`].
|
||||||
|
///
|
||||||
|
/// When creating spans this can be used to try to match the path against some
|
||||||
|
/// known paths. If the path matches value returned is the templated path. This
|
||||||
|
/// can be used in span names as it will not contain values that would
|
||||||
|
/// increase the cardinality.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// /// # use reqwest_middleware::Result;
|
||||||
|
/// use reqwest_middleware::{ClientBuilder, Extension};
|
||||||
|
/// use reqwest_tracing::{
|
||||||
|
/// TracingMiddleware, OtelPathNames
|
||||||
|
/// };
|
||||||
|
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let reqwest_client = reqwest::Client::builder().build()?;
|
||||||
|
/// let client = ClientBuilder::new(reqwest_client)
|
||||||
|
/// // Inserts the extension before the request is started
|
||||||
|
/// .with_init(Extension(OtelPathNames::known_paths(["/payment/:paymentId"])?))
|
||||||
|
/// // Makes use of that extension to specify the otel name
|
||||||
|
/// .with(TracingMiddleware::default())
|
||||||
|
/// .build();
|
||||||
|
///
|
||||||
|
/// let resp = client.get("https://truelayer.com/payment/id-123").send().await?;
|
||||||
|
///
|
||||||
|
/// // Or specify it on the individual request (will take priority)
|
||||||
|
/// let resp = client.post("https://api.truelayer.com/payment/id-123/authorization-flow")
|
||||||
|
/// .with_extension(OtelPathNames::known_paths(["/payment/:paymentId/authorization-flow"])?)
|
||||||
|
/// .send()
|
||||||
|
/// .await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OtelPathNames(matchit::Router<String>);
|
||||||
|
|
||||||
|
impl OtelPathNames {
|
||||||
|
/// Create a new [`OtelPathNames`] from a set of known paths.
|
||||||
|
///
|
||||||
|
/// Paths in this set will be found with `find`.
|
||||||
|
///
|
||||||
|
/// Paths can have different parameters:
|
||||||
|
/// - Named parameters like `:paymentId` match anything until the next `/` or the end of the path.
|
||||||
|
/// - Catch-all parameters start with `*` and match everything after the `/`. They must be at the end of the route.
|
||||||
|
/// ```
|
||||||
|
/// # use reqwest_tracing::OtelPathNames;
|
||||||
|
/// OtelPathNames::known_paths([
|
||||||
|
/// "/",
|
||||||
|
/// "/payment",
|
||||||
|
/// "/payment/:paymentId",
|
||||||
|
/// "/payment/:paymentId/*action",
|
||||||
|
/// ]).unwrap();
|
||||||
|
/// ```
|
||||||
|
pub fn known_paths<Paths, Path>(paths: Paths) -> anyhow::Result<Self>
|
||||||
|
where
|
||||||
|
Paths: IntoIterator<Item = Path>,
|
||||||
|
Path: Into<String>,
|
||||||
|
{
|
||||||
|
let mut router = Router::new();
|
||||||
|
for path in paths {
|
||||||
|
let path = path.into();
|
||||||
|
router.insert(path.clone(), path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(router))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the templated path from the actual path.
|
||||||
|
///
|
||||||
|
/// Returns the templated path if a match is found.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use reqwest_tracing::OtelPathNames;
|
||||||
|
/// let path_names = OtelPathNames::known_paths(["/payment/:paymentId"]).unwrap();
|
||||||
|
/// let path = path_names.find("/payment/payment-id-123");
|
||||||
|
/// assert_eq!(path, Some("/payment/:paymentId"));
|
||||||
|
/// ```
|
||||||
|
pub fn find(&self, path: &str) -> Option<&str> {
|
||||||
|
self.0.at(path).map(|mtch| mtch.value.as_str()).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Removes the username and/or password parts of the url, if present.
|
/// Removes the username and/or password parts of the url, if present.
|
||||||
fn remove_credentials(url: &Url) -> Cow<'_, str> {
|
fn remove_credentials(url: &Url) -> Cow<'_, str> {
|
||||||
if !url.username().is_empty() || url.password().is_some() {
|
if !url.username().is_empty() || url.password().is_some() {
|
||||||
|
|
Loading…
Reference in New Issue