Make http.url field opt-in (#70)
parent
4fb158f785
commit
197f19781d
|
@ -5,7 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
- HTTP URL is captured in traces as the `http.url` attribute.
|
### Added
|
||||||
|
- `SpanBackendWithUrl` for capturing `http.url` in traces
|
||||||
- Require an explicit otel name in the macros. Reduces cardinality and complies
|
- Require an explicit otel name in the macros. Reduces cardinality and complies
|
||||||
with otel specification for HTTP bindings.
|
with otel specification for HTTP bindings.
|
||||||
- Permit customisation of the otel name from the non-macro layer.
|
- Permit customisation of the otel name from the non-macro layer.
|
||||||
|
|
|
@ -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, ERROR_CAUSE_CHAIN, ERROR_MESSAGE,
|
DefaultSpanBackend, OtelName, ReqwestOtelSpanBackend, SpanBackendWithUrl, ERROR_CAUSE_CHAIN,
|
||||||
HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_URL, HTTP_USER_AGENT,
|
ERROR_MESSAGE, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_URL,
|
||||||
NET_HOST_PORT, OTEL_KIND, OTEL_NAME, OTEL_STATUS_CODE,
|
HTTP_USER_AGENT, NET_HOST_PORT, OTEL_KIND, OTEL_NAME, OTEL_STATUS_CODE,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use reqwest::header::{HeaderMap, HeaderValue};
|
use reqwest::header::{HeaderMap, HeaderValue};
|
||||||
use reqwest::{Request, Response, StatusCode as RequestStatusCode};
|
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::Span;
|
||||||
|
@ -88,7 +88,8 @@ pub fn default_on_request_failure(span: &Span, e: &Error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The default [`ReqwestOtelSpanBackend`] for [`TracingMiddleware`].
|
/// The default [`ReqwestOtelSpanBackend`] for [`TracingMiddleware`]. Note that it doesn't include
|
||||||
|
/// the `http.url` field in spans, you can use [`SpanBackendWithUrl`] to add it.
|
||||||
///
|
///
|
||||||
/// [`TracingMiddleware`]: crate::middleware::TracingMiddleware
|
/// [`TracingMiddleware`]: crate::middleware::TracingMiddleware
|
||||||
pub struct DefaultSpanBackend;
|
pub struct DefaultSpanBackend;
|
||||||
|
@ -112,6 +113,26 @@ fn get_header_value(key: &str, headers: &HeaderMap) -> String {
|
||||||
format!("{:?}", headers.get(key).unwrap_or(header_default)).replace('"', "")
|
format!("{:?}", headers.get(key).unwrap_or(header_default)).replace('"', "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Similar to [`DefaultSpanBackend`] but also adds the `http.url` attribute to request spans.
|
||||||
|
///
|
||||||
|
/// [`TracingMiddleware`]: crate::middleware::TracingMiddleware
|
||||||
|
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");
|
||||||
|
|
||||||
|
reqwest_otel_span!(name = name, req, http.url = %remove_credentials(req.url()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_request_end(span: &Span, outcome: &Result<Response>, _: &mut Extensions) {
|
||||||
|
default_on_request_end(span, outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// HTTP Mapping <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status>
|
/// HTTP Mapping <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status>
|
||||||
///
|
///
|
||||||
/// Maps the the http status to an Opentelemetry span status following the the specified convention above.
|
/// Maps the the http status to an Opentelemetry span status following the the specified convention above.
|
||||||
|
@ -163,6 +184,21 @@ 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>);
|
||||||
|
|
||||||
|
/// 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() {
|
||||||
|
let mut url = url.clone();
|
||||||
|
// Errors settings username/password are set when the URL can't have credentials, so
|
||||||
|
// they're just ignored.
|
||||||
|
url.set_username("")
|
||||||
|
.and_then(|_| url.set_password(None))
|
||||||
|
.ok();
|
||||||
|
url.to_string().into()
|
||||||
|
} else {
|
||||||
|
url.as_ref().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -176,4 +212,32 @@ mod tests {
|
||||||
let value = get_header_value("test", &header_map);
|
let value = get_header_value("test", &header_map);
|
||||||
assert_eq!(value, expect);
|
assert_eq!(value, expect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_credentials_from_url_without_credentials_is_noop() {
|
||||||
|
let url = "http://nocreds.com/".parse().unwrap();
|
||||||
|
let clean = remove_credentials(&url);
|
||||||
|
assert_eq!(clean, "http://nocreds.com/");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_credentials_removes_username_only() {
|
||||||
|
let url = "http://user@withuser.com/".parse().unwrap();
|
||||||
|
let clean = remove_credentials(&url);
|
||||||
|
assert_eq!(clean, "http://withuser.com/");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_credentials_removes_password_only() {
|
||||||
|
let url = "http://:123@withpwd.com/".parse().unwrap();
|
||||||
|
let clean = remove_credentials(&url);
|
||||||
|
assert_eq!(clean, "http://withpwd.com/");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_credentials_removes_username_and_password() {
|
||||||
|
let url = "http://user:123@both.com/".parse().unwrap();
|
||||||
|
let clean = remove_credentials(&url);
|
||||||
|
assert_eq!(clean, "http://both.com/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,10 @@
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// If nothing else is specified, the span generated by `reqwest_otel_span!` is identical to the one you'd
|
/// If nothing else is specified, the span generated by `reqwest_otel_span!` is identical to the one you'd
|
||||||
/// get by using [`DefaultSpanBackend`].
|
/// get by using [`DefaultSpanBackend`]. Note that to avoid leaking sensitive information, the
|
||||||
|
/// macro doesn't include `http.url`, even though it's required by opentelemetry. You can add the
|
||||||
|
/// URL attribute explicitly by usng [`SpanBackendWithUrl`] instead of `DefaultSpanBackend` or
|
||||||
|
/// adding the field on your own implementation.
|
||||||
///
|
///
|
||||||
/// You can define new fields following the same syntax of [`tracing::info_span!`] for fields:
|
/// You can define new fields following the same syntax of [`tracing::info_span!`] for fields:
|
||||||
///
|
///
|
||||||
|
@ -95,6 +98,7 @@
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// [`DefaultSpanBackend`]: crate::reqwest_otel_span_builder::DefaultSpanBackend
|
/// [`DefaultSpanBackend`]: crate::reqwest_otel_span_builder::DefaultSpanBackend
|
||||||
|
/// [`SpanBackendWithUrl`]: crate::reqwest_otel_span_builder::DefaultSpanBackend
|
||||||
/// [`default_on_request_success`]: crate::reqwest_otel_span_builder::default_on_request_success
|
/// [`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_failure`]: crate::reqwest_otel_span_builder::default_on_request_failure
|
||||||
/// [`default_on_request_end`]: crate::reqwest_otel_span_builder::default_on_request_end
|
/// [`default_on_request_end`]: crate::reqwest_otel_span_builder::default_on_request_end
|
||||||
|
@ -129,7 +133,6 @@ macro_rules! reqwest_otel_span {
|
||||||
http.method = %method,
|
http.method = %method,
|
||||||
http.scheme = %scheme,
|
http.scheme = %scheme,
|
||||||
http.host = %host,
|
http.host = %host,
|
||||||
http.url = %url,
|
|
||||||
net.host.port = %host_port,
|
net.host.port = %host_port,
|
||||||
otel.kind = "client",
|
otel.kind = "client",
|
||||||
otel.name = %otel_name,
|
otel.name = %otel_name,
|
||||||
|
|
Loading…
Reference in New Issue