1use anyhow::{Context, Result};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16#[derive(Debug, Clone, Deserialize, Serialize)]
18pub struct TracerConfig {
19 #[serde(rename = "networkMagic")]
21 pub network_magic: u32,
22
23 pub network: Network,
25
26 #[serde(rename = "loRequestNum")]
28 pub lo_request_num: Option<u16>,
29
30 #[serde(rename = "ekgRequestFreq")]
32 pub ekg_request_freq: Option<f64>,
33
34 #[serde(rename = "hasEKG")]
36 pub has_ekg: Option<Endpoint>,
37
38 #[serde(rename = "hasPrometheus")]
40 pub has_prometheus: Option<Endpoint>,
41
42 #[serde(rename = "hasForwarding")]
44 pub has_forwarding: Option<ReForwardingConfig>,
45
46 pub logging: Vec<LoggingParams>,
48
49 pub rotation: Option<RotationParams>,
51
52 pub verbosity: Option<Verbosity>,
54
55 #[serde(rename = "metricsNoSuffix")]
57 pub metrics_no_suffix: Option<bool>,
58
59 #[serde(rename = "ekgRequestFull")]
61 pub ekg_request_full: Option<bool>,
62
63 #[serde(rename = "prometheusLabels")]
65 pub prometheus_labels: Option<HashMap<String, String>>,
66}
67
68impl TracerConfig {
69 pub fn from_file(path: &Path) -> Result<Self> {
71 let content = std::fs::read_to_string(path)
72 .with_context(|| format!("reading config file {}", path.display()))?;
73 Self::from_yaml(&content)
74 }
75
76 pub fn from_yaml(yaml: &str) -> Result<Self> {
78 serde_yaml::from_str(yaml).context("parsing TracerConfig YAML")
79 }
80
81 pub fn lo_request_num(&self) -> u16 {
83 self.lo_request_num.unwrap_or(100)
84 }
85
86 pub fn ekg_request_freq(&self) -> f64 {
88 self.ekg_request_freq.unwrap_or(1.0)
89 }
90}
91
92#[derive(Debug, Clone, Deserialize, Serialize)]
94#[serde(tag = "tag", content = "contents")]
95pub enum Network {
96 AcceptAt(Address),
98 ConnectTo(Vec<Address>),
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum Address {
105 LocalPipe(PathBuf),
107 RemoteSocket(String, u16),
109}
110
111impl Address {
112 pub fn to_node_id(&self) -> String {
114 match self {
115 Address::LocalPipe(p) => p.display().to_string(),
116 Address::RemoteSocket(host, port) => format!("{}:{}", host, port),
117 }
118 }
119}
120
121impl<'de> Deserialize<'de> for Address {
122 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
123 let s = String::deserialize(de)?;
124 if let Some(idx) = s.rfind(':') {
126 let potential_port = &s[idx + 1..];
127 if let Ok(port) = potential_port.parse::<u16>() {
128 let host = s[..idx].to_string();
129 if !host.contains('/') {
131 return Ok(Address::RemoteSocket(host, port));
132 }
133 }
134 }
135 Ok(Address::LocalPipe(PathBuf::from(s)))
136 }
137}
138
139impl Serialize for Address {
140 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
141 match self {
142 Address::LocalPipe(p) => s.serialize_str(&p.display().to_string()),
143 Address::RemoteSocket(host, port) => s.serialize_str(&format!("{}:{}", host, port)),
144 }
145 }
146}
147
148#[derive(Debug, Clone, Deserialize, Serialize)]
150pub struct Endpoint {
151 #[serde(rename = "epHost")]
153 pub ep_host: String,
154 #[serde(rename = "epPort")]
156 pub ep_port: u16,
157}
158
159impl Endpoint {
160 pub fn to_addr(&self) -> String {
162 format!("{}:{}", self.ep_host, self.ep_port)
163 }
164}
165
166#[derive(Debug, Clone, Deserialize, Serialize)]
168pub struct LoggingParams {
169 #[serde(rename = "logRoot")]
171 pub log_root: PathBuf,
172
173 #[serde(rename = "logMode")]
175 pub log_mode: LogMode,
176
177 #[serde(rename = "logFormat")]
179 pub log_format: LogFormat,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
184pub enum LogMode {
185 FileMode,
187 JournalMode,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
193pub enum LogFormat {
194 ForHuman,
196 ForMachine,
198}
199
200#[derive(Debug, Clone, Deserialize, Serialize)]
202pub struct RotationParams {
203 #[serde(rename = "rpFrequencySecs")]
205 pub rp_frequency_secs: u32,
206
207 #[serde(rename = "rpLogLimitBytes")]
209 pub rp_log_limit_bytes: u64,
210
211 #[serde(rename = "rpMaxAgeHours")]
213 pub rp_max_age_hours: u64,
214
215 #[serde(rename = "rpKeepFilesNum")]
217 pub rp_keep_files_num: u32,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
222pub enum Verbosity {
223 Maximum,
225 ErrorsOnly,
227 Minimum,
229}
230
231#[derive(Debug, Clone, Deserialize, Serialize)]
236pub struct ReForwardingConfig {
237 pub network: Network,
239
240 #[serde(rename = "namespaceFilters")]
242 pub namespace_filters: Option<Vec<Vec<String>>>,
243
244 #[serde(rename = "forwarderOpts")]
246 pub forwarder_opts: TraceOptionForwarder,
247}
248
249#[derive(Debug, Clone, Deserialize, Serialize)]
251pub struct TraceOptionForwarder {
252 #[serde(rename = "queueSize", default = "default_queue_size")]
254 pub queue_size: usize,
255}
256
257fn default_queue_size() -> usize {
258 1000
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 const MINIMAL_YAML: &str = r#"
266networkMagic: 42
267network:
268 tag: AcceptAt
269 contents: "/tmp/hermod.sock"
270logging:
271- logRoot: "/tmp/hermod-logs"
272 logMode: FileMode
273 logFormat: ForMachine
274"#;
275
276 const COMPLETE_YAML: &str = r#"
277networkMagic: 42
278network:
279 tag: ConnectTo
280 contents:
281 - "/tmp/hermod.sock"
282loRequestNum: 100
283ekgRequestFreq: 2
284hasEKG:
285 epHost: 127.0.0.1
286 epPort: 9754
287hasPrometheus:
288 epHost: 127.0.0.1
289 epPort: 9753
290logging:
291- logRoot: "/tmp/hermod-logs-human"
292 logMode: FileMode
293 logFormat: ForHuman
294- logRoot: "/tmp/hermod-logs"
295 logMode: FileMode
296 logFormat: ForMachine
297rotation:
298 rpFrequencySecs: 15
299 rpKeepFilesNum: 1
300 rpLogLimitBytes: 50000
301 rpMaxAgeHours: 1
302verbosity: ErrorsOnly
303"#;
304
305 #[test]
306 fn test_parse_minimal_yaml() {
307 let cfg = TracerConfig::from_yaml(MINIMAL_YAML).unwrap();
308 assert_eq!(cfg.network_magic, 42);
309 assert!(matches!(cfg.network, Network::AcceptAt(_)));
310 if let Network::AcceptAt(addr) = &cfg.network {
311 assert_eq!(*addr, Address::LocalPipe("/tmp/hermod.sock".into()));
312 }
313 assert_eq!(cfg.logging.len(), 1);
314 assert_eq!(cfg.logging[0].log_format, LogFormat::ForMachine);
315 assert_eq!(cfg.lo_request_num(), 100); assert!((cfg.ekg_request_freq() - 1.0).abs() < f64::EPSILON); }
318
319 #[test]
320 fn test_parse_complete_yaml() {
321 let cfg = TracerConfig::from_yaml(COMPLETE_YAML).unwrap();
322 assert_eq!(cfg.network_magic, 42);
323 assert!(matches!(cfg.network, Network::ConnectTo(_)));
324 if let Network::ConnectTo(addrs) = &cfg.network {
325 assert_eq!(addrs.len(), 1);
326 assert_eq!(addrs[0], Address::LocalPipe("/tmp/hermod.sock".into()));
327 }
328 assert_eq!(cfg.lo_request_num(), 100);
329 assert!((cfg.ekg_request_freq() - 2.0).abs() < f64::EPSILON);
330 assert!(cfg.has_ekg.is_some());
331 assert!(cfg.has_prometheus.is_some());
332 let prom = cfg.has_prometheus.as_ref().unwrap();
333 assert_eq!(prom.ep_host, "127.0.0.1");
334 assert_eq!(prom.ep_port, 9753);
335 assert_eq!(cfg.logging.len(), 2);
336 assert_eq!(cfg.logging[0].log_format, LogFormat::ForHuman);
337 assert_eq!(cfg.logging[1].log_format, LogFormat::ForMachine);
338 let rot = cfg.rotation.as_ref().unwrap();
339 assert_eq!(rot.rp_frequency_secs, 15);
340 assert_eq!(rot.rp_log_limit_bytes, 50000);
341 assert_eq!(rot.rp_max_age_hours, 1);
342 assert_eq!(rot.rp_keep_files_num, 1);
343 assert_eq!(cfg.verbosity, Some(Verbosity::ErrorsOnly));
344 }
345
346 #[test]
347 fn test_address_parsing_unix() {
348 let addr: Address = serde_yaml::from_str("\"/tmp/my.sock\"").unwrap();
349 assert_eq!(addr, Address::LocalPipe("/tmp/my.sock".into()));
350 }
351
352 #[test]
353 fn test_address_parsing_tcp() {
354 let addr: Address = serde_yaml::from_str("\"127.0.0.1:9999\"").unwrap();
355 assert_eq!(addr, Address::RemoteSocket("127.0.0.1".to_string(), 9999));
356 }
357
358 #[test]
359 fn test_lo_request_num_default() {
360 let cfg = TracerConfig::from_yaml(MINIMAL_YAML).unwrap();
361 assert_eq!(cfg.lo_request_num(), 100);
362 }
363}