1use crate::state::LogEvent;
13use serde_json::{json, Value};
14
15#[derive(Debug, Default)]
17pub struct ParsedQuery {
18 pub commands: Vec<Command>,
19}
20
21#[derive(Debug)]
22pub enum Command {
23 Fields(Vec<String>),
24 Display(Vec<String>),
25 Filter(FilterClause),
26 Sort { field: String, desc: bool },
27 Limit(usize),
28 Parse(ParseSpec),
29 Dedup(Vec<String>),
30 Stats { aggs: Vec<AggExpr>, by: Vec<String> },
31}
32
33#[derive(Debug)]
34pub enum FilterClause {
35 Equals { field: String, value: String },
37 NotEquals { field: String, value: String },
39 Like { field: String, pattern: String },
41}
42
43#[derive(Debug)]
46pub struct ParseSpec {
47 pub source: String,
48 pub pattern: String,
49 pub names: Vec<String>,
50}
51
52#[derive(Debug)]
53pub struct AggExpr {
54 pub func: AggFunc,
55 pub field: Option<String>,
56 pub alias: String,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq)]
60pub enum AggFunc {
61 Count,
62 Sum,
63 Avg,
64 Min,
65 Max,
66 CountDistinct,
67}
68
69fn strip_keyword<'a>(cmd: &'a str, kw: &str) -> Option<&'a str> {
72 let rest = cmd.strip_prefix(kw)?;
73 if rest.starts_with(|c: char| c.is_whitespace()) {
76 Some(rest.trim_start())
77 } else {
78 None
79 }
80}
81
82fn parse_field_list(s: &str) -> Vec<String> {
83 s.split(',')
84 .map(|f| f.trim().to_string())
85 .filter(|f| !f.is_empty())
86 .collect()
87}
88
89pub fn parse_query(query: &str) -> ParsedQuery {
91 let mut parsed = ParsedQuery::default();
92
93 for raw in query.split('|') {
94 let cmd = raw.trim();
95 if cmd.is_empty() {
96 continue;
97 }
98
99 if let Some(rest) = strip_keyword(cmd, "fields") {
100 parsed
101 .commands
102 .push(Command::Fields(parse_field_list(rest)));
103 } else if let Some(rest) = strip_keyword(cmd, "display") {
104 parsed
105 .commands
106 .push(Command::Display(parse_field_list(rest)));
107 } else if let Some(rest) = strip_keyword(cmd, "filter") {
108 if let Some(clause) = parse_filter_clause(rest.trim()) {
109 parsed.commands.push(Command::Filter(clause));
110 }
111 } else if let Some(rest) = strip_keyword(cmd, "stats") {
112 parsed.commands.push(parse_stats(rest));
113 } else if let Some(rest) = strip_keyword(cmd, "parse") {
114 if let Some(spec) = parse_parse(rest) {
115 parsed.commands.push(Command::Parse(spec));
116 }
117 } else if let Some(rest) = strip_keyword(cmd, "dedup") {
118 parsed.commands.push(Command::Dedup(parse_field_list(rest)));
119 } else if let Some(rest) = strip_keyword(cmd, "sort") {
120 let parts: Vec<&str> = rest.split_whitespace().collect();
121 if !parts.is_empty() {
122 parsed.commands.push(Command::Sort {
123 field: parts[0].to_string(),
124 desc: parts.get(1).map(|s| s.eq_ignore_ascii_case("desc")) == Some(true),
125 });
126 }
127 } else if let Some(rest) = strip_keyword(cmd, "limit") {
128 if let Ok(n) = rest.trim().parse::<usize>() {
129 parsed.commands.push(Command::Limit(n));
130 }
131 }
132 }
133
134 parsed
135}
136
137fn parse_filter_clause(s: &str) -> Option<FilterClause> {
138 if let Some(like_pos) = s.find(" like ") {
140 let field = s[..like_pos].trim().to_string();
141 let pattern_str = s[like_pos + 6..].trim();
142 let pattern = if pattern_str.starts_with('/') && pattern_str.ends_with('/') {
143 pattern_str[1..pattern_str.len() - 1].to_string()
144 } else {
145 unquote(pattern_str)
146 };
147 return Some(FilterClause::Like { field, pattern });
148 }
149
150 if let Some(ne_pos) = s.find(" != ") {
151 let field = s[..ne_pos].trim().to_string();
152 let value = unquote(s[ne_pos + 4..].trim());
153 return Some(FilterClause::NotEquals { field, value });
154 }
155
156 if let Some(eq_pos) = s.find(" = ") {
157 let field = s[..eq_pos].trim().to_string();
158 let value = unquote(s[eq_pos + 3..].trim());
159 return Some(FilterClause::Equals { field, value });
160 }
161
162 None
163}
164
165fn parse_stats(rest: &str) -> Command {
167 let lower = rest.to_ascii_lowercase();
169 let (agg_part, by_part) = match lower.find(" by ") {
170 Some(pos) => (&rest[..pos], Some(rest[pos + 4..].trim())),
171 None => (rest, None),
172 };
173 let aggs = split_top_level_commas(agg_part)
174 .iter()
175 .filter_map(|s| parse_agg_expr(s.trim()))
176 .collect();
177 let by = by_part.map(parse_field_list).unwrap_or_default();
178 Command::Stats { aggs, by }
179}
180
181fn split_top_level_commas(s: &str) -> Vec<String> {
184 let mut out = Vec::new();
185 let mut depth = 0i32;
186 let mut current = String::new();
187 for c in s.chars() {
188 match c {
189 '(' => {
190 depth += 1;
191 current.push(c);
192 }
193 ')' => {
194 depth -= 1;
195 current.push(c);
196 }
197 ',' if depth == 0 => {
198 out.push(std::mem::take(&mut current));
199 }
200 _ => current.push(c),
201 }
202 }
203 if !current.trim().is_empty() {
204 out.push(current);
205 }
206 out
207}
208
209fn parse_agg_expr(s: &str) -> Option<AggExpr> {
210 let (expr, alias) = match s.to_ascii_lowercase().find(" as ") {
212 Some(pos) => (s[..pos].trim(), Some(s[pos + 4..].trim().to_string())),
213 None => (s.trim(), None),
214 };
215 let open = expr.find('(')?;
216 let close = expr.rfind(')')?;
217 if close < open {
218 return None;
219 }
220 let func_name = expr[..open].trim().to_ascii_lowercase();
221 let arg = expr[open + 1..close].trim();
222 let func = match func_name.as_str() {
223 "count" => AggFunc::Count,
224 "sum" => AggFunc::Sum,
225 "avg" | "average" => AggFunc::Avg,
226 "min" => AggFunc::Min,
227 "max" => AggFunc::Max,
228 "count_distinct" | "countdistinct" => AggFunc::CountDistinct,
229 _ => return None,
230 };
231 let field = if arg.is_empty() || arg == "*" {
232 None
233 } else {
234 Some(arg.to_string())
235 };
236 let alias = alias.unwrap_or_else(|| expr.to_string());
237 Some(AggExpr { func, field, alias })
238}
239
240fn parse_parse(rest: &str) -> Option<ParseSpec> {
242 let rest = rest.trim();
243 let quote_pos = rest.find(['"', '\''])?;
245 let source = rest[..quote_pos].trim().to_string();
246 if source.is_empty() {
247 return None;
248 }
249 let quote = rest.as_bytes()[quote_pos] as char;
250 let after = &rest[quote_pos + 1..];
251 let end = after.find(quote)?;
252 let pattern = after[..end].to_string();
253 let tail = after[end + 1..].trim();
254 let names = match tail.to_ascii_lowercase().strip_prefix("as ") {
255 Some(_) => parse_field_list(&tail[3..]),
256 None => Vec::new(),
257 };
258 Some(ParseSpec {
259 source,
260 pattern,
261 names,
262 })
263}
264
265fn unquote(s: &str) -> String {
266 if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
267 s[1..s.len() - 1].to_string()
268 } else {
269 s.to_string()
270 }
271}
272
273#[derive(Clone, Default)]
276struct Record {
277 fields: Vec<(String, String)>,
278}
279
280impl Record {
281 fn get(&self, name: &str) -> Option<&str> {
282 self.fields
283 .iter()
284 .find(|(k, _)| k == name)
285 .map(|(_, v)| v.as_str())
286 }
287
288 fn set(&mut self, name: &str, value: String) {
289 if let Some(entry) = self.fields.iter_mut().find(|(k, _)| k == name) {
290 entry.1 = value;
291 } else {
292 self.fields.push((name.to_string(), value));
293 }
294 }
295}
296
297fn format_timestamp(ms: i64) -> String {
299 let secs = ms / 1000;
300 let nsecs = ((ms % 1000) * 1_000_000) as u32;
301 match chrono::DateTime::from_timestamp(secs, nsecs) {
302 Some(dt) => dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string(),
303 None => ms.to_string(),
304 }
305}
306
307fn encode_ptr(group: &str, stream: &str, index: usize) -> String {
309 use base64::Engine;
310 base64::engine::general_purpose::STANDARD.encode(format!("{group}|{stream}|{index}").as_bytes())
311}
312
313fn build_record(event: &LogEvent, group: &str, stream: &str, index: usize) -> Record {
316 let mut r = Record::default();
317 r.set("@timestamp", format_timestamp(event.timestamp));
318 r.set("@message", event.message.clone());
319 r.set("@logStream", stream.to_string());
320 r.set("@ingestionTime", event.ingestion_time.to_string());
321 r.set("@ptr", encode_ptr(group, stream, index));
322 if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(&event.message) {
323 for (k, v) in map {
324 let value = match v {
325 Value::String(s) => s,
326 other => other.to_string(),
327 };
328 if r.get(&k).is_none() {
330 r.set(&k, value);
331 }
332 }
333 }
334 r
335}
336
337fn record_field(r: &Record, name: &str) -> Option<String> {
340 if let Some(v) = r.get(name) {
341 return Some(v.to_string());
342 }
343 let stripped = name.strip_prefix('@').unwrap_or(name);
344 r.get(stripped).map(|v| v.to_string())
345}
346
347fn matches_pattern(haystack: &str, pattern: &str) -> bool {
348 if let Some(inner) = pattern.strip_prefix('^').and_then(|p| p.strip_suffix('$')) {
349 haystack == inner
350 } else if let Some(prefix) = pattern.strip_prefix('^') {
351 haystack.starts_with(prefix)
352 } else if let Some(suffix) = pattern.strip_suffix('$') {
353 haystack.ends_with(suffix)
354 } else {
355 haystack.contains(pattern)
356 }
357}
358
359fn record_matches_filter(r: &Record, clause: &FilterClause) -> bool {
360 match clause {
361 FilterClause::Equals { field, value } => {
362 record_field(r, field).map(|v| v == *value).unwrap_or(false)
363 }
364 FilterClause::NotEquals { field, value } => {
365 record_field(r, field).map(|v| v != *value).unwrap_or(true)
366 }
367 FilterClause::Like { field, pattern } => record_field(r, field)
368 .map(|v| matches_pattern(&v, pattern))
369 .unwrap_or(false),
370 }
371}
372
373fn apply_parse(r: &mut Record, spec: &ParseSpec) {
375 let Some(source) = record_field(r, &spec.source) else {
376 return;
377 };
378 let segments: Vec<&str> = spec.pattern.split('*').collect();
379 let mut captures: Vec<String> = Vec::new();
380 let mut pos = 0usize;
381 if let Some(first) = segments.first() {
383 if !source[pos..].starts_with(first) {
384 return;
385 }
386 pos += first.len();
387 }
388 for seg in segments.iter().skip(1) {
389 if seg.is_empty() {
390 captures.push(source[pos..].to_string());
392 pos = source.len();
393 continue;
394 }
395 let Some(found) = source[pos..].find(seg) else {
396 return;
397 };
398 captures.push(source[pos..pos + found].to_string());
399 pos += found + seg.len();
400 }
401 for (name, value) in spec.names.iter().zip(captures) {
402 r.set(name, value);
403 }
404}
405
406fn parse_number(s: &str) -> Option<f64> {
407 s.trim().parse::<f64>().ok()
408}
409
410fn format_number(n: f64) -> String {
413 if n.fract() == 0.0 && n.abs() < 1e15 {
414 format!("{}", n as i64)
415 } else {
416 format!("{n}")
417 }
418}
419
420fn compute_agg(agg: &AggExpr, group: &[&Record]) -> String {
421 match agg.func {
422 AggFunc::Count => match &agg.field {
423 None => format_number(group.len() as f64),
424 Some(f) => {
425 let n = group
426 .iter()
427 .filter(|r| record_field(r, f).is_some())
428 .count();
429 format_number(n as f64)
430 }
431 },
432 AggFunc::CountDistinct => {
433 let field = match &agg.field {
434 Some(f) => f,
435 None => return "0".to_string(),
436 };
437 let mut seen = std::collections::BTreeSet::new();
438 for r in group {
439 if let Some(v) = record_field(r, field) {
440 seen.insert(v);
441 }
442 }
443 format_number(seen.len() as f64)
444 }
445 AggFunc::Sum | AggFunc::Avg | AggFunc::Min | AggFunc::Max => {
446 let field = match &agg.field {
447 Some(f) => f,
448 None => return "0".to_string(),
449 };
450 let nums: Vec<f64> = group
451 .iter()
452 .filter_map(|r| record_field(r, field).and_then(|v| parse_number(&v)))
453 .collect();
454 if nums.is_empty() {
455 return "0".to_string();
456 }
457 let v = match agg.func {
458 AggFunc::Sum => nums.iter().sum(),
459 AggFunc::Avg => nums.iter().sum::<f64>() / nums.len() as f64,
460 AggFunc::Min => nums.iter().cloned().fold(f64::INFINITY, f64::min),
461 AggFunc::Max => nums.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
462 _ => unreachable!(),
463 };
464 format_number(v)
465 }
466 }
467}
468
469pub struct QueryStream {
471 pub group_name: String,
472 pub stream_name: String,
473 pub events: Vec<(usize, LogEvent)>,
476}
477
478pub fn execute_query(
482 query: &ParsedQuery,
483 streams: &[QueryStream],
484 start_time_secs: i64,
485 end_time_secs: i64,
486) -> Vec<Value> {
487 let mut records: Vec<Record> = Vec::new();
489 for stream in streams {
490 for (index, event) in &stream.events {
491 let event_time_secs = event.timestamp / 1000;
492 if event_time_secs >= start_time_secs && event_time_secs < end_time_secs {
493 records.push(build_record(
494 event,
495 &stream.group_name,
496 &stream.stream_name,
497 *index,
498 ));
499 }
500 }
501 }
502
503 records.sort_by(|a, b| {
505 a.get("@timestamp")
506 .unwrap_or_default()
507 .cmp(b.get("@timestamp").unwrap_or_default())
508 });
509
510 let mut explicit_fields: Option<Vec<String>> = None;
511 let mut aggregated = false;
512
513 for cmd in &query.commands {
514 match cmd {
515 Command::Filter(clause) => {
516 records.retain(|r| record_matches_filter(r, clause));
517 }
518 Command::Parse(spec) => {
519 for r in records.iter_mut() {
520 apply_parse(r, spec);
521 }
522 }
523 Command::Fields(f) | Command::Display(f) => {
524 explicit_fields = Some(f.clone());
525 }
526 Command::Dedup(fields) => {
527 let mut seen = std::collections::HashSet::new();
528 records.retain(|r| {
529 let key: Vec<Option<String>> =
530 fields.iter().map(|f| record_field(r, f)).collect();
531 seen.insert(format!("{key:?}"))
532 });
533 }
534 Command::Sort { field, desc } => {
535 records.sort_by(|a, b| {
536 let va = record_field(a, field).unwrap_or_default();
537 let vb = record_field(b, field).unwrap_or_default();
538 if *desc {
539 vb.cmp(&va)
540 } else {
541 va.cmp(&vb)
542 }
543 });
544 }
545 Command::Limit(n) => {
546 records.truncate(*n);
547 }
548 Command::Stats { aggs, by } => {
549 records = run_stats(&records, aggs, by);
550 aggregated = true;
551 let mut cols: Vec<String> = by.clone();
554 cols.extend(aggs.iter().map(|a| a.alias.clone()));
555 explicit_fields = Some(cols);
556 }
557 }
558 }
559
560 let mut output_fields: Vec<String> = match explicit_fields {
562 Some(f) => f,
563 None => vec![
564 "@timestamp".to_string(),
565 "@message".to_string(),
566 "@ptr".to_string(),
567 ],
568 };
569 if !aggregated && !output_fields.iter().any(|f| f == "@ptr") {
571 output_fields.push("@ptr".to_string());
572 }
573
574 records
575 .iter()
576 .map(|r| {
577 let row: Vec<Value> = output_fields
578 .iter()
579 .filter_map(|field| {
580 record_field(r, field).map(|value| json!({"field": field, "value": value}))
581 })
582 .collect();
583 Value::Array(row)
584 })
585 .collect()
586}
587
588fn run_stats(records: &[Record], aggs: &[AggExpr], by: &[String]) -> Vec<Record> {
591 use std::collections::BTreeMap;
592 let mut groups: BTreeMap<Vec<String>, Vec<&Record>> = BTreeMap::new();
593 for r in records {
594 let key: Vec<String> = by
595 .iter()
596 .map(|f| record_field(r, f).unwrap_or_default())
597 .collect();
598 groups.entry(key).or_default().push(r);
599 }
600
601 let mut out = Vec::new();
602 for (key, group) in groups {
603 let mut rec = Record::default();
604 for (field, value) in by.iter().zip(key.iter()) {
605 rec.set(field, value.clone());
606 }
607 for agg in aggs {
608 rec.set(&agg.alias, compute_agg(agg, &group));
609 }
610 out.push(rec);
611 }
612 out
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 fn stream(name: &str, events: Vec<LogEvent>) -> QueryStream {
620 QueryStream {
621 group_name: "/g".to_string(),
622 stream_name: name.to_string(),
623 events: events.into_iter().enumerate().collect(),
624 }
625 }
626
627 fn ev(ts: i64, msg: &str) -> LogEvent {
628 LogEvent {
629 timestamp: ts,
630 message: msg.to_string(),
631 ingestion_time: ts,
632 }
633 }
634
635 #[test]
636 fn parse_fields_and_limit() {
637 let q = parse_query("fields @timestamp, @message | limit 5");
638 assert_eq!(q.commands.len(), 2);
639 match &q.commands[0] {
640 Command::Fields(f) => assert_eq!(f, &vec!["@timestamp", "@message"]),
641 _ => panic!("expected Fields"),
642 }
643 match &q.commands[1] {
644 Command::Limit(n) => assert_eq!(*n, 5),
645 _ => panic!("expected Limit"),
646 }
647 }
648
649 #[test]
650 fn execute_filters_events() {
651 let streams = vec![stream(
652 "s1",
653 vec![
654 ev(1000000, "ERROR: broke"),
655 ev(2000000, "INFO: ok"),
656 ev(3000000, "ERROR: again"),
657 ],
658 )];
659 let q = parse_query("filter @message like /ERROR/ | limit 10");
660 let r = execute_query(&q, &streams, 0, 10000);
661 assert_eq!(r.len(), 2);
662 }
663
664 #[test]
665 fn execute_json_field_filter() {
666 let streams = vec![stream(
667 "s1",
668 vec![
669 ev(1000000, r#"{"level":"ERROR","msg":"fail"}"#),
670 ev(2000000, r#"{"level":"INFO","msg":"ok"}"#),
671 ],
672 )];
673 let q = parse_query(r#"filter level = "ERROR""#);
674 let r = execute_query(&q, &streams, 0, 10000);
675 assert_eq!(r.len(), 1);
676 }
677
678 #[test]
679 fn execute_not_equals_filter() {
680 let streams = vec![stream(
681 "s1",
682 vec![
683 ev(1000000, r#"{"level":"ERROR"}"#),
684 ev(2000000, r#"{"level":"INFO"}"#),
685 ],
686 )];
687 let q = parse_query(r#"filter level != "ERROR""#);
688 let r = execute_query(&q, &streams, 0, 10000);
689 assert_eq!(r.len(), 1);
690 }
691
692 #[test]
693 fn stats_count_by_field() {
694 let streams = vec![stream(
695 "s1",
696 vec![
697 ev(1000000, r#"{"level":"ERROR"}"#),
698 ev(2000000, r#"{"level":"INFO"}"#),
699 ev(3000000, r#"{"level":"ERROR"}"#),
700 ev(4000000, r#"{"level":"ERROR"}"#),
701 ],
702 )];
703 let q = parse_query("stats count(*) by level");
704 let rows = execute_query(&q, &streams, 0, 10000);
705 assert_eq!(rows.len(), 2);
706 let error_row = rows
707 .iter()
708 .find(|row| {
709 row.as_array()
710 .unwrap()
711 .iter()
712 .any(|f| f["field"] == "level" && f["value"] == "ERROR")
713 })
714 .unwrap();
715 let count = error_row
716 .as_array()
717 .unwrap()
718 .iter()
719 .find(|f| f["field"] == "count(*)")
720 .unwrap();
721 assert_eq!(count["value"], "3");
722 }
723
724 #[test]
725 fn stats_sum_avg() {
726 let streams = vec![stream(
727 "s1",
728 vec![
729 ev(1000000, r#"{"svc":"a","latency":10}"#),
730 ev(2000000, r#"{"svc":"a","latency":30}"#),
731 ],
732 )];
733 let q = parse_query("stats sum(latency) as total, avg(latency) as mean by svc");
734 let rows = execute_query(&q, &streams, 0, 10000);
735 assert_eq!(rows.len(), 1);
736 let row = rows[0].as_array().unwrap();
737 let total = row.iter().find(|f| f["field"] == "total").unwrap();
738 let mean = row.iter().find(|f| f["field"] == "mean").unwrap();
739 assert_eq!(total["value"], "40");
740 assert_eq!(mean["value"], "20");
741 }
742
743 #[test]
744 fn ptr_is_base64_group_stream_index() {
745 use base64::Engine;
746 let streams = vec![stream("s1", vec![ev(1000000, "hello")])];
747 let q = parse_query("fields @message");
748 let rows = execute_query(&q, &streams, 0, 10000);
749 let row = rows[0].as_array().unwrap();
750 let ptr = row.iter().find(|f| f["field"] == "@ptr").unwrap();
751 let decoded = base64::engine::general_purpose::STANDARD
752 .decode(ptr["value"].as_str().unwrap())
753 .unwrap();
754 assert_eq!(String::from_utf8(decoded).unwrap(), "/g|s1|0");
755 }
756
757 #[test]
758 fn parse_glob_extracts_fields() {
759 let streams = vec![stream("s1", vec![ev(1000000, "GET /api 200")])];
760 let q =
761 parse_query("parse @message \"* * *\" as method, path, code | filter code = \"200\"");
762 let rows = execute_query(&q, &streams, 0, 10000);
763 assert_eq!(rows.len(), 1);
764 }
765
766 #[test]
767 fn dedup_drops_duplicates() {
768 let streams = vec![stream(
769 "s1",
770 vec![
771 ev(1000000, r#"{"level":"ERROR"}"#),
772 ev(2000000, r#"{"level":"ERROR"}"#),
773 ev(3000000, r#"{"level":"INFO"}"#),
774 ],
775 )];
776 let q = parse_query("dedup level");
777 let rows = execute_query(&q, &streams, 0, 10000);
778 assert_eq!(rows.len(), 2);
779 }
780
781 #[test]
782 fn sort_desc_by_timestamp() {
783 let streams = vec![stream(
784 "s1",
785 vec![
786 ev(1000000, "first"),
787 ev(3000000, "third"),
788 ev(2000000, "second"),
789 ],
790 )];
791 let q = parse_query("sort @timestamp desc");
792 let rows = execute_query(&q, &streams, 0, 10000);
793 let first = rows[0].as_array().unwrap();
794 let msg = first.iter().find(|f| f["field"] == "@message").unwrap();
795 assert_eq!(msg["value"], "third");
796 }
797}