batata_client/logging/
mod.rs1use std::path::PathBuf;
21
22use tracing_appender::non_blocking::WorkerGuard;
23use tracing_appender::rolling::{RollingFileAppender, Rotation};
24use tracing_subscriber::layer::SubscriberExt;
25use tracing_subscriber::util::SubscriberInitExt;
26use tracing_subscriber::EnvFilter;
27
28#[derive(Clone, Debug, Default)]
30pub enum LogRotation {
31 #[default]
33 Daily,
34 Hourly,
36 Minutely,
38 Never,
40}
41
42impl LogRotation {
43 fn to_rotation(&self) -> Rotation {
44 match self {
45 LogRotation::Daily => Rotation::DAILY,
46 LogRotation::Hourly => Rotation::HOURLY,
47 LogRotation::Minutely => Rotation::MINUTELY,
48 LogRotation::Never => Rotation::NEVER,
49 }
50 }
51}
52
53#[derive(Clone, Debug)]
57pub struct LogConfig {
58 pub log_dir: PathBuf,
60 pub log_file_prefix: String,
62 pub rotation: LogRotation,
64 pub max_files: Option<usize>,
66 pub level: String,
68 pub stdout: bool,
70}
71
72impl Default for LogConfig {
73 fn default() -> Self {
74 Self {
75 log_dir: PathBuf::from("logs"),
76 log_file_prefix: "batata-client".to_string(),
77 rotation: LogRotation::Daily,
78 max_files: Some(7),
79 level: "info".to_string(),
80 stdout: true,
81 }
82 }
83}
84
85impl LogConfig {
86 pub fn new(log_dir: impl Into<PathBuf>) -> Self {
88 Self {
89 log_dir: log_dir.into(),
90 ..Default::default()
91 }
92 }
93
94 pub fn with_level(mut self, level: &str) -> Self {
96 self.level = level.to_string();
97 self
98 }
99
100 pub fn with_rotation(mut self, rotation: LogRotation) -> Self {
102 self.rotation = rotation;
103 self
104 }
105
106 pub fn with_max_files(mut self, max_files: usize) -> Self {
108 self.max_files = Some(max_files);
109 self
110 }
111
112 pub fn with_prefix(mut self, prefix: &str) -> Self {
114 self.log_file_prefix = prefix.to_string();
115 self
116 }
117
118 pub fn with_stdout(mut self, enabled: bool) -> Self {
120 self.stdout = enabled;
121 self
122 }
123
124 pub fn init(&self) -> Result<LogGuard, Box<dyn std::error::Error + Send + Sync>> {
143 std::fs::create_dir_all(&self.log_dir)?;
145
146 let mut builder = RollingFileAppender::builder()
148 .rotation(self.rotation.to_rotation())
149 .filename_prefix(&self.log_file_prefix)
150 .filename_suffix("log");
151
152 if let Some(max_files) = self.max_files {
153 builder = builder.max_log_files(max_files);
154 }
155
156 let file_appender = builder.build(&self.log_dir)?;
157
158 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
160
161 let filter = EnvFilter::try_from_default_env()
163 .unwrap_or_else(|_| EnvFilter::new(&self.level));
164
165 let file_layer = tracing_subscriber::fmt::layer()
167 .with_writer(non_blocking)
168 .with_ansi(false)
169 .with_target(true)
170 .with_thread_ids(true);
171
172 let subscriber = tracing_subscriber::registry()
174 .with(filter)
175 .with(file_layer);
176
177 if self.stdout {
178 let stdout_layer = tracing_subscriber::fmt::layer()
179 .with_target(true)
180 .with_thread_ids(false);
181
182 subscriber.with(stdout_layer).init();
183 } else {
184 subscriber.init();
185 }
186
187 Ok(LogGuard { _guard: guard })
188 }
189
190 pub fn init_file_only(&self) -> Result<LogGuard, Box<dyn std::error::Error + Send + Sync>> {
192 let mut config = self.clone();
193 config.stdout = false;
194 config.init()
195 }
196}
197
198pub struct LogGuard {
203 _guard: WorkerGuard,
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_log_config_defaults() {
212 let config = LogConfig::default();
213 assert_eq!(config.log_dir, PathBuf::from("logs"));
214 assert_eq!(config.log_file_prefix, "batata-client");
215 assert_eq!(config.level, "info");
216 assert!(config.stdout);
217 assert_eq!(config.max_files, Some(7));
218 }
219
220 #[test]
221 fn test_log_config_builder() {
222 let config = LogConfig::new("/var/log/test")
223 .with_level("debug")
224 .with_rotation(LogRotation::Hourly)
225 .with_max_files(10)
226 .with_prefix("my-app")
227 .with_stdout(false);
228
229 assert_eq!(config.log_dir, PathBuf::from("/var/log/test"));
230 assert_eq!(config.level, "debug");
231 assert_eq!(config.log_file_prefix, "my-app");
232 assert!(!config.stdout);
233 assert_eq!(config.max_files, Some(10));
234 }
235
236 #[test]
237 fn test_log_rotation_conversion() {
238 assert!(matches!(LogRotation::Daily.to_rotation(), Rotation::DAILY));
239 assert!(matches!(LogRotation::Hourly.to_rotation(), Rotation::HOURLY));
240 assert!(matches!(
241 LogRotation::Minutely.to_rotation(),
242 Rotation::MINUTELY
243 ));
244 assert!(matches!(LogRotation::Never.to_rotation(), Rotation::NEVER));
245 }
246}