/home/b/lab/denet/src/config.rs
Line | Count | Source |
1 | | //! Configuration structures for denet monitoring |
2 | | //! |
3 | | //! This module provides builder patterns and configuration structs to replace |
4 | | //! scattered parameters throughout the codebase. |
5 | | |
6 | | use crate::core::constants::defaults; |
7 | | use crate::error::{DenetError, Result}; |
8 | | use std::path::PathBuf; |
9 | | use std::time::Duration; |
10 | | |
11 | | /// Output format options for monitoring data |
12 | | #[derive(Clone, Debug, PartialEq, Default)] |
13 | | pub enum OutputFormat { |
14 | | #[default] |
15 | | JsonLines, |
16 | | Json, |
17 | | Csv, |
18 | | } |
19 | | |
20 | | impl std::str::FromStr for OutputFormat { |
21 | | type Err = DenetError; |
22 | | |
23 | 10 | fn from_str(s: &str) -> Result<Self> { |
24 | 10 | match s.to_lowercase().as_str() { |
25 | 10 | "json" => Ok(OutputFormat::Json)2 , |
26 | 8 | "jsonl" | "jsonlines"6 => Ok(OutputFormat::JsonLines)3 , |
27 | 5 | "csv" => Ok(OutputFormat::Csv)3 , |
28 | 2 | _ => Err(DenetError::InvalidConfiguration(format!( |
29 | 2 | "Unknown output format: {s}" |
30 | 2 | ))), |
31 | | } |
32 | 10 | } |
33 | | } |
34 | | |
35 | | impl std::fmt::Display for OutputFormat { |
36 | 3 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
37 | 3 | match self { |
38 | 1 | OutputFormat::Json => write!(f, "json"), |
39 | 1 | OutputFormat::JsonLines => write!(f, "jsonl"), |
40 | 1 | OutputFormat::Csv => write!(f, "csv"), |
41 | | } |
42 | 3 | } |
43 | | } |
44 | | |
45 | | /// Configuration for process monitoring behavior |
46 | | #[derive(Clone, Debug)] |
47 | | pub struct MonitorConfig { |
48 | | /// Base sampling interval |
49 | | pub base_interval: Duration, |
50 | | /// Maximum sampling interval for adaptive sampling |
51 | | pub max_interval: Duration, |
52 | | /// Whether to show I/O since process start instead of since monitoring start |
53 | | pub since_process_start: bool, |
54 | | /// Whether to include child processes in monitoring |
55 | | pub include_children: bool, |
56 | | /// Maximum monitoring duration (None for unlimited) |
57 | | pub max_duration: Option<Duration>, |
58 | | /// Enable eBPF profiling |
59 | | pub enable_ebpf: bool, |
60 | | } |
61 | | |
62 | | impl Default for MonitorConfig { |
63 | 10 | fn default() -> Self { |
64 | 10 | Self { |
65 | 10 | base_interval: defaults::BASE_INTERVAL, |
66 | 10 | max_interval: defaults::MAX_INTERVAL, |
67 | 10 | since_process_start: false, |
68 | 10 | include_children: true, |
69 | 10 | max_duration: None, |
70 | 10 | enable_ebpf: false, |
71 | 10 | } |
72 | 10 | } |
73 | | } |
74 | | |
75 | | impl MonitorConfig { |
76 | | /// Create a new monitor configuration builder |
77 | 1 | pub fn builder() -> MonitorConfigBuilder { |
78 | 1 | MonitorConfigBuilder::default() |
79 | 1 | } |
80 | | |
81 | | /// Validate the configuration |
82 | 9 | pub fn validate(&self) -> Result<()> { |
83 | 9 | if self.base_interval > self.max_interval { |
84 | 2 | return Err(DenetError::InvalidConfiguration( |
85 | 2 | "Base interval cannot be greater than max interval".to_string(), |
86 | 2 | )); |
87 | 7 | } |
88 | 7 | if self.base_interval.is_zero() { |
89 | 1 | return Err(DenetError::InvalidConfiguration( |
90 | 1 | "Base interval cannot be zero".to_string(), |
91 | 1 | )); |
92 | 6 | } |
93 | 6 | Ok(()) |
94 | 9 | } |
95 | | } |
96 | | |
97 | | /// Configuration for output behavior |
98 | | #[derive(Clone, Debug)] |
99 | | pub struct OutputConfig { |
100 | | /// File to write output to (None for no file output) |
101 | | pub output_file: Option<PathBuf>, |
102 | | /// Output format |
103 | | pub format: OutputFormat, |
104 | | /// Whether to store samples in memory |
105 | | pub store_in_memory: bool, |
106 | | /// Whether to suppress stdout output |
107 | | pub quiet: bool, |
108 | | /// Whether to update output in place (vs new lines) |
109 | | pub update_in_place: bool, |
110 | | } |
111 | | |
112 | | impl Default for OutputConfig { |
113 | 7 | fn default() -> Self { |
114 | 7 | Self { |
115 | 7 | output_file: None, |
116 | 7 | format: OutputFormat::default(), |
117 | 7 | store_in_memory: true, |
118 | 7 | quiet: false, |
119 | 7 | update_in_place: true, |
120 | 7 | } |
121 | 7 | } |
122 | | } |
123 | | |
124 | | impl OutputConfig { |
125 | | /// Create a new output configuration builder |
126 | 1 | pub fn builder() -> OutputConfigBuilder { |
127 | 1 | OutputConfigBuilder::default() |
128 | 1 | } |
129 | | } |
130 | | |
131 | | /// Builder for MonitorConfig |
132 | | #[derive(Default)] |
133 | | pub struct MonitorConfigBuilder { |
134 | | base_interval: Option<Duration>, |
135 | | max_interval: Option<Duration>, |
136 | | since_process_start: Option<bool>, |
137 | | include_children: Option<bool>, |
138 | | max_duration: Option<Duration>, |
139 | | enable_ebpf: Option<bool>, |
140 | | } |
141 | | |
142 | | impl MonitorConfigBuilder { |
143 | 1 | pub fn base_interval(mut self, interval: Duration) -> Self { |
144 | 1 | self.base_interval = Some(interval); |
145 | 1 | self |
146 | 1 | } |
147 | | |
148 | 2 | pub fn base_interval_ms(mut self, ms: u64) -> Self { |
149 | 2 | self.base_interval = Some(Duration::from_millis(ms)); |
150 | 2 | self |
151 | 2 | } |
152 | | |
153 | 1 | pub fn max_interval(mut self, interval: Duration) -> Self { |
154 | 1 | self.max_interval = Some(interval); |
155 | 1 | self |
156 | 1 | } |
157 | | |
158 | 2 | pub fn max_interval_ms(mut self, ms: u64) -> Self { |
159 | 2 | self.max_interval = Some(Duration::from_millis(ms)); |
160 | 2 | self |
161 | 2 | } |
162 | | |
163 | 1 | pub fn since_process_start(mut self, since_start: bool) -> Self { |
164 | 1 | self.since_process_start = Some(since_start); |
165 | 1 | self |
166 | 1 | } |
167 | | |
168 | 1 | pub fn include_children(mut self, include: bool) -> Self { |
169 | 1 | self.include_children = Some(include); |
170 | 1 | self |
171 | 1 | } |
172 | | |
173 | 1 | pub fn max_duration(mut self, duration: Duration) -> Self { |
174 | 1 | self.max_duration = Some(duration); |
175 | 1 | self |
176 | 1 | } |
177 | | |
178 | 2 | pub fn max_duration_secs(mut self, secs: u64) -> Self { |
179 | 2 | if secs > 0 { |
180 | 1 | self.max_duration = Some(Duration::from_secs(secs)); |
181 | 1 | } |
182 | 2 | self |
183 | 2 | } |
184 | | |
185 | 1 | pub fn enable_ebpf(mut self, enable: bool) -> Self { |
186 | 1 | self.enable_ebpf = Some(enable); |
187 | 1 | self |
188 | 1 | } |
189 | | |
190 | 6 | pub fn build(self) -> Result<MonitorConfig> { |
191 | 6 | let config = MonitorConfig { |
192 | 6 | base_interval: self.base_interval.unwrap_or(defaults::BASE_INTERVAL), |
193 | 6 | max_interval: self.max_interval.unwrap_or(defaults::MAX_INTERVAL), |
194 | 6 | since_process_start: self.since_process_start.unwrap_or(false), |
195 | 6 | include_children: self.include_children.unwrap_or(true), |
196 | 6 | max_duration: self.max_duration, |
197 | 6 | enable_ebpf: self.enable_ebpf.unwrap_or(false), |
198 | 6 | }; |
199 | 6 | config.validate()?1 ; |
200 | 5 | Ok(config) |
201 | 6 | } |
202 | | } |
203 | | |
204 | | /// Builder for OutputConfig |
205 | | #[derive(Default)] |
206 | | pub struct OutputConfigBuilder { |
207 | | output_file: Option<PathBuf>, |
208 | | format: Option<OutputFormat>, |
209 | | store_in_memory: Option<bool>, |
210 | | quiet: Option<bool>, |
211 | | update_in_place: Option<bool>, |
212 | | } |
213 | | |
214 | | impl OutputConfigBuilder { |
215 | 1 | pub fn output_file<P: Into<PathBuf>>(mut self, path: P) -> Self { |
216 | 1 | self.output_file = Some(path.into()); |
217 | 1 | self |
218 | 1 | } |
219 | | |
220 | 1 | pub fn format(mut self, format: OutputFormat) -> Self { |
221 | 1 | self.format = Some(format); |
222 | 1 | self |
223 | 1 | } |
224 | | |
225 | 2 | pub fn format_str(mut self, format: &str) -> Result<Self> { |
226 | 2 | self.format = Some(format.parse()?1 ); |
227 | 1 | Ok(self) |
228 | 2 | } |
229 | | |
230 | 1 | pub fn store_in_memory(mut self, store: bool) -> Self { |
231 | 1 | self.store_in_memory = Some(store); |
232 | 1 | self |
233 | 1 | } |
234 | | |
235 | 1 | pub fn quiet(mut self, quiet: bool) -> Self { |
236 | 1 | self.quiet = Some(quiet); |
237 | 1 | self |
238 | 1 | } |
239 | | |
240 | 1 | pub fn update_in_place(mut self, update: bool) -> Self { |
241 | 1 | self.update_in_place = Some(update); |
242 | 1 | self |
243 | 1 | } |
244 | | |
245 | 3 | pub fn build(self) -> OutputConfig { |
246 | 3 | OutputConfig { |
247 | 3 | output_file: self.output_file, |
248 | 3 | format: self.format.unwrap_or_default(), |
249 | 3 | store_in_memory: self.store_in_memory.unwrap_or(true), |
250 | 3 | quiet: self.quiet.unwrap_or(false), |
251 | 3 | update_in_place: self.update_in_place.unwrap_or(true), |
252 | 3 | } |
253 | 3 | } |
254 | | } |
255 | | |
256 | | /// Combined configuration for monitoring operations |
257 | | #[derive(Clone, Debug, Default)] |
258 | | pub struct DenetConfig { |
259 | | pub monitor: MonitorConfig, |
260 | | pub output: OutputConfig, |
261 | | } |
262 | | |
263 | | impl DenetConfig { |
264 | 1 | pub fn builder() -> DenetConfigBuilder { |
265 | 1 | DenetConfigBuilder::default() |
266 | 1 | } |
267 | | } |
268 | | |
269 | | /// Builder for DenetConfig |
270 | | #[derive(Default)] |
271 | | pub struct DenetConfigBuilder { |
272 | | monitor: Option<MonitorConfig>, |
273 | | output: Option<OutputConfig>, |
274 | | } |
275 | | |
276 | | impl DenetConfigBuilder { |
277 | 2 | pub fn monitor(mut self, config: MonitorConfig) -> Self { |
278 | 2 | self.monitor = Some(config); |
279 | 2 | self |
280 | 2 | } |
281 | | |
282 | 1 | pub fn output(mut self, config: OutputConfig) -> Self { |
283 | 1 | self.output = Some(config); |
284 | 1 | self |
285 | 1 | } |
286 | | |
287 | 3 | pub fn build(self) -> DenetConfig { |
288 | 3 | DenetConfig { |
289 | 3 | monitor: self.monitor.unwrap_or_default(), |
290 | 3 | output: self.output.unwrap_or_default(), |
291 | 3 | } |
292 | 3 | } |
293 | | } |
294 | | |
295 | | #[cfg(test)] |
296 | | mod tests { |
297 | | use super::*; |
298 | | use std::str::FromStr; |
299 | | use std::time::Duration; |
300 | | |
301 | | #[test] |
302 | 1 | fn test_output_format_from_str() { |
303 | 1 | // Test valid formats |
304 | 1 | assert_eq!(OutputFormat::from_str("json").unwrap(), OutputFormat::Json); |
305 | 1 | assert_eq!( |
306 | 1 | OutputFormat::from_str("jsonl").unwrap(), |
307 | 1 | OutputFormat::JsonLines |
308 | 1 | ); |
309 | 1 | assert_eq!( |
310 | 1 | OutputFormat::from_str("jsonlines").unwrap(), |
311 | 1 | OutputFormat::JsonLines |
312 | 1 | ); |
313 | 1 | assert_eq!(OutputFormat::from_str("csv").unwrap(), OutputFormat::Csv); |
314 | | |
315 | | // Test case insensitivity |
316 | 1 | assert_eq!(OutputFormat::from_str("JSON").unwrap(), OutputFormat::Json); |
317 | 1 | assert_eq!( |
318 | 1 | OutputFormat::from_str("JSONL").unwrap(), |
319 | 1 | OutputFormat::JsonLines |
320 | 1 | ); |
321 | 1 | assert_eq!(OutputFormat::from_str("CSV").unwrap(), OutputFormat::Csv); |
322 | | |
323 | | // Test invalid format |
324 | 1 | let result = OutputFormat::from_str("invalid"); |
325 | 1 | assert!(matches!0 (result, Err(DenetError::InvalidConfiguration(_)))); |
326 | 1 | } |
327 | | |
328 | | #[test] |
329 | 1 | fn test_output_format_display() { |
330 | 1 | assert_eq!(OutputFormat::Json.to_string(), "json"); |
331 | 1 | assert_eq!(OutputFormat::JsonLines.to_string(), "jsonl"); |
332 | 1 | assert_eq!(OutputFormat::Csv.to_string(), "csv"); |
333 | 1 | } |
334 | | |
335 | | #[test] |
336 | 1 | fn test_output_format_default() { |
337 | 1 | assert_eq!(OutputFormat::default(), OutputFormat::JsonLines); |
338 | 1 | } |
339 | | |
340 | | #[test] |
341 | 1 | fn test_monitor_config_default() { |
342 | 1 | let config = MonitorConfig::default(); |
343 | 1 | assert_eq!(config.base_interval, defaults::BASE_INTERVAL); |
344 | 1 | assert_eq!(config.max_interval, defaults::MAX_INTERVAL); |
345 | 1 | assert!(!config.since_process_start); |
346 | 1 | assert!(config.include_children); |
347 | 1 | assert!(config.max_duration.is_none()); |
348 | 1 | assert!(!config.enable_ebpf); |
349 | 1 | } |
350 | | |
351 | | #[test] |
352 | 1 | fn test_monitor_config_builder() { |
353 | 1 | let config = MonitorConfig::builder().build().unwrap(); |
354 | 1 | assert_eq!(config.base_interval, defaults::BASE_INTERVAL); |
355 | 1 | assert_eq!(config.max_interval, defaults::MAX_INTERVAL); |
356 | 1 | } |
357 | | |
358 | | #[test] |
359 | 1 | fn test_monitor_config_validate_success() { |
360 | 1 | let config = MonitorConfig::default(); |
361 | 1 | assert!(config.validate().is_ok()); |
362 | 1 | } |
363 | | |
364 | | #[test] |
365 | 1 | fn test_monitor_config_validate_base_greater_than_max() { |
366 | 1 | let config = MonitorConfig { |
367 | 1 | base_interval: Duration::from_millis(2000), |
368 | 1 | max_interval: Duration::from_millis(1000), |
369 | 1 | ..Default::default() |
370 | 1 | }; |
371 | 1 | let result = config.validate(); |
372 | 1 | assert!(matches!0 (result, Err(DenetError::InvalidConfiguration(_)))); |
373 | 1 | if let Err(DenetError::InvalidConfiguration(msg)) = result { |
374 | 1 | assert!(msg.contains("Base interval cannot be greater than max interval")); |
375 | 0 | } |
376 | 1 | } |
377 | | |
378 | | #[test] |
379 | 1 | fn test_monitor_config_validate_zero_base_interval() { |
380 | 1 | let config = MonitorConfig { |
381 | 1 | base_interval: Duration::from_millis(0), |
382 | 1 | ..Default::default() |
383 | 1 | }; |
384 | 1 | let result = config.validate(); |
385 | 1 | assert!(matches!0 (result, Err(DenetError::InvalidConfiguration(_)))); |
386 | 1 | if let Err(DenetError::InvalidConfiguration(msg)) = result { |
387 | 1 | assert!(msg.contains("Base interval cannot be zero")); |
388 | 0 | } |
389 | 1 | } |
390 | | |
391 | | #[test] |
392 | 1 | fn test_output_config_default() { |
393 | 1 | let config = OutputConfig::default(); |
394 | 1 | assert!(config.output_file.is_none()); |
395 | 1 | assert_eq!(config.format, OutputFormat::JsonLines); |
396 | 1 | assert!(config.store_in_memory); |
397 | 1 | assert!(!config.quiet); |
398 | 1 | assert!(config.update_in_place); |
399 | 1 | } |
400 | | |
401 | | #[test] |
402 | 1 | fn test_output_config_builder() { |
403 | 1 | let config = OutputConfig::builder().build(); |
404 | 1 | assert!(config.output_file.is_none()); |
405 | 1 | assert_eq!(config.format, OutputFormat::JsonLines); |
406 | 1 | } |
407 | | |
408 | | #[test] |
409 | 1 | fn test_monitor_config_builder_all_options() { |
410 | 1 | let config = MonitorConfigBuilder::default() |
411 | 1 | .base_interval(Duration::from_millis(200)) |
412 | 1 | .max_interval(Duration::from_millis(2000)) |
413 | 1 | .since_process_start(true) |
414 | 1 | .include_children(false) |
415 | 1 | .max_duration(Duration::from_secs(60)) |
416 | 1 | .enable_ebpf(true) |
417 | 1 | .build() |
418 | 1 | .unwrap(); |
419 | 1 | |
420 | 1 | assert_eq!(config.base_interval, Duration::from_millis(200)); |
421 | 1 | assert_eq!(config.max_interval, Duration::from_millis(2000)); |
422 | 1 | assert!(config.since_process_start); |
423 | 1 | assert!(!config.include_children); |
424 | 1 | assert_eq!(config.max_duration, Some(Duration::from_secs(60))); |
425 | 1 | assert!(config.enable_ebpf); |
426 | 1 | } |
427 | | |
428 | | #[test] |
429 | 1 | fn test_monitor_config_builder_ms_methods() { |
430 | 1 | let config = MonitorConfigBuilder::default() |
431 | 1 | .base_interval_ms(300) |
432 | 1 | .max_interval_ms(3000) |
433 | 1 | .build() |
434 | 1 | .unwrap(); |
435 | 1 | |
436 | 1 | assert_eq!(config.base_interval, Duration::from_millis(300)); |
437 | 1 | assert_eq!(config.max_interval, Duration::from_millis(3000)); |
438 | 1 | } |
439 | | |
440 | | #[test] |
441 | 1 | fn test_monitor_config_builder_max_duration_secs() { |
442 | 1 | let config = MonitorConfigBuilder::default() |
443 | 1 | .max_duration_secs(120) |
444 | 1 | .build() |
445 | 1 | .unwrap(); |
446 | 1 | |
447 | 1 | assert_eq!(config.max_duration, Some(Duration::from_secs(120))); |
448 | 1 | } |
449 | | |
450 | | #[test] |
451 | 1 | fn test_monitor_config_builder_max_duration_secs_zero() { |
452 | 1 | let config = MonitorConfigBuilder::default() |
453 | 1 | .max_duration_secs(0) |
454 | 1 | .build() |
455 | 1 | .unwrap(); |
456 | 1 | |
457 | 1 | assert!(config.max_duration.is_none()); |
458 | 1 | } |
459 | | |
460 | | #[test] |
461 | 1 | fn test_monitor_config_builder_validation_fails() { |
462 | 1 | let result = MonitorConfigBuilder::default() |
463 | 1 | .base_interval_ms(2000) |
464 | 1 | .max_interval_ms(1000) |
465 | 1 | .build(); |
466 | 1 | |
467 | 1 | assert!(result.is_err()); |
468 | 1 | } |
469 | | |
470 | | #[test] |
471 | 1 | fn test_output_config_builder_all_options() { |
472 | 1 | let config = OutputConfigBuilder::default() |
473 | 1 | .output_file("output.json") |
474 | 1 | .format(OutputFormat::Json) |
475 | 1 | .store_in_memory(false) |
476 | 1 | .quiet(true) |
477 | 1 | .update_in_place(false) |
478 | 1 | .build(); |
479 | 1 | |
480 | 1 | assert_eq!(config.output_file, Some(PathBuf::from("output.json"))); |
481 | 1 | assert_eq!(config.format, OutputFormat::Json); |
482 | 1 | assert!(!config.store_in_memory); |
483 | 1 | assert!(config.quiet); |
484 | 1 | assert!(!config.update_in_place); |
485 | 1 | } |
486 | | |
487 | | #[test] |
488 | 1 | fn test_output_config_builder_format_str() { |
489 | 1 | let result = OutputConfigBuilder::default().format_str("csv"); |
490 | 1 | assert!(result.is_ok()); |
491 | 1 | let config = result.unwrap().build(); |
492 | 1 | assert_eq!(config.format, OutputFormat::Csv); |
493 | 1 | } |
494 | | |
495 | | #[test] |
496 | 1 | fn test_output_config_builder_format_str_invalid() { |
497 | 1 | let result = OutputConfigBuilder::default().format_str("invalid"); |
498 | 1 | assert!(result.is_err()); |
499 | 1 | } |
500 | | |
501 | | #[test] |
502 | 1 | fn test_denet_config_default() { |
503 | 1 | let config = DenetConfig::default(); |
504 | 1 | assert_eq!(config.monitor.base_interval, defaults::BASE_INTERVAL); |
505 | 1 | assert_eq!(config.output.format, OutputFormat::JsonLines); |
506 | 1 | } |
507 | | |
508 | | #[test] |
509 | 1 | fn test_denet_config_builder() { |
510 | 1 | let config = DenetConfig::builder().build(); |
511 | 1 | assert_eq!(config.monitor.base_interval, defaults::BASE_INTERVAL); |
512 | 1 | assert_eq!(config.output.format, OutputFormat::JsonLines); |
513 | 1 | } |
514 | | |
515 | | #[test] |
516 | 1 | fn test_denet_config_builder_with_configs() { |
517 | 1 | let monitor_config = MonitorConfig { |
518 | 1 | base_interval: Duration::from_millis(250), |
519 | 1 | ..Default::default() |
520 | 1 | }; |
521 | 1 | let output_config = OutputConfig { |
522 | 1 | format: OutputFormat::Csv, |
523 | 1 | ..Default::default() |
524 | 1 | }; |
525 | 1 | |
526 | 1 | let config = DenetConfigBuilder::default() |
527 | 1 | .monitor(monitor_config.clone()) |
528 | 1 | .output(output_config.clone()) |
529 | 1 | .build(); |
530 | 1 | |
531 | 1 | assert_eq!(config.monitor.base_interval, monitor_config.base_interval); |
532 | 1 | assert_eq!(config.output.format, output_config.format); |
533 | 1 | } |
534 | | |
535 | | #[test] |
536 | 1 | fn test_denet_config_builder_partial() { |
537 | 1 | let monitor_config = MonitorConfig { |
538 | 1 | base_interval: Duration::from_millis(250), |
539 | 1 | ..Default::default() |
540 | 1 | }; |
541 | 1 | |
542 | 1 | let config = DenetConfigBuilder::default() |
543 | 1 | .monitor(monitor_config) |
544 | 1 | .build(); |
545 | 1 | |
546 | 1 | assert_eq!(config.monitor.base_interval, Duration::from_millis(250)); |
547 | 1 | assert_eq!(config.output.format, OutputFormat::JsonLines); // Default |
548 | 1 | } |
549 | | } |