Make http.url field opt-in (#70)

This commit is contained in:
tl-rodrigo-gryzinski 2022-11-10 13:21:07 +00:00 committed by GitHub
parent 4fb158f785
commit 197f19781d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 76 additions and 8 deletions

View file

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

View file

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

View file

@ -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/");
}
} }

View file

@ -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,