1#![warn(missing_docs)]
12
13mod directives;
14mod event_format;
15mod fast_local_time;
16mod layers;
17mod stderr_writer;
18
19pub(crate) type DefaultLogger = stderr_writer::MakeStderrWriter;
20
21pub use directives::*;
22pub use subsoil_macros::*;
23
24use ::tracing::Subscriber;
25use is_terminal::IsTerminal;
26use std::io;
27use tracing_subscriber::{
28 filter::LevelFilter,
29 fmt::{
30 format, FormatEvent, FormatFields, Formatter, Layer as FmtLayer, MakeWriter,
31 SubscriberBuilder,
32 },
33 layer::{self, SubscriberExt},
34 registry::LookupSpan,
35 EnvFilter, FmtSubscriber, Layer, Registry,
36};
37
38pub use event_format::*;
39pub use fast_local_time::FastLocalTime;
40pub use layers::*;
41
42use stderr_writer::MakeStderrWriter;
43
44pub type Result<T> = std::result::Result<T, Error>;
46
47#[derive(Debug, thiserror::Error)]
49#[allow(missing_docs)]
50#[non_exhaustive]
51#[error(transparent)]
52pub enum Error {
53 IoError(#[from] io::Error),
54 SetGlobalDefaultError(#[from] ::tracing::subscriber::SetGlobalDefaultError),
55 DirectiveParseError(#[from] tracing_subscriber::filter::ParseError),
56 SetLoggerError(#[from] tracing_log::log_tracer::SetLoggerError),
57}
58
59macro_rules! enable_log_reloading {
60 ($builder:expr) => {{
61 let builder = $builder.with_filter_reloading();
62 let handle = builder.reload_handle();
63 set_reload_handle(handle);
64 builder
65 }};
66}
67
68fn to_log_level_filter(level_filter: Option<LevelFilter>) -> log::LevelFilter {
72 match level_filter {
73 Some(LevelFilter::INFO) | None => log::LevelFilter::Info,
74 Some(LevelFilter::TRACE) => log::LevelFilter::Trace,
75 Some(LevelFilter::WARN) => log::LevelFilter::Warn,
76 Some(LevelFilter::ERROR) => log::LevelFilter::Error,
77 Some(LevelFilter::DEBUG) => log::LevelFilter::Debug,
78 Some(LevelFilter::OFF) => log::LevelFilter::Off,
79 }
80}
81
82fn prepare_subscriber<N, E, F, W>(
84 directives: &str,
85 profiling_targets: Option<&str>,
86 force_colors: Option<bool>,
87 detailed_output: bool,
88 builder_hook: impl Fn(
89 SubscriberBuilder<format::DefaultFields, EventFormat, EnvFilter, DefaultLogger>,
90 ) -> SubscriberBuilder<N, E, F, W>,
91) -> Result<impl Subscriber + for<'a> LookupSpan<'a>>
92where
93 N: for<'writer> FormatFields<'writer> + 'static,
94 E: FormatEvent<Registry, N> + 'static,
95 W: for<'writer> MakeWriter<'writer> + 'static,
96 F: layer::Layer<Formatter<N, E, W>> + Send + Sync + 'static,
97 FmtLayer<Registry, N, E, W>: layer::Layer<Registry> + Send + Sync + 'static,
98{
99 fn parse_user_directives(mut env_filter: EnvFilter, dirs: &str) -> Result<EnvFilter> {
101 for dir in dirs.split(',') {
102 env_filter = env_filter.add_directive(parse_default_directive(dir)?);
103 }
104 Ok(env_filter)
105 }
106
107 let mut env_filter = EnvFilter::default()
110 .add_directive(parse_default_directive("info").expect("provided directive is valid"))
112 .add_directive(parse_default_directive("ws=off").expect("provided directive is valid"))
114 .add_directive(parse_default_directive("yamux=off").expect("provided directive is valid"))
115 .add_directive(
116 parse_default_directive("regalloc=off").expect("provided directive is valid"),
117 )
118 .add_directive(
119 parse_default_directive("cranelift_codegen=off").expect("provided directive is valid"),
120 )
121 .add_directive(
123 parse_default_directive("cranelift_wasm=warn").expect("provided directive is valid"),
124 )
125 .add_directive(parse_default_directive("hyper=warn").expect("provided directive is valid"))
126 .add_directive(
127 parse_default_directive("trust_dns_proto=off").expect("provided directive is valid"),
128 )
129 .add_directive(
130 parse_default_directive("hickory_proto=off").expect("provided directive is valid"),
131 )
132 .add_directive(
133 parse_default_directive("libp2p_mdns::behaviour::iface=off")
134 .expect("provided directive is valid"),
135 )
136 .add_directive(
138 parse_default_directive("rustls::common_state=off")
139 .expect("provided directive is valid"),
140 )
141 .add_directive(
142 parse_default_directive("rustls::conn=off").expect("provided directive is valid"),
143 );
144
145 if let Ok(lvl) = std::env::var("RUST_LOG") {
146 if lvl != "" {
147 env_filter = parse_user_directives(env_filter, &lvl)?;
148 }
149 }
150
151 if directives != "" {
152 env_filter = parse_user_directives(env_filter, directives)?;
153 }
154
155 if let Some(profiling_targets) = profiling_targets {
156 env_filter = parse_user_directives(env_filter, profiling_targets)?;
157 env_filter = env_filter.add_directive(
158 parse_default_directive("sc_tracing=trace").expect("provided directive is valid"),
159 );
160 }
161
162 let max_level_hint = Layer::<FmtSubscriber>::max_level_hint(&env_filter);
163 let max_level = to_log_level_filter(max_level_hint);
164
165 tracing_log::LogTracer::builder()
166 .with_interest_cache(tracing_log::InterestCacheConfig::default())
167 .with_max_level(max_level)
168 .init()?;
169
170 let detailed_output = match max_level_hint {
172 Some(level) if level <= tracing_subscriber::filter::LevelFilter::INFO => false,
173 _ => true,
174 } || detailed_output;
175
176 let enable_color = force_colors.unwrap_or_else(|| io::stderr().is_terminal());
177 let timer = fast_local_time::FastLocalTime { with_fractional: detailed_output };
178
179 console::set_colors_enabled(enable_color);
181 console::set_colors_enabled_stderr(enable_color);
182
183 let event_format = EventFormat {
184 timer,
185 display_target: detailed_output,
186 display_level: detailed_output,
187 display_thread_name: detailed_output,
188 dup_to_stdout: !io::stderr().is_terminal() && io::stdout().is_terminal(),
189 };
190 let builder = FmtSubscriber::builder().with_env_filter(env_filter);
191
192 let builder = builder.with_span_events(format::FmtSpan::NONE);
193
194 let builder = builder.with_writer(MakeStderrWriter::default());
195
196 let builder = builder.event_format(event_format);
197
198 let builder = builder_hook(builder);
199
200 let subscriber = builder.finish().with(PrefixLayer);
201
202 Ok(subscriber)
203}
204
205pub struct LoggerBuilder {
207 directives: String,
208 profiling: Option<(super::TracingReceiver, String)>,
209 custom_profiler: Option<Box<dyn super::TraceHandler>>,
210 log_reloading: bool,
211 force_colors: Option<bool>,
212 detailed_output: bool,
213}
214
215impl LoggerBuilder {
216 pub fn new<S: Into<String>>(directives: S) -> Self {
218 Self {
219 directives: directives.into(),
220 profiling: None,
221 custom_profiler: None,
222 log_reloading: false,
223 force_colors: None,
224 detailed_output: false,
225 }
226 }
227
228 pub fn with_profiling<S: Into<String>>(
230 &mut self,
231 tracing_receiver: super::TracingReceiver,
232 profiling_targets: S,
233 ) -> &mut Self {
234 self.profiling = Some((tracing_receiver, profiling_targets.into()));
235 self
236 }
237
238 pub fn with_custom_profiling(
240 &mut self,
241 custom_profiler: Box<dyn super::TraceHandler>,
242 ) -> &mut Self {
243 self.custom_profiler = Some(custom_profiler);
244 self
245 }
246
247 pub fn with_log_reloading(&mut self, enabled: bool) -> &mut Self {
249 self.log_reloading = enabled;
250 self
251 }
252
253 pub fn with_detailed_output(&mut self, detailed: bool) -> &mut Self {
260 self.detailed_output = detailed;
261 self
262 }
263
264 pub fn with_colors(&mut self, enable: bool) -> &mut Self {
266 self.force_colors = Some(enable);
267 self
268 }
269
270 pub fn init(self) -> Result<()> {
274 if let Some((tracing_receiver, profiling_targets)) = self.profiling {
275 if self.log_reloading {
276 let subscriber = prepare_subscriber(
277 &self.directives,
278 Some(&profiling_targets),
279 self.force_colors,
280 self.detailed_output,
281 |builder| enable_log_reloading!(builder),
282 )?;
283 let mut profiling =
284 super::ProfilingLayer::new(tracing_receiver, &profiling_targets);
285
286 self.custom_profiler
287 .into_iter()
288 .for_each(|profiler| profiling.add_handler(profiler));
289
290 ::tracing::subscriber::set_global_default(subscriber.with(profiling))?;
291
292 Ok(())
293 } else {
294 let subscriber = prepare_subscriber(
295 &self.directives,
296 Some(&profiling_targets),
297 self.force_colors,
298 self.detailed_output,
299 |builder| builder,
300 )?;
301 let mut profiling =
302 super::ProfilingLayer::new(tracing_receiver, &profiling_targets);
303
304 self.custom_profiler
305 .into_iter()
306 .for_each(|profiler| profiling.add_handler(profiler));
307
308 ::tracing::subscriber::set_global_default(subscriber.with(profiling))?;
309
310 Ok(())
311 }
312 } else if self.log_reloading {
313 let subscriber = prepare_subscriber(
314 &self.directives,
315 None,
316 self.force_colors,
317 self.detailed_output,
318 |builder| enable_log_reloading!(builder),
319 )?;
320
321 ::tracing::subscriber::set_global_default(subscriber)?;
322
323 Ok(())
324 } else {
325 let subscriber = prepare_subscriber(
326 &self.directives,
327 None,
328 self.force_colors,
329 self.detailed_output,
330 |builder| builder,
331 )?;
332
333 ::tracing::subscriber::set_global_default(subscriber)?;
334
335 Ok(())
336 }
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use ::tracing::{metadata::Kind, subscriber::Interest, Callsite, Level, Metadata};
344 use log::info;
345 use std::{
346 collections::BTreeMap,
347 env,
348 process::Command,
349 sync::{
350 atomic::{AtomicBool, AtomicUsize, Ordering},
351 Arc,
352 },
353 };
354
355 const EXPECTED_LOG_MESSAGE: &'static str = "yeah logging works as expected";
356 const EXPECTED_NODE_NAME: &'static str = "THE_NODE";
357
358 fn init_logger(directives: &str) {
359 let _ = LoggerBuilder::new(directives).init().unwrap();
360 }
361
362 fn run_test_in_another_process(
363 test_name: &str,
364 test_body: impl FnOnce(),
365 ) -> Option<std::process::Output> {
366 if env::var("RUN_FORKED_TEST").is_ok() {
367 test_body();
368 None
369 } else {
370 let output = Command::new(env::current_exe().unwrap())
371 .arg("--nocapture")
372 .arg(test_name)
373 .env("RUN_FORKED_TEST", "1")
374 .env("RUST_LOG", "info")
375 .output()
376 .unwrap();
377
378 assert!(output.status.success());
379 Some(output)
380 }
381 }
382
383 #[test]
384 fn test_logger_filters() {
385 run_test_in_another_process("test_logger_filters", || {
386 let test_directives =
387 "grandpa=debug,sync=trace,client=warn,telemetry,something-with-dash=error";
388 init_logger(&test_directives);
389
390 ::tracing::dispatcher::get_default(|dispatcher| {
391 let test_filter = |target, level| {
392 struct DummyCallSite;
393 impl Callsite for DummyCallSite {
394 fn set_interest(&self, _: Interest) {}
395 fn metadata(&self) -> &Metadata<'_> {
396 unreachable!();
397 }
398 }
399
400 let metadata = ::tracing::metadata!(
401 name: "",
402 target: target,
403 level: level,
404 fields: &[],
405 callsite: &DummyCallSite,
406 kind: Kind::SPAN,
407 );
408
409 dispatcher.enabled(&metadata)
410 };
411
412 assert!(test_filter("grandpa", Level::INFO));
413 assert!(test_filter("grandpa", Level::DEBUG));
414 assert!(!test_filter("grandpa", Level::TRACE));
415
416 assert!(test_filter("sync", Level::TRACE));
417 assert!(test_filter("client", Level::WARN));
418
419 assert!(test_filter("telemetry", Level::TRACE));
420 assert!(test_filter("something-with-dash", Level::ERROR));
421 });
422 });
423 }
424
425 #[test]
428 fn dash_in_target_name_works() {
429 let executable = env::current_exe().unwrap();
430 let output = Command::new(executable)
431 .env("ENABLE_LOGGING", "1")
432 .env("RUST_LOG", "info")
433 .args(&["--nocapture", "log_something_with_dash_target_name"])
434 .output()
435 .unwrap();
436
437 let output = String::from_utf8(output.stderr).unwrap();
438 assert!(output.contains(EXPECTED_LOG_MESSAGE));
439 }
440
441 #[test]
446 fn log_something_with_dash_target_name() {
447 if env::var("ENABLE_LOGGING").is_ok() {
448 let test_directives = "test-target=info";
449 let _guard = init_logger(&test_directives);
450
451 log::info!(target: "test-target", "{}", EXPECTED_LOG_MESSAGE);
452 }
453 }
454
455 #[test]
456 fn prefix_in_log_lines() {
457 let re = regex::Regex::new(&format!(
458 r"^\d{{4}}-\d{{2}}-\d{{2}} \d{{2}}:\d{{2}}:\d{{2}} \[{}\] {}$",
459 EXPECTED_NODE_NAME, EXPECTED_LOG_MESSAGE,
460 ))
461 .unwrap();
462 let executable = env::current_exe().unwrap();
463 let output = Command::new(executable)
464 .env("ENABLE_LOGGING", "1")
465 .env("RUST_LOG", "info")
466 .args(&["--nocapture", "prefix_in_log_lines_entrypoint"])
467 .output()
468 .unwrap();
469
470 let output = String::from_utf8(output.stderr).unwrap();
471 assert!(re.is_match(output.trim()), "Expected:\n{}\nGot:\n{}", re, output);
472 }
473
474 #[test]
478 fn prefix_in_log_lines_entrypoint() {
479 if env::var("ENABLE_LOGGING").is_ok() {
480 let _guard = init_logger("");
481 prefix_in_log_lines_process();
482 }
483 }
484
485 #[crate::tracing::logging::prefix_logs_with(EXPECTED_NODE_NAME)]
486 fn prefix_in_log_lines_process() {
487 log::info!("{}", EXPECTED_LOG_MESSAGE);
488 }
489
490 #[test]
494 fn do_not_write_with_colors_on_tty_entrypoint() {
495 if env::var("ENABLE_LOGGING").is_ok() {
496 let _guard = init_logger("");
497 log::info!("{}", console::style(EXPECTED_LOG_MESSAGE).yellow());
498 }
499 }
500
501 #[test]
502 fn do_not_write_with_colors_on_tty() {
503 let re = regex::Regex::new(&format!(
504 r"^\d{{4}}-\d{{2}}-\d{{2}} \d{{2}}:\d{{2}}:\d{{2}} {}$",
505 EXPECTED_LOG_MESSAGE,
506 ))
507 .unwrap();
508 let executable = env::current_exe().unwrap();
509 let output = Command::new(executable)
510 .env("ENABLE_LOGGING", "1")
511 .env("RUST_LOG", "info")
512 .args(&["--nocapture", "do_not_write_with_colors_on_tty_entrypoint"])
513 .output()
514 .unwrap();
515
516 let output = String::from_utf8(output.stderr).unwrap();
517 assert!(re.is_match(output.trim()), "Expected:\n{}\nGot:\n{}", re, output);
518 }
519
520 #[test]
521 fn log_max_level_is_set_properly() {
522 fn run_test(rust_log: Option<String>, tracing_targets: Option<String>) -> String {
523 let executable = env::current_exe().unwrap();
524 let mut command = Command::new(executable);
525
526 command
527 .env("PRINT_MAX_LOG_LEVEL", "1")
528 .args(&["--nocapture", "log_max_level_is_set_properly"]);
529
530 if let Some(rust_log) = rust_log {
531 command.env("RUST_LOG", rust_log);
532 }
533
534 if let Some(tracing_targets) = tracing_targets {
535 command.env("TRACING_TARGETS", tracing_targets);
536 }
537
538 let output = command.output().unwrap();
539
540 dbg!(String::from_utf8(output.stderr)).unwrap()
541 }
542
543 if env::var("PRINT_MAX_LOG_LEVEL").is_ok() {
544 let mut builder = LoggerBuilder::new("");
545
546 if let Ok(targets) = env::var("TRACING_TARGETS") {
547 builder.with_profiling(crate::tracing::TracingReceiver::Log, targets);
548 }
549
550 builder.init().unwrap();
551
552 eprint!("MAX_LOG_LEVEL={:?}", log::max_level());
553 } else {
554 assert_eq!("MAX_LOG_LEVEL=Info", run_test(None, None));
555 assert_eq!("MAX_LOG_LEVEL=Trace", run_test(Some("test=trace".into()), None));
556 assert_eq!("MAX_LOG_LEVEL=Debug", run_test(Some("test=debug".into()), None));
557 assert_eq!("MAX_LOG_LEVEL=Trace", run_test(None, Some("test=info".into())));
558 }
559 }
560
561 fn run_on_many_threads(thread_count: usize, callback: impl Fn(usize) + 'static + Send + Clone) {
564 let started_count = Arc::new(AtomicUsize::new(0));
565 let barrier = Arc::new(AtomicBool::new(false));
566 let threads: Vec<_> = (0..thread_count)
567 .map(|nth_thread| {
568 let started_count = started_count.clone();
569 let barrier = barrier.clone();
570 let callback = callback.clone();
571
572 std::thread::spawn(move || {
573 started_count.fetch_add(1, Ordering::SeqCst);
574 while !barrier.load(Ordering::SeqCst) {
575 std::thread::yield_now();
576 }
577
578 callback(nth_thread);
579 })
580 })
581 .collect();
582
583 while started_count.load(Ordering::SeqCst) != thread_count {
584 std::thread::yield_now();
585 }
586 barrier.store(true, Ordering::SeqCst);
587
588 for thread in threads {
589 if let Err(error) = thread.join() {
590 println!("error: failed to join thread: {:?}", error);
591 unsafe { libc::abort() }
592 }
593 }
594 }
595
596 #[test]
597 fn parallel_logs_from_multiple_threads_are_properly_gathered() {
598 const THREAD_COUNT: usize = 128;
599 const LOGS_PER_THREAD: usize = 1024;
600
601 let output = run_test_in_another_process(
602 "parallel_logs_from_multiple_threads_are_properly_gathered",
603 || {
604 let builder = LoggerBuilder::new("");
605 builder.init().unwrap();
606
607 run_on_many_threads(THREAD_COUNT, |nth_thread| {
608 for _ in 0..LOGS_PER_THREAD {
609 info!("Thread <<{}>>", nth_thread);
610 }
611 });
612 },
613 );
614
615 if let Some(output) = output {
616 let stderr = String::from_utf8(output.stderr).unwrap();
617 let mut count_per_thread = BTreeMap::new();
618 for line in stderr.split("\n") {
619 if let Some(index_s) = line.find("Thread <<") {
620 let index_s = index_s + "Thread <<".len();
621 let index_e = line.find(">>").unwrap();
622 let nth_thread: usize = line[index_s..index_e].parse().unwrap();
623 *count_per_thread.entry(nth_thread).or_insert(0) += 1;
624 }
625 }
626
627 assert_eq!(count_per_thread.len(), THREAD_COUNT);
628 for (_, count) in count_per_thread {
629 assert_eq!(count, LOGS_PER_THREAD);
630 }
631 }
632 }
633
634 #[test]
635 fn huge_single_line_log_is_properly_printed_out() {
636 let mut line = String::new();
637 line.push_str("$$START$$");
638 for n in 0..16 * 1024 * 1024 {
639 let ch = b'a' + (n as u8 % (b'z' - b'a'));
640 line.push(char::from(ch));
641 }
642 line.push_str("$$END$$");
643
644 let output =
645 run_test_in_another_process("huge_single_line_log_is_properly_printed_out", || {
646 let builder = LoggerBuilder::new("");
647 builder.init().unwrap();
648 info!("{}", line);
649 });
650
651 if let Some(output) = output {
652 let stderr = String::from_utf8(output.stderr).unwrap();
653 assert!(stderr.contains(&line));
654 }
655 }
656}