1use 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#[derive(Args, Debug, Default, Serialize, Deserialize, Clone)]
13pub struct LogArgs {
14 #[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 #[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 #[arg(
42 long = "logs.stdout.format",
43 default_value = "full",
44 env = "KONA_NODE_LOG_STDOUT_FORMAT"
45 )]
46 pub stdout_format: LogFormat,
47 #[arg(long = "logs.file.directory", env = "KONA_NODE_LOG_FILE_DIRECTORY")]
50 pub file_directory: Option<PathBuf>,
51 #[arg(long = "logs.file.format", default_value = "full", env = "KONA_NODE_LOG_FILE_FORMAT")]
58 pub file_format: LogFormat,
59 #[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#[derive(Debug, Clone, Serialize, Deserialize, ValueEnum, Default)]
71#[serde(rename_all = "lowercase")]
72pub enum LogRotation {
73 Minutely,
75 Hourly,
77 Daily,
79 #[default]
81 Never,
82}
83
84#[derive(Debug, Clone)]
86pub struct FileLogConfig {
87 pub directory_path: PathBuf,
89 pub format: LogFormat,
91 pub rotation: LogRotation,
93}
94
95#[derive(Debug, Clone)]
97pub struct StdoutLogConfig {
98 pub format: LogFormat,
100}
101
102#[derive(Debug, Clone)]
105pub struct LogConfig {
106 pub global_level: LevelFilter,
108 pub stdout_logs: Option<StdoutLogConfig>,
110 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 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 #[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}