Skip to main content

mxr_store/
event_log.rs

1use mxr_core::AccountId;
2use sqlx::Row;
3
4pub struct EventLogEntry {
5    pub id: i64,
6    pub timestamp: i64,
7    pub level: String,
8    pub category: String,
9    pub account_id: Option<AccountId>,
10    pub message_id: Option<String>,
11    pub rule_id: Option<String>,
12    pub summary: String,
13    pub details: Option<String>,
14}
15
16#[derive(Clone, Copy, Debug, Default)]
17pub struct EventLogRefs<'a> {
18    pub account_id: Option<&'a AccountId>,
19    pub message_id: Option<&'a str>,
20    pub rule_id: Option<&'a str>,
21}
22
23impl super::Store {
24    pub async fn insert_event_refs(
25        &self,
26        level: &str,
27        category: &str,
28        summary: &str,
29        refs: EventLogRefs<'_>,
30        details: Option<&str>,
31    ) -> Result<(), sqlx::Error> {
32        let now = chrono::Utc::now().timestamp();
33        let aid = refs.account_id.map(|account_id| account_id.as_str());
34        sqlx::query(
35            "INSERT INTO event_log (
36                timestamp,
37                level,
38                category,
39                account_id,
40                message_id,
41                rule_id,
42                summary,
43                details
44             ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
45        )
46        .bind(now)
47        .bind(level)
48        .bind(category)
49        .bind(aid)
50        .bind(refs.message_id)
51        .bind(refs.rule_id)
52        .bind(summary)
53        .bind(details)
54        .execute(self.writer())
55        .await?;
56        Ok(())
57    }
58
59    pub async fn insert_event(
60        &self,
61        level: &str,
62        category: &str,
63        summary: &str,
64        account_id: Option<&AccountId>,
65        details: Option<&str>,
66    ) -> Result<(), sqlx::Error> {
67        self.insert_event_refs(
68            level,
69            category,
70            summary,
71            EventLogRefs {
72                account_id,
73                ..EventLogRefs::default()
74            },
75            details,
76        )
77        .await
78    }
79
80    // Dynamic SQL — kept as runtime query since the WHERE clause is conditionally built
81    pub async fn list_events(
82        &self,
83        limit: u32,
84        level: Option<&str>,
85        category: Option<&str>,
86    ) -> Result<Vec<EventLogEntry>, sqlx::Error> {
87        let mut sql = String::from("SELECT * FROM event_log WHERE 1=1");
88        if level.is_some() {
89            sql.push_str(" AND level = ?");
90        }
91        if category.is_some() {
92            sql.push_str(" AND category = ?");
93        }
94        sql.push_str(" ORDER BY timestamp DESC LIMIT ?");
95
96        let mut query = sqlx::query(&sql);
97        if let Some(l) = level {
98            query = query.bind(l);
99        }
100        if let Some(c) = category {
101            query = query.bind(c);
102        }
103        query = query.bind(limit);
104
105        let rows = query.fetch_all(self.reader()).await?;
106
107        Ok(rows
108            .iter()
109            .map(|r| {
110                let aid: Option<String> = r.get("account_id");
111                EventLogEntry {
112                    id: r.get("id"),
113                    timestamp: r.get("timestamp"),
114                    level: r.get("level"),
115                    category: r.get("category"),
116                    account_id: aid
117                        .map(|s| AccountId::from_uuid(uuid::Uuid::parse_str(&s).unwrap())),
118                    message_id: r.get("message_id"),
119                    rule_id: r.get("rule_id"),
120                    summary: r.get("summary"),
121                    details: r.get("details"),
122                }
123            })
124            .collect())
125    }
126
127    pub async fn prune_events_before(&self, cutoff_timestamp: i64) -> Result<u64, sqlx::Error> {
128        let result = sqlx::query("DELETE FROM event_log WHERE timestamp < ?")
129            .bind(cutoff_timestamp)
130            .execute(self.writer())
131            .await?;
132        Ok(result.rows_affected())
133    }
134
135    pub async fn latest_event_timestamp(
136        &self,
137        category: &str,
138        summary_prefix: Option<&str>,
139    ) -> Result<Option<chrono::DateTime<chrono::Utc>>, sqlx::Error> {
140        let mut sql = String::from("SELECT timestamp FROM event_log WHERE category = ?");
141        if summary_prefix.is_some() {
142            sql.push_str(" AND summary LIKE ?");
143        }
144        sql.push_str(" ORDER BY timestamp DESC LIMIT 1");
145
146        let mut query = sqlx::query_scalar::<_, i64>(&sql).bind(category);
147        if let Some(prefix) = summary_prefix {
148            query = query.bind(format!("{prefix}%"));
149        }
150
151        Ok(query
152            .fetch_optional(self.reader())
153            .await?
154            .and_then(|timestamp| chrono::DateTime::from_timestamp(timestamp, 0)))
155    }
156}