qubit_http/
http_logger.rs1use bytes::Bytes;
18use http::{HeaderMap, Method, StatusCode};
19use url::Url;
20
21use crate::constants::{
22 SENSITIVE_HEADER_MASK_EDGE_CHARS, SENSITIVE_HEADER_MASK_PLACEHOLDER,
23 SENSITIVE_HEADER_MASK_SHORT_LEN,
24};
25use crate::{HttpLoggingOptions, SensitiveHeaders};
26
27#[derive(Debug, Clone, Copy)]
29pub struct HttpLogger<'a> {
30 options: &'a HttpLoggingOptions,
31 sensitive_headers: &'a SensitiveHeaders,
32}
33
34impl<'a> HttpLogger<'a> {
35 pub fn new(options: &'a HttpLoggingOptions, sensitive_headers: &'a SensitiveHeaders) -> Self {
44 Self {
45 options,
46 sensitive_headers,
47 }
48 }
49
50 pub fn log_request(
61 &self,
62 method: &Method,
63 url: &Url,
64 headers: &HeaderMap,
65 body: Option<&Bytes>,
66 ) {
67 if !self.is_trace_enabled() {
68 return;
69 }
70
71 tracing::trace!("--> {} {}", method, url);
72
73 if self.options.log_request_header {
74 for (name, value) in headers {
75 let value = value.to_str().unwrap_or("<non-utf8>");
76 let masked = self.mask_header_value(name.as_str(), value);
77 tracing::trace!("{}: {}", name.as_str(), masked);
78 }
79 }
80
81 if self.options.log_request_body {
82 match body {
83 Some(bytes) => tracing::trace!("Request body: {}", self.render_body(bytes)),
84 None => tracing::trace!("Request body: <empty>"),
85 }
86 }
87 }
88
89 pub fn log_response(&self, status: StatusCode, url: &Url, headers: &HeaderMap, body: &Bytes) {
100 if !self.is_trace_enabled() {
101 return;
102 }
103
104 tracing::trace!("<-- {} {}", status.as_u16(), url);
105
106 if self.options.log_response_header {
107 for (name, value) in headers {
108 let value = value.to_str().unwrap_or("<non-utf8>");
109 let masked = self.mask_header_value(name.as_str(), value);
110 tracing::trace!("{}: {}", name.as_str(), masked);
111 }
112 }
113
114 if self.options.log_response_body {
115 tracing::trace!("Response body: {}", self.render_body(body));
116 }
117 }
118
119 pub fn log_stream_response_headers(&self, status: StatusCode, url: &Url, headers: &HeaderMap) {
129 if !self.is_trace_enabled() {
130 return;
131 }
132
133 tracing::trace!("<-- {} {} (stream)", status.as_u16(), url);
134
135 if self.options.log_response_header {
136 for (name, value) in headers {
137 let value = value.to_str().unwrap_or("<non-utf8>");
138 let masked = self.mask_header_value(name.as_str(), value);
139 tracing::trace!("{}: {}", name.as_str(), masked);
140 }
141 }
142 }
143
144 fn is_trace_enabled(&self) -> bool {
149 self.options.enabled && tracing::enabled!(tracing::Level::TRACE)
150 }
151
152 fn mask_header_value(&self, name: &str, value: &str) -> String {
161 if value.is_empty() {
162 return String::new();
163 }
164 if !self.sensitive_headers.contains(name) {
165 return value.to_string();
166 }
167
168 let chars: Vec<char> = value.chars().collect();
169 if chars.len() <= SENSITIVE_HEADER_MASK_SHORT_LEN {
170 SENSITIVE_HEADER_MASK_PLACEHOLDER.to_string()
171 } else {
172 let edge = SENSITIVE_HEADER_MASK_EDGE_CHARS;
173 let prefix: String = chars[..edge].iter().collect();
174 let suffix: String = chars[chars.len() - edge..].iter().collect();
175 format!("{prefix}{SENSITIVE_HEADER_MASK_PLACEHOLDER}{suffix}")
176 }
177 }
178
179 fn render_body(&self, body: &Bytes) -> String {
187 if body.is_empty() {
188 return "<empty>".to_string();
189 }
190
191 let max_bytes = self.options.body_size_limit;
192 let limit = body.len().min(max_bytes);
193 let prefix = &body[..limit];
194 let suffix = if body.len() > max_bytes {
195 format!("...<truncated {} bytes>", body.len() - max_bytes)
196 } else {
197 String::new()
198 };
199
200 match std::str::from_utf8(prefix) {
201 Ok(text) => format!("{}{}", text, suffix),
202 Err(_) => format!("<binary {} bytes>{}", body.len(), suffix),
203 }
204 }
205}