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, FileWorkerGuard, LayerInfo, Layers, LogFormat,
7    RethTracer, Tracer,
8};
9use std::{fmt, fmt::Display, 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 tracy. Only useful when profiling.
87    #[arg(long = "log.tracy", global = true, hide = true, default_value_t = DefaultLogArgs::get_global().tracy)]
88    pub tracy: bool,
89
90    /// The filter to use for traces emitted to tracy.
91    #[arg(
92        long = "log.tracy.filter",
93        value_name = "FILTER",
94        global = true,
95        default_value_t = DefaultLogArgs::get_global().tracy_filter.clone(),
96        hide = true
97    )]
98    pub tracy_filter: String,
99
100    /// Sets whether or not the formatter emits ANSI terminal escape codes for colors and other
101    /// text formatting.
102    #[arg(
103        long,
104        value_name = "COLOR",
105        global = true,
106        default_value_t = DefaultLogArgs::get_global().color
107    )]
108    pub color: ColorMode,
109
110    /// The verbosity settings for the tracer.
111    #[command(flatten)]
112    pub verbosity: Verbosity,
113}
114
115impl LogArgs {
116    /// The default number of log files for the `node` subcommand.
117    pub const DEFAULT_MAX_LOG_FILES_NODE: usize = 5;
118
119    /// Returns the effective maximum number of log files.
120    ///
121    /// If `log_file_max_files` was explicitly set, returns that value.
122    /// Otherwise returns 0 (file logging disabled).
123    ///
124    /// Note: Callers should apply the node-specific default (5) before calling
125    /// `init_tracing` if the command is the `node` subcommand.
126    pub fn effective_log_file_max_files(&self) -> usize {
127        self.log_file_max_files.unwrap_or(0)
128    }
129
130    /// Applies the default `log_file_max_files` value for the `node` subcommand
131    /// if not explicitly set by the user.
132    pub const fn apply_node_defaults(&mut self) {
133        if self.log_file_max_files.is_none() {
134            self.log_file_max_files = Some(Self::DEFAULT_MAX_LOG_FILES_NODE);
135        }
136    }
137
138    /// Creates a [`LayerInfo`] instance.
139    fn layer_info(&self, format: LogFormat, filter: String, use_color: bool) -> LayerInfo {
140        LayerInfo::new(
141            format,
142            self.verbosity.directive().to_string(),
143            filter,
144            use_color.then(|| self.color.to_string()),
145        )
146    }
147
148    /// File info from the current log options.
149    fn file_info(&self) -> FileInfo {
150        FileInfo::new(
151            self.log_file_directory.clone().into(),
152            self.log_file_name.clone(),
153            self.log_file_max_size * MB_TO_BYTES,
154            self.effective_log_file_max_files(),
155        )
156    }
157
158    /// Initializes tracing with the configured options from cli args.
159    ///
160    /// Returns the file worker guard if a file worker was configured.
161    pub fn init_tracing(&self) -> eyre::Result<Option<FileWorkerGuard>> {
162        self.init_tracing_with_layers(Layers::new(), false)
163    }
164
165    /// Initializes tracing with the configured options from cli args.
166    ///
167    /// When `enable_reload` is true, a global log handle is installed that allows changing
168    /// log levels at runtime via RPC methods like `debug_verbosity` and `debug_vmodule`.
169    ///
170    /// # Arguments
171    /// * `layers` - Pre-configured layers to include
172    /// * `enable_reload` - If true, enables runtime log level changes
173    ///
174    /// Returns the file worker guard if a file worker was configured.
175    pub fn init_tracing_with_layers(
176        &self,
177        layers: Layers,
178        enable_reload: bool,
179    ) -> eyre::Result<Option<FileWorkerGuard>> {
180        let mut tracer = RethTracer::new();
181
182        let stdout = self.layer_info(self.log_stdout_format, self.log_stdout_filter.clone(), true);
183        tracer = tracer.with_stdout(stdout);
184
185        if self.journald {
186            tracer = tracer.with_journald(self.journald_filter.clone());
187        }
188
189        if self.effective_log_file_max_files() > 0 {
190            let info = self.file_info();
191            let file = self.layer_info(self.log_file_format, self.log_file_filter.clone(), false);
192            tracer = tracer.with_file(file, info);
193        }
194
195        if self.samply {
196            let config = self.layer_info(LogFormat::Terminal, self.samply_filter.clone(), false);
197            tracer = tracer.with_samply(config);
198        }
199
200        if self.tracy {
201            #[cfg(feature = "tracy")]
202            {
203                let config = self.layer_info(LogFormat::Terminal, self.tracy_filter.clone(), false);
204                tracer = tracer.with_tracy(config);
205            }
206            #[cfg(not(feature = "tracy"))]
207            {
208                tracing::warn!("`--log.tracy` requested but `tracy` feature was not compiled in");
209            }
210        }
211
212        tracer.with_reload(enable_reload).init_with_layers(layers)
213    }
214}
215
216/// Default values for log configuration that can be customized.
217///
218/// Global defaults can be set via [`DefaultLogArgs::try_init`].
219#[derive(Debug, Clone)]
220pub struct DefaultLogArgs {
221    log_stdout_format: LogFormat,
222    log_stdout_filter: String,
223    log_file_format: LogFormat,
224    log_file_filter: String,
225    log_file_name: String,
226    log_file_max_size: u64,
227    journald: bool,
228    journald_filter: String,
229    samply: bool,
230    samply_filter: String,
231    tracy: bool,
232    tracy_filter: String,
233    color: ColorMode,
234}
235
236impl DefaultLogArgs {
237    /// Initialize the global log defaults with this configuration.
238    pub fn try_init(self) -> Result<(), Self> {
239        LOG_DEFAULTS.set(self)
240    }
241
242    /// Get a reference to the global log defaults.
243    pub fn get_global() -> &'static Self {
244        LOG_DEFAULTS.get_or_init(Self::default)
245    }
246
247    /// Set the default stdout log format.
248    pub const fn with_log_stdout_format(mut self, v: LogFormat) -> Self {
249        self.log_stdout_format = v;
250        self
251    }
252
253    /// Set the default stdout log filter.
254    pub fn with_log_stdout_filter(mut self, v: String) -> Self {
255        self.log_stdout_filter = v;
256        self
257    }
258
259    /// Set the default file log format.
260    pub const fn with_log_file_format(mut self, v: LogFormat) -> Self {
261        self.log_file_format = v;
262        self
263    }
264
265    /// Set the default file log filter.
266    pub fn with_log_file_filter(mut self, v: String) -> Self {
267        self.log_file_filter = v;
268        self
269    }
270
271    /// Set the default log file name.
272    pub fn with_log_file_name(mut self, v: String) -> Self {
273        self.log_file_name = v;
274        self
275    }
276
277    /// Set the default max log file size in MB.
278    pub const fn with_log_file_max_size(mut self, v: u64) -> Self {
279        self.log_file_max_size = v;
280        self
281    }
282
283    /// Set whether journald logging is enabled by default.
284    pub const fn with_journald(mut self, v: bool) -> Self {
285        self.journald = v;
286        self
287    }
288
289    /// Set the default journald filter.
290    pub fn with_journald_filter(mut self, v: String) -> Self {
291        self.journald_filter = v;
292        self
293    }
294
295    /// Set whether samply tracing is enabled by default.
296    pub const fn with_samply(mut self, v: bool) -> Self {
297        self.samply = v;
298        self
299    }
300
301    /// Set the default samply filter.
302    pub fn with_samply_filter(mut self, v: String) -> Self {
303        self.samply_filter = v;
304        self
305    }
306
307    /// Set whether tracy tracing is enabled by default.
308    pub const fn with_tracy(mut self, v: bool) -> Self {
309        self.tracy = v;
310        self
311    }
312
313    /// Set the default tracy filter.
314    pub fn with_tracy_filter(mut self, v: String) -> Self {
315        self.tracy_filter = v;
316        self
317    }
318
319    /// Set the default color mode.
320    pub const fn with_color(mut self, v: ColorMode) -> Self {
321        self.color = v;
322        self
323    }
324}
325
326impl Default for DefaultLogArgs {
327    fn default() -> Self {
328        Self {
329            log_stdout_format: LogFormat::Terminal,
330            log_stdout_filter: String::new(),
331            log_file_format: LogFormat::Terminal,
332            log_file_filter: "debug".to_string(),
333            log_file_name: "reth.log".to_string(),
334            log_file_max_size: 200,
335            journald: false,
336            journald_filter: "error".to_string(),
337            samply: false,
338            samply_filter: PROFILER_TRACING_FILTER.to_string(),
339            tracy: false,
340            tracy_filter: PROFILER_TRACING_FILTER.to_string(),
341            color: ColorMode::Always,
342        }
343    }
344}
345
346/// The color mode for the cli.
347#[derive(Debug, Copy, Clone, ValueEnum, Eq, PartialEq)]
348pub enum ColorMode {
349    /// Colors on
350    Always,
351    /// Auto-detect
352    Auto,
353    /// Colors off
354    Never,
355}
356
357impl Display for ColorMode {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        match self {
360            Self::Always => write!(f, "always"),
361            Self::Auto => write!(f, "auto"),
362            Self::Never => write!(f, "never"),
363        }
364    }
365}
366
367/// The verbosity settings for the cli.
368#[derive(Debug, Copy, Clone, Args)]
369#[command(next_help_heading = "Display")]
370pub struct Verbosity {
371    /// Set the minimum log level.
372    ///
373    /// -v      Errors
374    /// -vv     Warnings
375    /// -vvv    Info
376    /// -vvvv   Debug
377    /// -vvvvv  Traces (warning: very verbose!)
378    #[arg(short, long, action = ArgAction::Count, global = true, default_value_t = 3, verbatim_doc_comment, help_heading = "Display")]
379    verbosity: u8,
380
381    /// Silence all log output.
382    #[arg(long, alias = "silent", short = 'q', global = true, help_heading = "Display")]
383    quiet: bool,
384}
385
386impl Verbosity {
387    /// Get the corresponding [Directive] for the given verbosity, or none if the verbosity
388    /// corresponds to silent.
389    pub fn directive(&self) -> Directive {
390        if self.quiet {
391            LevelFilter::OFF.into()
392        } else {
393            let level = match self.verbosity - 1 {
394                0 => Level::ERROR,
395                1 => Level::WARN,
396                2 => Level::INFO,
397                3 => Level::DEBUG,
398                _ => Level::TRACE,
399            };
400
401            level.into()
402        }
403    }
404}