Skip to content

Commit 0bba586

Browse files
committed
feat: filter log types using include/exclude patterns
Allow users to filter log types reported by log plugins using include and exclude patterns in the plugin configuration.
1 parent 2952453 commit 0bba586

File tree

11 files changed

+477
-9
lines changed

11 files changed

+477
-9
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/extensions/tedge_log_manager/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ repository = { workspace = true }
1313
anyhow = { workspace = true }
1414
async-trait = { workspace = true }
1515
camino = { workspace = true }
16+
glob = { workspace = true }
17+
serde = { workspace = true }
1618
serde_json = { workspace = true }
1719
tedge_actors = { workspace = true }
1820
tedge_api = { workspace = true }
@@ -30,6 +32,7 @@ tracing = { workspace = true }
3032
[dev-dependencies]
3133
tedge_actors = { workspace = true, features = ["test-helpers"] }
3234
tedge_test_utils = { workspace = true }
35+
tempfile = { workspace = true }
3336
time = { workspace = true, features = ["macros"] }
3437

3538
[lints]

crates/extensions/tedge_log_manager/src/actor.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::error::LogManagementError;
22
use super::LogManagerConfig;
33
use super::DEFAULT_PLUGIN_CONFIG_FILE_NAME;
4+
use crate::config::PluginConfig;
45
use crate::plugin_manager::ExternalPlugins;
56
use async_trait::async_trait;
67
use std::collections::HashMap;
@@ -59,6 +60,7 @@ pub struct LogManagerActor {
5960
messages: SimpleMessageBox<LogInput, LogOutput>,
6061
upload_sender: DynSender<LogUploadRequest>,
6162
external_plugins: ExternalPlugins,
63+
plugin_config: PluginConfig,
6264
}
6365

6466
#[async_trait]
@@ -97,12 +99,14 @@ impl LogManagerActor {
9799
upload_sender: DynSender<LogUploadRequest>,
98100
external_plugins: ExternalPlugins,
99101
) -> Self {
102+
let plugin_config = PluginConfig::from_file(&config.plugin_config_path);
100103
Self {
101104
config,
102105
pending_operations: HashMap::new(),
103106
messages,
104107
upload_sender,
105108
external_plugins,
109+
plugin_config,
106110
}
107111
}
108112

