orchestrator_config/config/
observability.rs1use serde::{Deserialize, Serialize};
2use tracing::Level;
3
4#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
6#[serde(rename_all = "snake_case")]
7pub enum LogLevel {
8 Error,
10 Warn,
12 #[default]
14 Info,
15 Debug,
17 Trace,
19}
20
21impl LogLevel {
22 pub fn as_tracing_level(self) -> Level {
24 match self {
25 Self::Error => Level::ERROR,
26 Self::Warn => Level::WARN,
27 Self::Info => Level::INFO,
28 Self::Debug => Level::DEBUG,
29 Self::Trace => Level::TRACE,
30 }
31 }
32
33 pub fn parse(value: &str) -> Option<Self> {
35 match value.trim().to_ascii_lowercase().as_str() {
36 "error" => Some(Self::Error),
37 "warn" | "warning" => Some(Self::Warn),
38 "info" => Some(Self::Info),
39 "debug" => Some(Self::Debug),
40 "trace" => Some(Self::Trace),
41 _ => None,
42 }
43 }
44
45 pub fn max(self, other: Self) -> Self {
47 use LogLevel::*;
48 match (self, other) {
49 (Trace, _) | (_, Trace) => Trace,
50 (Debug, _) | (_, Debug) => Debug,
51 (Info, _) | (_, Info) => Info,
52 (Warn, _) | (_, Warn) => Warn,
53 (Error, Error) => Error,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
60#[serde(rename_all = "snake_case")]
61pub enum LoggingFormat {
62 #[default]
64 Pretty,
65 Json,
67}
68
69impl LoggingFormat {
70 pub fn parse(value: &str) -> Option<Self> {
72 match value.trim().to_ascii_lowercase().as_str() {
73 "pretty" | "compact" | "text" => Some(Self::Pretty),
74 "json" => Some(Self::Json),
75 _ => None,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82pub struct ConsoleLoggingConfig {
83 #[serde(default = "default_enabled")]
85 pub enabled: bool,
86 #[serde(default)]
88 pub format: LoggingFormat,
89 #[serde(default = "default_enabled")]
91 pub ansi: bool,
92}
93
94impl Default for ConsoleLoggingConfig {
95 fn default() -> Self {
96 Self {
97 enabled: true,
98 format: LoggingFormat::Pretty,
99 ansi: true,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106pub struct FileLoggingConfig {
107 #[serde(default = "default_enabled")]
109 pub enabled: bool,
110 #[serde(default = "default_file_format")]
112 pub format: LoggingFormat,
113 #[serde(default = "default_log_directory")]
115 pub directory: String,
116}
117
118impl Default for FileLoggingConfig {
119 fn default() -> Self {
120 Self {
121 enabled: true,
122 format: LoggingFormat::Json,
123 directory: default_log_directory(),
124 }
125 }
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
130pub struct LoggingConfig {
131 #[serde(default)]
133 pub level: LogLevel,
134 #[serde(default)]
136 pub console: ConsoleLoggingConfig,
137 #[serde(default)]
139 pub file: FileLoggingConfig,
140 #[serde(default = "default_enabled")]
142 pub event_bridge: bool,
143}
144
145impl Default for LoggingConfig {
146 fn default() -> Self {
147 Self {
148 level: LogLevel::Info,
149 console: ConsoleLoggingConfig::default(),
150 file: FileLoggingConfig::default(),
151 event_bridge: true,
152 }
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
158pub struct ObservabilityConfig {
159 #[serde(default)]
161 pub logging: LoggingConfig,
162}
163
164fn default_enabled() -> bool {
165 true
166}
167
168fn default_file_format() -> LoggingFormat {
169 LoggingFormat::Json
170}
171
172fn default_log_directory() -> String {
173 "logs/system".to_string()
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn observability_defaults_are_safe() {
182 let cfg = ObservabilityConfig::default();
183 assert_eq!(cfg.logging.level, LogLevel::Info);
184 assert!(cfg.logging.console.enabled);
185 assert_eq!(cfg.logging.console.format, LoggingFormat::Pretty);
186 assert!(cfg.logging.file.enabled);
187 assert_eq!(cfg.logging.file.format, LoggingFormat::Json);
188 assert_eq!(cfg.logging.file.directory, "logs/system");
189 assert!(cfg.logging.event_bridge);
190 }
191
192 #[test]
193 fn observability_serde_defaults_missing_fields() {
194 let cfg: ObservabilityConfig = serde_json::from_str("{}").expect("deserialize defaults");
195 assert_eq!(cfg, ObservabilityConfig::default());
196 }
197
198 #[test]
199 fn level_parse_accepts_common_variants() {
200 assert_eq!(LogLevel::parse("warning"), Some(LogLevel::Warn));
201 assert_eq!(LogLevel::parse("TRACE"), Some(LogLevel::Trace));
202 assert_eq!(LogLevel::parse("bogus"), None);
203 }
204
205 #[test]
206 fn format_parse_accepts_common_variants() {
207 assert_eq!(LoggingFormat::parse("text"), Some(LoggingFormat::Pretty));
208 assert_eq!(LoggingFormat::parse("json"), Some(LoggingFormat::Json));
209 assert_eq!(LoggingFormat::parse("xml"), None);
210 }
211}