tdm_server_rust/middleware/
debug_log.rs1use super::http_log_fmt::HttpLogStyle;
20use crate::profile::{
21 append_jsonl_report, http_request_span, min_show_duration, scope, RequestProfile,
22};
23use axum::{
24 body::Body,
25 http::{header, HeaderMap, Request},
26 middleware::Next,
27 response::Response,
28};
29use bytes::Bytes;
30use http_body_util::BodyExt;
31use std::sync::{Arc, Mutex};
32use std::time::Instant;
33use tracing::Instrument;
34
35const MAX_LOG_BODY: usize = 64 * 1024;
37
38#[tracing::instrument(skip_all, level = "info")]
40pub async fn debug_log_middleware(req: Request<Body>, next: Next) -> Response {
41 HttpLogStyle::enable_ansi_support();
42 let style = HttpLogStyle::detect();
43 let method = req.method().clone();
44 let uri = req.uri().clone();
45 let started = Instant::now();
46
47 let (parts, body) = req.into_parts();
48 let req_headers = parts.headers.clone();
49 let body_bytes = body
50 .collect()
51 .await
52 .map(|b| b.to_bytes())
53 .unwrap_or_default();
54
55 let req = Request::from_parts(parts, Body::from(body_bytes.clone()));
56 let profile = Arc::new(Mutex::new(RequestProfile::new()));
57 let profile_for_print = profile.clone();
58 let span = http_request_span(&method, &uri);
59
60 let response = scope(profile, async { next.run(req).instrument(span).await }).await;
61
62 let (mut resp_parts, resp_body) = response.into_parts();
63 let resp_bytes = resp_body
64 .collect()
65 .await
66 .map(|b| b.to_bytes())
67 .unwrap_or_default();
68 let elapsed = started.elapsed();
69
70 let mut lines = Vec::new();
71 lines.push(style.divider());
72 lines.push(style.summary(
73 &method,
74 &uri,
75 resp_parts.status,
76 elapsed,
77 ));
78 let (_prof_lines, server_timing) = {
79 let guard = profile_for_print.lock().unwrap_or_else(|e| e.into_inner());
80 let min_show = min_show_duration();
81 if let Some(report) = guard.build_report(&method, &uri, resp_parts.status, elapsed, min_show)
82 {
83 append_jsonl_report(&report);
84 }
85 let prof = guard.format_report(&method, &uri, elapsed, min_show);
86 lines.extend(prof.clone());
87 ((), guard.format_server_timing(elapsed, min_show))
88 };
89 if let Some(value) = server_timing.and_then(|v| header::HeaderValue::from_str(&v).ok()) {
90 resp_parts
91 .headers
92 .insert(header::HeaderName::from_static("server-timing"), value);
93 resp_parts.headers.insert(
94 header::HeaderName::from_static("timing-allow-origin"),
95 header::HeaderValue::from_static("*"),
96 );
97 }
98 let quiet = should_quiet_log(uri.path(), &resp_parts.headers);
99 if !body_bytes.is_empty() && !quiet && should_log_body_text(uri.path(), &body_bytes, &req_headers) {
100 lines.push(format!(
101 "{} {}",
102 style.label("req │"),
103 log_bytes(&body_bytes)
104 ));
105 } else if !body_bytes.is_empty() && !quiet {
106 lines.push(format!(
107 "{} {} bytes ({})",
108 style.label("req │"),
109 body_bytes.len(),
110 content_type_label(&req_headers)
111 ));
112 }
113 if !resp_bytes.is_empty() && !quiet && !is_html_response(&resp_parts.headers) {
114 if should_log_body_text(uri.path(), &resp_bytes, &resp_parts.headers) {
115 lines.push(format!(
116 "{} {}",
117 style.label("res │"),
118 log_bytes(&resp_bytes)
119 ));
120 } else {
121 lines.push(format!(
122 "{} {} bytes ({})",
123 style.label("res │"),
124 resp_bytes.len(),
125 content_type_label(&resp_parts.headers)
126 ));
127 }
128 } else if !resp_bytes.is_empty() && !quiet {
129 lines.push(format!(
130 "{} {} bytes",
131 style.label("res │"),
132 resp_bytes.len()
133 ));
134 }
135 lines.push(style.divider());
136 HttpLogStyle::print_lines(&lines);
137
138 crate::telemetry::log_http_summary(
139 method.as_str(),
140 uri.path(),
141 resp_parts.status.as_u16(),
142 elapsed.as_millis(),
143 );
144
145 Response::from_parts(resp_parts, Body::from(resp_bytes))
146}
147
148fn log_bytes(bytes: &Bytes) -> String {
150 if bytes.is_empty() {
151 return String::new();
152 }
153 let raw = if bytes.len() > MAX_LOG_BODY {
154 format!(
155 "{}...(truncated, total {} bytes)",
156 String::from_utf8_lossy(&bytes[..MAX_LOG_BODY]),
157 bytes.len()
158 )
159 } else {
160 String::from_utf8_lossy(bytes).into_owned()
161 };
162 raw.replace('\r', " ").replace('\n', " ")
163}
164
165fn should_quiet_log(path: &str, _headers: &HeaderMap) -> bool {
167 path.starts_with("/dev/")
168 || path.starts_with("/doc/")
169 || path.starts_with("/swagger-ui")
170 || path.contains("openapi")
171}
172
173fn should_log_body_text(path: &str, bytes: &Bytes, headers: &HeaderMap) -> bool {
175 if should_quiet_log(path, headers) {
176 return false;
177 }
178 if bytes.len() > 1024 && (is_json_content(headers) || looks_like_json_bytes(bytes)) {
179 return false;
180 }
181 true
182}
183
184fn is_json_content(headers: &HeaderMap) -> bool {
185 headers
186 .get(header::CONTENT_TYPE)
187 .and_then(|v| v.to_str().ok())
188 .is_some_and(|ct| ct.contains("json"))
189}
190
191fn looks_like_json_bytes(bytes: &Bytes) -> bool {
192 bytes
193 .iter()
194 .find(|b| !b.is_ascii_whitespace())
195 .is_some_and(|b| *b == b'{' || *b == b'[')
196}
197
198fn content_type_label(headers: &HeaderMap) -> &str {
199 headers
200 .get(header::CONTENT_TYPE)
201 .and_then(|v| v.to_str().ok())
202 .and_then(|ct| ct.split(';').next())
203 .unwrap_or("binary")
204}
205
206fn is_html_response(headers: &HeaderMap) -> bool {
208 headers
209 .get(header::CONTENT_TYPE)
210 .and_then(|v| v.to_str().ok())
211 .is_some_and(|ct| ct.contains("text/html"))
212}