Skip to main content

hdds_logger/
lib.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2// Copyright (c) 2025-2026 naskel.com
3
4//! HDDS Distributed Logger
5//!
6//! Aggregate and centralize logs from distributed DDS participants.
7//!
8//! # Features
9//!
10//! - **Log Collection**: Subscribe to DDS log topics or aggregate telemetry
11//! - **Multiple Formats**: JSON (ELK-ready), plain text, syslog (RFC 5424)
12//! - **Flexible Output**: File (with rotation), stdout, syslog daemon
13//! - **Filtering**: By log level, participant, topic pattern
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use hdds_logger::{LogCollector, LogConfig, OutputFormat};
19//!
20//! let config = LogConfig::builder()
21//!     .format(OutputFormat::Json)
22//!     .output_file("logs/hdds.log")
23//!     .level(LogLevel::Debug)
24//!     .build();
25//!
26//! let collector = LogCollector::new(config)?;
27//! collector.run()?;
28//! ```
29
30mod collector;
31mod filter;
32mod formatter;
33mod output;
34
35pub use collector::{LogCollector, LogEntry, LogSource, StopHandle};
36pub use filter::{LogFilter, LogLevel};
37pub use formatter::{LogFormatter, OutputFormat};
38pub use output::{FileRotation, LogOutput, OutputConfig};
39
40use serde::{Deserialize, Serialize};
41use std::path::PathBuf;
42
43/// Logger configuration.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct LogConfig {
46    /// Output format.
47    pub format: OutputFormat,
48    /// Output configuration.
49    pub output: OutputConfig,
50    /// Log filter settings.
51    pub filter: LogFilter,
52    /// DDS domain ID to monitor.
53    pub domain_id: u32,
54    /// Log topic name pattern (supports wildcards).
55    pub topic_pattern: String,
56}
57
58impl Default for LogConfig {
59    fn default() -> Self {
60        Self {
61            format: OutputFormat::Text,
62            output: OutputConfig::Stdout,
63            filter: LogFilter::default(),
64            domain_id: 0,
65            topic_pattern: "rt/rosout".to_string(),
66        }
67    }
68}
69
70impl LogConfig {
71    /// Create a new builder.
72    pub fn builder() -> LogConfigBuilder {
73        LogConfigBuilder::default()
74    }
75}
76
77/// Builder for LogConfig.
78#[derive(Debug, Default)]
79pub struct LogConfigBuilder {
80    format: Option<OutputFormat>,
81    output: Option<OutputConfig>,
82    filter: Option<LogFilter>,
83    domain_id: Option<u32>,
84    topic_pattern: Option<String>,
85}
86
87impl LogConfigBuilder {
88    /// Set output format.
89    pub fn format(mut self, format: OutputFormat) -> Self {
90        self.format = Some(format);
91        self
92    }
93
94    /// Set output to file with optional rotation.
95    pub fn output_file(mut self, path: impl Into<PathBuf>) -> Self {
96        self.output = Some(OutputConfig::File {
97            path: path.into(),
98            rotation: None,
99        });
100        self
101    }
102
103    /// Set output to file with rotation.
104    pub fn output_file_rotated(mut self, path: impl Into<PathBuf>, rotation: FileRotation) -> Self {
105        self.output = Some(OutputConfig::File {
106            path: path.into(),
107            rotation: Some(rotation),
108        });
109        self
110    }
111
112    /// Set output to stdout.
113    pub fn output_stdout(mut self) -> Self {
114        self.output = Some(OutputConfig::Stdout);
115        self
116    }
117
118    /// Set output to syslog.
119    pub fn output_syslog(mut self, facility: SyslogFacility) -> Self {
120        self.output = Some(OutputConfig::Syslog { facility });
121        self
122    }
123
124    /// Set minimum log level.
125    pub fn level(mut self, level: LogLevel) -> Self {
126        let mut filter = self.filter.take().unwrap_or_default();
127        filter.min_level = level;
128        self.filter = Some(filter);
129        self
130    }
131
132    /// Set participant filter (GUID prefix pattern).
133    pub fn participant_filter(mut self, pattern: impl Into<String>) -> Self {
134        let mut filter = self.filter.take().unwrap_or_default();
135        filter.participant_pattern = Some(pattern.into());
136        self.filter = Some(filter);
137        self
138    }
139
140    /// Set topic filter pattern.
141    pub fn topic_filter(mut self, pattern: impl Into<String>) -> Self {
142        let mut filter = self.filter.take().unwrap_or_default();
143        filter.topic_pattern = Some(pattern.into());
144        self.filter = Some(filter);
145        self
146    }
147
148    /// Set DDS domain ID.
149    pub fn domain_id(mut self, id: u32) -> Self {
150        self.domain_id = Some(id);
151        self
152    }
153
154    /// Set log topic pattern to subscribe.
155    pub fn topic_pattern(mut self, pattern: impl Into<String>) -> Self {
156        self.topic_pattern = Some(pattern.into());
157        self
158    }
159
160    /// Build the configuration.
161    pub fn build(self) -> LogConfig {
162        LogConfig {
163            format: self.format.unwrap_or(OutputFormat::Text),
164            output: self.output.unwrap_or(OutputConfig::Stdout),
165            filter: self.filter.unwrap_or_default(),
166            domain_id: self.domain_id.unwrap_or(0),
167            topic_pattern: self
168                .topic_pattern
169                .unwrap_or_else(|| "rt/rosout".to_string()),
170        }
171    }
172}
173
174/// Syslog facility (RFC 5424).
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
176pub enum SyslogFacility {
177    Kern,
178    User,
179    Mail,
180    Daemon,
181    Auth,
182    Syslog,
183    Lpr,
184    News,
185    Uucp,
186    Cron,
187    #[default]
188    Local0,
189    Local1,
190    Local2,
191    Local3,
192    Local4,
193    Local5,
194    Local6,
195    Local7,
196}
197
198impl SyslogFacility {
199    /// Get the numeric facility code.
200    pub fn code(&self) -> u8 {
201        match self {
202            Self::Kern => 0,
203            Self::User => 1,
204            Self::Mail => 2,
205            Self::Daemon => 3,
206            Self::Auth => 4,
207            Self::Syslog => 5,
208            Self::Lpr => 6,
209            Self::News => 7,
210            Self::Uucp => 8,
211            Self::Cron => 9,
212            Self::Local0 => 16,
213            Self::Local1 => 17,
214            Self::Local2 => 18,
215            Self::Local3 => 19,
216            Self::Local4 => 20,
217            Self::Local5 => 21,
218            Self::Local6 => 22,
219            Self::Local7 => 23,
220        }
221    }
222}