1use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12use crate::error::ParseError;
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21#[non_exhaustive]
22pub enum MonitorQuery {
23 Metric(MetricQuery),
25 Search(SearchQuery),
27 ServiceCheck(CheckQuery),
29 Slo(SloQuery),
31 Composite(CompositeExpr),
33 #[serde(rename_all = "camelCase")]
35 Unparsed {
36 raw: String,
38 reason: String,
40 offset: usize,
42 },
43}
44
45impl MonitorQuery {
46 #[must_use]
48 pub fn unparsed(raw: impl Into<String>, err: &ParseError) -> Self {
49 Self::Unparsed {
50 raw: raw.into(),
51 reason: err.reason().to_string(),
52 offset: err.offset(),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct MetricQuery {
61 pub time_aggregation: TimeAgg,
63 pub window: Window,
65 pub expr: MetricExpr,
67 pub condition: Condition,
69}
70
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74#[non_exhaustive]
75pub enum MetricExpr {
76 Series(Series),
78 #[serde(rename_all = "camelCase")]
80 Function {
81 name: FuncKind,
83 arg: Box<MetricExpr>,
85 params: Vec<FuncParam>,
87 },
88 #[serde(rename_all = "camelCase")]
91 Transform {
92 name: String,
94 arg: Box<MetricExpr>,
96 args: Vec<ParamValue>,
99 },
100 #[serde(rename_all = "camelCase")]
104 Combine {
105 name: String,
107 args: Vec<MetricExpr>,
109 },
110 #[serde(rename_all = "camelCase")]
112 Arith {
113 op: ArithOp,
115 lhs: Box<MetricExpr>,
117 rhs: Box<MetricExpr>,
119 },
120 #[serde(rename_all = "camelCase")]
122 Change {
123 kind: ChangeKind,
125 inner_agg: TimeAgg,
127 shift: Window,
129 arg: Box<MetricExpr>,
131 },
132 Scalar(Scalar),
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct Series {
140 pub space_aggregation: SpaceAgg,
142 pub metric: String,
144 pub filter: Vec<Filter>,
146 pub group_by: Vec<String>,
148 pub modifiers: Vec<Modifier>,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(tag = "type", rename_all = "camelCase")]
155pub enum Filter {
156 #[serde(rename_all = "camelCase")]
158 Tag {
159 negated: bool,
161 key: String,
163 value: String,
165 },
166 All,
168 Glob(String),
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct Modifier {
176 pub name: String,
178 pub args: Vec<String>,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct Condition {
186 pub operator: CmpOp,
188 pub critical: Scalar,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub critical_recovery: Option<Scalar>,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub warning: Option<Scalar>,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub warning_recovery: Option<Scalar>,
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct SearchQuery {
205 pub source: SearchSource,
207 pub raw_search: String,
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub index: Option<String>,
213 pub rollup_method: String,
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub rollup_arg: Option<String>,
218 pub group_by: Vec<String>,
220 pub last: String,
222 pub condition: Condition,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
228#[serde(rename_all = "kebab-case")]
229pub enum SearchSource {
230 Logs,
232 Events,
234 Rum,
236 ErrorTracking,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct CheckQuery {
244 pub check: String,
246 pub over: Vec<String>,
248 pub by: Vec<String>,
250 pub last: u32,
252}
253
254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct SloQuery {
258 pub id: String,
260 pub over: String,
262 pub condition: Condition,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268#[serde(rename_all = "camelCase")]
269pub enum CompositeExpr {
270 Ref(MonitorRef),
272 Not(Box<CompositeExpr>),
274 #[serde(rename_all = "camelCase")]
276 Binary {
277 op: BoolOp,
279 lhs: Box<CompositeExpr>,
281 rhs: Box<CompositeExpr>,
283 },
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289#[serde(rename_all = "camelCase")]
290pub struct MonitorRef {
291 pub id: String,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct Window {
302 pub raw: String,
304 pub seconds: u64,
306 pub display: String,
308}
309
310impl Window {
311 #[must_use]
318 pub fn parse(raw: &str) -> Option<Self> {
319 let (body, prefix_word) = if let Some(rest) = raw.strip_prefix("last_") {
323 (rest, "last")
324 } else if let Some(rest) = raw.strip_prefix("current_") {
325 (rest, "current")
326 } else {
327 (raw, "last")
328 };
329 let (digits, unit) = body.split_at(body.find(|c: char| !c.is_ascii_digit())?);
330 if digits.is_empty() {
331 return None;
332 }
333 let count: u64 = digits.parse().ok()?;
334 let (secs_per, unit_name) = match unit {
335 "s" => (1, "second"),
336 "m" => (60, "minute"),
337 "h" => (3600, "hour"),
338 "d" => (86_400, "day"),
339 "w" => (604_800, "week"),
340 "mo" => (2_592_000, "month"), _ => return None,
342 };
343 let seconds = count.checked_mul(secs_per)?;
344 let plural = if count == 1 { "" } else { "s" };
345 Some(Self {
346 raw: raw.to_string(),
347 seconds,
348 display: format!("{prefix_word} {count} {unit_name}{plural}"),
349 })
350 }
351}
352
353#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
355#[serde(transparent)]
356pub struct Scalar {
357 pub value: f64,
359}
360
361impl Scalar {
362 #[must_use]
364 pub fn new(value: f64) -> Self {
365 Self { value }
366 }
367}
368
369impl fmt::Display for Scalar {
370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371 if self.value.fract() == 0.0 && self.value.is_finite() {
372 write!(f, "{}", self.value as i64)
373 } else {
374 write!(f, "{}", self.value)
375 }
376 }
377}
378
379#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct FuncParam {
386 #[serde(skip_serializing_if = "Option::is_none")]
388 pub key: Option<String>,
389 pub value: ParamValue,
391}
392
393impl FuncParam {
394 #[must_use]
396 pub fn positional(value: ParamValue) -> Self {
397 Self { key: None, value }
398 }
399
400 #[must_use]
402 pub fn keyword(key: impl Into<String>, value: ParamValue) -> Self {
403 Self {
404 key: Some(key.into()),
405 value,
406 }
407 }
408}
409
410#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
412#[serde(untagged)]
413pub enum ParamValue {
414 Bool(bool),
416 Number(f64),
418 Str(String),
420}
421
422impl fmt::Display for ParamValue {
423 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424 match self {
425 ParamValue::Bool(b) => write!(f, "{b}"),
426 ParamValue::Number(n) => write!(f, "{n}"),
427 ParamValue::Str(s) => write!(f, "{s}"),
428 }
429 }
430}
431
432macro_rules! str_enum {
433 (
434 $(#[$meta:meta])*
435 $name:ident { $( $variant:ident => $text:literal ),+ $(,)? }
436 ) => {
437 $(#[$meta])*
438 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
439 #[serde(rename_all = "camelCase")]
440 pub enum $name {
441 $(
442 #[doc = concat!("`", $text, "`")]
443 $variant,
444 )+
445 }
446
447 impl $name {
448 #[must_use]
450 pub fn from_token(s: &str) -> Option<Self> {
451 match s {
452 $( $text => Some(Self::$variant), )+
453 _ => None,
454 }
455 }
456
457 #[must_use]
459 pub fn as_token(self) -> &'static str {
460 match self {
461 $( Self::$variant => $text, )+
462 }
463 }
464 }
465
466 impl ::std::fmt::Display for $name {
467 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
468 f.write_str(self.as_token())
469 }
470 }
471 };
472}
473
474str_enum! {
475 TimeAgg {
477 Avg => "avg",
478 Sum => "sum",
479 Min => "min",
480 Max => "max",
481 Count => "count",
482 Percentile => "percentile",
483 }
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
492#[serde(rename_all = "camelCase")]
493pub enum SpaceAgg {
494 Avg,
496 Sum,
498 Min,
500 Max,
502 Count,
504 Percentile(String),
507}
508
509impl SpaceAgg {
510 #[must_use]
515 pub fn from_token(s: &str) -> Option<Self> {
516 match s {
517 "avg" => Some(Self::Avg),
518 "sum" => Some(Self::Sum),
519 "min" => Some(Self::Min),
520 "max" => Some(Self::Max),
521 "count" => Some(Self::Count),
522 _ => {
523 let rank = s.strip_prefix('p')?;
524 let value: f64 = rank.parse().ok()?;
525 (0.0..=100.0)
526 .contains(&value)
527 .then(|| Self::Percentile(rank.to_string()))
528 }
529 }
530 }
531}
532
533impl ::std::fmt::Display for SpaceAgg {
534 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
535 match self {
536 Self::Avg => f.write_str("avg"),
537 Self::Sum => f.write_str("sum"),
538 Self::Min => f.write_str("min"),
539 Self::Max => f.write_str("max"),
540 Self::Count => f.write_str("count"),
541 Self::Percentile(rank) => write!(f, "p{rank}"),
542 }
543 }
544}
545
546str_enum! {
547 FuncKind {
549 Anomalies => "anomalies",
550 Outliers => "outliers",
551 Forecast => "forecast",
552 }
553}
554
555str_enum! {
556 ArithOp {
558 Add => "+",
559 Sub => "-",
560 Mul => "*",
561 Div => "/",
562 }
563}
564
565str_enum! {
566 ChangeKind {
568 Change => "change",
569 PctChange => "pct_change",
570 }
571}
572
573str_enum! {
574 BoolOp {
576 And => "&&",
577 Or => "||",
578 }
579}
580
581str_enum! {
582 CmpOp {
584 Ge => ">=",
585 Le => "<=",
586 Eq => "==",
587 Ne => "!=",
588 Gt => ">",
589 Lt => "<",
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use rstest::rstest;
596
597 use super::*;
598
599 #[rstest]
600 #[case("last_5m", 300, "last 5 minutes")]
601 #[case("last_1m", 60, "last 1 minute")]
602 #[case("last_1d", 86_400, "last 1 day")]
603 #[case("last_1w", 604_800, "last 1 week")]
604 #[case("last_30s", 30, "last 30 seconds")]
605 #[case("4h", 14_400, "last 4 hours")]
606 fn test_should_normalize_window(
607 #[case] raw: &str,
608 #[case] seconds: u64,
609 #[case] display: &str,
610 ) {
611 let window = Window::parse(raw).expect("valid window");
612 assert_eq!(window.seconds, seconds);
613 assert_eq!(window.display, display);
614 assert_eq!(window.raw, raw);
615 }
616
617 #[rstest]
618 #[case("")]
619 #[case("last_")]
620 #[case("5x")]
621 #[case("m")]
622 #[case("last_5mm")]
623 #[case("abc")]
624 fn test_should_reject_invalid_window(#[case] raw: &str) {
625 assert!(Window::parse(raw).is_none());
626 }
627
628 #[test]
629 fn test_should_reject_overflowing_window() {
630 assert!(Window::parse("18446744073709551615w").is_none());
631 }
632
633 #[test]
634 fn test_should_render_scalar_without_trailing_zero() {
635 assert_eq!(Scalar::new(90.0).to_string(), "90");
636 assert_eq!(Scalar::new(0.5).to_string(), "0.5");
637 assert_eq!(Scalar::new(-3.0).to_string(), "-3");
638 }
639
640 #[test]
641 fn test_should_roundtrip_through_json() {
642 let agg = TimeAgg::Avg;
643 let json = serde_json::to_string(&agg).expect("serialize");
644 assert_eq!(json, "\"avg\"");
645 let back: TimeAgg = serde_json::from_str(&json).expect("deserialize");
646 assert_eq!(back, agg);
647 }
648}