Skip to main content

orchestrator_config/config/
observability.rs

1use serde::{Deserialize, Serialize};
2use tracing::Level;
3
4/// Supported log verbosity levels for orchestrator components.
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
6#[serde(rename_all = "snake_case")]
7pub enum LogLevel {
8    /// Emit only error events.
9    Error,
10    /// Emit warnings and errors.
11    Warn,
12    /// Emit standard operational information.
13    #[default]
14    Info,
15    /// Emit debug diagnostics.
16    Debug,
17    /// Emit highly verbose trace diagnostics.
18    Trace,
19}
20
21impl LogLevel {
22    /// Converts the config enum to the corresponding `tracing` level.
23    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    /// Parses a case-insensitive log level string.
34    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    /// Returns the more verbose of two log levels.
46    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/// Output encoding used by log sinks.
59#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
60#[serde(rename_all = "snake_case")]
61pub enum LoggingFormat {
62    /// Human-readable text output.
63    #[default]
64    Pretty,
65    /// Structured JSON output.
66    Json,
67}
68
69impl LoggingFormat {
70    /// Parses a case-insensitive logging-format string.
71    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/// Settings for console log output.
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82pub struct ConsoleLoggingConfig {
83    /// Enables or disables console logging.
84    #[serde(default = "default_enabled")]
85    pub enabled: bool,
86    /// Output format used for console logs.
87    #[serde(default)]
88    pub format: LoggingFormat,
89    /// Enables ANSI coloring for console logs.
90    #[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/// Settings for file-based log output.
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106pub struct FileLoggingConfig {
107    /// Enables or disables file logging.
108    #[serde(default = "default_enabled")]
109    pub enabled: bool,
110    /// Output format used for file logs.
111    #[serde(default = "default_file_format")]
112    pub format: LoggingFormat,
113    /// Directory where log files are written.
114    #[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/// Aggregate logging configuration for the orchestrator runtime.
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
130pub struct LoggingConfig {
131    /// Minimum log level emitted by configured sinks.
132    #[serde(default)]
133    pub level: LogLevel,
134    /// Console sink configuration.
135    #[serde(default)]
136    pub console: ConsoleLoggingConfig,
137    /// File sink configuration.
138    #[serde(default)]
139    pub file: FileLoggingConfig,
140    /// Whether to bridge internal events into the log stream.
141    #[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/// Top-level observability configuration.
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
158pub struct ObservabilityConfig {
159    /// Logging configuration for the runtime.
160    #[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}