zebrad/components/tracing/
component.rs
1use std::{
4 fs::{self, File},
5 io::Write,
6};
7
8use abscissa_core::{Component, FrameworkError, Shutdown};
9
10use tokio::sync::watch;
11use tracing::{field::Visit, Level};
12use tracing_appender::non_blocking::{NonBlocking, NonBlockingBuilder, WorkerGuard};
13use tracing_error::ErrorLayer;
14use tracing_subscriber::{
15 fmt::{format, Formatter},
16 layer::SubscriberExt,
17 reload::Handle,
18 util::SubscriberInitExt,
19 EnvFilter, Layer,
20};
21use zebra_chain::parameters::Network;
22
23use crate::{application::build_version, components::tracing::Config};
24
25#[cfg(feature = "flamegraph")]
26use super::flame;
27
28static ZEBRA_ART: [u8; include_bytes!("zebra.utf8").len()] = *include_bytes!("zebra.utf8");
48
49pub type BoxWrite = Box<dyn Write + Send + Sync + 'static>;
51
52pub struct Tracing {
54 filter_handle: Option<
58 Handle<
59 EnvFilter,
60 Formatter<format::DefaultFields, format::Format<format::Full>, NonBlocking>,
61 >,
62 >,
63
64 initial_filter: String,
66
67 #[cfg(feature = "flamegraph")]
69 flamegrapher: Option<flame::Grapher>,
70
71 _guard: Option<WorkerGuard>,
76}
77
78impl Tracing {
79 #[allow(clippy::print_stdout, clippy::print_stderr, clippy::unwrap_in_result)]
86 pub fn new(
87 network: &Network,
88 config: Config,
89 uses_intro: bool,
90 ) -> Result<Self, FrameworkError> {
91 let use_color = config.use_color_stdout();
94 let use_color_stderr = config.use_color_stderr();
95
96 let filter = config.filter.clone().unwrap_or_default();
97 let flame_root = &config.flamegraph;
98
99 if uses_intro {
101 if use_color_stderr {
105 eprint!("\x1B[2J");
107 eprintln!(
108 "{}",
109 std::str::from_utf8(&ZEBRA_ART)
110 .expect("should always work on a UTF-8 encoded constant")
111 );
112 }
113
114 eprintln!(
115 "Thank you for running a {} zebrad {} node!",
116 network.lowercase_name(),
117 build_version()
118 );
119 eprintln!(
120 "You're helping to strengthen the network and contributing to a social good :)"
121 );
122 }
123
124 let writer = if let Some(log_file) = config.log_file.as_ref() {
125 let log_file_dir = log_file.parent();
137 if let Some(log_file_dir) = log_file_dir {
138 if !log_file_dir.exists() {
139 eprintln!("Directory for log file {log_file:?} does not exist, trying to create it...");
140
141 if let Err(create_dir_error) = fs::create_dir_all(log_file_dir) {
142 eprintln!("Failed to create directory for log file: {create_dir_error}");
143 eprintln!("Trying log file anyway...");
144 }
145 }
146 }
147
148 if uses_intro {
149 println!("Sending logs to {log_file:?}...");
151 }
152 let log_file = File::options().append(true).create(true).open(log_file)?;
153 Box::new(log_file) as BoxWrite
154 } else {
155 let stdout = std::io::stdout();
156 Box::new(stdout) as BoxWrite
157 };
158
159 let (non_blocking, worker_guard) = NonBlockingBuilder::default()
163 .buffered_lines_limit(config.buffer_limit.max(100))
164 .finish(writer);
165
166 #[cfg(not(all(feature = "tokio-console", tokio_unstable)))]
171 let (subscriber, filter_handle) = {
172 use tracing_subscriber::FmtSubscriber;
173
174 let logger = FmtSubscriber::builder()
175 .with_ansi(use_color)
176 .with_writer(non_blocking)
177 .with_env_filter(&filter);
178
179 #[cfg(feature = "filter-reload")]
181 let (filter_handle, logger) = {
182 let logger = logger.with_filter_reloading();
183
184 (Some(logger.reload_handle()), logger)
185 };
186
187 #[cfg(not(feature = "filter-reload"))]
188 let filter_handle = None;
189
190 let warn_error_layer = LastWarnErrorLayer {
191 last_warn_error_sender: crate::application::LAST_WARN_ERROR_LOG_SENDER.clone(),
192 };
193 let subscriber = logger
194 .finish()
195 .with(warn_error_layer)
196 .with(ErrorLayer::default());
197
198 (subscriber, filter_handle)
199 };
200
201 #[cfg(all(feature = "tokio-console", tokio_unstable))]
207 let (subscriber, filter_handle) = {
208 use tracing_subscriber::{fmt, Layer};
209
210 let subscriber = tracing_subscriber::registry();
211 let logger = fmt::Layer::new()
218 .with_ansi(use_color)
219 .with_writer(non_blocking)
220 .with_filter(EnvFilter::from(&filter));
221
222 let subscriber = subscriber.with(logger);
223
224 let span_logger = ErrorLayer::default().with_filter(EnvFilter::from(&filter));
225 let subscriber = subscriber.with(span_logger);
226
227 (subscriber, None)
228 };
229
230 #[cfg(feature = "flamegraph")]
234 let (flamelayer, flamegrapher) = if let Some(path) = flame_root {
235 let (flamelayer, flamegrapher) = flame::layer(path);
236
237 (Some(flamelayer), Some(flamegrapher))
238 } else {
239 (None, None)
240 };
241 #[cfg(feature = "flamegraph")]
242 let subscriber = subscriber.with(flamelayer);
243
244 #[cfg(feature = "journald")]
245 let journaldlayer = if config.use_journald {
246 use abscissa_core::FrameworkErrorKind;
247
248 let layer = tracing_journald::layer()
249 .map_err(|e| FrameworkErrorKind::ComponentError.context(e))?;
250
251 #[cfg(all(feature = "tokio-console", tokio_unstable))]
254 let layer = {
255 use tracing_subscriber::Layer;
256 layer.with_filter(EnvFilter::from(&filter))
257 };
258
259 Some(layer)
260 } else {
261 None
262 };
263 #[cfg(feature = "journald")]
264 let subscriber = subscriber.with(journaldlayer);
265
266 #[cfg(feature = "sentry")]
267 let subscriber = subscriber.with(sentry::integrations::tracing::layer());
268
269 #[cfg(all(feature = "tokio-console", tokio_unstable))]
272 let subscriber = subscriber.with(console_subscriber::spawn());
273
274 subscriber.init();
276
277 tracing::info!(
279 ?filter,
280 TRACING_STATIC_MAX_LEVEL = ?tracing::level_filters::STATIC_MAX_LEVEL,
281 LOG_STATIC_MAX_LEVEL = ?log::STATIC_MAX_LEVEL,
282 "started tracing component",
283 );
284
285 if flame_root.is_some() {
286 if cfg!(feature = "flamegraph") {
287 info!(flamegraph = ?flame_root, "installed flamegraph tracing layer");
288 } else {
289 warn!(
290 flamegraph = ?flame_root,
291 "unable to activate configured flamegraph: \
292 enable the 'flamegraph' feature when compiling zebrad",
293 );
294 }
295 }
296
297 if config.use_journald {
298 if cfg!(feature = "journald") {
299 info!("installed journald tracing layer");
300 } else {
301 warn!(
302 "unable to activate configured journald tracing: \
303 enable the 'journald' feature when compiling zebrad",
304 );
305 }
306 }
307
308 #[cfg(feature = "sentry")]
309 info!("installed sentry tracing layer");
310
311 #[cfg(all(feature = "tokio-console", tokio_unstable))]
312 info!(
313 TRACING_STATIC_MAX_LEVEL = ?tracing::level_filters::STATIC_MAX_LEVEL,
314 LOG_STATIC_MAX_LEVEL = ?log::STATIC_MAX_LEVEL,
315 "installed tokio-console tracing layer",
316 );
317
318 #[cfg(feature = "progress-bar")]
322 if let Some(progress_bar_config) = config.progress_bar.as_ref() {
323 use howudoin::consumers::TermLine;
324 use std::time::Duration;
325
326 const PROGRESS_BAR_DEBOUNCE: Duration = Duration::from_secs(2);
328
329 let terminal_consumer = TermLine::with_debounce(PROGRESS_BAR_DEBOUNCE);
330 howudoin::init(terminal_consumer);
331
332 info!(?progress_bar_config, "activated progress bars");
333 } else {
334 info!(
335 "set 'tracing.progress_bar =\"summary\"' in zebrad.toml to activate progress bars"
336 );
337 }
338
339 Ok(Self {
340 filter_handle,
341 initial_filter: filter,
342 #[cfg(feature = "flamegraph")]
343 flamegrapher,
344 _guard: Some(worker_guard),
345 })
346 }
347
348 pub fn shutdown(&mut self) {
351 self.filter_handle.take();
352
353 #[cfg(feature = "flamegraph")]
354 self.flamegrapher.take();
355
356 self._guard.take();
357 }
358
359 pub fn filter(&self) -> String {
361 if let Some(filter_handle) = self.filter_handle.as_ref() {
362 filter_handle
363 .with_current(|filter| filter.to_string())
364 .expect("the subscriber is not dropped before the component is")
365 } else {
366 self.initial_filter.clone()
367 }
368 }
369
370 pub fn reload_filter(&self, filter: impl Into<EnvFilter>) {
374 let filter = filter.into();
375
376 if let Some(filter_handle) = self.filter_handle.as_ref() {
377 tracing::info!(
378 ?filter,
379 TRACING_STATIC_MAX_LEVEL = ?tracing::level_filters::STATIC_MAX_LEVEL,
380 LOG_STATIC_MAX_LEVEL = ?log::STATIC_MAX_LEVEL,
381 "reloading tracing filter",
382 );
383
384 filter_handle
385 .reload(filter)
386 .expect("the subscriber is not dropped before the component is");
387 } else {
388 tracing::warn!(
389 ?filter,
390 TRACING_STATIC_MAX_LEVEL = ?tracing::level_filters::STATIC_MAX_LEVEL,
391 LOG_STATIC_MAX_LEVEL = ?log::STATIC_MAX_LEVEL,
392 "attempted to reload tracing filter, but filter reloading is disabled",
393 );
394 }
395 }
396}
397
398impl std::fmt::Debug for Tracing {
399 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400 f.debug_struct("Tracing").finish()
401 }
402}
403
404impl<A: abscissa_core::Application> Component<A> for Tracing {
405 fn id(&self) -> abscissa_core::component::Id {
406 abscissa_core::component::Id::new("zebrad::components::tracing::component::Tracing")
407 }
408
409 fn version(&self) -> abscissa_core::Version {
410 build_version()
411 }
412
413 fn before_shutdown(&self, _kind: Shutdown) -> Result<(), FrameworkError> {
414 #[cfg(feature = "flamegraph")]
415 if let Some(ref grapher) = self.flamegrapher {
416 use abscissa_core::FrameworkErrorKind;
417
418 info!("writing flamegraph");
419
420 grapher
421 .write_flamegraph()
422 .map_err(|e| FrameworkErrorKind::ComponentError.context(e))?
423 }
424
425 #[cfg(feature = "progress-bar")]
426 howudoin::disable();
427
428 Ok(())
429 }
430}
431
432impl Drop for Tracing {
433 fn drop(&mut self) {
434 #[cfg(feature = "progress-bar")]
435 howudoin::disable();
436 }
437}
438
439struct MessageVisitor {
441 message: Option<String>,
442}
443
444impl Visit for MessageVisitor {
445 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
446 if field.name() == "message" {
447 self.message = Some(format!("{value:?}"));
448 }
449 }
450}
451
452#[derive(Debug, Clone)]
454struct LastWarnErrorLayer {
455 last_warn_error_sender: watch::Sender<Option<(String, Level, chrono::DateTime<chrono::Utc>)>>,
456}
457
458impl<S> Layer<S> for LastWarnErrorLayer
459where
460 S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
461{
462 fn on_event(
463 &self,
464 event: &tracing::Event<'_>,
465 _ctx: tracing_subscriber::layer::Context<'_, S>,
466 ) {
467 let level = *event.metadata().level();
468 let timestamp = chrono::Utc::now();
469
470 if level == Level::WARN || level == Level::ERROR {
471 let mut visitor = MessageVisitor { message: None };
472 event.record(&mut visitor);
473
474 if let Some(message) = visitor.message {
475 let _ = self
476 .last_warn_error_sender
477 .send(Some((message, level, timestamp)));
478 }
479 }
480 }
481}