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")]
90 Transform {
91 name: String,
93 arg: Box<MetricExpr>,
95 args: Vec<Scalar>,
97 },
98 #[serde(rename_all = "camelCase")]
100 Arith {
101 op: ArithOp,
103 lhs: Box<MetricExpr>,
105 rhs: Box<MetricExpr>,
107 },
108 #[serde(rename_all = "camelCase")]
110 Change {
111 kind: ChangeKind,
113 inner_agg: TimeAgg,
115 shift: Window,
117 arg: Box<MetricExpr>,
119 },
120 Scalar(Scalar),
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct Series {
128 pub space_aggregation: SpaceAgg,
130 pub metric: String,
132 pub filter: Vec<Filter>,
134 pub group_by: Vec<String>,
136 pub modifiers: Vec<Modifier>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(tag = "type", rename_all = "camelCase")]
143pub enum Filter {
144 #[serde(rename_all = "camelCase")]
146 Tag {
147 negated: bool,
149 key: String,
151 value: String,
153 },
154 All,
156 Glob(String),
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct Modifier {
164 pub name: String,
166 pub args: Vec<String>,
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct Condition {
174 pub operator: CmpOp,
176 pub critical: Scalar,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub critical_recovery: Option<Scalar>,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub warning: Option<Scalar>,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub warning_recovery: Option<Scalar>,
187}
188
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct SearchQuery {
193 pub source: SearchSource,
195 pub raw_search: String,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub index: Option<String>,
201 pub rollup_method: String,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub rollup_arg: Option<String>,
206 pub group_by: Vec<String>,
208 pub last: String,
210 pub condition: Condition,
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "kebab-case")]
217pub enum SearchSource {
218 Logs,
220 Events,
222 Rum,
224 ErrorTracking,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct CheckQuery {
232 pub check: String,
234 pub over: Vec<String>,
236 pub by: Vec<String>,
238 pub last: u32,
240}
241
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct SloQuery {
246 pub id: String,
248 pub over: String,
250 pub condition: Condition,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub enum CompositeExpr {
258 Ref(MonitorRef),
260 Not(Box<CompositeExpr>),
262 #[serde(rename_all = "camelCase")]
264 Binary {
265 op: BoolOp,
267 lhs: Box<CompositeExpr>,
269 rhs: Box<CompositeExpr>,
271 },
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct MonitorRef {
279 pub id: String,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct Window {
290 pub raw: String,
292 pub seconds: u64,
294 pub display: String,
296}
297
298impl Window {
299 #[must_use]
304 pub fn parse(raw: &str) -> Option<Self> {
305 let body = raw.strip_prefix("last_").unwrap_or(raw);
306 let (digits, unit) = body.split_at(body.find(|c: char| !c.is_ascii_digit())?);
307 if digits.is_empty() || unit.len() != 1 {
308 return None;
309 }
310 let count: u64 = digits.parse().ok()?;
311 let (secs_per, unit_name) = match unit {
312 "s" => (1, "second"),
313 "m" => (60, "minute"),
314 "h" => (3600, "hour"),
315 "d" => (86_400, "day"),
316 "w" => (604_800, "week"),
317 _ => return None,
318 };
319 let seconds = count.checked_mul(secs_per)?;
320 let plural = if count == 1 { "" } else { "s" };
321 Some(Self {
322 raw: raw.to_string(),
323 seconds,
324 display: format!("last {count} {unit_name}{plural}"),
325 })
326 }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
331#[serde(transparent)]
332pub struct Scalar {
333 pub value: f64,
335}
336
337impl Scalar {
338 #[must_use]
340 pub fn new(value: f64) -> Self {
341 Self { value }
342 }
343}
344
345impl fmt::Display for Scalar {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 if self.value.fract() == 0.0 && self.value.is_finite() {
348 write!(f, "{}", self.value as i64)
349 } else {
350 write!(f, "{}", self.value)
351 }
352 }
353}
354
355#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
360#[serde(rename_all = "camelCase")]
361pub struct FuncParam {
362 #[serde(skip_serializing_if = "Option::is_none")]
364 pub key: Option<String>,
365 pub value: ParamValue,
367}
368
369impl FuncParam {
370 #[must_use]
372 pub fn positional(value: ParamValue) -> Self {
373 Self { key: None, value }
374 }
375
376 #[must_use]
378 pub fn keyword(key: impl Into<String>, value: ParamValue) -> Self {
379 Self {
380 key: Some(key.into()),
381 value,
382 }
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
388#[serde(untagged)]
389pub enum ParamValue {
390 Bool(bool),
392 Number(f64),
394 Str(String),
396}
397
398impl fmt::Display for ParamValue {
399 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400 match self {
401 ParamValue::Bool(b) => write!(f, "{b}"),
402 ParamValue::Number(n) => write!(f, "{n}"),
403 ParamValue::Str(s) => write!(f, "{s}"),
404 }
405 }
406}
407
408macro_rules! str_enum {
409 (
410 $(#[$meta:meta])*
411 $name:ident { $( $variant:ident => $text:literal ),+ $(,)? }
412 ) => {
413 $(#[$meta])*
414 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
415 #[serde(rename_all = "camelCase")]
416 pub enum $name {
417 $(
418 #[doc = concat!("`", $text, "`")]
419 $variant,
420 )+
421 }
422
423 impl $name {
424 #[must_use]
426 pub fn from_token(s: &str) -> Option<Self> {
427 match s {
428 $( $text => Some(Self::$variant), )+
429 _ => None,
430 }
431 }
432
433 #[must_use]
435 pub fn as_token(self) -> &'static str {
436 match self {
437 $( Self::$variant => $text, )+
438 }
439 }
440 }
441
442 impl ::std::fmt::Display for $name {
443 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
444 f.write_str(self.as_token())
445 }
446 }
447 };
448}
449
450str_enum! {
451 TimeAgg {
453 Avg => "avg",
454 Sum => "sum",
455 Min => "min",
456 Max => "max",
457 Count => "count",
458 }
459}
460
461str_enum! {
462 SpaceAgg {
464 Avg => "avg",
465 Sum => "sum",
466 Min => "min",
467 Max => "max",
468 Count => "count",
469 }
470}
471
472str_enum! {
473 FuncKind {
475 Anomalies => "anomalies",
476 Outliers => "outliers",
477 Forecast => "forecast",
478 }
479}
480
481str_enum! {
482 ArithOp {
484 Add => "+",
485 Sub => "-",
486 Mul => "*",
487 Div => "/",
488 }
489}
490
491str_enum! {
492 ChangeKind {
494 Change => "change",
495 PctChange => "pct_change",
496 }
497}
498
499str_enum! {
500 BoolOp {
502 And => "&&",
503 Or => "||",
504 }
505}
506
507str_enum! {
508 CmpOp {
510 Ge => ">=",
511 Le => "<=",
512 Eq => "==",
513 Gt => ">",
514 Lt => "<",
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use rstest::rstest;
521
522 use super::*;
523
524 #[rstest]
525 #[case("last_5m", 300, "last 5 minutes")]
526 #[case("last_1m", 60, "last 1 minute")]
527 #[case("last_1d", 86_400, "last 1 day")]
528 #[case("last_1w", 604_800, "last 1 week")]
529 #[case("last_30s", 30, "last 30 seconds")]
530 #[case("4h", 14_400, "last 4 hours")]
531 fn test_should_normalize_window(
532 #[case] raw: &str,
533 #[case] seconds: u64,
534 #[case] display: &str,
535 ) {
536 let window = Window::parse(raw).expect("valid window");
537 assert_eq!(window.seconds, seconds);
538 assert_eq!(window.display, display);
539 assert_eq!(window.raw, raw);
540 }
541
542 #[rstest]
543 #[case("")]
544 #[case("last_")]
545 #[case("5x")]
546 #[case("m")]
547 #[case("last_5mm")]
548 #[case("abc")]
549 fn test_should_reject_invalid_window(#[case] raw: &str) {
550 assert!(Window::parse(raw).is_none());
551 }
552
553 #[test]
554 fn test_should_reject_overflowing_window() {
555 assert!(Window::parse("18446744073709551615w").is_none());
556 }
557
558 #[test]
559 fn test_should_render_scalar_without_trailing_zero() {
560 assert_eq!(Scalar::new(90.0).to_string(), "90");
561 assert_eq!(Scalar::new(0.5).to_string(), "0.5");
562 assert_eq!(Scalar::new(-3.0).to_string(), "-3");
563 }
564
565 #[test]
566 fn test_should_roundtrip_through_json() {
567 let agg = TimeAgg::Avg;
568 let json = serde_json::to_string(&agg).expect("serialize");
569 assert_eq!(json, "\"avg\"");
570 let back: TimeAgg = serde_json::from_str(&json).expect("deserialize");
571 assert_eq!(back, agg);
572 }
573}