use crate::constants::{
SENSITIVE_HEADER_MASK_EDGE_CHARS,
SENSITIVE_HEADER_MASK_PLACEHOLDER,
SENSITIVE_HEADER_MASK_SHORT_LEN,
};
use crate::{
HttpClientOptions,
HttpLoggingOptions,
HttpRequest,
HttpRequestBody,
HttpResponse,
HttpResponseMeta,
SensitiveHttpHeaders,
};
use bytes::Bytes;
#[derive(Debug, Clone, Copy)]
pub struct HttpLogger<'a> {
options: &'a HttpLoggingOptions,
sensitive_headers: &'a SensitiveHttpHeaders,
}
impl<'a> HttpLogger<'a> {
pub fn new(options: &'a HttpClientOptions) -> Self {
Self {
options: &options.logging,
sensitive_headers: &options.sensitive_headers,
}
}
pub fn log_request(&self, request: &HttpRequest) {
if !self.is_trace_enabled() {
return;
}
let url = Self::request_log_url(request);
tracing::trace!("--> {} {}", request.method(), url);
let headers = request
.effective_headers_cached()
.unwrap_or_else(|| request.headers());
if self.options.log_request_header {
for (name, value) in headers {
let value = value.to_str().unwrap_or("<non-utf8>");
let masked = self.mask_header_value(name.as_str(), value);
tracing::trace!("{}: {}", name.as_str(), masked);
}
}
if self.options.log_request_body {
match Self::clone_request_body_for_log(request.body()) {
Some(bytes) => tracing::trace!("Request body: {}", self.render_body(&bytes)),
None => tracing::trace!("Request body: <empty>"),
}
}
}
pub async fn log_response(&self, response: &mut HttpResponse) -> crate::HttpResult<()> {
if !self.is_trace_enabled() {
return Ok(());
}
tracing::trace!("<-- {} {}", response.status().as_u16(), response.url());
if self.options.log_response_header {
for (name, value) in response.headers() {
let value = value.to_str().unwrap_or("<non-utf8>");
let masked = self.mask_header_value(name.as_str(), value);
tracing::trace!("{}: {}", name.as_str(), masked);
}
}
if self.options.log_response_body {
if let Some(body) = response.buffered_body_for_logging() {
tracing::trace!("Response body: {}", self.render_body(body));
} else if response.can_buffer_body_for_logging(self.options.body_size_limit) {
let body = response.bytes().await?;
tracing::trace!("Response body: {}", self.render_body(&body));
} else {
tracing::trace!("Response body: <skipped: streaming or unknown-size body>");
}
}
Ok(())
}
pub fn log_stream_response_headers(&self, response_meta: &HttpResponseMeta) {
if !self.is_trace_enabled() {
return;
}
tracing::trace!(
"<-- {} {} (stream)",
response_meta.status.as_u16(),
&response_meta.url
);
if self.options.log_response_header {
for (name, value) in &response_meta.headers {
let value = value.to_str().unwrap_or("<non-utf8>");
let masked = self.mask_header_value(name.as_str(), value);
tracing::trace!("{}: {}", name.as_str(), masked);
}
}
}
pub fn is_trace_enabled(&self) -> bool {
self.options.enabled && tracing::enabled!(tracing::Level::TRACE)
}
fn request_log_url(request: &HttpRequest) -> String {
request
.resolved_url_with_query()
.map(|url| url.to_string())
.unwrap_or_else(|_| request.path().to_string())
}
fn mask_header_value(&self, name: &str, value: &str) -> String {
if value.is_empty() {
return String::new();
}
if !self.sensitive_headers.contains(name) {
return value.to_string();
}
let chars: Vec<char> = value.chars().collect();
if chars.len() <= SENSITIVE_HEADER_MASK_SHORT_LEN {
SENSITIVE_HEADER_MASK_PLACEHOLDER.to_string()
} else {
let edge = SENSITIVE_HEADER_MASK_EDGE_CHARS;
let prefix: String = chars[..edge].iter().collect();
let suffix: String = chars[chars.len() - edge..].iter().collect();
format!("{prefix}{SENSITIVE_HEADER_MASK_PLACEHOLDER}{suffix}")
}
}
fn render_body(&self, body: &Bytes) -> String {
if body.is_empty() {
return "<empty>".to_string();
}
let max_bytes = self.options.body_size_limit;
let limit = body.len().min(max_bytes);
let prefix = &body[..limit];
let suffix = if body.len() > max_bytes {
format!("...<truncated {} bytes>", body.len() - max_bytes)
} else {
String::new()
};
match std::str::from_utf8(prefix) {
Ok(text) => format!("{}{}", text, suffix),
Err(_) => format!("<binary {} bytes>{}", body.len(), suffix),
}
}
fn clone_request_body_for_log(body: &HttpRequestBody) -> Option<Bytes> {
match body {
HttpRequestBody::Bytes(bytes)
| HttpRequestBody::Json(bytes)
| HttpRequestBody::Form(bytes)
| HttpRequestBody::Multipart(bytes)
| HttpRequestBody::Ndjson(bytes) => Some(bytes.clone()),
HttpRequestBody::Text(text) => Some(Bytes::from(text.clone())),
HttpRequestBody::Stream(_) => None,
HttpRequestBody::Empty => None,
}
}
}