@@ -280,6 +284,9 @@ impl LogManagerActor {
280284
async fn reload_supported_log_types(&mut self) -> Result<(), RuntimeError> {
281285
info!(target: "log plugins", "Reloading supported log types");
282286

287+
// Reload plugin configuration for up-to-date filtering rules
288+
self.plugin_config = PluginConfig::from_file(&self.config.plugin_config_path);
289+
283290
// Note: The log manager now only handles external plugins.
284291
// The file-based plugin configuration is handled by the standalone plugin.
285292
self.external_plugins.load().await?;
@@ -305,7 +312,21 @@ impl LogManagerActor {
305312
target: "log plugins",
306313
"Plugin {} supports log types: {:?}", plugin_type, log_types
307314
);
308-
for log_type in log_types {
315+
316+
// Apply filtering from plugin config
317+
let filtered_log_types =
318+
self.plugin_config.filter_log_types(&plugin_type, log_types);
319+
320+
if !filtered_log_types.is_empty() {
321+
info!(
322+
target: "log plugins",
323+
"Log types from {} plugin after filtering: {:?}",
324+
plugin_type,
325+
filtered_log_types
326+
);
327+
}
328+
329+
for log_type in filtered_log_types {
309330
if plugin_type == "file" {
310331
// For the file plugin, add log types without suffix (default behavior)
311332
types.push(log_type);

crates/extensions/tedge_log_manager/src/config.rs

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use camino::Utf8Path;
22
use camino::Utf8PathBuf;
3+
use serde::Deserialize;
4+
use std::collections::HashMap;
35
use std::path::PathBuf;
46
use std::sync::Arc;
57
use tedge_api::mqtt_topics::Channel;
@@ -87,3 +89,289 @@ impl LogManagerConfig {
8789
})
8890
}
8991
}
92+
93+
/// Plugin filtering configuration parsed from tedge-log-plugin.toml
94+
#[derive(Clone, Deserialize, Debug, Default, PartialEq, Eq)]
95+
pub struct PluginConfig {
96+
#[serde(default)]
97+
pub plugins: HashMap<String, Vec<PluginFilterEntry>>,
98+
}
99+
100+
/// Individual filter entry for a plugin
101+
#[derive(Clone, Deserialize, Debug, PartialEq, Eq)]
102+
pub struct PluginFilterEntry {
103+
#[serde(default)]
104+
pub include: Option<String>,
105+
#[serde(default)]
106+
pub exclude: Option<String>,
107+
}
108+
109+
impl PluginConfig {
110+
/// Load plugin configuration from the given path
111+
pub fn from_file(path: &std::path::Path) -> Self {
112+
match std::fs::read_to_string(path) {
113+
Ok(contents) => match toml::from_str(&contents) {
114+
Ok(config) => config,
115+
Err(err) => {
116+
tracing::warn!(
117+
"Failed to parse plugin config from {}: {}",
118+
path.display(),
119+
err
120+
);
121+
Self::default()
122+
}
123+
},
124+
Err(_) => {
125+
// File doesn't exist or not readable - this is OK, just use defaults
126+
Self::default()
127+
}
128+
}
129+
}
130+
131+
pub(crate) fn get_filters(&self, plugin_name: &str) -> Option<&Vec<PluginFilterEntry>> {
132+
self.plugins.get(plugin_name)
133+
}
134+
135+
/// Apply filtering to log types from a plugin
136+
///
137+
/// Filtering logic:
138+
/// - If no filters defined, return all types as-is
139+
/// - When only include is specified: only types matching any include filter are returned
140+
/// - When only exclude is specified: all types not matching any exclude filter are returned
141+
/// - When both are specified: include the types that matches any include filter OR doesn't match any exclude filter
142+
pub fn filter_log_types(&self, plugin_name: &str, log_types: Vec<String>) -> Vec<String> {
143+
let Some(filters) = self.get_filters(plugin_name) else {
144+
// No filters defined for the plugin type, include all types
145+
return log_types;
146+
};
147+
148+
let mut exclude_patterns = Vec::new();
149+
let mut include_patterns = Vec::new();
150+
151+
for filter in filters {
152+
if let Some(pattern) = &filter.include {
153+
if let Ok(compiled) = glob::Pattern::new(pattern) {
154+
include_patterns.push(compiled);
155+
} else {
156+
tracing::warn!(
157+
"Invalid include pattern for plugin {}: {}",
158+
plugin_name,
159+
pattern
160+
);
161+
}
162+
}
163+
if let Some(pattern) = &filter.exclude {
164+
if let Ok(compiled) = glob::Pattern::new(pattern) {
165+
exclude_patterns.push(compiled);
166+
} else {
167+
tracing::warn!(
168+
"Invalid exclude pattern for plugin {}: {}",
169+
plugin_name,
170+
pattern
171+
);
172+
}
173+
}
174+
}
175+
176+
// If no filter patterns are defined, include all
177+
if include_patterns.is_empty() && exclude_patterns.is_empty() {
178+
return log_types;
179+
}
180+
181+
let has_include = !include_patterns.is_empty();
182+
let has_exclude = !exclude_patterns.is_empty();
183+
184+
// include a type if it matches any include pattern OR doesn't match any exclude pattern
185+
log_types
186+
.into_iter()
187+
.filter(|log_type| {
188+
let matches_include = include_patterns.iter().any(|p| p.matches(log_type));
189+
let matches_exclude = exclude_patterns.iter().any(|p| p.matches(log_type));
190+
191+
match (has_include, has_exclude) {
192+
(true, true) => {
193+
// Both specified: include if the entry matches include OR doesn't match exclude
194+
matches_include || !matches_exclude
195+
}
196+
(true, false) => {
197+
// Only include specified: entry must match any include filter
198+
matches_include
199+
}
200+
(false, true) => {
201+
// Only exclude specified: entry must not match any exclude filter
202+
!matches_exclude
203+
}
204+
(false, false) => {
205+
// No filter patterns are defined, include
206+
true
207+
}
208+
}
209+
})
210+
.collect()
211+
}
212+
}
213+
214+
#[cfg(test)]
215+
mod tests {
216+
use crate::config::PluginConfig;
217+
use crate::config::PluginFilterEntry;
218+
use std::collections::HashMap;
219+
use std::io::Write;
220+
use tempfile::NamedTempFile;
221+
222+
#[test]
223+
fn test_plugin_config_from_toml() {
224+
let toml_content = r#"
225+
[[plugins.journald]]
226+
include = "ssh"
227+
228+
[[plugins.journald]]
229+
include = "tedge-agent"
230+
231+
[[plugins.dmesg]]
232+
exclude = "all"
233+
234+
[[plugins.file]]
235+
exclude = "unwanted-*"
236+
"#;
237+
238+
let mut temp_file = NamedTempFile::new().unwrap();
239+
temp_file.write_all(toml_content.as_bytes()).unwrap();
240+
temp_file.flush().unwrap();
241+
242+
let config = PluginConfig::from_file(temp_file.path());
243+
244+
// Check journald filters
245+
let journald_filters = config.get_filters("journald").unwrap();
246+
assert_eq!(journald_filters.len(), 2);
247+
assert_eq!(journald_filters[0].include.as_ref().unwrap(), "ssh");
248+
assert_eq!(journald_filters[1].include.as_ref().unwrap(), "tedge-agent");
249+
250+
// Check dmesg filters
251+
let dmesg_filters = config.get_filters("dmesg").unwrap();
252+
assert_eq!(dmesg_filters.len(), 1);
253+
assert_eq!(dmesg_filters[0].exclude.as_ref().unwrap(), "all");
254+
255+
// Check file filters
256+
let file_filters = config.get_filters("file").unwrap();
257+
assert_eq!(file_filters.len(), 1);
258+
assert_eq!(file_filters[0].exclude.as_ref().unwrap(), "unwanted-*");
259+
}
260+
261+
#[test]
262+
fn test_plugin_config_no_filters_returns_all() {
263+
let config = PluginConfig::default();
264+
let log_types: Vec<String> = ["ssh", "tedge-agent", "mosquitto"]
265+
.iter()
266+
.map(|s| s.to_string())
267+
.collect();
268+
269+
let filtered = config.filter_log_types("journald", log_types.clone());
270+
assert_eq!(filtered, log_types);
271+
}
272+
273+
#[test]
274+
fn test_plugin_filter_include_only() {
275+
let mut plugins = HashMap::new();
276+
plugins.insert(
277+
"journald".to_string(),
278+
vec![
279+
PluginFilterEntry {
280+
include: Some("ssh".to_string()),
281+
exclude: None,
282+
},
283+
PluginFilterEntry {
284+
include: Some("tedge-*".to_string()),
285+
exclude: None,
286+
},
287+
],
288+
);
289+
290+
let config = PluginConfig { plugins };
291+
let log_types: Vec<String> = ["ssh", "tedge-agent", "mosquitto", "systemd-logind"]
292+
.iter()
293+
.map(|s| s.to_string())
294+
.collect();
295+
296+
let filtered = config.filter_log_types("journald", log_types);
297+
assert_eq!(filtered, vec!["ssh".to_string(), "tedge-agent".to_string()]);
298+
}
299+
300+
#[test]
301+
fn test_plugin_filter_exclude_only() {
302+
let mut plugins = HashMap::new();
303+
plugins.insert(
304+
"journald".to_string(),
305+
vec![
306+
PluginFilterEntry {
307+
include: None,
308+
exclude: Some("mosquitto".to_string()),
309+
},
310+
PluginFilterEntry {
311+
include: None,
312+
exclude: Some("systemd-*".to_string()),
313+
},
314+
],
315+
);
316+
317+
let config = PluginConfig { plugins };
318+
let log_types: Vec<String> = [
319+
"ssh",
320+
"tedge-agent",
321+
"mosquitto",
322+
"systemd-logind",
323+
"systemd-journald",
324+
]
325+
.iter()
326+
.map(|s| s.to_string())
327+
.collect();
328+
329+
let filtered = config.filter_log_types("journald", log_types);
330+
assert_eq!(filtered, vec!["ssh".to_string(), "tedge-agent".to_string()]);
331+
}
332+
333+
#[test]
334+
fn test_plugin_filter_combined_include_and_exclude() {
335+
let mut plugins = HashMap::new();
336+
plugins.insert(
337+
"journald".to_string(),
338+
vec![
339+
PluginFilterEntry {
340+
include: None,
341+
exclude: Some("systemd-*".to_string()),
342+
},
343+
PluginFilterEntry {
344+
include: Some("systemd-logind".to_string()),
345+
exclude: None,
346+
},
347+
PluginFilterEntry {
348+
include: Some("tedge-*".to_string()),
349+
exclude: None,
350+
},
351+
],
352+
);
353+
354+
let config = PluginConfig { plugins };
355+
let log_types: Vec<String> = [
356+
"ssh",
357+
"tedge-agent",
358+
"mosquitto",
359+
"systemd-logind",
360+
"systemd-journald",
361+
]
362+
.iter()
363+
.map(|s| s.to_string())
364+
.collect();
365+
366+
let filtered = config.filter_log_types("journald", log_types);
367+
assert_eq!(
368+
filtered,
369+
vec![
370+
"ssh".to_string(),
371+
"tedge-agent".to_string(),
372+
"mosquitto".to_string(),
373+
"systemd-logind".to_string()
374+
]
375+
);
376+
}
377+
}

0 commit comments

Comments
 (0)