kona_cli/
log.rs

1//! Arguments for logging.
2
3use std::path::PathBuf;
4
5use clap::{ArgAction, Args, ValueEnum};
6use serde::{Deserialize, Serialize};
7use tracing::level_filters::LevelFilter;
8
9use crate::tracing::LogFormat;
10
11/// Global configuration arguments.
12#[derive(Args, Debug, Default, Serialize, Deserialize, Clone)]
13pub struct LogArgs {
14    /// Verbosity level (1-5).
15    /// By default, the verbosity level is set to 3 (info level).
16    ///
17    /// This verbosity level is shared by both stdout and file logging (if enabled).
18    #[arg(
19        short = 'v',
20        global = true,
21        default_value = "3",
22        env = "KONA_NODE_LOG_LEVEL",
23        action = ArgAction::Count,
24    )]
25    pub level: u8,
26    /// If set, no logs are printed to stdout.
27    #[arg(
28        long = "logs.stdout.quiet",
29        short = 'q',
30        global = true,
31        default_value = "false",
32        env = "KONA_NODE_STDOUT_LOG_QUIET"
33    )]
34    pub stdout_quiet: bool,
35    /// The format of the logs printed to stdout. One of: full, json, pretty, compact.
36    ///
37    /// full: The default rust log format.
38    /// json: The logs are printed in JSON structured format.
39    /// pretty: The logs are printed in a pretty, human readable format.
40    /// compact: The logs are printed in a compact format.
41    #[arg(
42        long = "logs.stdout.format",
43        default_value = "full",
44        env = "KONA_NODE_LOG_STDOUT_FORMAT"
45    )]
46    pub stdout_format: LogFormat,
47    /// The directory to store the log files.
48    /// If not set, no logs are printed to files.
49    #[arg(long = "logs.file.directory", env = "KONA_NODE_LOG_FILE_DIRECTORY")]
50    pub file_directory: Option<PathBuf>,
51    /// The format of the logs printed to log files. One of: full, json, pretty, compact.
52    ///
53    /// full: The default rust log format.
54    /// json: The logs are printed in JSON structured format.
55    /// pretty: The logs are printed in a pretty, human readable format.
56    /// compact: The logs are printed in a compact format.
57    #[arg(long = "logs.file.format", default_value = "full", env = "KONA_NODE_LOG_FILE_FORMAT")]
58    pub file_format: LogFormat,
59    /// The rotation of the log files. One of: hourly, daily, weekly, monthly, never.
60    /// If set, new log files will be created every interval.
61    #[arg(
62        long = "logs.file.rotation",
63        default_value = "never",
64        env = "KONA_NODE_LOG_FILE_ROTATION"
65    )]
66    pub file_rotation: LogRotation,
67}
68
69/// The rotation of the log files.
70#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum, Default)]
71#[serde(rename_all = "lowercase")]
72pub enum LogRotation {
73    /// Rotate the log files every minute.
74    Minutely,
75    /// Rotate the log files hourly.
76    Hourly,
77    /// Rotate the log files daily.
78    Daily,
79    /// Do not rotate the log files.
80    #[default]
81    Never,
82}
83
84/// Configuration for file logging.
85#[derive(Debug, Clone)]
86pub struct FileLogConfig {
87    /// The path to the directory where the log files are stored.
88    pub directory_path: PathBuf,
89    /// The format of the logs printed to the log file.
90    pub format: LogFormat,
91    /// The rotation of the log files.
92    pub rotation: LogRotation,
93}
94
95/// Configuration for stdout logging.
96#[derive(Debug, Clone)]
97pub struct StdoutLogConfig {
98    /// The format of the logs printed to stdout.
99    pub format: LogFormat,
100}
101
102/// Global configuration for logging.
103/// Default is to only print logs to stdout in full format.
104#[derive(Debug, Clone)]
105pub struct LogConfig {
106    /// Global verbosity level for logging.
107    pub global_level: LevelFilter,
108    /// The configuration for stdout logging.
109    pub stdout_logs: Option<StdoutLogConfig>,
110    /// The configuration for file logging.
111    pub file_logs: Option<FileLogConfig>,
112}
113
114impl Default for LogConfig {
115    fn default() -> Self {
116        Self {
117            global_level: LevelFilter::INFO,
118            stdout_logs: Some(StdoutLogConfig { format: LogFormat::Full }),
119            file_logs: None,
120        }
121    }
122}
123
124impl From<LogArgs> for LogConfig {
125    fn from(args: LogArgs) -> Self {
126        Self::new(args)
127    }
128}
129
130impl LogConfig {
131    /// Creates a new `LogConfig` from `LogArgs`.
132    pub fn new(args: LogArgs) -> Self {
133        let level = match args.level {
134            1 => LevelFilter::ERROR,
135            2 => LevelFilter::WARN,
136            3 => LevelFilter::INFO,
137            4 => LevelFilter::DEBUG,
138            _ => LevelFilter::TRACE,
139        };
140
141        let stdout_logs = if args.stdout_quiet {
142            None
143        } else {
144            Some(StdoutLogConfig { format: args.stdout_format })
145        };
146
147        let file_logs = args.file_directory.as_ref().map(|path| FileLogConfig {
148            directory_path: path.clone(),
149            format: args.file_format,
150            rotation: args.file_rotation,
151        });
152
153        Self { global_level: level, stdout_logs, file_logs }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use clap::Parser;
161
162    // Helper struct to parse GlobalArgs within a test CLI structure
163    #[derive(Parser, Debug)]
164    struct TestCli {
165        #[command(flatten)]
166        global: LogArgs,
167    }
168
169    #[test]
170    fn test_default_verbosity_level() {
171        let cli = TestCli::parse_from(["test_app"]);
172        assert_eq!(
173            cli.global.level, 3,
174            "Default verbosity should be 3 when no -v flag is present."
175        );
176    }
177
178    #[test]
179    fn test_verbosity_count() {
180        let cli_v1 = TestCli::parse_from(["test_app", "-v"]);
181        assert_eq!(cli_v1.global.level, 1, "Verbosity with a single -v should be 1.");
182
183        let cli_v3 = TestCli::parse_from(["test_app", "-vvv"]);
184        assert_eq!(cli_v3.global.level, 3, "Verbosity with -vvv should be 3.");
185
186        let cli_v5 = TestCli::parse_from(["test_app", "-vvvvv"]);
187        assert_eq!(cli_v5.global.level, 5, "Verbosity with -vvvvv should be 5.");
188    }
189}