qubit_http/client/
http_logger.rs1use crate::constants::{
16 SENSITIVE_HEADER_MASK_EDGE_CHARS,
17 SENSITIVE_HEADER_MASK_PLACEHOLDER,
18 SENSITIVE_HEADER_MASK_SHORT_LEN,
19};
20use crate::{
21 HttpClientOptions,
22 HttpLoggingOptions,
23 HttpRequest,
24 HttpRequestBody,
25 HttpResponse,
26 HttpResponseMeta,
27 SensitiveHttpHeaders,
28};
29use bytes::Bytes;
30
31#[derive(Debug, Clone, Copy)]
33pub struct HttpLogger<'a> {
34 options: &'a HttpLoggingOptions,
35 sensitive_headers: &'a SensitiveHttpHeaders,
36}
37
38impl<'a> HttpLogger<'a> {
39 pub fn new(options: &'a HttpClientOptions) -> Self {
48 Self {
49 options: &options.logging,
50 sensitive_headers: &options.sensitive_headers,
51 }
52 }
53
54 pub fn log_request(&self, request: &HttpRequest) {
63 if !self.is_trace_enabled() {
64 return;
65 }
66
67 let url = Self::request_log_url(request);
68 tracing::trace!("--> {} {}", request.method(), url);
69
70 let headers = request
71 .effective_headers_cached()
72 .unwrap_or_else(|| request.headers());
73
74 if self.options.log_request_header {
75 for (name, value) in headers {
76 let value = value.to_str().unwrap_or("<non-utf8>");
77 let masked = self.mask_header_value(name.as_str(), value);
78 tracing::trace!("{}: {}", name.as_str(), masked);
79 }
80 }
81
82 if self.options.log_request_body {
83 match Self::clone_request_body_for_log(request.body()) {
84 Some(bytes) => tracing::trace!("Request body: {}", self.render_body(&bytes)),
85 None => tracing::trace!("Request body: <empty>"),
86 }
87 }
88 }
89
90 pub async fn log_response(&self, response: &mut HttpResponse) -> crate::HttpResult<()> {
101 if !self.is_trace_enabled() {
102 return Ok(());
103 }
104
105 tracing::trace!("<-- {} {}", response.status().as_u16(), response.url());
106
107 if self.options.log_response_header {
108 for (name, value) in response.headers() {
109 let value = value.to_str().unwrap_or("<non-utf8>");
110 let masked = self.mask_header_value(name.as_str(), value);
111 tracing::trace!("{}: {}", name.as_str(), masked);
112 }
113 }
114
115 if self.options.log_response_body {
116 if let Some(body) = response.buffered_body_for_logging() {
117 tracing::trace!("Response body: {}", self.render_body(body));
118 } else if response.can_buffer_body_for_logging(self.options.body_size_limit) {
119 let body = response.bytes().await?;
120 tracing::trace!("Response body: {}", self.render_body(&body));
121 } else {
122 tracing::trace!("Response body: <skipped: streaming or unknown-size body>");
123 }
124 }
125 Ok(())
126 }
127
128 pub fn log_stream_response_headers(&self, response_meta: &HttpResponseMeta) {
136 if !self.is_trace_enabled() {
137 return;
138 }
139
140 tracing::trace!(
141 "<-- {} {} (stream)",
142 response_meta.status.as_u16(),
143 &response_meta.url
144 );
145
146 if self.options.log_response_header {
147 for (name, value) in &response_meta.headers {
148 let value = value.to_str().unwrap_or("<non-utf8>");
149 let masked = self.mask_header_value(name.as_str(), value);
150 tracing::trace!("{}: {}", name.as_str(), masked);
151 }
152 }
153 }
154
155 pub fn is_trace_enabled(&self) -> bool {
160 self.options.enabled && tracing::enabled!(tracing::Level::TRACE)
161 }
162
163 fn request_log_url(request: &HttpRequest) -> String {
172 request
173 .resolved_url_with_query()
174 .map(|url| url.to_string())
175 .unwrap_or_else(|_| request.path().to_string())
176 }
177
178 fn mask_header_value(&self, name: &str, value: &str) -> String {
187 if value.is_empty() {
188 return String::new();
189 }
190 if !self.sensitive_headers.contains(name) {
191 return value.to_string();
192 }
193
194 let chars: Vec<char> = value.chars().collect();
195 if chars.len() <= SENSITIVE_HEADER_MASK_SHORT_LEN {
196 SENSITIVE_HEADER_MASK_PLACEHOLDER.to_string()
197 } else {
198 let edge = SENSITIVE_HEADER_MASK_EDGE_CHARS;
199 let prefix: String = chars[..edge].iter().collect();
200 let suffix: String = chars[chars.len() - edge..].iter().collect();
201 format!("{prefix}{SENSITIVE_HEADER_MASK_PLACEHOLDER}{suffix}")
202 }
203 }
204
205 fn render_body(&self, body: &Bytes) -> String {
213 if body.is_empty() {
214 return "<empty>".to_string();
215 }
216
217 let max_bytes = self.options.body_size_limit;
218 let limit = body.len().min(max_bytes);
219 let prefix = &body[..limit];
220 let suffix = if body.len() > max_bytes {
221 format!("...<truncated {} bytes>", body.len() - max_bytes)
222 } else {
223 String::new()
224 };
225
226 match std::str::from_utf8(prefix) {
227 Ok(text) => format!("{}{}", text, suffix),
228 Err(_) => format!("<binary {} bytes>{}", body.len(), suffix),
229 }
230 }
231
232 fn clone_request_body_for_log(body: &HttpRequestBody) -> Option<Bytes> {
240 match body {
241 HttpRequestBody::Bytes(bytes)
242 | HttpRequestBody::Json(bytes)
243 | HttpRequestBody::Form(bytes)
244 | HttpRequestBody::Multipart(bytes)
245 | HttpRequestBody::Ndjson(bytes) => Some(bytes.clone()),
246 HttpRequestBody::Text(text) => Some(Bytes::from(text.clone())),
247 HttpRequestBody::Stream(_) => None,
248 HttpRequestBody::Empty => None,
249 }
250 }
251}