qubit_http/client/
http_logger.rs1use crate::constants::{
16 SENSITIVE_HEADER_MASK_EDGE_CHARS, SENSITIVE_HEADER_MASK_PLACEHOLDER,
17 SENSITIVE_HEADER_MASK_SHORT_LEN,
18};
19use crate::{
20 HttpClientOptions, HttpLoggingOptions, HttpRequest, HttpRequestBody, HttpResponse,
21 HttpResponseMeta, SensitiveHttpHeaders,
22};
23use bytes::Bytes;
24
25#[derive(Debug, Clone, Copy)]
27pub struct HttpLogger<'a> {
28 options: &'a HttpLoggingOptions,
29 sensitive_headers: &'a SensitiveHttpHeaders,
30}
31
32impl<'a> HttpLogger<'a> {
33 pub fn new(options: &'a HttpClientOptions) -> Self {
42 Self {
43 options: &options.logging,
44 sensitive_headers: &options.sensitive_headers,
45 }
46 }
47
48 pub fn log_request(&self, request: &HttpRequest) {
57 if !self.is_trace_enabled() {
58 return;
59 }
60
61 let url = Self::request_log_url(request);
62 tracing::trace!("--> {} {}", request.method(), url);
63
64 let headers = request
65 .effective_headers_cached()
66 .unwrap_or_else(|| request.headers());
67
68 if self.options.log_request_header {
69 for (name, value) in headers {
70 let value = value.to_str().unwrap_or("<non-utf8>");
71 let masked = self.mask_header_value(name.as_str(), value);
72 tracing::trace!("{}: {}", name.as_str(), masked);
73 }
74 }
75
76 if self.options.log_request_body {
77 match Self::clone_request_body_for_log(request.body()) {
78 Some(bytes) => tracing::trace!("Request body: {}", self.render_body(&bytes)),
79 None => tracing::trace!("Request body: <empty>"),
80 }
81 }
82 }
83
84 pub async fn log_response(&self, response: &mut HttpResponse) -> crate::HttpResult<()> {
95 if !self.is_trace_enabled() {
96 return Ok(());
97 }
98
99 tracing::trace!("<-- {} {}", response.status().as_u16(), response.url());
100
101 if self.options.log_response_header {
102 for (name, value) in response.headers() {
103 let value = value.to_str().unwrap_or("<non-utf8>");
104 let masked = self.mask_header_value(name.as_str(), value);
105 tracing::trace!("{}: {}", name.as_str(), masked);
106 }
107 }
108
109 if self.options.log_response_body {
110 if let Some(body) = response.buffered_body_for_logging() {
111 tracing::trace!("Response body: {}", self.render_body(body));
112 } else if response.can_buffer_body_for_logging(self.options.body_size_limit) {
113 let body = response.bytes().await?;
114 tracing::trace!("Response body: {}", self.render_body(&body));
115 } else {
116 tracing::trace!("Response body: <skipped: streaming or unknown-size body>");
117 }
118 }
119 Ok(())
120 }
121
122 pub fn log_stream_response_headers(&self, response_meta: &HttpResponseMeta) {
130 if !self.is_trace_enabled() {
131 return;
132 }
133
134 tracing::trace!(
135 "<-- {} {} (stream)",
136 response_meta.status.as_u16(),
137 &response_meta.url
138 );
139
140 if self.options.log_response_header {
141 for (name, value) in &response_meta.headers {
142 let value = value.to_str().unwrap_or("<non-utf8>");
143 let masked = self.mask_header_value(name.as_str(), value);
144 tracing::trace!("{}: {}", name.as_str(), masked);
145 }
146 }
147 }
148
149 pub fn is_trace_enabled(&self) -> bool {
154 self.options.enabled && tracing::enabled!(tracing::Level::TRACE)
155 }
156
157 fn request_log_url(request: &HttpRequest) -> String {
166 request
167 .resolved_url_with_query()
168 .map(|url| url.to_string())
169 .unwrap_or_else(|_| request.path().to_string())
170 }
171
172 fn mask_header_value(&self, name: &str, value: &str) -> String {
181 if value.is_empty() {
182 return String::new();
183 }
184 if !self.sensitive_headers.contains(name) {
185 return value.to_string();
186 }
187
188 let chars: Vec<char> = value.chars().collect();
189 if chars.len() <= SENSITIVE_HEADER_MASK_SHORT_LEN {
190 SENSITIVE_HEADER_MASK_PLACEHOLDER.to_string()
191 } else {
192 let edge = SENSITIVE_HEADER_MASK_EDGE_CHARS;
193 let prefix: String = chars[..edge].iter().collect();
194 let suffix: String = chars[chars.len() - edge..].iter().collect();
195 format!("{prefix}{SENSITIVE_HEADER_MASK_PLACEHOLDER}{suffix}")
196 }
197 }
198
199 fn render_body(&self, body: &Bytes) -> String {
207 if body.is_empty() {
208 return "<empty>".to_string();
209 }
210
211 let max_bytes = self.options.body_size_limit;
212 let limit = body.len().min(max_bytes);
213 let prefix = &body[..limit];
214 let suffix = if body.len() > max_bytes {
215 format!("...<truncated {} bytes>", body.len() - max_bytes)
216 } else {
217 String::new()
218 };
219
220 match std::str::from_utf8(prefix) {
221 Ok(text) => format!("{}{}", text, suffix),
222 Err(_) => format!("<binary {} bytes>{}", body.len(), suffix),
223 }
224 }
225
226 fn clone_request_body_for_log(body: &HttpRequestBody) -> Option<Bytes> {
234 match body {
235 HttpRequestBody::Bytes(bytes)
236 | HttpRequestBody::Json(bytes)
237 | HttpRequestBody::Form(bytes)
238 | HttpRequestBody::Multipart(bytes)
239 | HttpRequestBody::Ndjson(bytes) => Some(bytes.clone()),
240 HttpRequestBody::Text(text) => Some(Bytes::from(text.clone())),
241 HttpRequestBody::Stream(_) => None,
242 HttpRequestBody::Empty => None,
243 }
244 }
245}