use std::borrow::Cow;
use http::Extensions;
use matchit::Router;
use reqwest::{Request, Response, StatusCode as RequestStatusCode, Url};
use reqwest_middleware::{Error, Result};
use tracing::{warn, Span};
use crate::reqwest_otel_span;
pub const HTTP_REQUEST_METHOD: &str = "http.request.method";
pub const URL_SCHEME: &str = "url.scheme";
pub const SERVER_ADDRESS: &str = "server.address";
pub const SERVER_PORT: &str = "server.port";
pub const URL_FULL: &str = "url.full";
pub const USER_AGENT_ORIGINAL: &str = "user_agent.original";
pub const OTEL_KIND: &str = "otel.kind";
pub const OTEL_NAME: &str = "otel.name";
pub const OTEL_STATUS_CODE: &str = "otel.status_code";
pub const HTTP_RESPONSE_STATUS_CODE: &str = "http.response.status_code";
pub const ERROR_MESSAGE: &str = "error.message";
pub const ERROR_CAUSE_CHAIN: &str = "error.cause_chain";
#[cfg(feature = "deprecated_attributes")]
pub const HTTP_METHOD: &str = "http.method";
#[cfg(feature = "deprecated_attributes")]
pub const HTTP_SCHEME: &str = "http.scheme";
#[cfg(feature = "deprecated_attributes")]
pub const HTTP_HOST: &str = "http.host";
#[cfg(feature = "deprecated_attributes")]
pub const HTTP_URL: &str = "http.url";
#[cfg(feature = "deprecated_attributes")]
pub const NET_HOST_PORT: &str = "net.host.port";
#[cfg(feature = "deprecated_attributes")]
pub const HTTP_STATUS_CODE: &str = "http.status_code";
#[cfg(feature = "deprecated_attributes")]
pub const HTTP_USER_AGENT: &str = "http.user_agent";
pub trait ReqwestOtelSpanBackend {
fn on_request_start(req: &Request, extension: &mut Extensions) -> Span;
fn on_request_end(span: &Span, outcome: &Result<Response>, extension: &mut Extensions);
}
#[inline]
pub fn default_on_request_end(span: &Span, outcome: &Result<Response>) {
match outcome {
Ok(res) => default_on_request_success(span, res),
Err(err) => default_on_request_failure(span, err),
}
}
#[cfg(feature = "deprecated_attributes")]
fn get_header_value(key: &str, headers: &reqwest::header::HeaderMap) -> String {
let header_default = &reqwest::header::HeaderValue::from_static("");
format!("{:?}", headers.get(key).unwrap_or(header_default)).replace('"', "")
}
#[inline]
pub fn default_on_request_success(span: &Span, response: &Response) {
let span_status = get_span_status(response.status());
if let Some(span_status) = span_status {
span.record(OTEL_STATUS_CODE, span_status);
}
span.record(HTTP_RESPONSE_STATUS_CODE, response.status().as_u16());
#[cfg(feature = "deprecated_attributes")]
{
let user_agent = get_header_value("user_agent", response.headers());
span.record(HTTP_STATUS_CODE, response.status().as_u16());
span.record(HTTP_USER_AGENT, user_agent.as_str());
}
}
#[inline]
pub fn default_on_request_failure(span: &Span, e: &Error) {
let error_message = e.to_string();
let error_cause_chain = format!("{:?}", e);
span.record(OTEL_STATUS_CODE, "ERROR");
span.record(ERROR_MESSAGE, error_message.as_str());
span.record(ERROR_CAUSE_CHAIN, error_cause_chain.as_str());
if let Error::Reqwest(e) = e {
if let Some(status) = e.status() {
span.record(HTTP_RESPONSE_STATUS_CODE, status.as_u16());
#[cfg(feature = "deprecated_attributes")]
{
span.record(HTTP_STATUS_CODE, status.as_u16());
}
}
}
}
#[inline]
pub fn default_span_name<'a>(req: &'a Request, ext: &'a Extensions) -> Cow<'a, str> {
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())
}
}
pub struct DefaultSpanBackend;
impl ReqwestOtelSpanBackend for DefaultSpanBackend {
fn on_request_start(req: &Request, ext: &mut Extensions) -> Span {
let name = default_span_name(req, ext);
reqwest_otel_span!(name = name, req)
}
fn on_request_end(span: &Span, outcome: &Result<Response>, _: &mut Extensions) {
default_on_request_end(span, outcome)
}
}
pub struct SpanBackendWithUrl;
impl ReqwestOtelSpanBackend for SpanBackendWithUrl {
fn on_request_start(req: &Request, ext: &mut Extensions) -> Span {
let name = default_span_name(req, ext);
let url = remove_credentials(req.url());
let span = reqwest_otel_span!(name = name, req, url.full = %url);
#[cfg(feature = "deprecated_attributes")]
{
span.record(HTTP_URL, url.to_string());
}
span
}
fn on_request_end(span: &Span, outcome: &Result<Response>, _: &mut Extensions) {
default_on_request_end(span, outcome)
}
}
fn get_span_status(request_status: RequestStatusCode) -> Option<&'static str> {
match request_status.as_u16() {
100..=399 => None,
400..=499 => Some("ERROR"),
_ => Some("ERROR"),
}
}
#[derive(Clone)]
pub struct OtelName(pub Cow<'static, str>);
#[derive(Clone)]
pub struct OtelPathNames(matchit::Router<String>);
impl OtelPathNames {
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))
}
pub fn find(&self, path: &str) -> Option<&str> {
self.0.at(path).map(|mtch| mtch.value.as_str()).ok()
}
}
#[derive(Clone)]
pub struct DisableOtelPropagation;
fn remove_credentials(url: &Url) -> Cow<'_, str> {
if !url.username().is_empty() || url.password().is_some() {
let mut url = url.clone();
url.set_username("")
.and_then(|_| url.set_password(None))
.ok();
url.to_string().into()
} else {
url.as_ref().into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::header::{HeaderMap, HeaderValue};
fn get_header_value(key: &str, headers: &HeaderMap) -> String {
let header_default = &HeaderValue::from_static("");
format!("{:?}", headers.get(key).unwrap_or(header_default)).replace('"', "")
}
#[test]
fn get_header_value_for_span_attribute() {
let expect = "IMPORTANT_HEADER";
let mut header_map = HeaderMap::new();
header_map.insert("test", expect.parse().unwrap());
let value = get_header_value("test", &header_map);
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/");
}
}