qubit_http/client/
http_logger.rs1use http::header::CONTENT_TYPE;
16
17use crate::sanitize::{
18 BodyLogContext,
19 SanitizedLogger,
20};
21use crate::{
22 HttpClientOptions,
23 HttpLoggingOptions,
24 HttpRequest,
25 HttpRequestBody,
26 HttpResponse,
27 HttpResponseMeta,
28};
29
30const UNRESOLVED_REQUEST_URL: &str = "<unresolved request URL>";
31const STREAMING_REQUEST_BODY_SKIPPED: &str = "<skipped: streaming request body>";
32
33#[derive(Debug, Clone)]
35pub struct HttpLogger<'a> {
36 options: &'a HttpLoggingOptions,
37 sanitized_logger: SanitizedLogger,
38}
39
40enum RequestBodyLogPreview<'a> {
42 Bytes(&'a [u8]),
44 Empty,
46 Skipped(&'static str),
48}
49
50impl<'a> HttpLogger<'a> {
51 pub fn new(options: &'a HttpClientOptions) -> Self {
60 Self {
61 options: &options.logging,
62 sanitized_logger: SanitizedLogger::from_options(options),
63 }
64 }
65
66 pub fn log_request(&self, request: &HttpRequest) {
75 if !self.is_trace_enabled() {
76 return;
77 }
78
79 let url = self.request_log_url(request);
80 tracing::trace!("--> {} {}", request.method(), url);
81
82 let headers = request.effective_headers_cached().unwrap_or_else(|| request.headers());
83
84 if self.options.log_request_header {
85 for (name, value) in headers {
86 let masked = self.sanitized_logger.header_value(name, value);
87 tracing::trace!("{}: {}", name.as_str(), masked);
88 }
89 }
90
91 if self.options.log_request_body {
92 match Self::request_body_for_log(request) {
93 RequestBodyLogPreview::Bytes(bytes) => {
94 let content_type = Self::content_type(headers);
95 tracing::trace!(
96 "Request body: {}",
97 self.sanitized_logger.body(bytes, BodyLogContext::Request, content_type)
98 );
99 }
100 RequestBodyLogPreview::Empty => tracing::trace!("Request body: <empty>"),
101 RequestBodyLogPreview::Skipped(reason) => tracing::trace!("Request body: {reason}"),
102 }
103 }
104 }
105
106 pub async fn log_response(&self, response: &mut HttpResponse) -> crate::HttpResult<()> {
117 if !self.is_trace_enabled() {
118 return Ok(());
119 }
120
121 tracing::trace!(
122 "<-- {} {}",
123 response.status().as_u16(),
124 self.sanitized_logger.url(response.url())
125 );
126
127 if self.options.log_response_header {
128 for (name, value) in response.headers() {
129 let masked = self.sanitized_logger.header_value(name, value);
130 tracing::trace!("{}: {}", name.as_str(), masked);
131 }
132 }
133
134 if self.options.log_response_body {
135 let content_type = Self::content_type(response.headers()).map(str::to_string);
136 if let Some(body) = response.buffered_body_for_logging() {
137 tracing::trace!(
138 "Response body: {}",
139 self.sanitized_logger
140 .body(body.as_ref(), BodyLogContext::Response, content_type.as_deref())
141 );
142 } else if response.can_buffer_body_for_logging(self.options.body_size_limit) {
143 let body = response.bytes().await?;
144 tracing::trace!(
145 "Response body: {}",
146 self.sanitized_logger
147 .body(body.as_ref(), BodyLogContext::Response, content_type.as_deref())
148 );
149 } else {
150 tracing::trace!("Response body: <skipped: streaming or unknown-size body>");
151 }
152 }
153 Ok(())
154 }
155
156 pub fn log_stream_response_headers(&self, response_meta: &HttpResponseMeta) {
164 if !self.is_trace_enabled() {
165 return;
166 }
167
168 tracing::trace!(
169 "<-- {} {} (stream)",
170 response_meta.status().as_u16(),
171 self.sanitized_logger.url(response_meta.url())
172 );
173
174 if self.options.log_response_header {
175 for (name, value) in response_meta.headers() {
176 let masked = self.sanitized_logger.header_value(name, value);
177 tracing::trace!("{}: {}", name.as_str(), masked);
178 }
179 }
180 }
181
182 pub fn is_trace_enabled(&self) -> bool {
187 self.options.enabled && tracing::enabled!(tracing::Level::TRACE)
188 }
189
190 fn request_log_url(&self, request: &HttpRequest) -> String {
199 request
200 .resolved_url()
201 .map(|url| self.sanitized_logger.url(&url))
202 .unwrap_or_else(|_| UNRESOLVED_REQUEST_URL.to_string())
203 }
204
205 fn request_body_for_log(request: &HttpRequest) -> RequestBodyLogPreview<'_> {
213 if request.has_streaming_body() {
214 return RequestBodyLogPreview::Skipped(STREAMING_REQUEST_BODY_SKIPPED);
215 }
216 match request.body() {
217 HttpRequestBody::Bytes(bytes)
218 | HttpRequestBody::Json(bytes)
219 | HttpRequestBody::Form(bytes)
220 | HttpRequestBody::Multipart(bytes)
221 | HttpRequestBody::Ndjson(bytes) => RequestBodyLogPreview::Bytes(bytes.as_ref()),
222 HttpRequestBody::Text(text) => RequestBodyLogPreview::Bytes(text.as_bytes()),
223 HttpRequestBody::Stream(_) => RequestBodyLogPreview::Skipped(STREAMING_REQUEST_BODY_SKIPPED),
224 HttpRequestBody::Empty => RequestBodyLogPreview::Empty,
225 }
226 }
227
228 fn content_type(headers: &http::HeaderMap) -> Option<&str> {
236 headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok())
237 }
238}