pub mod constant;
pub mod log_replay;
pub mod log_template;
pub mod sawtooth;
pub mod sine;
pub mod uniform;
use std::collections::HashMap;
use serde::Deserialize;
use self::constant::Constant;
use self::log_replay::LogReplayGenerator;
use self::log_template::{LogTemplateGenerator, TemplateEntry};
use self::sawtooth::Sawtooth;
use self::sine::Sine;
use self::uniform::UniformRandom;
use crate::model::log::{LogEvent, Severity};
use crate::SondaError;
pub trait ValueGenerator: Send + Sync {
fn value(&self, tick: u64) -> f64;
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum GeneratorConfig {
#[serde(rename = "constant")]
Constant {
value: f64,
},
#[serde(rename = "uniform")]
Uniform {
min: f64,
max: f64,
seed: Option<u64>,
},
#[serde(rename = "sine")]
Sine {
amplitude: f64,
period_secs: f64,
offset: f64,
},
#[serde(rename = "sawtooth")]
Sawtooth {
min: f64,
max: f64,
period_secs: f64,
},
}
pub fn create_generator(config: &GeneratorConfig, rate: f64) -> Box<dyn ValueGenerator> {
match config {
GeneratorConfig::Constant { value } => Box::new(Constant::new(*value)),
GeneratorConfig::Uniform { min, max, seed } => {
Box::new(UniformRandom::new(*min, *max, seed.unwrap_or(0)))
}
GeneratorConfig::Sine {
amplitude,
period_secs,
offset,
} => Box::new(Sine::new(*amplitude, *period_secs, *offset, rate)),
GeneratorConfig::Sawtooth {
min,
max,
period_secs,
} => Box::new(Sawtooth::new(*min, *max, *period_secs, rate)),
}
}
pub trait LogGenerator: Send + Sync {
fn generate(&self, tick: u64) -> LogEvent;
}
#[derive(Debug, Clone, Deserialize)]
pub struct TemplateConfig {
pub message: String,
#[serde(default)]
pub field_pools: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum LogGeneratorConfig {
#[serde(rename = "template")]
Template {
templates: Vec<TemplateConfig>,
#[serde(default)]
severity_weights: Option<HashMap<String, f64>>,
seed: Option<u64>,
},
#[serde(rename = "replay")]
Replay {
file: String,
},
}
fn parse_severity(s: &str) -> Result<Severity, SondaError> {
match s.to_lowercase().as_str() {
"trace" => Ok(Severity::Trace),
"debug" => Ok(Severity::Debug),
"info" => Ok(Severity::Info),
"warn" | "warning" => Ok(Severity::Warn),
"error" => Ok(Severity::Error),
"fatal" => Ok(Severity::Fatal),
other => Err(SondaError::Config(format!(
"unknown severity {:?}: must be one of trace, debug, info, warn, error, fatal",
other
))),
}
}
pub fn create_log_generator(
config: &LogGeneratorConfig,
) -> Result<Box<dyn LogGenerator>, SondaError> {
match config {
LogGeneratorConfig::Template {
templates,
severity_weights,
seed,
} => {
let seed = seed.unwrap_or(0);
let weights: Vec<(Severity, f64)> = if let Some(map) = severity_weights {
let mut pairs = Vec::with_capacity(map.len());
for (name, weight) in map {
let severity = parse_severity(name)?;
pairs.push((severity, *weight));
}
pairs.sort_by(|a, b| a.0.cmp(&b.0));
pairs
} else {
vec![]
};
let entries: Vec<TemplateEntry> = templates
.iter()
.map(|tc| TemplateEntry {
message: tc.message.clone(),
field_pools: tc.field_pools.clone(),
})
.collect();
Ok(Box::new(LogTemplateGenerator::new(entries, weights, seed)))
}
LogGeneratorConfig::Replay { file } => {
let path = std::path::Path::new(file);
Ok(Box::new(LogReplayGenerator::from_file(path)?))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn factory_constant_returns_configured_value() {
let config = GeneratorConfig::Constant { value: 1.0 };
let gen = create_generator(&config, 100.0);
assert_eq!(gen.value(0), 1.0);
assert_eq!(gen.value(1_000_000), 1.0);
}
#[test]
fn factory_uniform_returns_values_in_range() {
let config = GeneratorConfig::Uniform {
min: 0.0,
max: 1.0,
seed: Some(7),
};
let gen = create_generator(&config, 100.0);
for tick in 0..1000 {
let v = gen.value(tick);
assert!(
v >= 0.0 && v <= 1.0,
"uniform value {v} out of [0,1] at tick {tick}"
);
}
}
#[test]
fn factory_uniform_seed_none_defaults_to_zero_seed() {
let config_none = GeneratorConfig::Uniform {
min: 0.0,
max: 1.0,
seed: None,
};
let config_zero = GeneratorConfig::Uniform {
min: 0.0,
max: 1.0,
seed: Some(0),
};
let gen_none = create_generator(&config_none, 1.0);
let gen_zero = create_generator(&config_zero, 1.0);
for tick in 0..100 {
assert_eq!(
gen_none.value(tick),
gen_zero.value(tick),
"seed=None must equal seed=Some(0) at tick {tick}"
);
}
}
#[test]
fn factory_sine_value_at_zero_equals_offset() {
let config = GeneratorConfig::Sine {
amplitude: 5.0,
period_secs: 10.0,
offset: 3.0,
};
let gen = create_generator(&config, 1.0);
assert!(
(gen.value(0) - 3.0).abs() < 1e-10,
"sine factory: value(0) must equal offset"
);
}
#[test]
fn factory_sawtooth_value_at_zero_equals_min() {
let config = GeneratorConfig::Sawtooth {
min: 5.0,
max: 15.0,
period_secs: 10.0,
};
let gen = create_generator(&config, 1.0);
assert_eq!(
gen.value(0),
5.0,
"sawtooth factory: value(0) must equal min"
);
}
#[test]
fn deserialize_constant_config() {
let yaml = "type: constant\nvalue: 42.0\n";
let config: GeneratorConfig = serde_yaml::from_str(yaml).expect("deserialize constant");
match config {
GeneratorConfig::Constant { value } => {
assert_eq!(value, 42.0);
}
_ => panic!("expected Constant variant"),
}
}
#[test]
fn deserialize_uniform_config_with_seed() {
let yaml = "type: uniform\nmin: 1.0\nmax: 5.0\nseed: 99\n";
let config: GeneratorConfig = serde_yaml::from_str(yaml).expect("deserialize uniform");
match config {
GeneratorConfig::Uniform { min, max, seed } => {
assert_eq!(min, 1.0);
assert_eq!(max, 5.0);
assert_eq!(seed, Some(99));
}
_ => panic!("expected Uniform variant"),
}
}
#[test]
fn deserialize_uniform_config_without_seed() {
let yaml = "type: uniform\nmin: 0.0\nmax: 10.0\n";
let config: GeneratorConfig =
serde_yaml::from_str(yaml).expect("deserialize uniform no seed");
match config {
GeneratorConfig::Uniform { min, max, seed } => {
assert_eq!(min, 0.0);
assert_eq!(max, 10.0);
assert_eq!(seed, None);
}
_ => panic!("expected Uniform variant"),
}
}
#[test]
fn deserialize_sine_config() {
let yaml = "type: sine\namplitude: 5.0\nperiod_secs: 30\noffset: 10.0\n";
let config: GeneratorConfig = serde_yaml::from_str(yaml).expect("deserialize sine");
match config {
GeneratorConfig::Sine {
amplitude,
period_secs,
offset,
} => {
assert_eq!(amplitude, 5.0);
assert_eq!(period_secs, 30.0);
assert_eq!(offset, 10.0);
}
_ => panic!("expected Sine variant"),
}
}
#[test]
fn deserialize_sawtooth_config() {
let yaml = "type: sawtooth\nmin: 0.0\nmax: 100.0\nperiod_secs: 60.0\n";
let config: GeneratorConfig = serde_yaml::from_str(yaml).expect("deserialize sawtooth");
match config {
GeneratorConfig::Sawtooth {
min,
max,
period_secs,
} => {
assert_eq!(min, 0.0);
assert_eq!(max, 100.0);
assert_eq!(period_secs, 60.0);
}
_ => panic!("expected Sawtooth variant"),
}
}
fn assert_send_sync<T: Send + Sync>() {}
#[test]
fn generators_are_send_and_sync() {
assert_send_sync::<crate::generator::uniform::UniformRandom>();
assert_send_sync::<crate::generator::sine::Sine>();
assert_send_sync::<crate::generator::sawtooth::Sawtooth>();
assert_send_sync::<crate::generator::constant::Constant>();
}
#[test]
fn deserialize_log_template_config_minimal() {
let yaml = "\
type: template
templates:
- message: \"hello {name}\"
field_pools:
name:
- alice
- bob
";
let config: LogGeneratorConfig =
serde_yaml::from_str(yaml).expect("deserialize template config");
match config {
LogGeneratorConfig::Template {
templates,
severity_weights,
seed,
} => {
assert_eq!(templates.len(), 1);
assert_eq!(templates[0].message, "hello {name}");
assert!(templates[0].field_pools.contains_key("name"));
assert_eq!(
templates[0].field_pools["name"],
vec!["alice".to_string(), "bob".to_string()]
);
assert!(
severity_weights.is_none(),
"severity_weights must default to None"
);
assert!(seed.is_none(), "seed must default to None");
}
_ => panic!("expected Template variant"),
}
}
#[test]
fn deserialize_log_template_config_with_weights_and_seed() {
let yaml = "\
type: template
templates:
- message: \"msg\"
field_pools: {}
severity_weights:
info: 0.7
warn: 0.2
error: 0.1
seed: 42
";
let config: LogGeneratorConfig =
serde_yaml::from_str(yaml).expect("deserialize template config with weights");
match config {
LogGeneratorConfig::Template {
severity_weights,
seed,
..
} => {
let weights = severity_weights.expect("severity_weights should be present");
assert!((weights["info"] - 0.7).abs() < 1e-10);
assert!((weights["warn"] - 0.2).abs() < 1e-10);
assert!((weights["error"] - 0.1).abs() < 1e-10);
assert_eq!(seed, Some(42));
}
_ => panic!("expected Template variant"),
}
}
#[test]
fn deserialize_log_replay_config() {
let yaml = "type: replay\nfile: /var/log/app.log\n";
let config: LogGeneratorConfig =
serde_yaml::from_str(yaml).expect("deserialize replay config");
match config {
LogGeneratorConfig::Replay { file } => {
assert_eq!(file, "/var/log/app.log");
}
_ => panic!("expected Replay variant"),
}
}
#[test]
fn factory_template_config_creates_working_generator() {
let config = LogGeneratorConfig::Template {
templates: vec![TemplateConfig {
message: "event {id}".into(),
field_pools: {
let mut m = HashMap::new();
m.insert("id".into(), vec!["1".into(), "2".into(), "3".into()]);
m
},
}],
severity_weights: None,
seed: Some(0),
};
let gen = create_log_generator(&config).expect("template factory must succeed");
let event = gen.generate(0);
assert!(!event.message.contains('{'));
}
#[test]
fn factory_template_config_seed_none_defaults_correctly() {
let config = LogGeneratorConfig::Template {
templates: vec![TemplateConfig {
message: "static message".into(),
field_pools: HashMap::new(),
}],
severity_weights: None,
seed: None,
};
let gen = create_log_generator(&config).expect("template with seed=None must succeed");
assert_eq!(gen.generate(0).message, "static message");
}
#[test]
fn factory_template_invalid_severity_key_returns_error() {
let config = LogGeneratorConfig::Template {
templates: vec![TemplateConfig {
message: "msg".into(),
field_pools: HashMap::new(),
}],
severity_weights: {
let mut m = HashMap::new();
m.insert("bogus".into(), 1.0);
Some(m)
},
seed: None,
};
let result = create_log_generator(&config);
assert!(
result.is_err(),
"invalid severity key 'bogus' must produce Err"
);
}
#[test]
fn factory_replay_config_missing_file_returns_error() {
let config = LogGeneratorConfig::Replay {
file: "/this/path/does/not/exist.log".into(),
};
let result = create_log_generator(&config);
assert!(result.is_err(), "missing replay file must produce Err");
}
#[test]
fn factory_replay_config_creates_working_generator() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut tmp = NamedTempFile::new().expect("create temp file");
writeln!(tmp, "line one").expect("write");
writeln!(tmp, "line two").expect("write");
let config = LogGeneratorConfig::Replay {
file: tmp.path().to_string_lossy().into_owned(),
};
let gen =
create_log_generator(&config).expect("replay factory with real file must succeed");
assert_eq!(gen.generate(0).message, "line one");
assert_eq!(gen.generate(1).message, "line two");
assert_eq!(gen.generate(2).message, "line one");
}
#[test]
fn log_generators_are_send_and_sync() {
assert_send_sync::<crate::generator::log_template::LogTemplateGenerator>();
assert_send_sync::<crate::generator::log_replay::LogReplayGenerator>();
}
}