Skip to main content

reth_node_core/args/
log.rs

1//! clap [Args](clap::Args) for logging configuration.
2
3use crate::dirs::{LogsDir, PlatformPath};
4use clap::{ArgAction, Args, ValueEnum};
5use reth_tracing::{
6    tracing_subscriber::filter::Directive, FileInfo, LayerInfo, Layers, LogFormat, RethTracer,
7    Tracer, TracingGuards,
8};
9use std::{fmt, fmt::Display, path::PathBuf, sync::OnceLock};
10use tracing::{level_filters::LevelFilter, Level};
11
12/// Constant to convert megabytes to bytes
13const MB_TO_BYTES: u64 = 1024 * 1024;
14
15const PROFILER_TRACING_FILTER: &str = "debug";
16
17/// Global static log defaults
18static LOG_DEFAULTS: OnceLock<DefaultLogArgs> = OnceLock::new();
19
20/// The log configuration.
21#[derive(Debug, Args)]
22#[command(next_help_heading = "Logging")]
23pub struct LogArgs {
24    /// The format to use for logs written to stdout.
25    #[arg(long = "log.stdout.format", value_name = "FORMAT", global = true, default_value_t = DefaultLogArgs::get_global().log_stdout_format)]
26    pub log_stdout_format: LogFormat,
27
28    /// The filter to use for logs written to stdout.
29    #[arg(long = "log.stdout.filter", value_name = "FILTER", global = true, default_value_t = DefaultLogArgs::get_global().log_stdout_filter.clone())]
30    pub log_stdout_filter: String,
31
32    /// The format to use for logs written to the log file.
33    #[arg(long = "log.file.format", value_name = "FORMAT", global = true, default_value_t = DefaultLogArgs::get_global().log_file_format)]
34    pub log_file_format: LogFormat,
35
36    /// The filter to use for logs written to the log file.
37    #[arg(long = "log.file.filter", value_name = "FILTER", global = true, default_value_t = DefaultLogArgs::get_global().log_file_filter.clone())]
38    pub log_file_filter: String,
39
40    /// The path to put log files in.
41    #[arg(long = "log.file.directory", value_name = "PATH", global = true, default_value_t)]
42    pub log_file_directory: PlatformPath<LogsDir>,
43
44    /// The prefix name of the log files.
45    #[arg(long = "log.file.name", value_name = "NAME", global = true, default_value_t = DefaultLogArgs::get_global().log_file_name.clone())]
46    pub log_file_name: String,
47
48    /// The maximum size (in MB) of one log file.
49    #[arg(long = "log.file.max-size", value_name = "SIZE", global = true, default_value_t = DefaultLogArgs::get_global().log_file_max_size)]
50    pub log_file_max_size: u64,
51
52    /// The maximum amount of log files that will be stored. If set to 0, background file logging
53    /// is disabled.
54    ///
55    /// Default: 5 for `node` command, 0 for non-node utility subcommands.
56    #[arg(long = "log.file.max-files", value_name = "COUNT", global = true)]
57    pub log_file_max_files: Option<usize>,
58
59    /// Write logs to journald.
60    #[arg(long = "log.journald", global = true, default_value_t = DefaultLogArgs::get_global().journald)]
61    pub journald: bool,
62
63    /// The filter to use for logs written to journald.
64    #[arg(
65        long = "log.journald.filter",
66        value_name = "FILTER",
67        global = true,
68        default_value_t = DefaultLogArgs::get_global().journald_filter.clone()
69    )]
70    pub journald_filter: String,
71
72    /// Emit traces to samply. Only useful when profiling.
73    #[arg(long = "log.samply", global = true, hide = true, default_value_t = DefaultLogArgs::get_global().samply)]
74    pub samply: bool,
75
76    /// The filter to use for traces emitted to samply.
77    #[arg(
78        long = "log.samply.filter",
79        value_name = "FILTER",
80        global = true,
81        default_value_t = DefaultLogArgs::get_global().samply_filter.clone(),
82        hide = true
83    )]
84    pub samply_filter: String,
85
86    /// Emit traces to a Chrome trace JSON file. Only useful when profiling.
87    #[arg(long = "log.tracing-chrome", global = true, hide = true, default_value_t = DefaultLogArgs::get_global().tracing_chrome)]
88    pub tracing_chrome: bool,
89
90    /// The path to write Chrome trace JSON to.
91    #[arg(
92        long = "log.tracing-chrome.file",
93        value_name = "PATH",
94        global = true,
95        default_value = "trace.json",
96        hide = true
97    )]
98    pub tracing_chrome_file: PathBuf,
99
100    /// The filter to use for traces emitted to Chrome trace JSON.
101    #[arg(
102        long = "log.tracing-chrome.filter",
103        value_name = "FILTER",
104        global = true,
105        default_value_t = DefaultLogArgs::get_global().tracing_chrome_filter.clone(),
106        hide = true
107    )]
108    pub tracing_chrome_filter: String,
109
110    /// Emit traces to tracy. Only useful when profiling.
111    #[arg(long = "log.tracy", global = true, hide = true, default_value_t = DefaultLogArgs::get_global().tracy)]
112    pub tracy: bool,
113
114    /// The filter to use for traces emitted to tracy.
115    #[arg(
116        long = "log.tracy.filter",
117        value_name = "FILTER",
118        global = true,
119        default_value_t = DefaultLogArgs::get_global().tracy_filter.clone(),
120        hide = true
121    )]
122    pub tracy_filter: String,
123
124    /// Sets whether or not the formatter emits ANSI terminal escape codes for colors and other
125    /// text formatting.
126    #[arg(
127        long,
128        value_name = "COLOR",
129        global = true,
130        default_value_t = DefaultLogArgs::get_global().color
131    )]
132    pub color: ColorMode,
133
134    /// The verbosity settings for the tracer.
135    #[command(flatten)]
136    pub verbosity: Verbosity,
137}
138
139impl LogArgs {
140    /// The default number of log files for the `node` subcommand.
141    pub const DEFAULT_MAX_LOG_FILES_NODE: usize = 5;
142
143    /// Returns the effective maximum number of log files.
144    ///
145    /// If `log_file_max_files` was explicitly set, returns that value.
146    /// Otherwise returns 0 (file logging disabled).
147    ///
148    /// Note: Callers should apply the node-specific default (5) before calling
149    /// `init_tracing` if the command is the `node` subcommand.
150    pub fn effective_log_file_max_files(&self) -> usize {
151        self.log_file_max_files.unwrap_or(0)
152    }
153
154    /// Applies the default `log_file_max_files` value for the `node` subcommand
155    /// if not explicitly set by the user.
156    pub const fn apply_node_defaults(&mut self) {
157        if self.log_file_max_files.is_none() {
158            self.log_file_max_files = Some(Self::DEFAULT_MAX_LOG_FILES_NODE);
159        }
160    }
161
162    /// Creates a [`LayerInfo`] instance.
163    fn layer_info(&self, format: LogFormat, filter: String, use_color: bool) -> LayerInfo {
164        LayerInfo::new(
165            format,
166            self.verbosity.directive().to_string(),
167            filter,
168            use_color.then(|| self.color.to_string()),
169        )
170    }
171
172    /// File info from the current log options.
173    fn file_info(&self) -> FileInfo {
174        FileInfo::new(
175            self.log_file_directory.clone().into(),
176            self.log_file_name.clone(),
177            self.log_file_max_size * MB_TO_BYTES,
178            self.effective_log_file_max_files(),
179        )
180    }
181
182    /// Initializes tracing with the configured options from cli args.
183    ///
184    /// Returns guards for tracing layers that need to stay alive.
185    pub fn init_tracing(&self) -> eyre::Result<TracingGuards> {
186        self.init_tracing_with_layers(Layers::new(), false)
187    }
188
189    /// Initializes tracing with the configured options from cli args.
190    ///
191    /// When `enable_reload` is true, a global log handle is installed that allows changing
192    /// log levels at runtime.
193    ///
194    /// # Arguments
195    /// * `layers` - Pre-configured layers to include
196    /// * `enable_reload` - If true, enables runtime log level changes
197    ///
198    /// Returns guards for tracing layers that need to stay alive.
199    pub fn init_tracing_with_layers(
200        &self,
201        layers: Layers,
202        enable_reload: bool,
203    ) -> eyre::Result<TracingGuards> {
204        let mut tracer = RethTracer::new();
205
206        let stdout = self.layer_info(self.log_stdout_format, self.log_stdout_filter.clone(), true);
207        tracer = tracer.with_stdout(stdout);
208
209        if self.journald {
210            tracer = tracer.with_journald(self.journald_filter.clone());
211        }
212
213        if self.effective_log_file_max_files() > 0 {
214            let info = self.file_info();
215            let file = self.layer_info(self.log_file_format, self.log_file_filter.clone(), false);
216            tracer = tracer.with_file(file, info);
217        }
218
219        if self.samply {
220            let config = self.layer_info(LogFormat::Terminal, self.samply_filter.clone(), false);
221            tracer = tracer.with_samply(config);
222        }
223
224        if self.tracing_chrome {
225            let config =
226                self.layer_info(LogFormat::Terminal, self.tracing_chrome_filter.clone(), false);
227            tracer = tracer.with_chrome(config, self.tracing_chrome_file.clone());
228        }
229
230        if self.tracy {
231            #[cfg(feature = "tracy")]
232            {
233                let config = self.layer_info(LogFormat::Terminal, self.tracy_filter.clone(), false);
234                tracer = tracer.with_tracy(config);
235            }
236            #[cfg(not(feature = "tracy"))]
237            {
238                tracing::warn!("`--log.tracy` requested but `tracy` feature was not compiled in");
239            }
240        }
241
242        tracer.with_reload(enable_reload).init_with_layers(layers)
243    }
244}
245
246/// Default values for log configuration that can be customized.
247///
248/// Global defaults can be set via [`DefaultLogArgs::try_init`].
249#[derive(Debug, Clone)]
250pub struct DefaultLogArgs {
251    log_stdout_format: LogFormat,
252    log_stdout_filter: String,
253    log_file_format: LogFormat,
254    log_file_filter: String,
255    log_file_name: String,
256    log_file_max_size: u64,
257    journald: bool,
258    journald_filter: String,
259    samply: bool,
260    samply_filter: String,
261    tracing_chrome: bool,
262    tracing_chrome_filter: String,
263    tracy: bool,
264    tracy_filter: String,
265    color: ColorMode,
266}
267
268impl DefaultLogArgs {
269    /// Initialize the global log defaults with this configuration.
270    pub fn try_init(self) -> Result<(), Self> {
271        LOG_DEFAULTS.set(self)
272    }
273
274    /// Get a reference to the global log defaults.
275    pub fn get_global() -> &'static Self {
276        LOG_DEFAULTS.get_or_init(Self::default)
277    }
278
279    /// Set the default stdout log format.
280    pub const fn with_log_stdout_format(mut self, v: LogFormat) -> Self {
281        self.log_stdout_format = v;
282        self
283    }
284
285    /// Set the default stdout log filter.
286    pub fn with_log_stdout_filter(mut self, v: String) -> Self {
287        self.log_stdout_filter = v;
288        self
289    }
290
291    /// Set the default file log format.
292    pub const fn with_log_file_format(mut self, v: LogFormat) -> Self {
293        self.log_file_format = v;
294        self
295    }
296
297    /// Set the default file log filter.
298    pub fn with_log_file_filter(mut self, v: String) -> Self {
299        self.log_file_filter = v;
300        self
301    }
302
303    /// Set the default log file name.
304    pub fn with_log_file_name(mut self, v: String) -> Self {
305        self.log_file_name = v;
306        self
307    }
308
309    /// Set the default max log file size in MB.
310    pub const fn with_log_file_max_size(mut self, v: u64) -> Self {
311        self.log_file_max_size = v;
312        self
313    }
314
315    /// Set whether journald logging is enabled by default.
316    pub const fn with_journald(mut self, v: bool) -> Self {
317        self.journald = v;
318        self
319    }
320
321    /// Set the default journald filter.
322    pub fn with_journald_filter(mut self, v: String) -> Self {
323        self.journald_filter = v;
324        self
325    }
326
327    /// Set whether samply tracing is enabled by default.
328    pub const fn with_samply(mut self, v: bool) -> Self {
329        self.samply = v;
330        self
331    }
332
333    /// Set the default samply filter.
334    pub fn with_samply_filter(mut self, v: String) -> Self {
335        self.samply_filter = v;
336        self
337    }
338
339    /// Set whether Chrome trace JSON tracing is enabled by default.
340    pub const fn with_tracing_chrome(mut self, v: bool) -> Self {
341        self.tracing_chrome = v;
342        self
343    }
344
345    /// Set the default Chrome trace JSON filter.
346    pub fn with_tracing_chrome_filter(mut self, v: String) -> Self {
347        self.tracing_chrome_filter = v;
348        self
349    }
350
351    /// Set whether tracy tracing is enabled by default.
352    pub const fn with_tracy(mut self, v: bool) -> Self {
353        self.tracy = v;
354        self
355    }
356
357    /// Set the default tracy filter.
358    pub fn with_tracy_filter(mut self, v: String) -> Self {
359        self.tracy_filter = v;
360        self
361    }
362
363    /// Set the default color mode.
364    pub const fn with_color(mut self, v: ColorMode) -> Self {
365        self.color = v;
366        self
367    }
368}
369
370impl Default for DefaultLogArgs {
371    fn default() -> Self {
372        Self {
373            log_stdout_format: LogFormat::Terminal,
374            log_stdout_filter: String::new(),
375            log_file_format: LogFormat::Terminal,
376            log_file_filter: "debug".to_string(),
377            log_file_name: "reth.log".to_string(),
378            log_file_max_size: 200,
379            journald: false,
380            journald_filter: "error".to_string(),
381            samply: false,
382            samply_filter: PROFILER_TRACING_FILTER.to_string(),
383            tracing_chrome: false,
384            tracing_chrome_filter: PROFILER_TRACING_FILTER.to_string(),
385            tracy: false,
386            tracy_filter: PROFILER_TRACING_FILTER.to_string(),
387            color: ColorMode::Always,
388        }
389    }
390}
391
392/// The color mode for the cli.
393#[derive(Debug, Copy, Clone, ValueEnum, Eq, PartialEq)]
394pub enum ColorMode {
395    /// Colors on
396    Always,
397    /// Auto-detect
398    Auto,
399    /// Colors off
400    Never,
401}
402
403impl Display for ColorMode {
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        match self {
406            Self::Always => write!(f, "always"),
407            Self::Auto => write!(f, "auto"),
408            Self::Never => write!(f, "never"),
409        }
410    }
411}
412
413/// The verbosity settings for the cli.
414#[derive(Debug, Copy, Clone, Args)]
415#[command(next_help_heading = "Display")]
416pub struct Verbosity {
417    /// Set the minimum log level.
418    ///
419    /// -v      Errors
420    /// -vv     Warnings
421    /// -vvv    Info
422    /// -vvvv   Debug
423    /// -vvvvv  Traces (warning: very verbose!)
424    #[arg(short, long, action = ArgAction::Count, global = true, default_value_t = 3, verbatim_doc_comment, help_heading = "Display")]
425    verbosity: u8,
426
427    /// Silence all log output.
428    #[arg(long, alias = "silent", short = 'q', global = true, help_heading = "Display")]
429    quiet: bool,
430}
431
432impl Verbosity {
433    /// Get the corresponding [Directive] for the given verbosity, or none if the verbosity
434    /// corresponds to silent.
435    pub fn directive(&self) -> Directive {
436        if self.quiet {
437            LevelFilter::OFF.into()
438        } else {
439            let level = match self.verbosity - 1 {
440                0 => Level::ERROR,
441                1 => Level::WARN,
442                2 => Level::INFO,
443                3 => Level::DEBUG,
444                _ => Level::TRACE,
445            };
446
447            level.into()
448        }
449    }
450}