1use rustc_hash::FxHashMap;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct LogEntry {
9 pub timestamp_ns: i64,
11 pub message: String,
13 pub labels: FxHashMap<String, String>,
15 pub level: Option<LogLevel>,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum LogLevel {
23 Trace,
24 Debug,
25 Info,
26 Warn,
27 Error,
28}
29
30impl LogLevel {
31 #[must_use]
35 pub fn parse(s: &str) -> Option<Self> {
36 match s.to_lowercase().as_str() {
37 "trace" | "trc" => Some(Self::Trace),
38 "debug" | "dbg" => Some(Self::Debug),
39 "info" | "inf" => Some(Self::Info),
40 "warn" | "warning" | "wrn" => Some(Self::Warn),
41 "error" | "err" => Some(Self::Error),
42 _ => None,
43 }
44 }
45
46 #[must_use]
50 pub fn detect_from_message(message: &str) -> Option<Self> {
51 let upper = message.to_uppercase();
52
53 for level in ["TRACE", "DEBUG", "INFO", "WARN", "WARNING", "ERROR", "ERR"] {
55 if upper.contains(&format!("[{level}]")) || upper.contains(&format!("{level}:")) {
56 return Self::parse(level);
57 }
58 }
59
60 if let Some(pos) = upper.find("LEVEL=") {
62 let after = &message[pos + 6..];
63 let value: String = after
64 .trim_start_matches('"')
65 .chars()
66 .take_while(|c| c.is_alphabetic())
67 .collect();
68 if let Some(level) = Self::parse(&value) {
69 return Some(level);
70 }
71 }
72
73 None
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
79#[serde(rename_all = "lowercase")]
80pub enum QueryDirection {
81 Forward,
83 #[default]
85 Backward,
86}
87
88#[derive(Debug, Clone)]
90pub struct LogsQuery {
91 pub query: Option<String>,
94 pub labels: FxHashMap<String, String>,
96 pub contains: Option<String>,
98 pub start_ns: i64,
100 pub end_ns: i64,
102 pub limit: usize,
104 pub direction: QueryDirection,
106}
107
108impl LogsQuery {
109 #[must_use]
111 pub fn new(start_ns: i64, end_ns: i64) -> Self {
112 Self {
113 query: None,
114 labels: FxHashMap::default(),
115 contains: None,
116 start_ns,
117 end_ns,
118 limit: 1000,
119 direction: QueryDirection::Backward,
120 }
121 }
122
123 #[must_use]
125 pub fn with_query(mut self, query: impl Into<String>) -> Self {
126 self.query = Some(query.into());
127 self
128 }
129
130 #[must_use]
132 pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
133 self.labels.insert(key.into(), value.into());
134 self
135 }
136
137 #[must_use]
139 pub fn with_contains(mut self, text: impl Into<String>) -> Self {
140 self.contains = Some(text.into());
141 self
142 }
143
144 #[must_use]
146 pub fn with_limit(mut self, limit: usize) -> Self {
147 self.limit = limit;
148 self
149 }
150
151 #[must_use]
153 pub fn with_direction(mut self, direction: QueryDirection) -> Self {
154 self.direction = direction;
155 self
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct LogsResponse {
162 pub entries: Vec<LogEntry>,
164 pub streams_count: usize,
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_log_level_parse() {
174 assert_eq!(LogLevel::parse("info"), Some(LogLevel::Info));
175 assert_eq!(LogLevel::parse("INFO"), Some(LogLevel::Info));
176 assert_eq!(LogLevel::parse("warn"), Some(LogLevel::Warn));
177 assert_eq!(LogLevel::parse("warning"), Some(LogLevel::Warn));
178 assert_eq!(LogLevel::parse("error"), Some(LogLevel::Error));
179 assert_eq!(LogLevel::parse("err"), Some(LogLevel::Error));
180 assert_eq!(LogLevel::parse("unknown"), None);
181 }
182
183 #[test]
184 fn test_log_level_detect_bracketed() {
185 assert_eq!(
186 LogLevel::detect_from_message("[INFO] Starting server"),
187 Some(LogLevel::Info)
188 );
189 assert_eq!(
190 LogLevel::detect_from_message("[ERROR] Connection failed"),
191 Some(LogLevel::Error)
192 );
193 assert_eq!(
194 LogLevel::detect_from_message("[WARN] Deprecated API"),
195 Some(LogLevel::Warn)
196 );
197 }
198
199 #[test]
200 fn test_log_level_detect_colon() {
201 assert_eq!(
202 LogLevel::detect_from_message("INFO: Starting server"),
203 Some(LogLevel::Info)
204 );
205 assert_eq!(
206 LogLevel::detect_from_message("ERROR: Connection failed"),
207 Some(LogLevel::Error)
208 );
209 }
210
211 #[test]
212 fn test_log_level_detect_key_value() {
213 assert_eq!(
214 LogLevel::detect_from_message("level=info msg=\"hello\""),
215 Some(LogLevel::Info)
216 );
217 assert_eq!(
218 LogLevel::detect_from_message("level=\"error\" msg=\"failed\""),
219 Some(LogLevel::Error)
220 );
221 }
222
223 #[test]
224 fn test_logs_query_builder() {
225 let query = LogsQuery::new(1000, 2000)
226 .with_query("{app=\"myservice\"}")
227 .with_label("env", "prod")
228 .with_contains("SELECT")
229 .with_limit(500)
230 .with_direction(QueryDirection::Forward);
231
232 assert_eq!(query.query, Some("{app=\"myservice\"}".to_string()));
233 assert_eq!(query.labels.get("env"), Some(&"prod".to_string()));
234 assert_eq!(query.contains, Some("SELECT".to_string()));
235 assert_eq!(query.limit, 500);
236 assert_eq!(query.direction, QueryDirection::Forward);
237 assert_eq!(query.start_ns, 1000);
238 assert_eq!(query.end_ns, 2000);
239 }
240}