feat(m1): pattern system — PatternFile loader and classify()
Add EventSource enum and update SystemEvent in watcher.rs (M0 stub updated to support Task 5 clean deletion). Create patterns.rs with PatternFile/Pattern/MatchedEvent types, TOML loader, and classify() matching against source + text. Five unit tests covering journald, applog, no-match, and source discrimination cases.
This commit is contained in:
parent
a94e3dbb66
commit
1e733a062b
3 changed files with 179 additions and 5 deletions
|
|
@ -1,6 +1,7 @@
|
|||
mod commands;
|
||||
mod config;
|
||||
mod distro;
|
||||
mod patterns;
|
||||
mod tray;
|
||||
mod watcher;
|
||||
|
||||
|
|
|
|||
168
src-tauri/src/patterns.rs
Normal file
168
src-tauri/src/patterns.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct PatternMeta {
|
||||
pub source_os: String,
|
||||
pub target_distro_family: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Pattern {
|
||||
pub id: String,
|
||||
pub sources: Vec<String>,
|
||||
pub match_text: String,
|
||||
pub severity: String,
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct PatternFile {
|
||||
pub meta: PatternMeta,
|
||||
#[serde(default)]
|
||||
pub log_paths: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub patterns: Vec<Pattern>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MatchedEvent {
|
||||
pub pattern_id: String,
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub severity: String,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
pub fn load(source_os: &str, distro_family: &str) -> Result<PatternFile> {
|
||||
let resource_path = format!("patterns/{}-to-{}.toml", source_os, distro_family);
|
||||
let content = std::fs::read_to_string(&resource_path)
|
||||
.with_context(|| format!("pattern file not found: {resource_path}"))?;
|
||||
toml::from_str(&content).with_context(|| format!("failed to parse {resource_path}"))
|
||||
}
|
||||
|
||||
pub fn classify(event: &crate::watcher::SystemEvent, pf: &PatternFile) -> Option<MatchedEvent> {
|
||||
use crate::watcher::EventSource;
|
||||
|
||||
for pattern in &pf.patterns {
|
||||
if !event.raw_line.contains(&pattern.match_text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let source_matches = pattern.sources.iter().any(|s| {
|
||||
if s == "any" {
|
||||
return true;
|
||||
}
|
||||
match &event.source {
|
||||
EventSource::Journald => s == "journald",
|
||||
EventSource::Kmsg => s == "kmsg",
|
||||
EventSource::AppLog(app) => s == &format!("applog:{app}"),
|
||||
}
|
||||
});
|
||||
|
||||
if source_matches {
|
||||
return Some(MatchedEvent {
|
||||
pattern_id: pattern.id.clone(),
|
||||
title: pattern.title.clone(),
|
||||
body: pattern.body.clone(),
|
||||
severity: pattern.severity.clone(),
|
||||
timestamp: event.timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const FIXTURE: &str = r#"
|
||||
[meta]
|
||||
source_os = "macos"
|
||||
target_distro_family = "arch"
|
||||
|
||||
[log_paths]
|
||||
steam = "~/.local/share/Steam/logs/content_log.txt"
|
||||
|
||||
[[patterns]]
|
||||
id = "aur-build-failure"
|
||||
sources = ["journald"]
|
||||
match_text = "error: failed to build"
|
||||
severity = "warn"
|
||||
title = "AUR package build failed"
|
||||
body = "A package failed to compile from source."
|
||||
|
||||
[[patterns]]
|
||||
id = "proton-runtime"
|
||||
sources = ["applog:steam"]
|
||||
match_text = "wine: cannot find"
|
||||
severity = "warn"
|
||||
title = "Proton runtime issue"
|
||||
body = "Steam Proton couldn't find a required file."
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn loads_pattern_file_from_toml() {
|
||||
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
|
||||
assert_eq!(pf.meta.source_os, "macos");
|
||||
assert_eq!(pf.patterns.len(), 2);
|
||||
assert_eq!(
|
||||
pf.log_paths.get("steam").unwrap(),
|
||||
"~/.local/share/Steam/logs/content_log.txt"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_matches_journald_event() {
|
||||
use crate::watcher::{EventSource, SystemEvent};
|
||||
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
|
||||
let event = SystemEvent {
|
||||
source: EventSource::Journald,
|
||||
raw_line: ":: error: failed to build (foo-git)".into(),
|
||||
timestamp: 0,
|
||||
};
|
||||
let matched = classify(&event, &pf).unwrap();
|
||||
assert_eq!(matched.pattern_id, "aur-build-failure");
|
||||
assert_eq!(matched.title, "AUR package build failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_no_match_returns_none() {
|
||||
use crate::watcher::{EventSource, SystemEvent};
|
||||
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
|
||||
let event = SystemEvent {
|
||||
source: EventSource::Journald,
|
||||
raw_line: "systemd: Started NetworkManager.service".into(),
|
||||
timestamp: 0,
|
||||
};
|
||||
assert!(classify(&event, &pf).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_applog_matches_correct_source() {
|
||||
use crate::watcher::{EventSource, SystemEvent};
|
||||
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
|
||||
let event = SystemEvent {
|
||||
source: EventSource::AppLog("steam".into()),
|
||||
raw_line: "wine: cannot find /run/media/user/game.exe".into(),
|
||||
timestamp: 0,
|
||||
};
|
||||
let matched = classify(&event, &pf).unwrap();
|
||||
assert_eq!(matched.pattern_id, "proton-runtime");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_does_not_match_wrong_source() {
|
||||
use crate::watcher::{EventSource, SystemEvent};
|
||||
let pf: PatternFile = toml::from_str(FIXTURE).unwrap();
|
||||
let event = SystemEvent {
|
||||
source: EventSource::Journald,
|
||||
raw_line: "wine: cannot find something".into(),
|
||||
timestamp: 0,
|
||||
};
|
||||
assert!(classify(&event, &pf).is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
///
|
||||
/// M0 stub: defines the types and the spawn interface so the rest of the app
|
||||
/// can wire up event handling now. Actual journald/dmesg reading lands in M1.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -13,11 +12,17 @@ pub enum EventSeverity {
|
|||
Crit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EventSource {
|
||||
Journald,
|
||||
Kmsg,
|
||||
AppLog(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SystemEvent {
|
||||
pub severity: EventSeverity,
|
||||
pub source: String,
|
||||
pub message: String,
|
||||
pub source: EventSource,
|
||||
pub raw_line: String,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue