use serde::Deserialize;
use serde_json::Value;
#[derive(Debug, Clone, Deserialize)]
pub struct ObservabilityConfig {
pub log: LogConfig,
pub trace: TraceConfig,
pub metrics: Option<MetricsConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LogConfig {
pub format: Option<LogFormat>,
pub filter: LogFilterConfig,
pub console: Option<LogConsoleConfig>,
pub local: LogLocalConfig,
pub remote: LogRemoteConfig,
pub dynamic: DynamicLogConfig,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum LogFormat {
Json,
Compact,
Full,
Pretty,
}
impl Default for LogFormat {
fn default() -> Self {
Self::Json
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct LogConsoleConfig {
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LogFilterConfig {
pub default_level: String,
#[serde(default)]
pub overrides: Vec<LogFilterOverrideConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LogFilterOverrideConfig {
pub name: String,
pub enabled: Option<bool>,
pub level: String,
#[serde(default)]
pub priority: i64,
#[serde(default)]
pub fuzzy_rules: Vec<FuzzyRuleConfig>,
#[serde(default)]
pub field_rules: Vec<FieldRuleConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LogLocalConfig {
pub enabled: Option<bool>,
pub file_dir: String,
pub file_name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LogRemoteConfig {
pub enabled: Option<bool>,
pub endpoint: String,
pub filter: Option<LogFilterConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DynamicLogConfig {
pub enabled: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TraceConfig {
pub enabled: bool,
pub exporter: String,
pub endpoint: String,
pub service_name: String,
pub service_version: Option<String>,
pub filter: Option<LogFilterConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MetricsConfig {
pub enabled: bool,
pub exporter: String,
pub endpoint: String,
pub service_name: String,
pub service_version: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FuzzyRuleKind {
Target,
Span,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FuzzyMatchType {
Exact,
Prefix,
Contains,
Glob,
Regex,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FuzzyRuleConfig {
pub kind: FuzzyRuleKind,
pub match_type: FuzzyMatchType,
pub pattern: String,
pub level: Option<String>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FieldRuleOp {
Eq,
Ne,
Contains,
StartsWith,
EndsWith,
Regex,
In,
Exists,
NotExists,
Gt,
Gte,
Lt,
Lte,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FieldRuleConfig {
pub field: String,
pub op: FieldRuleOp,
#[serde(default)]
pub value: Value,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn toml_config_parses_filter_overrides_and_remote_filter() {
let config: ObservabilityConfig = toml::from_str(
r#"
[log]
format = "json"
[log.filter]
default_level = "info"
[[log.filter.overrides]]
name = "decoder_debug"
enabled = true
level = "debug"
priority = 100
[[log.filter.overrides.fuzzy_rules]]
kind = "target"
match_type = "contains"
pattern = "decoder"
[[log.filter.overrides.field_rules]]
field = "id"
op = "eq"
value = 100
[log.console]
enabled = true
[log.local]
enabled = false
file_dir = "logs"
file_name = "app.log"
[log.remote]
enabled = true
endpoint = "http://127.0.0.1:4318/v1/logs"
[log.remote.filter]
default_level = "off"
[[log.remote.filter.overrides]]
name = "remote_id_100"
enabled = true
level = "info"
priority = 200
[[log.remote.filter.overrides.fuzzy_rules]]
kind = "target"
match_type = "exact"
pattern = "app::remote"
[[log.remote.filter.overrides.field_rules]]
field = "id"
op = "eq"
value = 100
[log.dynamic]
enabled = true
[trace]
enabled = true
exporter = "otlp"
endpoint = "http://127.0.0.1:4318/v1/traces"
service_name = "pi_logger_test"
service_version = "1.0.0"
[trace.filter]
default_level = "off"
[[trace.filter.overrides]]
name = "trace_test_on"
enabled = true
level = "info"
priority = 100
[[trace.filter.overrides.fuzzy_rules]]
kind = "target"
match_type = "contains"
pattern = "trace_test"
[metrics]
enabled = true
exporter = "otlp"
endpoint = "http://127.0.0.1:4318/v1/metrics"
service_name = "pi_logger_test"
service_version = "1.0.0"
"#,
)
.unwrap();
assert_eq!(config.log.format, Some(LogFormat::Json));
assert_eq!(config.log.filter.default_level, "info");
assert_eq!(config.log.filter.overrides.len(), 1);
assert_eq!(config.log.filter.overrides[0].name, "decoder_debug");
assert_eq!(config.log.filter.overrides[0].enabled, Some(true));
assert_eq!(
config.log.filter.overrides[0].fuzzy_rules[0].kind,
FuzzyRuleKind::Target
);
assert_eq!(
config.log.filter.overrides[0].fuzzy_rules[0].match_type,
FuzzyMatchType::Contains
);
assert_eq!(
config.log.filter.overrides[0].field_rules[0].op,
FieldRuleOp::Eq
);
let remote_filter = config.log.remote.filter.as_ref().unwrap();
assert_eq!(remote_filter.default_level, "off");
assert_eq!(remote_filter.overrides[0].name, "remote_id_100");
assert_eq!(remote_filter.overrides[0].priority, 200);
assert_eq!(config.log.console.unwrap().enabled, Some(true));
assert_eq!(config.log.local.enabled, Some(false));
assert_eq!(config.log.remote.enabled, Some(true));
assert!(config.log.dynamic.enabled);
assert!(config.trace.enabled);
assert_eq!(config.trace.service_name, "pi_logger_test");
assert_eq!(config.trace.service_version.as_deref(), Some("1.0.0"));
assert_eq!(
config.trace.filter.as_ref().unwrap().overrides[0].name,
"trace_test_on"
);
let metrics = config.metrics.as_ref().unwrap();
assert!(metrics.enabled);
assert_eq!(metrics.exporter, "otlp");
assert_eq!(metrics.service_name, "pi_logger_test");
assert_eq!(metrics.service_version.as_deref(), Some("1.0.0"));
}
#[test]
fn toml_override_defaults_empty_rule_lists_and_priority() {
let config: LogFilterConfig = toml::from_str(
r#"
default_level = "warn"
[[overrides]]
name = "global_info"
level = "info"
"#,
)
.unwrap();
assert_eq!(config.default_level, "warn");
assert_eq!(config.overrides.len(), 1);
assert_eq!(config.overrides[0].enabled, None);
assert_eq!(config.overrides[0].priority, 0);
assert!(config.overrides[0].fuzzy_rules.is_empty());
assert!(config.overrides[0].field_rules.is_empty());
}
}