use std::fmt;
use serde::{Deserialize, Serialize};
use crate::error::ParseError;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum MonitorQuery {
Metric(MetricQuery),
Search(SearchQuery),
ServiceCheck(CheckQuery),
Slo(SloQuery),
Composite(CompositeExpr),
#[serde(rename_all = "camelCase")]
Unparsed {
raw: String,
reason: String,
offset: usize,
},
}
impl MonitorQuery {
#[must_use]
pub fn unparsed(raw: impl Into<String>, err: &ParseError) -> Self {
Self::Unparsed {
raw: raw.into(),
reason: err.reason().to_string(),
offset: err.offset(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetricQuery {
pub time_aggregation: TimeAgg,
pub window: Window,
pub expr: MetricExpr,
pub condition: Condition,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum MetricExpr {
Series(Series),
#[serde(rename_all = "camelCase")]
Function {
name: FuncKind,
arg: Box<MetricExpr>,
params: Vec<FuncParam>,
},
#[serde(rename_all = "camelCase")]
Transform {
name: String,
arg: Box<MetricExpr>,
args: Vec<ParamValue>,
},
#[serde(rename_all = "camelCase")]
Combine {
name: String,
args: Vec<MetricExpr>,
},
#[serde(rename_all = "camelCase")]
Arith {
op: ArithOp,
lhs: Box<MetricExpr>,
rhs: Box<MetricExpr>,
},
#[serde(rename_all = "camelCase")]
Change {
kind: ChangeKind,
inner_agg: TimeAgg,
shift: Window,
arg: Box<MetricExpr>,
},
Scalar(Scalar),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Series {
pub space_aggregation: SpaceAgg,
pub metric: String,
pub filter: Vec<Filter>,
pub group_by: Vec<String>,
pub modifiers: Vec<Modifier>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Filter {
#[serde(rename_all = "camelCase")]
Tag {
negated: bool,
key: String,
value: String,
},
All,
Glob(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Modifier {
pub name: String,
pub args: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Condition {
pub operator: CmpOp,
pub critical: Scalar,
#[serde(skip_serializing_if = "Option::is_none")]
pub critical_recovery: Option<Scalar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub warning: Option<Scalar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub warning_recovery: Option<Scalar>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchQuery {
pub source: SearchSource,
pub raw_search: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<String>,
pub rollup_method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub rollup_arg: Option<String>,
pub group_by: Vec<String>,
pub last: String,
pub condition: Condition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SearchSource {
Logs,
Events,
Rum,
ErrorTracking,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckQuery {
pub check: String,
pub over: Vec<String>,
pub by: Vec<String>,
pub last: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SloQuery {
pub id: String,
pub over: String,
pub condition: Condition,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CompositeExpr {
Ref(MonitorRef),
Not(Box<CompositeExpr>),
#[serde(rename_all = "camelCase")]
Binary {
op: BoolOp,
lhs: Box<CompositeExpr>,
rhs: Box<CompositeExpr>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MonitorRef {
pub id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Window {
pub raw: String,
pub seconds: u64,
pub display: String,
}
impl Window {
#[must_use]
pub fn parse(raw: &str) -> Option<Self> {
let (body, prefix_word) = if let Some(rest) = raw.strip_prefix("last_") {
(rest, "last")
} else if let Some(rest) = raw.strip_prefix("current_") {
(rest, "current")
} else {
(raw, "last")
};
let (digits, unit) = body.split_at(body.find(|c: char| !c.is_ascii_digit())?);
if digits.is_empty() {
return None;
}
let count: u64 = digits.parse().ok()?;
let (secs_per, unit_name) = match unit {
"s" => (1, "second"),
"m" => (60, "minute"),
"h" => (3600, "hour"),
"d" => (86_400, "day"),
"w" => (604_800, "week"),
"mo" => (2_592_000, "month"), _ => return None,
};
let seconds = count.checked_mul(secs_per)?;
let plural = if count == 1 { "" } else { "s" };
Some(Self {
raw: raw.to_string(),
seconds,
display: format!("{prefix_word} {count} {unit_name}{plural}"),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Scalar {
pub value: f64,
}
impl Scalar {
#[must_use]
pub fn new(value: f64) -> Self {
Self { value }
}
}
impl fmt::Display for Scalar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.value.fract() == 0.0 && self.value.is_finite() {
write!(f, "{}", self.value as i64)
} else {
write!(f, "{}", self.value)
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FuncParam {
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
pub value: ParamValue,
}
impl FuncParam {
#[must_use]
pub fn positional(value: ParamValue) -> Self {
Self { key: None, value }
}
#[must_use]
pub fn keyword(key: impl Into<String>, value: ParamValue) -> Self {
Self {
key: Some(key.into()),
value,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ParamValue {
Bool(bool),
Number(f64),
Str(String),
}
impl fmt::Display for ParamValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParamValue::Bool(b) => write!(f, "{b}"),
ParamValue::Number(n) => write!(f, "{n}"),
ParamValue::Str(s) => write!(f, "{s}"),
}
}
}
macro_rules! str_enum {
(
$(#[$meta:meta])*
$name:ident { $( $variant:ident => $text:literal ),+ $(,)? }
) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum $name {
$(
#[doc = concat!("`", $text, "`")]
$variant,
)+
}
impl $name {
#[must_use]
pub fn from_token(s: &str) -> Option<Self> {
match s {
$( $text => Some(Self::$variant), )+
_ => None,
}
}
#[must_use]
pub fn as_token(self) -> &'static str {
match self {
$( Self::$variant => $text, )+
}
}
}
impl ::std::fmt::Display for $name {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
f.write_str(self.as_token())
}
}
};
}
str_enum! {
TimeAgg {
Avg => "avg",
Sum => "sum",
Min => "min",
Max => "max",
Count => "count",
Percentile => "percentile",
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SpaceAgg {
Avg,
Sum,
Min,
Max,
Count,
Percentile(String),
}
impl SpaceAgg {
#[must_use]
pub fn from_token(s: &str) -> Option<Self> {
match s {
"avg" => Some(Self::Avg),
"sum" => Some(Self::Sum),
"min" => Some(Self::Min),
"max" => Some(Self::Max),
"count" => Some(Self::Count),
_ => {
let rank = s.strip_prefix('p')?;
let value: f64 = rank.parse().ok()?;
(0.0..=100.0)
.contains(&value)
.then(|| Self::Percentile(rank.to_string()))
}
}
}
}
impl ::std::fmt::Display for SpaceAgg {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match self {
Self::Avg => f.write_str("avg"),
Self::Sum => f.write_str("sum"),
Self::Min => f.write_str("min"),
Self::Max => f.write_str("max"),
Self::Count => f.write_str("count"),
Self::Percentile(rank) => write!(f, "p{rank}"),
}
}
}
str_enum! {
FuncKind {
Anomalies => "anomalies",
Outliers => "outliers",
Forecast => "forecast",
}
}
str_enum! {
ArithOp {
Add => "+",
Sub => "-",
Mul => "*",
Div => "/",
}
}
str_enum! {
ChangeKind {
Change => "change",
PctChange => "pct_change",
}
}
str_enum! {
BoolOp {
And => "&&",
Or => "||",
}
}
str_enum! {
CmpOp {
Ge => ">=",
Le => "<=",
Eq => "==",
Ne => "!=",
Gt => ">",
Lt => "<",
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("last_5m", 300, "last 5 minutes")]
#[case("last_1m", 60, "last 1 minute")]
#[case("last_1d", 86_400, "last 1 day")]
#[case("last_1w", 604_800, "last 1 week")]
#[case("last_30s", 30, "last 30 seconds")]
#[case("4h", 14_400, "last 4 hours")]
fn test_should_normalize_window(
#[case] raw: &str,
#[case] seconds: u64,
#[case] display: &str,
) {
let window = Window::parse(raw).expect("valid window");
assert_eq!(window.seconds, seconds);
assert_eq!(window.display, display);
assert_eq!(window.raw, raw);
}
#[rstest]
#[case("")]
#[case("last_")]
#[case("5x")]
#[case("m")]
#[case("last_5mm")]
#[case("abc")]
fn test_should_reject_invalid_window(#[case] raw: &str) {
assert!(Window::parse(raw).is_none());
}
#[test]
fn test_should_reject_overflowing_window() {
assert!(Window::parse("18446744073709551615w").is_none());
}
#[test]
fn test_should_render_scalar_without_trailing_zero() {
assert_eq!(Scalar::new(90.0).to_string(), "90");
assert_eq!(Scalar::new(0.5).to_string(), "0.5");
assert_eq!(Scalar::new(-3.0).to_string(), "-3");
}
#[test]
fn test_should_roundtrip_through_json() {
let agg = TimeAgg::Avg;
let json = serde_json::to_string(&agg).expect("serialize");
assert_eq!(json, "\"avg\"");
let back: TimeAgg = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, agg);
}
}