use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TracerConfig {
#[serde(rename = "networkMagic")]
pub network_magic: u32,
pub network: Network,
#[serde(rename = "loRequestNum")]
pub lo_request_num: Option<u16>,
#[serde(rename = "ekgRequestFreq")]
pub ekg_request_freq: Option<f64>,
#[serde(rename = "hasEKG")]
pub has_ekg: Option<Endpoint>,
#[serde(rename = "hasPrometheus")]
pub has_prometheus: Option<Endpoint>,
#[serde(rename = "hasForwarding")]
pub has_forwarding: Option<ReForwardingConfig>,
pub logging: Vec<LoggingParams>,
pub rotation: Option<RotationParams>,
pub verbosity: Option<Verbosity>,
#[serde(rename = "metricsNoSuffix")]
pub metrics_no_suffix: Option<bool>,
#[serde(rename = "ekgRequestFull")]
pub ekg_request_full: Option<bool>,
#[serde(rename = "prometheusLabels")]
pub prometheus_labels: Option<HashMap<String, String>>,
}
impl TracerConfig {
pub fn from_file(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("reading config file {}", path.display()))?;
Self::from_yaml(&content)
}
pub fn from_yaml(yaml: &str) -> Result<Self> {
serde_yaml::from_str(yaml).context("parsing TracerConfig YAML")
}
pub fn lo_request_num(&self) -> u16 {
self.lo_request_num.unwrap_or(100)
}
pub fn ekg_request_freq(&self) -> f64 {
self.ekg_request_freq.unwrap_or(1.0)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "tag", content = "contents")]
pub enum Network {
AcceptAt(Address),
ConnectTo(Vec<Address>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Address {
LocalPipe(PathBuf),
RemoteSocket(String, u16),
}
impl Address {
pub fn to_node_id(&self) -> String {
match self {
Address::LocalPipe(p) => p.display().to_string(),
Address::RemoteSocket(host, port) => format!("{}:{}", host, port),
}
}
}
impl<'de> Deserialize<'de> for Address {
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
let s = String::deserialize(de)?;
if let Some(idx) = s.rfind(':') {
let potential_port = &s[idx + 1..];
if let Ok(port) = potential_port.parse::<u16>() {
let host = s[..idx].to_string();
if !host.contains('/') {
return Ok(Address::RemoteSocket(host, port));
}
}
}
Ok(Address::LocalPipe(PathBuf::from(s)))
}
}
impl Serialize for Address {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
match self {
Address::LocalPipe(p) => s.serialize_str(&p.display().to_string()),
Address::RemoteSocket(host, port) => s.serialize_str(&format!("{}:{}", host, port)),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Endpoint {
#[serde(rename = "epHost")]
pub ep_host: String,
#[serde(rename = "epPort")]
pub ep_port: u16,
}
impl Endpoint {
pub fn to_addr(&self) -> String {
format!("{}:{}", self.ep_host, self.ep_port)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LoggingParams {
#[serde(rename = "logRoot")]
pub log_root: PathBuf,
#[serde(rename = "logMode")]
pub log_mode: LogMode,
#[serde(rename = "logFormat")]
pub log_format: LogFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum LogMode {
FileMode,
JournalMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum LogFormat {
ForHuman,
ForMachine,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RotationParams {
#[serde(rename = "rpFrequencySecs")]
pub rp_frequency_secs: u32,
#[serde(rename = "rpLogLimitBytes")]
pub rp_log_limit_bytes: u64,
#[serde(rename = "rpMaxAgeHours")]
pub rp_max_age_hours: u64,
#[serde(rename = "rpKeepFilesNum")]
pub rp_keep_files_num: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum Verbosity {
Maximum,
ErrorsOnly,
Minimum,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ReForwardingConfig {
pub network: Network,
#[serde(rename = "namespaceFilters")]
pub namespace_filters: Option<Vec<Vec<String>>>,
#[serde(rename = "forwarderOpts")]
pub forwarder_opts: TraceOptionForwarder,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TraceOptionForwarder {
#[serde(rename = "queueSize", default = "default_queue_size")]
pub queue_size: usize,
}
fn default_queue_size() -> usize {
1000
}
#[cfg(test)]
mod tests {
use super::*;
const MINIMAL_YAML: &str = r#"
networkMagic: 42
network:
tag: AcceptAt
contents: "/tmp/hermod.sock"
logging:
- logRoot: "/tmp/hermod-logs"
logMode: FileMode
logFormat: ForMachine
"#;
const COMPLETE_YAML: &str = r#"
networkMagic: 42
network:
tag: ConnectTo
contents:
- "/tmp/hermod.sock"
loRequestNum: 100
ekgRequestFreq: 2
hasEKG:
epHost: 127.0.0.1
epPort: 9754
hasPrometheus:
epHost: 127.0.0.1
epPort: 9753
logging:
- logRoot: "/tmp/hermod-logs-human"
logMode: FileMode
logFormat: ForHuman
- logRoot: "/tmp/hermod-logs"
logMode: FileMode
logFormat: ForMachine
rotation:
rpFrequencySecs: 15
rpKeepFilesNum: 1
rpLogLimitBytes: 50000
rpMaxAgeHours: 1
verbosity: ErrorsOnly
"#;
#[test]
fn test_parse_minimal_yaml() {
let cfg = TracerConfig::from_yaml(MINIMAL_YAML).unwrap();
assert_eq!(cfg.network_magic, 42);
assert!(matches!(cfg.network, Network::AcceptAt(_)));
if let Network::AcceptAt(addr) = &cfg.network {
assert_eq!(*addr, Address::LocalPipe("/tmp/hermod.sock".into()));
}
assert_eq!(cfg.logging.len(), 1);
assert_eq!(cfg.logging[0].log_format, LogFormat::ForMachine);
assert_eq!(cfg.lo_request_num(), 100); assert!((cfg.ekg_request_freq() - 1.0).abs() < f64::EPSILON); }
#[test]
fn test_parse_complete_yaml() {
let cfg = TracerConfig::from_yaml(COMPLETE_YAML).unwrap();
assert_eq!(cfg.network_magic, 42);
assert!(matches!(cfg.network, Network::ConnectTo(_)));
if let Network::ConnectTo(addrs) = &cfg.network {
assert_eq!(addrs.len(), 1);
assert_eq!(addrs[0], Address::LocalPipe("/tmp/hermod.sock".into()));
}
assert_eq!(cfg.lo_request_num(), 100);
assert!((cfg.ekg_request_freq() - 2.0).abs() < f64::EPSILON);
assert!(cfg.has_ekg.is_some());
assert!(cfg.has_prometheus.is_some());
let prom = cfg.has_prometheus.as_ref().unwrap();
assert_eq!(prom.ep_host, "127.0.0.1");
assert_eq!(prom.ep_port, 9753);
assert_eq!(cfg.logging.len(), 2);
assert_eq!(cfg.logging[0].log_format, LogFormat::ForHuman);
assert_eq!(cfg.logging[1].log_format, LogFormat::ForMachine);
let rot = cfg.rotation.as_ref().unwrap();
assert_eq!(rot.rp_frequency_secs, 15);
assert_eq!(rot.rp_log_limit_bytes, 50000);
assert_eq!(rot.rp_max_age_hours, 1);
assert_eq!(rot.rp_keep_files_num, 1);
assert_eq!(cfg.verbosity, Some(Verbosity::ErrorsOnly));
}
#[test]
fn test_address_parsing_unix() {
let addr: Address = serde_yaml::from_str("\"/tmp/my.sock\"").unwrap();
assert_eq!(addr, Address::LocalPipe("/tmp/my.sock".into()));
}
#[test]
fn test_address_parsing_tcp() {
let addr: Address = serde_yaml::from_str("\"127.0.0.1:9999\"").unwrap();
assert_eq!(addr, Address::RemoteSocket("127.0.0.1".to_string(), 9999));
}
#[test]
fn test_lo_request_num_default() {
let cfg = TracerConfig::from_yaml(MINIMAL_YAML).unwrap();
assert_eq!(cfg.lo_request_num(), 100);
}
}