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