use std::collections::HashMap;
use serde::Deserialize;
use crate::config::Config;
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogFormat {
#[default]
Plaintext,
Json,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct StdoutLayerOptions {
#[serde(default = "default_color")]
pub color: bool,
}
fn default_color() -> bool {
true
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RotationPeriod {
Hourly,
#[default]
Daily,
Weekly,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FileLayerOptions {
pub path: String,
#[serde(default)]
pub rotation: Option<RotationPeriod>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "lowercase", tag = "type")]
pub enum TracingLayerType {
Stdout(Box<StdoutLayerOptions>),
File(Box<FileLayerOptions>),
}
#[derive(Debug, Deserialize, Clone)]
pub struct TracingLayer {
pub name: String,
#[serde(default)]
pub format: Option<LogFormat>,
#[serde(default)]
pub filter: Option<String>,
#[serde(flatten)]
pub kind: TracingLayerType,
}
fn deserialize_layers<'de, D>(deserializer: D) -> Result<HashMap<String, TracingLayer>, D::Error>
where
D: serde::Deserializer<'de>,
{
let layers_vec: Vec<TracingLayer> = Vec::deserialize(deserializer)?;
Ok(
layers_vec
.into_iter()
.map(|l| (l.name.clone(), l))
.collect(),
)
}
#[derive(Debug, Clone, Deserialize)]
pub struct TracingConfig {
#[serde(default = "default_filter")]
pub filter: String,
#[serde(default)]
pub format: LogFormat,
#[serde(default, deserialize_with = "deserialize_layers")]
pub layers: HashMap<String, TracingLayer>,
#[serde(default, rename = "use", alias = "targets")]
pub targets: Vec<String>,
}
fn default_filter() -> String {
"info".to_string()
}
impl Default for TracingConfig {
fn default() -> Self {
Self {
filter: default_filter(),
format: LogFormat::default(),
layers: HashMap::new(),
targets: Vec::new(),
}
}
}
impl Config for TracingConfig {
fn name() -> &'static str {
"tracing"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tracing_config_deserialize() {
let toml_str = r#"
filter = "info"
format = "json"
layers = [
{ name = "stdout", type = "stdout", format = "plaintext" },
{ name = "dev_file", type = "file", path = "logs/dev.log" },
{ name = "prod_file", type = "file", path = "logs/prod.log", rotation = "daily" }
]
use = ["stdout", "dev_file"]
"#;
let config: TracingConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.filter, "info");
assert_eq!(config.layers.len(), 3);
assert!(config.layers.contains_key("stdout"));
assert!(config.layers.contains_key("dev_file"));
assert!(config.layers.contains_key("prod_file"));
assert_eq!(config.targets.len(), 2);
assert_eq!(config.targets[0], "stdout");
assert_eq!(config.targets[1], "dev_file");
}
#[test]
fn test_tracing_config_with_filter_field() {
let toml_str = r#"
filter = "my_crate=debug,other_crate=trace"
layers = [
{ name = "stdout", type = "stdout", filter = "debug" }
]
"#;
let config: TracingConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.filter, "my_crate=debug,other_crate=trace");
assert_eq!(
config.layers.get("stdout").unwrap().filter.as_deref(),
Some("debug")
);
}
#[test]
fn test_tracing_config_with_targets_alias() {
let toml_str = r#"
layers = [
{ name = "stdout", type = "stdout" }
]
targets = ["stdout"]
"#;
let config: TracingConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.targets, vec!["stdout"]);
}
#[test]
fn test_tracing_config_deserialize_duplicate_names() {
let toml_str = r#"
layers = [
{ name = "stdout", type = "stdout", format = "plaintext" },
{ name = "stdout", type = "stdout", format = "json" }
]
"#;
let config: TracingConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.layers.len(), 1);
assert!(config.layers.contains_key("stdout"));
}
}