forked from mirror/reqwest-middleware
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.main
parent
f8ff599f50
commit
3457bf5702
|
@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [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
|
||||
|
||||
### Added
|
||||
|
|
|
@ -21,7 +21,9 @@ opentelemetry_0_18 = ["opentelemetry_0_18_pkg", "tracing-opentelemetry_0_18_pkg"
|
|||
[dependencies]
|
||||
reqwest-middleware = { version = "0.2.0", path = "../reqwest-middleware" }
|
||||
|
||||
anyhow = "1.0.70"
|
||||
async-trait = "0.1.51"
|
||||
matchit = "0.7.0"
|
||||
reqwest = { version = "0.11", default-features = false }
|
||||
task-local-extensions = "0.1.4"
|
||||
tracing = "0.1.26"
|
||||
|
|
|
@ -96,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, OtelName, ReqwestOtelSpanBackend, SpanBackendWithUrl, 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, OtelPathNames, ReqwestOtelSpanBackend, SpanBackendWithUrl,
|
||||
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)]
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use matchit::Router;
|
||||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use reqwest::{Request, Response, StatusCode as RequestStatusCode, Url};
|
||||
use reqwest_middleware::{Error, Result};
|
||||
use task_local_extensions::Extensions;
|
||||
use tracing::Span;
|
||||
use tracing::{warn, Span};
|
||||
|
||||
use crate::reqwest_otel_span;
|
||||
|
||||
|
@ -96,10 +97,20 @@ pub struct DefaultSpanBackend;
|
|||
|
||||
impl ReqwestOtelSpanBackend for DefaultSpanBackend {
|
||||
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");
|
||||
let name = if let Some(name) = ext.get::<OtelName>() {
|
||||
Cow::Borrowed(name.0.as_ref())
|
||||
} else if let Some(path_names) = ext.get::<OtelPathNames>() {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -120,10 +131,19 @@ pub struct SpanBackendWithUrl;
|
|||
|
||||
impl ReqwestOtelSpanBackend for SpanBackendWithUrl {
|
||||
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");
|
||||
let name = if let Some(name) = ext.get::<OtelName>() {
|
||||
Cow::Borrowed(name.0.as_ref())
|
||||
} else if let Some(path_names) = ext.get::<OtelPathNames>() {
|
||||
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()))
|
||||
}
|
||||
|
@ -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
|
||||
/// DefaultSpanBackend.
|
||||
/// [`DefaultSpanBackend`] and [`SpanBackendWithUrl`].
|
||||
///
|
||||
/// Usage:
|
||||
/// ```no_run
|
||||
|
@ -184,6 +204,88 @@ fn get_span_status(request_status: RequestStatusCode) -> Option<&'static str> {
|
|||
#[derive(Clone)]
|
||||
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.
|
||||
fn remove_credentials(url: &Url) -> Cow<'_, str> {
|
||||
if !url.username().is_empty() || url.password().is_some() {
|
||||
|
|
Loading…
Reference in New Issue