1#![cfg_attr(feature = "fatal-warnings", deny(warnings))]
2use dashmap::DashMap;
45use std::fmt;
46use std::fs::File;
47use std::io;
48use std::io::Write;
49use std::net::TcpStream;
50use std::str::FromStr;
51use std::sync::LazyLock;
52
53static DIRTY_FILES: LazyLock<DashMap<&str, File>> = LazyLock::new(DashMap::new);
54
55static DIRTY_TCP: LazyLock<DashMap<(&str, u16), TcpStream>> = LazyLock::new(DashMap::new);
56
57#[macro_export]
75macro_rules! ddbg {
76 ($uri:expr, $f:literal) => {{
77 $crate::dirty_log_message(
78 $uri,
79 ::std::format_args!(::std::concat!("[{}:{}] ", $f), ::std::file!(), ::std::line!()),
80 );
81 }};
82 ($uri:expr, $f:literal, $($arg:tt)*) => {{
83 $crate::dirty_log_message(
84 $uri,
85 ::std::format_args!(::std::concat!("[{}:{}] ", $f), ::std::file!(), ::std::line!(), $($arg)*),
86 );
87 }};
88}
89
90#[inline(always)]
91fn dirty_log_str_writer(writer: &mut impl Write, args: fmt::Arguments<'_>) -> io::Result<()> {
92 writer.write_fmt(args)?;
93 writer.write_all("\n".as_bytes())?;
94
95 writer.flush()
98}
99
100#[inline(always)]
101fn dirty_log_str_file(filepath: &'static str, args: fmt::Arguments<'_>) -> io::Result<()> {
102 let mut entry = DIRTY_FILES.entry(filepath).or_try_insert_with(move || {
103 let file = File::options().create(true).append(true).open(filepath)?;
104 Ok::<_, io::Error>(file)
105 })?;
106
107 let file = entry.value_mut();
110
111 dirty_log_str_writer(file, args)
112}
113
114#[inline(always)]
115fn dirty_log_str_tcp(
116 hostname: &'static str,
117 port: u16,
118 args: fmt::Arguments<'_>,
119) -> io::Result<()> {
120 let mut entry = DIRTY_TCP.entry((hostname, port)).or_try_insert_with(move || {
121 let stream = TcpStream::connect((hostname, port))?;
122 Ok::<_, io::Error>(stream)
123 })?;
124
125 let stream = entry.value_mut();
128
129 dirty_log_str_writer(stream, args)
130}
131
132#[doc(hidden)]
135pub fn dirty_log_message(uri: &'static str, args: fmt::Arguments<'_>) {
136 let result = if let Some(authority) = uri.strip_prefix("tcp://") {
137 let (hostname, port) = authority.rsplit_once(':').expect("invalid tcp uri");
138
139 let hostname =
141 hostname.strip_prefix('[').and_then(|h| h.strip_suffix(']')).unwrap_or(hostname);
142 let port = u16::from_str(port).expect("invalid port number");
143
144 dirty_log_str_tcp(hostname, port, args)
145 } else {
146 let filepath = uri.strip_prefix("file://").unwrap_or(uri);
147
148 dirty_log_str_file(filepath, args)
149 };
150
151 if let Err(e) = result {
152 panic!("failed to log to \"{uri}\": {e}");
153 }
154}
155
156#[cfg(test)]
157mod test {
158 use indoc::indoc;
159 use std::collections::HashSet;
160 use std::io::Read;
161 use std::net::TcpStream;
162 use std::thread::JoinHandle;
163
164 struct TempFilepath {
165 filepath: String,
166 }
167
168 impl TempFilepath {
169 fn new() -> TempFilepath {
170 use rand::Rng;
171 use rand::distr::Alphanumeric;
172
173 let dir = std::env::temp_dir();
174 let filename: String =
175 rand::rng().sample_iter(&Alphanumeric).take(30).map(char::from).collect();
176
177 let filepath = dir.join(format!("dirty_debug_test_{filename}")).display().to_string();
178
179 TempFilepath { filepath }
180 }
181
182 fn read(&self) -> String {
183 std::fs::read_to_string(&self.filepath).unwrap()
184 }
185 }
186
187 impl Drop for TempFilepath {
188 fn drop(&mut self) {
189 let _result = std::fs::remove_file(&self.filepath);
190 }
191 }
192
193 struct Listener {
194 thread_handler: JoinHandle<String>,
195 port: u16,
196 }
197
198 impl Listener {
199 fn new() -> Listener {
200 Listener::new_with_bind("127.0.0.1")
201 }
202
203 fn new_with_bind(bind: &str) -> Listener {
204 use std::net::TcpListener;
205 use std::thread::spawn;
206
207 let listener: TcpListener =
208 TcpListener::bind(format!("{bind}:0")).expect("fail to bind");
209
210 let port: u16 = listener.local_addr().unwrap().port();
211
212 let thread_handler = spawn(move || {
213 let mut content: String = String::with_capacity(1024);
214 let mut stream: TcpStream = listener.incoming().next().unwrap().unwrap();
215
216 while !content.contains("==EOF==") {
217 let mut buffer: [u8; 8] = [0; 8];
218 let read = stream.read(&mut buffer).unwrap();
219 let s = std::str::from_utf8(&buffer[0..read]).unwrap();
220 content.push_str(s);
221 }
222
223 content
224 });
225
226 Listener { thread_handler, port }
227 }
228
229 fn content(self) -> String {
230 self.thread_handler.join().unwrap()
231 }
232 }
233
234 macro_rules! make_static {
237 ($str:expr) => {{
238 static CELL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
239 CELL.set($str.to_owned()).unwrap();
240 CELL.get().unwrap().as_str()
241 }};
242 }
243
244 fn read_log_strip_source_info(log: &str) -> String {
245 let mut stripped_log = String::with_capacity(log.len());
246
247 for line in log.lines() {
248 let stripped = match line.starts_with('[') {
249 true => line.split_once(' ').map_or("", |(_, s)| s),
250 false => line,
251 };
252
253 stripped_log.push_str(stripped);
254 stripped_log.push('\n');
255 }
256
257 stripped_log
258 }
259
260 fn assert_log(log: &str, expected: &str) {
261 let stripped_log = read_log_strip_source_info(log);
262
263 assert_eq!(stripped_log, expected);
264 }
265
266 #[test]
267 fn test_ddbg_file_and_line_number() {
268 let temp_file: TempFilepath = TempFilepath::new();
269 let filepath: &'static str = make_static!(temp_file.filepath);
270
271 ddbg!(filepath, "test");
272 let line = line!() - 1;
273
274 assert_eq!(temp_file.read(), format!("[{}:{line}] test\n", file!()));
275 }
276
277 #[test]
278 fn test_ddbg_simple() {
279 let temp_file: TempFilepath = TempFilepath::new();
280 let filepath: &'static str = make_static!(temp_file.filepath);
281
282 ddbg!(filepath, "numbers={:?}", [1, 2, 3]);
283
284 assert_log(&temp_file.read(), "numbers=[1, 2, 3]\n");
285 }
286
287 #[test]
288 fn test_ddbg_multiple_syntaxes() {
289 let temp_file: TempFilepath = TempFilepath::new();
290 let filepath: &'static str = make_static!(temp_file.filepath);
291
292 ddbg!(filepath, "nothing to format");
293 ddbg!(filepath, "another nothing to format",);
294 ddbg!(filepath, "");
295 ddbg!(filepath, "a {} b {}", 23, "foo");
296 ddbg!(filepath, "a {} b {}", 32, "bar",);
297
298 let expected = indoc! { r#"
299 nothing to format
300 another nothing to format
301
302 a 23 b foo
303 a 32 b bar
304 "#
305 };
306
307 assert_log(&temp_file.read(), expected);
308 }
309
310 #[test]
311 fn test_ddbg_file_append() {
312 let temp_file: TempFilepath = TempFilepath::new();
313 let filepath: &'static str = make_static!(temp_file.filepath);
314
315 std::fs::write(filepath, "[file.rs:23] first\n").unwrap();
316
317 ddbg!(filepath, "second");
318
319 let expected = indoc! { r#"
320 first
321 second
322 "#
323 };
324
325 assert_log(&temp_file.read(), expected);
326 }
327
328 #[test]
329 fn test_ddbg_multiline() {
330 let temp_file: TempFilepath = TempFilepath::new();
331 let filepath: &'static str = make_static!(temp_file.filepath);
332
333 ddbg!(filepath, "This log\nmessage\nspans multiple lines!");
334
335 let expected = indoc! { r#"
336 This log
337 message
338 spans multiple lines!
339 "#
340 };
341
342 assert_log(&temp_file.read(), expected);
343 }
344
345 #[test]
346 fn test_ddbg_uri_scheme_file() {
347 let temp_file: TempFilepath = TempFilepath::new();
348 let filepath: &'static str = make_static!(format!("file://{}", temp_file.filepath));
349
350 ddbg!(filepath, "test!");
351
352 assert_log(&temp_file.read(), "test!\n");
353 }
354
355 #[test]
356 fn test_ddbg_multithread_no_corrupted_lines() {
357 use std::str::FromStr;
358 use std::thread::{JoinHandle, spawn};
359
360 const THREAD_NUM: usize = 20;
361 const ITERATIONS: usize = 1000;
362 const REPETITIONS: usize = 1000;
363
364 let temp_file: TempFilepath = TempFilepath::new();
365 let filepath: &'static str = make_static!(temp_file.filepath);
366 let mut threads: Vec<JoinHandle<()>> = Vec::with_capacity(THREAD_NUM);
367
368 for i in 0..THREAD_NUM {
369 let thread = spawn(move || {
370 for j in 0..ITERATIONS {
371 ddbg!(filepath, "{}", format!("{i}:{j}_").repeat(REPETITIONS));
372 }
373 });
374
375 threads.push(thread);
376 }
377
378 for thread in threads {
379 thread.join().unwrap();
380 }
381
382 let mut lines_added: HashSet<(usize, usize)> =
383 HashSet::with_capacity(THREAD_NUM * ITERATIONS);
384
385 for i in 0..THREAD_NUM {
386 for j in 0..ITERATIONS {
387 lines_added.insert((i, j));
388 }
389 }
390
391 let log = read_log_strip_source_info(&temp_file.read());
392
393 for line in log.lines() {
394 let token = line.split('_').next().unwrap();
395 let mut iter = token.split(':');
396 let i = usize::from_str(iter.next().unwrap()).unwrap();
397 let j = usize::from_str(iter.next().unwrap()).unwrap();
398 let expected = format!("{i}:{j}_").repeat(REPETITIONS);
399
400 assert_eq!(line, expected);
401
402 lines_added.remove(&(i, j));
403 }
404
405 assert!(lines_added.is_empty());
406 }
407
408 #[test]
409 fn test_ddbg_uri_scheme_tcp_hostname() {
410 let tcp_listener: Listener = Listener::new();
411 let uri: &'static str = make_static!(format!("tcp://localhost:{}", tcp_listener.port));
412
413 ddbg!(uri, "test hostname!");
414 ddbg!(uri, "==EOF==");
415
416 assert_log(&tcp_listener.content(), "test hostname!\n==EOF==\n");
417 }
418
419 #[test]
420 fn test_ddbg_uri_scheme_tcp_ipv4() {
421 let tcp_listener: Listener = Listener::new();
422 let uri: &'static str = make_static!(format!("tcp://127.0.0.1:{}", tcp_listener.port));
423
424 ddbg!(uri, "test ipv4!");
425 ddbg!(uri, "==EOF==");
426
427 assert_log(&tcp_listener.content(), "test ipv4!\n==EOF==\n");
428 }
429
430 #[test]
431 fn test_ddbg_uri_scheme_tcp_ipv6() {
432 let tcp_listener: Listener = Listener::new_with_bind("::1");
433 let uri: &'static str = make_static!(format!("tcp://[::1]:{}", tcp_listener.port));
434
435 ddbg!(uri, "test ipv6!");
436 ddbg!(uri, "==EOF==");
437
438 assert_log(&tcp_listener.content(), "test ipv6!\n==EOF==\n");
439 }
440}