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 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}