1use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
9pub struct LoggingConfig {
10 #[serde(default = "default_log_level")]
12 pub level: String,
13
14 #[serde(default = "default_output_mode")]
16 pub output_mode: OutputMode,
17
18 #[serde(default)]
20 pub file: Option<LogFileConfig>,
21
22 #[serde(default)]
24 pub console: Option<LogConsoleConfig>,
25
26 #[serde(default = "default_true")]
28 pub enable_request_id: bool,
29
30 #[serde(default)]
32 pub fields: LogFieldsConfig,
33
34 #[serde(default)]
36 pub vector: Option<VectorConfig>,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
41#[serde(rename_all = "lowercase")]
42pub enum OutputMode {
43 File,
45 Console,
47 Both,
49}
50
51#[derive(Debug, Clone, Deserialize, Serialize)]
53pub struct LogFileConfig {
54 pub path: String,
56
57 #[serde(default = "default_true")]
59 pub enabled: bool,
60
61 #[serde(default)]
63 pub logrotate: Option<LogrotateConfig>,
64}
65
66#[derive(Debug, Clone, Deserialize, Serialize)]
68pub struct LogConsoleConfig {
69 #[serde(default = "default_true")]
71 pub enabled: bool,
72
73 #[serde(default = "default_true")]
75 pub ansi: bool,
76}
77
78#[derive(Debug, Clone, Deserialize, Serialize)]
80pub struct LogFieldsConfig {
81 #[serde(default = "default_true")]
83 pub enabled: bool,
84
85 #[serde(default)]
87 pub mappings: HashMap<String, String>,
88}
89
90impl Default for LogFieldsConfig {
91 fn default() -> Self {
92 Self {
93 enabled: default_true(),
94 mappings: HashMap::new(),
95 }
96 }
97}
98
99#[derive(Debug, Clone, Deserialize, Serialize)]
106pub struct LogrotateConfig {
107 #[serde(default = "default_false")]
109 pub enabled: bool,
110
111 pub path: String,
113
114 #[serde(default = "default_rotate_period")]
121 pub rotate: String,
122
123 #[serde(default)]
126 pub size: Option<String>,
127
128 #[serde(default)]
131 pub maxsize: Option<String>,
132
133 #[serde(default)]
136 pub minsize: Option<String>,
137
138 #[serde(default = "default_rotate_count")]
140 pub rotate_count: u32,
141
142 #[serde(default)]
145 pub maxage: Option<u32>,
146
147 #[serde(default = "default_one")]
149 pub start: u32,
150
151 #[serde(default = "default_true")]
153 pub compress: bool,
154
155 #[serde(default = "default_zero")]
157 pub delaycompress: u32,
158
159 #[serde(default)]
162 pub compresscmd: Option<String>,
163
164 #[serde(default)]
167 pub uncompresscmd: Option<String>,
168
169 #[serde(default = "default_compress_ext")]
172 pub compressext: String,
173
174 #[serde(default)]
177 pub compressoptions: Option<String>,
178
179 #[serde(default = "default_true")]
181 pub create: bool,
182
183 #[serde(default = "default_file_mode")]
185 pub create_mode: String,
186
187 #[serde(default)]
189 pub create_owner: Option<String>,
190
191 #[serde(default = "default_false")]
194 pub copy: bool,
195
196 #[serde(default = "default_false")]
199 pub copytruncate: bool,
200
201 #[serde(default = "default_false")]
203 pub postrotate: bool,
204
205 #[serde(default)]
208 pub postrotate_script: Option<String>,
209
210 #[serde(default = "default_false")]
212 pub prerotate: bool,
213
214 #[serde(default)]
216 pub prerotate_script: Option<String>,
217
218 #[serde(default = "default_false")]
220 pub firstaction: bool,
221
222 #[serde(default)]
224 pub firstaction_script: Option<String>,
225
226 #[serde(default = "default_false")]
228 pub lastaction: bool,
229
230 #[serde(default)]
232 pub lastaction_script: Option<String>,
233
234 #[serde(default = "default_false")]
236 pub missingok: bool,
237
238 #[serde(default = "default_false")]
240 pub notifempty: bool,
241
242 #[serde(default = "default_false")]
244 pub ifempty: bool,
245
246 #[serde(default = "default_false")]
248 pub sharedscripts: bool,
249
250 #[serde(default = "default_false")]
253 pub dateext: bool,
254
255 #[serde(default = "default_date_format")]
259 pub dateformat: String,
260
261 #[serde(default)]
264 pub olddir: Option<String>,
265
266 #[serde(default = "default_false")]
268 pub noolddir: bool,
269
270 #[serde(default)]
273 pub extension: Option<String>,
274
275 #[serde(default)]
278 pub tabooext: Vec<String>,
279
280 #[serde(default)]
283 pub su: Option<String>,
284
285 #[serde(default = "default_false")]
287 pub mail: bool,
288
289 #[serde(default)]
292 pub mailfirst: Option<String>,
293
294 #[serde(default = "default_false")]
296 pub maillast: bool,
297
298 #[serde(default = "default_false")]
300 pub nomail: bool,
301
302 #[serde(default)]
305 pub include: Vec<String>,
306
307 #[serde(default = "default_false")]
309 pub shred: bool,
310
311 #[serde(default = "default_zero")]
313 pub shredcycles: u32,
314
315 #[serde(default = "default_false")]
317 pub nocompress: bool,
318
319 #[serde(default)]
323 pub output_path: Option<String>,
324}
325
326impl LogrotateConfig {
327 pub fn to_logrotate_config(&self) -> String {
329 let mut config = String::new();
330
331 config.push_str(&format!("{}\n", self.path));
333 config.push_str("{\n");
334
335 config.push_str(&format!(" {}\n", self.rotate));
337
338 if let Some(size) = &self.size {
340 config.push_str(&format!(" size {}\n", size));
341 } else if let Some(maxsize) = &self.maxsize {
342 config.push_str(&format!(" maxsize {}\n", maxsize));
343 }
344
345 if let Some(minsize) = &self.minsize {
347 config.push_str(&format!(" minsize {}\n", minsize));
348 }
349
350 config.push_str(&format!(" rotate {}\n", self.rotate_count));
352
353 if let Some(maxage) = self.maxage {
355 config.push_str(&format!(" maxage {}\n", maxage));
356 }
357
358 if self.start != 1 {
360 config.push_str(&format!(" start {}\n", self.start));
361 }
362
363 if self.nocompress {
365 config.push_str(" nocompress\n");
366 } else if self.compress {
367 config.push_str(" compress\n");
368 if self.delaycompress > 0 {
369 config.push_str(&format!(" delaycompress {}\n", self.delaycompress));
370 }
371 if let Some(cmd) = &self.compresscmd {
372 config.push_str(&format!(" compresscmd {}\n", cmd));
373 }
374 if let Some(cmd) = &self.uncompresscmd {
375 config.push_str(&format!(" uncompresscmd {}\n", cmd));
376 }
377 if self.compressext != ".gz" {
378 config.push_str(&format!(" compressext {}\n", self.compressext));
379 }
380 if let Some(opts) = &self.compressoptions {
381 config.push_str(&format!(" compressoptions {}\n", opts));
382 }
383 }
384
385 if self.create {
387 let owner = self.create_owner.as_ref()
388 .map(|o| format!(" {}", o))
389 .unwrap_or_default();
390 config.push_str(&format!(" create {} {}{}\n",
391 self.create_mode, self.create_mode, owner));
392 }
393
394 if self.copy {
396 config.push_str(" copy\n");
397 }
398 if self.copytruncate {
399 config.push_str(" copytruncate\n");
400 }
401
402 if self.missingok {
404 config.push_str(" missingok\n");
405 }
406 if self.notifempty {
407 config.push_str(" notifempty\n");
408 }
409 if self.ifempty {
410 config.push_str(" ifempty\n");
411 }
412
413 if self.dateext {
415 config.push_str(" dateext\n");
416 if self.dateformat != "%Y%m%d" {
417 config.push_str(&format!(" dateformat {}\n", self.dateformat));
418 }
419 }
420
421 if let Some(olddir) = &self.olddir {
423 config.push_str(&format!(" olddir {}\n", olddir));
424 }
425 if self.noolddir {
426 config.push_str(" noolddir\n");
427 }
428
429 if let Some(ext) = &self.extension {
431 config.push_str(&format!(" extension {}\n", ext));
432 }
433
434 if !self.tabooext.is_empty() {
436 config.push_str(&format!(" tabooext {}\n", self.tabooext.join(" ")));
437 }
438
439 if self.sharedscripts {
441 config.push_str(" sharedscripts\n");
442 }
443
444 if let Some(su) = &self.su {
446 config.push_str(&format!(" su {}\n", su));
447 }
448
449 if self.nomail {
451 config.push_str(" nomail\n");
452 } else if self.mail {
453 config.push_str(" mail\n");
454 }
455 if let Some(addr) = &self.mailfirst {
456 config.push_str(&format!(" mailfirst {}\n", addr));
457 }
458 if self.maillast {
459 config.push_str(" maillast\n");
460 }
461
462 for include_path in &self.include {
464 config.push_str(&format!(" include {}\n", include_path));
465 }
466
467 if self.shred {
469 config.push_str(" shred\n");
470 if self.shredcycles > 0 {
471 config.push_str(&format!(" shredcycles {}\n", self.shredcycles));
472 }
473 }
474
475 if self.firstaction {
477 if let Some(script) = &self.firstaction_script {
478 config.push_str(" firstaction\n");
479 config.push_str(&format!(" {}\n", script));
480 config.push_str(" endscript\n");
481 }
482 }
483
484 if self.prerotate {
485 if let Some(script) = &self.prerotate_script {
486 config.push_str(" prerotate\n");
487 config.push_str(&format!(" {}\n", script));
488 config.push_str(" endscript\n");
489 }
490 }
491
492 if self.postrotate {
493 if let Some(script) = &self.postrotate_script {
494 config.push_str(" postrotate\n");
495 config.push_str(&format!(" {}\n", script));
496 config.push_str(" endscript\n");
497 }
498 }
499
500 if self.lastaction {
501 if let Some(script) = &self.lastaction_script {
502 config.push_str(" lastaction\n");
503 config.push_str(&format!(" {}\n", script));
504 config.push_str(" endscript\n");
505 }
506 }
507
508 config.push_str("}\n");
509 config
510 }
511
512 pub fn generate_config_file(&self, default_path: Option<&str>) -> Result<String> {
521 if !self.enabled {
522 return Err(anyhow::anyhow!("Logrotate is not enabled"));
523 }
524
525 let output_path_str: String = if let Some(path) = &self.output_path {
527 path.clone()
529 } else if let Some(path) = default_path {
530 path.to_string()
532 } else {
533 let exe_path = std::env::current_exe()
535 .context("Failed to get current executable path")?;
536 let exe_dir = exe_path.parent()
537 .ok_or_else(|| anyhow::anyhow!("Failed to get executable directory"))?;
538 let auto_path = exe_dir.join("logrotate").join("app.conf");
539 auto_path.to_string_lossy().to_string()
540 };
541
542 if let Some(parent) = std::path::Path::new(&output_path_str).parent() {
544 std::fs::create_dir_all(parent)
545 .with_context(|| format!("Failed to create logrotate directory: {}", parent.display()))?;
546 }
547
548 let config_content = self.to_logrotate_config();
550
551 std::fs::write(&output_path_str, config_content)
553 .with_context(|| format!("Failed to write logrotate config to {}", output_path_str))?;
554
555 Ok(output_path_str)
556 }
557}
558
559#[derive(Debug, Clone, Deserialize, Serialize)]
564pub struct VectorConfig {
565 #[serde(default = "default_false")]
567 pub enabled: bool,
568
569 #[serde(default = "default_vector_config_path")]
571 pub config_path: String,
572
573 #[serde(default)]
575 pub source: VectorSourceConfig,
576
577 #[serde(default)]
579 pub transforms: Vec<VectorTransformConfig>,
580
581 #[serde(default)]
583 pub sinks: Vec<VectorSinkConfig>,
584}
585
586#[derive(Debug, Clone, Deserialize, Serialize)]
588pub struct VectorSourceConfig {
589 #[serde(default = "default_source_name")]
591 pub name: String,
592
593 pub paths: Vec<String>,
595
596 #[serde(default = "default_read_from")]
600 pub read_from: String,
601
602 #[serde(default = "default_false")]
604 pub multiline: bool,
605
606 #[serde(default)]
609 pub multiline_pattern: Option<String>,
610
611 #[serde(default = "default_false")]
613 pub ignore_not_found: bool,
614
615 #[serde(default = "default_empty_vec")]
617 pub include: Vec<String>,
618
619 #[serde(default)]
621 pub exclude: Vec<String>,
622}
623
624#[derive(Debug, Clone, Deserialize, Serialize)]
626pub struct VectorTransformConfig {
627 pub name: String,
629
630 pub transform_type: String,
632
633 pub inputs: Vec<String>,
635
636 pub source: Option<String>,
639
640 #[serde(default)]
642 pub options: HashMap<String, String>,
643}
644
645#[derive(Debug, Clone, Deserialize, Serialize)]
647pub struct VectorSinkConfig {
648 pub name: String,
650
651 pub sink_type: String,
653
654 pub inputs: Vec<String>,
656
657 #[serde(default)]
663 pub endpoint: Option<String>,
664
665 #[serde(default)]
667 pub labels: HashMap<String, String>,
668
669 #[serde(default = "default_encoding")]
671 pub encoding: String,
672
673 #[serde(default = "default_compression")]
675 pub compression: String,
676
677 #[serde(default)]
679 pub batch: Option<VectorBatchConfig>,
680
681 #[serde(default)]
683 pub request: Option<VectorRequestConfig>,
684
685 #[serde(default)]
687 pub options: HashMap<String, String>,
688}
689
690#[derive(Debug, Clone, Deserialize, Serialize)]
692pub struct VectorBatchConfig {
693 #[serde(default = "default_batch_max_bytes")]
695 pub max_bytes: u64,
696
697 #[serde(default = "default_batch_max_events")]
699 pub max_events: u32,
700
701 #[serde(default = "default_batch_timeout")]
703 pub timeout_secs: u32,
704}
705
706#[derive(Debug, Clone, Deserialize, Serialize)]
708pub struct VectorRequestConfig {
709 #[serde(default = "default_retry_attempts")]
711 pub retry_attempts: u32,
712
713 #[serde(default = "default_retry_backoff")]
715 pub retry_backoff_secs: u32,
716
717 #[serde(default = "default_request_timeout")]
719 pub timeout_secs: u32,
720
721 #[serde(default)]
723 pub rate_limit: Option<u32>,
724}
725
726fn default_false() -> bool {
728 false
729}
730
731fn default_zero() -> u32 {
732 0
733}
734
735fn default_one() -> u32 {
736 1
737}
738
739fn default_rotate_period() -> String {
740 "daily".to_string()
741}
742
743fn default_rotate_count() -> u32 {
744 7
745}
746
747fn default_file_mode() -> String {
748 "0644".to_string()
749}
750
751fn default_date_format() -> String {
752 "%Y%m%d".to_string()
753}
754
755fn default_compress_ext() -> String {
756 ".gz".to_string()
757}
758
759fn default_vector_config_path() -> String {
760 "vector.toml".to_string()
761}
762
763fn default_source_name() -> String {
764 "app_logs".to_string()
765}
766
767fn default_read_from() -> String {
768 "beginning".to_string()
769}
770
771fn default_encoding() -> String {
772 "json".to_string()
773}
774
775fn default_compression() -> String {
776 "gzip".to_string()
777}
778
779fn default_batch_max_bytes() -> u64 {
780 1048576 }
782
783fn default_batch_max_events() -> u32 {
784 500
785}
786
787fn default_batch_timeout() -> u32 {
788 10
789}
790
791fn default_retry_attempts() -> u32 {
792 5
793}
794
795fn default_retry_backoff() -> u32 {
796 1
797}
798
799fn default_request_timeout() -> u32 {
800 30
801}
802
803fn default_empty_vec() -> Vec<String> {
804 Vec::new()
805}
806
807impl Default for VectorSourceConfig {
808 fn default() -> Self {
809 Self {
810 name: default_source_name(),
811 paths: vec!["logs/app.log".to_string()],
812 read_from: default_read_from(),
813 multiline: default_false(),
814 multiline_pattern: None,
815 ignore_not_found: default_false(),
816 include: default_empty_vec(),
817 exclude: Vec::new(),
818 }
819 }
820}
821
822fn default_log_level() -> String {
824 "info".to_string()
825}
826
827fn default_output_mode() -> OutputMode {
828 OutputMode::File
829}
830
831fn default_true() -> bool {
832 true
833}
834
835impl LoggingConfig {
836 pub fn validate(&self) -> Result<()> {
838 match self.level.as_str() {
840 "trace" | "debug" | "info" | "warn" | "error" => {},
841 _ => anyhow::bail!("Invalid log level: {}", self.level),
842 }
843
844 if matches!(self.output_mode, OutputMode::File | OutputMode::Both) {
846 if let Some(file_config) = &self.file {
847 if file_config.enabled && file_config.path.is_empty() {
848 anyhow::bail!("Log file path cannot be empty when file output is enabled");
849 }
850 } else {
851 anyhow::bail!("File config is required when output_mode is file or both");
852 }
853 }
854
855 if matches!(self.output_mode, OutputMode::Console | OutputMode::Both) {
857 if self.console.is_none() {
858 anyhow::bail!("Console config is required when output_mode is console or both");
859 }
860 }
861
862 Ok(())
863 }
864}
865
866impl Default for LoggingConfig {
867 fn default() -> Self {
868 Self {
869 level: default_log_level(),
870 output_mode: default_output_mode(),
871 file: Some(LogFileConfig {
872 path: "logs/app.log".to_string(),
873 enabled: true,
874 logrotate: None,
875 }),
876 console: None,
877 enable_request_id: true,
878 fields: LogFieldsConfig::default(),
879 vector: None,
880 }
881 }
882}