tc_tracing/logging/
mod.rs

1// This file is part of Tetcore.
2
3// Copyright (C) 2021 Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19//! Tetcore logging library.
20//!
21//! This crate uses tokio's [tracing](https://github.com/tokio-rs/tracing/) library for logging.
22
23#![warn(missing_docs)]
24
25mod directives;
26mod event_format;
27mod layers;
28
29pub use directives::*;
30pub use tc_tracing_proc_macro::*;
31
32use tc_telemetry::{ExtTransport, TelemetryWorker};
33use std::io;
34use tracing::Subscriber;
35use tracing_subscriber::{
36	fmt::time::ChronoLocal,
37	fmt::{
38		format, FormatEvent, FormatFields, Formatter, Layer as FmtLayer, MakeWriter,
39		SubscriberBuilder,
40	},
41	layer::{self, SubscriberExt},
42	registry::LookupSpan,
43	EnvFilter, FmtSubscriber, Layer, Registry,
44};
45
46pub use event_format::*;
47pub use layers::*;
48
49/// Logging Result typedef.
50pub type Result<T> = std::result::Result<T, Error>;
51
52/// Logging errors.
53#[derive(Debug, thiserror::Error)]
54#[allow(missing_docs)]
55#[non_exhaustive]
56#[error(transparent)]
57pub enum Error {
58	IoError(#[from] io::Error),
59	SetGlobalDefaultError(#[from] tracing::subscriber::SetGlobalDefaultError),
60	DirectiveParseError(#[from] tracing_subscriber::filter::ParseError),
61	SetLoggerError(#[from] tracing_log::log_tracer::SetLoggerError),
62}
63
64macro_rules! enable_log_reloading {
65	($builder:expr) => {{
66		let builder = $builder.with_filter_reloading();
67		let handle = builder.reload_handle();
68		set_reload_handle(handle);
69		builder
70	}};
71}
72
73/// Common implementation to get the subscriber.
74fn prepare_subscriber<N, E, F, W>(
75	directives: &str,
76	max_level: Option<log::LevelFilter>,
77	force_colors: Option<bool>,
78	telemetry_buffer_size: Option<usize>,
79	telemetry_external_transport: Option<ExtTransport>,
80	builder_hook: impl Fn(
81		SubscriberBuilder<
82			format::DefaultFields,
83			EventFormat<ChronoLocal>,
84			EnvFilter,
85			fn() -> std::io::Stderr,
86		>,
87	) -> SubscriberBuilder<N, E, F, W>,
88) -> Result<(impl Subscriber + for<'a> LookupSpan<'a>, TelemetryWorker)>
89where
90	N: for<'writer> FormatFields<'writer> + 'static,
91	E: FormatEvent<Registry, N> + 'static,
92	W: MakeWriter + 'static,
93	F: layer::Layer<Formatter<N, E, W>> + Send + Sync + 'static,
94	FmtLayer<Registry, N, E, W>: layer::Layer<Registry> + Send + Sync + 'static,
95{
96	// Accept all valid directives and print invalid ones
97	fn parse_user_directives(mut env_filter: EnvFilter, dirs: &str) -> Result<EnvFilter> {
98		for dir in dirs.split(',') {
99			env_filter = env_filter.add_directive(parse_default_directive(&dir)?);
100		}
101		Ok(env_filter)
102	}
103
104	// Initialize filter - ensure to use `parse_default_directive` for any defaults to persist
105	// after log filter reloading by RPC
106	let mut env_filter = EnvFilter::default()
107		// Enable info
108		.add_directive(parse_default_directive("info").expect("provided directive is valid"))
109		// Disable info logging by default for some modules.
110		.add_directive(parse_default_directive("ws=off").expect("provided directive is valid"))
111		.add_directive(parse_default_directive("remux=off").expect("provided directive is valid"))
112		.add_directive(
113			parse_default_directive("cranelift_codegen=off").expect("provided directive is valid"),
114		)
115		// Set warn logging by default for some modules.
116		.add_directive(
117			parse_default_directive("cranelift_wasm=warn").expect("provided directive is valid"),
118		)
119		.add_directive(parse_default_directive("hyper=warn").expect("provided directive is valid"));
120
121	if let Ok(lvl) = std::env::var("RUST_LOG") {
122		if lvl != "" {
123			env_filter = parse_user_directives(env_filter, &lvl)?;
124		}
125	}
126
127	if directives != "" {
128		// We're not sure if log or tracing is available at this moment, so silently ignore the
129		// parse error.
130		env_filter = parse_user_directives(env_filter, directives)?;
131	}
132
133	let max_level_hint = Layer::<FmtSubscriber>::max_level_hint(&env_filter);
134
135	let max_level = max_level.unwrap_or_else(|| match max_level_hint {
136		Some(tracing_subscriber::filter::LevelFilter::INFO) | None => log::LevelFilter::Info,
137		Some(tracing_subscriber::filter::LevelFilter::TRACE) => log::LevelFilter::Trace,
138		Some(tracing_subscriber::filter::LevelFilter::WARN) => log::LevelFilter::Warn,
139		Some(tracing_subscriber::filter::LevelFilter::ERROR) => log::LevelFilter::Error,
140		Some(tracing_subscriber::filter::LevelFilter::DEBUG) => log::LevelFilter::Debug,
141		Some(tracing_subscriber::filter::LevelFilter::OFF) => log::LevelFilter::Off,
142	});
143
144	tracing_log::LogTracer::builder()
145		.with_max_level(max_level)
146		.init()?;
147
148	// If we're only logging `INFO` entries then we'll use a simplified logging format.
149	let simple = match max_level_hint {
150		Some(level) if level <= tracing_subscriber::filter::LevelFilter::INFO => true,
151		_ => false,
152	};
153
154	let enable_color = force_colors.unwrap_or_else(|| atty::is(atty::Stream::Stderr));
155	let timer = ChronoLocal::with_format(if simple {
156		"%Y-%m-%d %H:%M:%S".to_string()
157	} else {
158		"%Y-%m-%d %H:%M:%S%.3f".to_string()
159	});
160
161	let (telemetry_layer, telemetry_worker) =
162		tc_telemetry::TelemetryLayer::new(telemetry_buffer_size, telemetry_external_transport)?;
163	let event_format = EventFormat {
164		timer,
165		display_target: !simple,
166		display_level: !simple,
167		display_thread_name: !simple,
168		enable_color,
169	};
170	let builder = FmtSubscriber::builder().with_env_filter(env_filter);
171
172	#[cfg(not(target_os = "unknown"))]
173	let builder = builder.with_writer(std::io::stderr as _);
174
175	#[cfg(target_os = "unknown")]
176	let builder = builder.with_writer(std::io::sink);
177
178	#[cfg(not(target_os = "unknown"))]
179	let builder = builder.event_format(event_format);
180
181	#[cfg(not(target_os = "unknown"))]
182	let builder = builder_hook(builder);
183
184	let subscriber = builder.finish().with(PrefixLayer).with(telemetry_layer);
185
186	#[cfg(target_os = "unknown")]
187	let subscriber = subscriber.with(ConsoleLogLayer::new(event_format));
188
189	Ok((subscriber, telemetry_worker))
190}
191
192/// A builder that is used to initialize the global logger.
193pub struct LoggerBuilder {
194	directives: String,
195	profiling: Option<(crate::TracingReceiver, String)>,
196	telemetry_buffer_size: Option<usize>,
197	telemetry_external_transport: Option<ExtTransport>,
198	log_reloading: bool,
199	force_colors: Option<bool>,
200}
201
202impl LoggerBuilder {
203	/// Create a new [`LoggerBuilder`] which can be used to initialize the global logger.
204	pub fn new<S: Into<String>>(directives: S) -> Self {
205		Self {
206			directives: directives.into(),
207			profiling: None,
208			telemetry_buffer_size: None,
209			telemetry_external_transport: None,
210			log_reloading: true,
211			force_colors: None,
212		}
213	}
214
215	/// Set up the profiling.
216	pub fn with_profiling<S: Into<String>>(
217		&mut self,
218		tracing_receiver: crate::TracingReceiver,
219		profiling_targets: S,
220	) -> &mut Self {
221		self.profiling = Some((tracing_receiver, profiling_targets.into()));
222		self
223	}
224
225	/// Wether or not to disable log reloading.
226	pub fn with_log_reloading(&mut self, enabled: bool) -> &mut Self {
227		self.log_reloading = enabled;
228		self
229	}
230
231	/// Set a custom buffer size for the telemetry.
232	pub fn with_telemetry_buffer_size(&mut self, buffer_size: usize) -> &mut Self {
233		self.telemetry_buffer_size = Some(buffer_size);
234		self
235	}
236
237	/// Set a custom network transport (used for the telemetry).
238	pub fn with_transport(&mut self, transport: ExtTransport) -> &mut Self {
239		self.telemetry_external_transport = Some(transport);
240		self
241	}
242
243	/// Force enable/disable colors.
244	pub fn with_colors(&mut self, enable: bool) -> &mut Self {
245		self.force_colors = Some(enable);
246		self
247	}
248
249	/// Initialize the global logger
250	///
251	/// This sets various global logging and tracing instances and thus may only be called once.
252	pub fn init(self) -> Result<TelemetryWorker> {
253		if let Some((tracing_receiver, profiling_targets)) = self.profiling {
254			// If profiling is activated, we require `trace` logging.
255			let max_level = Some(log::LevelFilter::Trace);
256
257			if self.log_reloading {
258				let (subscriber, telemetry_worker) = prepare_subscriber(
259					&format!("{},{},tc_tracing=trace", self.directives, profiling_targets),
260					max_level,
261					self.force_colors,
262					self.telemetry_buffer_size,
263					self.telemetry_external_transport,
264					|builder| enable_log_reloading!(builder),
265				)?;
266				let profiling = crate::ProfilingLayer::new(tracing_receiver, &profiling_targets);
267
268				tracing::subscriber::set_global_default(subscriber.with(profiling))?;
269
270				Ok(telemetry_worker)
271			} else {
272				let (subscriber, telemetry_worker) = prepare_subscriber(
273					&format!("{},{},tc_tracing=trace", self.directives, profiling_targets),
274					max_level,
275					self.force_colors,
276					self.telemetry_buffer_size,
277					self.telemetry_external_transport,
278					|builder| builder,
279				)?;
280				let profiling = crate::ProfilingLayer::new(tracing_receiver, &profiling_targets);
281
282				tracing::subscriber::set_global_default(subscriber.with(profiling))?;
283
284				Ok(telemetry_worker)
285			}
286		} else {
287			if self.log_reloading {
288				let (subscriber, telemetry_worker) = prepare_subscriber(
289					&self.directives,
290					None,
291					self.force_colors,
292					self.telemetry_buffer_size,
293					self.telemetry_external_transport,
294					|builder| enable_log_reloading!(builder),
295				)?;
296
297				tracing::subscriber::set_global_default(subscriber)?;
298
299				Ok(telemetry_worker)
300			} else {
301				let (subscriber, telemetry_worker) = prepare_subscriber(
302					&self.directives,
303					None,
304					self.force_colors,
305					self.telemetry_buffer_size,
306					self.telemetry_external_transport,
307					|builder| builder,
308				)?;
309
310				tracing::subscriber::set_global_default(subscriber)?;
311
312				Ok(telemetry_worker)
313			}
314		}
315	}
316}
317
318#[cfg(test)]
319mod tests {
320	use super::*;
321	use crate as tc_tracing;
322	use std::{env, process::Command};
323	use tracing::{metadata::Kind, subscriber::Interest, Callsite, Level, Metadata};
324
325	const EXPECTED_LOG_MESSAGE: &'static str = "yeah logging works as expected";
326	const EXPECTED_NODE_NAME: &'static str = "THE_NODE";
327
328	fn init_logger(directives: &str) {
329		let _ = LoggerBuilder::new(directives).init().unwrap();
330	}
331
332	fn run_in_process(test_name: &str) {
333		if env::var("RUN_IN_PROCESS").is_err() {
334			let status = Command::new(env::current_exe().unwrap())
335				.arg(test_name)
336				.env("RUN_IN_PROCESS", "true")
337				.status()
338				.unwrap();
339			assert!(status.success(), "process did not ended successfully");
340			std::process::exit(0);
341		}
342	}
343
344	#[test]
345	fn test_logger_filters() {
346		run_in_process("test_logger_filters");
347
348		let test_directives = "afg=debug,sync=trace,client=warn,telemetry,something-with-dash=error";
349		init_logger(&test_directives);
350
351		tracing::dispatcher::get_default(|dispatcher| {
352			let test_filter = |target, level| {
353				struct DummyCallSite;
354				impl Callsite for DummyCallSite {
355					fn set_interest(&self, _: Interest) {}
356					fn metadata(&self) -> &Metadata<'_> {
357						unreachable!();
358					}
359				}
360
361				let metadata = tracing::metadata!(
362					name: "",
363					target: target,
364					level: level,
365					fields: &[],
366					callsite: &DummyCallSite,
367					kind: Kind::SPAN,
368				);
369
370				dispatcher.enabled(&metadata)
371			};
372
373			assert!(test_filter("afg", Level::INFO));
374			assert!(test_filter("afg", Level::DEBUG));
375			assert!(!test_filter("afg", Level::TRACE));
376
377			assert!(test_filter("sync", Level::TRACE));
378			assert!(test_filter("client", Level::WARN));
379
380			assert!(test_filter("telemetry", Level::TRACE));
381			assert!(test_filter("something-with-dash", Level::ERROR));
382		});
383	}
384
385	/// This test ensures that using dash (`-`) in the target name in logs and directives actually
386	/// work.
387	#[test]
388	fn dash_in_target_name_works() {
389		let executable = env::current_exe().unwrap();
390		let output = Command::new(executable)
391			.env("ENABLE_LOGGING", "1")
392			.args(&["--nocapture", "log_something_with_dash_target_name"])
393			.output()
394			.unwrap();
395
396		let output = String::from_utf8(output.stderr).unwrap();
397		assert!(output.contains(EXPECTED_LOG_MESSAGE));
398	}
399
400	/// This is not an actual test, it is used by the `dash_in_target_name_works` test.
401	/// The given test will call the test executable and only execute this one test that
402	/// only prints `EXPECTED_LOG_MESSAGE` through logging while using a target
403	/// name that contains a dash. This ensures that target names with dashes work.
404	#[test]
405	fn log_something_with_dash_target_name() {
406		if env::var("ENABLE_LOGGING").is_ok() {
407			let test_directives = "test-target=info";
408			let _guard = init_logger(&test_directives);
409
410			log::info!(target: "test-target", "{}", EXPECTED_LOG_MESSAGE);
411		}
412	}
413
414	#[test]
415	fn prefix_in_log_lines() {
416		let re = regex::Regex::new(&format!(
417			r"^\d{{4}}-\d{{2}}-\d{{2}} \d{{2}}:\d{{2}}:\d{{2}}  \[{}\] {}$",
418			EXPECTED_NODE_NAME, EXPECTED_LOG_MESSAGE,
419		))
420		.unwrap();
421		let executable = env::current_exe().unwrap();
422		let output = Command::new(executable)
423			.env("ENABLE_LOGGING", "1")
424			.args(&["--nocapture", "prefix_in_log_lines_entrypoint"])
425			.output()
426			.unwrap();
427
428		let output = String::from_utf8(output.stderr).unwrap();
429		assert!(
430			re.is_match(output.trim()),
431			format!("Expected:\n{}\nGot:\n{}", re, output),
432		);
433	}
434
435	/// This is not an actual test, it is used by the `prefix_in_log_lines` test.
436	/// The given test will call the test executable and only execute this one test that
437	/// only prints a log line prefixed by the node name `EXPECTED_NODE_NAME`.
438	#[test]
439	fn prefix_in_log_lines_entrypoint() {
440		if env::var("ENABLE_LOGGING").is_ok() {
441			let _guard = init_logger("");
442			prefix_in_log_lines_process();
443		}
444	}
445
446	#[crate::logging::prefix_logs_with(EXPECTED_NODE_NAME)]
447	fn prefix_in_log_lines_process() {
448		log::info!("{}", EXPECTED_LOG_MESSAGE);
449	}
450
451	/// This is not an actual test, it is used by the `do_not_write_with_colors_on_tty` test.
452	/// The given test will call the test executable and only execute this one test that
453	/// only prints a log line with some colors in it.
454	#[test]
455	fn do_not_write_with_colors_on_tty_entrypoint() {
456		if env::var("ENABLE_LOGGING").is_ok() {
457			let _guard = init_logger("");
458			log::info!("{}", ansi_term::Colour::Yellow.paint(EXPECTED_LOG_MESSAGE));
459		}
460	}
461
462	#[test]
463	fn do_not_write_with_colors_on_tty() {
464		let re = regex::Regex::new(&format!(
465			r"^\d{{4}}-\d{{2}}-\d{{2}} \d{{2}}:\d{{2}}:\d{{2}}  {}$",
466			EXPECTED_LOG_MESSAGE,
467		))
468		.unwrap();
469		let executable = env::current_exe().unwrap();
470		let output = Command::new(executable)
471			.env("ENABLE_LOGGING", "1")
472			.args(&["--nocapture", "do_not_write_with_colors_on_tty_entrypoint"])
473			.output()
474			.unwrap();
475
476		let output = String::from_utf8(output.stderr).unwrap();
477		assert!(
478			re.is_match(output.trim()),
479			format!("Expected:\n{}\nGot:\n{}", re, output),
480		);
481	}
482
483	#[test]
484	fn log_max_level_is_set_properly() {
485		fn run_test(rust_log: Option<String>, tracing_targets: Option<String>) -> String {
486			let executable = env::current_exe().unwrap();
487			let mut command = Command::new(executable);
488
489			command
490				.env("PRINT_MAX_LOG_LEVEL", "1")
491				.args(&["--nocapture", "log_max_level_is_set_properly"]);
492
493			if let Some(rust_log) = rust_log {
494				command.env("RUST_LOG", rust_log);
495			}
496
497			if let Some(tracing_targets) = tracing_targets {
498				command.env("TRACING_TARGETS", tracing_targets);
499			}
500
501			let output = command.output().unwrap();
502
503			String::from_utf8(output.stderr).unwrap()
504		}
505
506		if env::var("PRINT_MAX_LOG_LEVEL").is_ok() {
507			init_logger(&env::var("TRACING_TARGETS").unwrap_or_default());
508			eprint!("MAX_LOG_LEVEL={:?}", log::max_level());
509		} else {
510			assert_eq!("MAX_LOG_LEVEL=Info", run_test(None, None));
511			assert_eq!(
512				"MAX_LOG_LEVEL=Trace",
513				run_test(Some("test=trace".into()), None)
514			);
515			assert_eq!(
516				"MAX_LOG_LEVEL=Debug",
517				run_test(Some("test=debug".into()), None)
518			);
519			assert_eq!(
520				"MAX_LOG_LEVEL=Trace",
521				run_test(None, Some("test=info".into()))
522			);
523		}
524	}
525}