use serde::{Deserialize, Deserializer};
#[derive(Clone, Debug, Default)]
pub struct TracerConfig {
pub enabled: bool,
pub tracing_enabled_explicit: bool,
pub pipeline_enabled: bool,
pub detail_level: DetailLevel,
pub outputs: TracerOutputs,
pub metrics_levers: MetricsLeversConfig,
}
impl<'de> Deserialize<'de> for TracerConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Raw {
#[serde(default)]
enabled: Option<bool>,
#[serde(default = "default_detail_level")]
detail_level: DetailLevel,
#[serde(default)]
outputs: TracerOutputs,
}
let raw = Raw::deserialize(deserializer)?;
Ok(Self {
enabled: raw.enabled.unwrap_or(false),
tracing_enabled_explicit: raw.enabled.is_some(),
pipeline_enabled: false,
detail_level: raw.detail_level,
outputs: raw.outputs,
metrics_levers: MetricsLeversConfig::default(),
})
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct TracerOutputs {
#[serde(default)]
pub stdout: StdoutOutput,
#[serde(default)]
pub file: Option<FileOutput>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct StdoutOutput {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_format")]
pub format: OutputFormat,
}
impl Default for StdoutOutput {
fn default() -> Self {
Self {
enabled: true,
format: OutputFormat::Json,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct FileOutput {
pub enabled: bool,
pub path: String,
#[serde(default = "default_format")]
pub format: OutputFormat,
}
#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum DetailLevel {
#[default]
Minimal,
Medium,
Full,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
#[default]
Json,
Plain,
}
#[derive(Clone, Debug, PartialEq)]
pub struct MetricsLeversConfig {
pub enabled: bool,
pub exchange: bool,
pub duration: bool,
pub components: bool,
}
impl MetricsLeversConfig {
pub fn exchanges_enabled(&self) -> bool {
self.enabled && self.exchange
}
pub fn durations_enabled(&self) -> bool {
self.enabled && self.duration
}
pub fn components_enabled(&self) -> bool {
self.enabled && self.components
}
}
impl Default for MetricsLeversConfig {
fn default() -> Self {
Self {
enabled: true,
exchange: true,
duration: true,
components: false,
}
}
}
impl<'de> Deserialize<'de> for MetricsLeversConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Raw {
#[serde(default)]
enabled: Option<bool>,
#[serde(default)]
exchange: Option<bool>,
#[serde(default)]
duration: Option<bool>,
#[serde(default)]
components: Option<bool>,
}
let raw = Raw::deserialize(deserializer)?;
Ok(Self {
enabled: raw.enabled.unwrap_or(true),
exchange: raw.exchange.unwrap_or(true),
duration: raw.duration.unwrap_or(true),
components: raw.components.unwrap_or(false),
})
}
}
fn default_detail_level() -> DetailLevel {
DetailLevel::Minimal
}
fn default_format() -> OutputFormat {
OutputFormat::Json
}
fn default_true() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tracer_config_defaults_are_stable() {
let cfg = TracerConfig::default();
assert!(!cfg.enabled);
assert_eq!(cfg.detail_level, DetailLevel::Minimal);
assert!(cfg.outputs.stdout.enabled);
assert!(matches!(cfg.outputs.stdout.format, OutputFormat::Json));
assert!(cfg.outputs.file.is_none());
}
#[test]
fn tracer_config_deserializes_lowercase_enums() {
let cfg: TracerConfig = serde_json::from_str(
r#"{
"enabled": true,
"detail_level": "full",
"outputs": {
"stdout": { "enabled": false, "format": "plain" },
"file": { "enabled": true, "path": "/tmp/trace.log", "format": "json" }
}
}"#,
)
.unwrap();
assert!(cfg.enabled);
assert_eq!(cfg.detail_level, DetailLevel::Full);
assert!(!cfg.outputs.stdout.enabled);
assert!(matches!(cfg.outputs.stdout.format, OutputFormat::Plain));
assert_eq!(cfg.outputs.file.as_ref().unwrap().path, "/tmp/trace.log");
}
}