1use crate::dispatcher::traits::SeverityF;
7use crate::protocol::types::{DetailLevel, Severity};
8use anyhow::{Context, Result};
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11use std::path::Path;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15pub enum FormatLogging {
16 HumanFormatColoured,
18 HumanFormatUncoloured,
20 MachineFormat,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
26pub enum BackendConfig {
27 Forwarder,
29 Stdout(FormatLogging),
31 EkgBackend,
33 DatapointBackend,
35}
36
37#[derive(Debug, Clone, PartialEq)]
41pub enum ConfigOption {
42 Severity(SeverityF),
44 Detail(DetailLevel),
46 Backends(Vec<BackendConfig>),
48 Limiter(f64),
50}
51
52#[derive(Debug, Clone, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct ForwarderOptions {
56 pub socket_path: Option<String>,
58 pub queue_size: Option<u32>,
60 pub max_reconnect_delay: Option<u32>,
62}
63
64#[derive(Debug, Clone, Default)]
68pub struct TraceConfig {
69 pub options: BTreeMap<Vec<String>, Vec<ConfigOption>>,
71 pub forwarder: Option<ForwarderOptions>,
73 pub node_name: Option<String>,
75}
76
77impl TraceConfig {
78 pub fn get_option<F, T>(&self, ns: &[String], selector: F) -> Option<T>
82 where
83 F: Fn(&ConfigOption) -> Option<T>,
84 {
85 let mut key = ns.to_vec();
87 loop {
88 if let Some(opts) = self.options.get(&key) {
89 if let Some(v) = opts.iter().find_map(&selector) {
90 return Some(v);
91 }
92 }
93 if key.is_empty() {
94 return None;
95 }
96 key.pop();
97 }
98 }
99
100 pub fn severity_for(&self, ns: &[String]) -> SeverityF {
102 self.get_option(ns, |o| {
103 if let ConfigOption::Severity(s) = o {
104 Some(*s)
105 } else {
106 None
107 }
108 })
109 .unwrap_or(SeverityF(Some(Severity::Warning)))
110 }
111
112 pub fn detail_for(&self, ns: &[String]) -> DetailLevel {
114 self.get_option(ns, |o| {
115 if let ConfigOption::Detail(d) = o {
116 Some(*d)
117 } else {
118 None
119 }
120 })
121 .unwrap_or(DetailLevel::DNormal)
122 }
123
124 pub fn backends_for(&self, ns: &[String]) -> Vec<BackendConfig> {
126 self.get_option(ns, |o| {
127 if let ConfigOption::Backends(b) = o {
128 Some(b.clone())
129 } else {
130 None
131 }
132 })
133 .unwrap_or_else(|| {
134 vec![
135 BackendConfig::Stdout(FormatLogging::MachineFormat),
136 BackendConfig::EkgBackend,
137 BackendConfig::Forwarder,
138 ]
139 })
140 }
141
142 pub fn limiter_for(&self, ns: &[String]) -> Option<f64> {
144 self.get_option(ns, |o| {
145 if let ConfigOption::Limiter(f) = o {
146 Some(*f)
147 } else {
148 None
149 }
150 })
151 }
152
153 pub fn forwarder_config(&self) -> Option<crate::forwarder::ForwarderConfig> {
159 let opts = self.forwarder.as_ref()?;
160 let mut cfg = crate::forwarder::ForwarderConfig::default();
161 if let Some(path) = &opts.socket_path {
162 cfg.address = crate::forwarder::ForwarderAddress::Unix(std::path::PathBuf::from(path));
163 }
164 if let Some(qs) = opts.queue_size {
165 cfg.queue_size = qs as usize;
166 }
167 if let Some(delay) = opts.max_reconnect_delay {
168 cfg.max_reconnect_delay = delay as u64;
169 }
170 cfg.node_name = self.node_name.clone();
171 Some(cfg)
172 }
173
174 pub fn from_yaml(path: &Path) -> Result<Self> {
176 let content =
177 std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
178 Self::from_yaml_str(&content)
179 }
180
181 pub fn from_yaml_str(yaml: &str) -> Result<Self> {
183 let raw: RawConfig = serde_yaml::from_str(yaml).context("parsing TraceConfig YAML")?;
184 Ok(raw.into_trace_config())
185 }
186}
187
188#[derive(Debug, Deserialize)]
194#[serde(rename_all = "PascalCase")]
195struct RawConfig {
196 #[serde(default)]
197 trace_options: BTreeMap<String, RawNamespaceOptions>,
198 #[serde(default)]
199 node_name: Option<String>,
200 }
202
203#[derive(Debug, Deserialize)]
204#[serde(rename_all = "camelCase")]
205struct RawNamespaceOptions {
206 severity: Option<RawSeverity>,
207 detail: Option<RawDetailLevel>,
208 #[serde(default)]
209 backends: Vec<String>,
210 max_frequency: Option<f64>,
211}
212
213#[derive(Debug, Deserialize)]
214#[serde(rename_all = "PascalCase")]
215enum RawSeverity {
216 Debug,
217 Info,
218 Notice,
219 Warning,
220 Error,
221 Critical,
222 Alert,
223 Emergency,
224 Silence,
225}
226
227#[derive(Debug, Deserialize)]
228#[allow(clippy::enum_variant_names)] enum RawDetailLevel {
230 DMinimal,
231 DNormal,
232 DDetailed,
233 DMaximum,
234}
235
236impl RawConfig {
237 fn into_trace_config(self) -> TraceConfig {
238 let mut options: BTreeMap<Vec<String>, Vec<ConfigOption>> = BTreeMap::new();
239
240 for (key, raw_opts) in self.trace_options {
241 let ns_key: Vec<String> = if key.is_empty() {
243 vec![]
244 } else {
245 key.split('.').map(|s| s.to_string()).collect()
246 };
247
248 let mut opts = Vec::new();
249
250 if let Some(sev) = raw_opts.severity {
251 opts.push(ConfigOption::Severity(sev.into()));
252 }
253 if let Some(det) = raw_opts.detail {
254 opts.push(ConfigOption::Detail(det.into()));
255 }
256 if !raw_opts.backends.is_empty() {
257 let backends: Vec<BackendConfig> = raw_opts
258 .backends
259 .iter()
260 .filter_map(|s| parse_backend(s))
261 .collect();
262 if !backends.is_empty() {
263 opts.push(ConfigOption::Backends(backends));
264 }
265 }
266 if let Some(freq) = raw_opts.max_frequency {
267 opts.push(ConfigOption::Limiter(freq));
268 }
269
270 if !opts.is_empty() {
271 options.insert(ns_key, opts);
272 }
273 }
274
275 TraceConfig {
276 options,
277 forwarder: None,
278 node_name: self.node_name,
279 }
280 }
281}
282
283impl From<RawSeverity> for SeverityF {
284 fn from(r: RawSeverity) -> Self {
285 match r {
286 RawSeverity::Debug => SeverityF(Some(Severity::Debug)),
287 RawSeverity::Info => SeverityF(Some(Severity::Info)),
288 RawSeverity::Notice => SeverityF(Some(Severity::Notice)),
289 RawSeverity::Warning => SeverityF(Some(Severity::Warning)),
290 RawSeverity::Error => SeverityF(Some(Severity::Error)),
291 RawSeverity::Critical => SeverityF(Some(Severity::Critical)),
292 RawSeverity::Alert => SeverityF(Some(Severity::Alert)),
293 RawSeverity::Emergency => SeverityF(Some(Severity::Emergency)),
294 RawSeverity::Silence => SeverityF(None),
295 }
296 }
297}
298
299impl From<RawDetailLevel> for DetailLevel {
300 fn from(r: RawDetailLevel) -> Self {
301 match r {
302 RawDetailLevel::DMinimal => DetailLevel::DMinimal,
303 RawDetailLevel::DNormal => DetailLevel::DNormal,
304 RawDetailLevel::DDetailed => DetailLevel::DDetailed,
305 RawDetailLevel::DMaximum => DetailLevel::DMaximum,
306 }
307 }
308}
309
310fn parse_backend(s: &str) -> Option<BackendConfig> {
311 match s.trim() {
312 "Forwarder" => Some(BackendConfig::Forwarder),
313 "EKGBackend" => Some(BackendConfig::EkgBackend),
314 "DatapointBackend" => Some(BackendConfig::DatapointBackend),
315 "Stdout HumanFormatColoured" => {
316 Some(BackendConfig::Stdout(FormatLogging::HumanFormatColoured))
317 }
318 "Stdout HumanFormatUncoloured" => {
319 Some(BackendConfig::Stdout(FormatLogging::HumanFormatUncoloured))
320 }
321 "Stdout MachineFormat" => Some(BackendConfig::Stdout(FormatLogging::MachineFormat)),
322 other => {
323 tracing::warn!("Unknown backend config string: {:?}", other);
324 None
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 const SAMPLE_YAML: &str = r#"
334UseTraceDispatcher: True
335
336TraceOptions:
337 "":
338 severity: Notice
339 detail: DNormal
340 backends:
341 - Stdout MachineFormat
342 - EKGBackend
343 - Forwarder
344
345 ChainDB:
346 severity: Info
347
348 ChainDB.AddBlockEvent.AddedBlockToQueue:
349 maxFrequency: 2.0
350"#;
351
352 #[test]
353 fn test_parse_yaml() {
354 let cfg = TraceConfig::from_yaml_str(SAMPLE_YAML).unwrap();
355
356 let global = cfg.options.get(&vec![] as &Vec<String>).unwrap();
358 assert!(
359 global
360 .iter()
361 .any(|o| matches!(o, ConfigOption::Severity(_)))
362 );
363 assert!(
364 global
365 .iter()
366 .any(|o| matches!(o, ConfigOption::Backends(_)))
367 );
368
369 let chaindb = cfg.options.get(&vec!["ChainDB".to_string()]).unwrap();
371 assert!(
372 chaindb
373 .iter()
374 .any(|o| matches!(o, ConfigOption::Severity(SeverityF(Some(Severity::Info)))))
375 );
376
377 let limiter_key = vec![
379 "ChainDB".to_string(),
380 "AddBlockEvent".to_string(),
381 "AddedBlockToQueue".to_string(),
382 ];
383 let limiter_opts = cfg.options.get(&limiter_key).unwrap();
384 assert!(
385 limiter_opts
386 .iter()
387 .any(|o| matches!(o, ConfigOption::Limiter(_)))
388 );
389 }
390
391 #[test]
392 fn test_longest_prefix_match() {
393 let cfg = TraceConfig::from_yaml_str(SAMPLE_YAML).unwrap();
394
395 let sev = cfg.severity_for(&["ChainDB".to_string()]);
397 assert_eq!(sev, SeverityF(Some(Severity::Info)));
398
399 let sev2 = cfg.severity_for(&["ChainDB".to_string(), "SomeChild".to_string()]);
401 assert_eq!(sev2, SeverityF(Some(Severity::Info)));
402
403 let sev3 = cfg.severity_for(&["Unknown".to_string()]);
405 assert_eq!(sev3, SeverityF(Some(Severity::Notice)));
406 }
407
408 #[test]
409 fn test_backends_parsing() {
410 let cfg = TraceConfig::from_yaml_str(SAMPLE_YAML).unwrap();
411 let backends = cfg.backends_for(&[]);
412 assert!(backends.contains(&BackendConfig::Forwarder));
413 assert!(backends.contains(&BackendConfig::Stdout(FormatLogging::MachineFormat)));
414 assert!(backends.contains(&BackendConfig::EkgBackend));
415 }
416}