use http::header::CONTENT_TYPE;
use crate::sanitize::{
BodyLogContext,
SanitizedLogger,
};
use crate::{
HttpClientOptions,
HttpLoggingOptions,
HttpRequest,
HttpRequestBody,
HttpResponse,
HttpResponseMeta,
};
const UNRESOLVED_REQUEST_URL: &str = "<unresolved request URL>";
const STREAMING_REQUEST_BODY_SKIPPED: &str = "<skipped: streaming request body>";
#[derive(Debug, Clone)]
pub struct HttpLogger<'a> {
options: &'a HttpLoggingOptions,
sanitized_logger: SanitizedLogger,
}
enum RequestBodyLogPreview<'a> {
Bytes(&'a [u8]),
Empty,
Skipped(&'static str),
}
impl<'a> HttpLogger<'a> {
pub fn new(options: &'a HttpClientOptions) -> Self {
Self {
options: &options.logging,
sanitized_logger: SanitizedLogger::from_options(options),
}
}
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 masked = self.sanitized_logger.header_value(name, value);
tracing::trace!("{}: {}", name.as_str(), masked);
}
}
if self.options.log_request_body {
match Self::request_body_for_log(request) {
RequestBodyLogPreview::Bytes(bytes) => {
let content_type = Self::content_type(headers);
tracing::trace!(
"Request body: {}",
self.sanitized_logger.body(bytes, BodyLogContext::Request, content_type)
);
}
RequestBodyLogPreview::Empty => tracing::trace!("Request body: <empty>"),
RequestBodyLogPreview::Skipped(reason) => tracing::trace!("Request body: {reason}"),
}
}
}
pub async fn log_response(&self, response: &mut HttpResponse) -> crate::HttpResult<()> {
if !self.is_trace_enabled() {
return Ok(());
}
tracing::trace!(
"<-- {} {}",
response.status().as_u16(),
self.sanitized_logger.url(response.url())
);
if self.options.log_response_header {
for (name, value) in response.headers() {
let masked = self.sanitized_logger.header_value(name, value);
tracing::trace!("{}: {}", name.as_str(), masked);
}
}
if self.options.log_response_body {
let content_type = Self::content_type(response.headers()).map(str::to_string);
if let Some(body) = response.buffered_body_for_logging() {
tracing::trace!(
"Response body: {}",
self.sanitized_logger
.body(body.as_ref(), BodyLogContext::Response, content_type.as_deref())
);
} else if response.can_buffer_body_for_logging(self.options.body_size_limit) {
let body = response.bytes().await?;
tracing::trace!(
"Response body: {}",
self.sanitized_logger
.body(body.as_ref(), BodyLogContext::Response, content_type.as_deref())
);
} 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(),
self.sanitized_logger.url(response_meta.url())
);
if self.options.log_response_header {
for (name, value) in response_meta.headers() {
let masked = self.sanitized_logger.header_value(name, 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(&self, request: &HttpRequest) -> String {
request
.resolved_url()
.map(|url| self.sanitized_logger.url(&url))
.unwrap_or_else(|_| UNRESOLVED_REQUEST_URL.to_string())
}
fn request_body_for_log(request: &HttpRequest) -> RequestBodyLogPreview<'_> {
if request.has_streaming_body() {
return RequestBodyLogPreview::Skipped(STREAMING_REQUEST_BODY_SKIPPED);
}
match request.body() {
HttpRequestBody::Bytes(bytes)
| HttpRequestBody::Json(bytes)
| HttpRequestBody::Form(bytes)
| HttpRequestBody::Multipart(bytes)
| HttpRequestBody::Ndjson(bytes) => RequestBodyLogPreview::Bytes(bytes.as_ref()),
HttpRequestBody::Text(text) => RequestBodyLogPreview::Bytes(text.as_bytes()),
HttpRequestBody::Stream(_) => RequestBodyLogPreview::Skipped(STREAMING_REQUEST_BODY_SKIPPED),
HttpRequestBody::Empty => RequestBodyLogPreview::Empty,
}
}
fn content_type(headers: &http::HeaderMap) -> Option<&str> {
headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok())
}
}