ddquery_core/lib.rs
1//! Parse and explain Datadog monitor queries.
2//!
3//! A Datadog monitor query packs an aggregation window, a metric, a scope, a
4//! grouping, transforms, an evaluation function, and a threshold into one
5//! dense line:
6//!
7//! ```text
8//! avg(last_1d):anomalies(sum:app.analytics.view_time{platform:android, country:au}.as_count(), 'agile', 5, direction='below') >= 1
9//! ```
10//!
11//! This crate turns that string into a typed, serializable AST
12//! ([`MonitorQuery`]) and renders it back as either a plain-language paragraph
13//! ([`explain`]) or a structured, render-ready breakdown ([`summarize`]).
14//!
15//! # Design
16//!
17//! - **Hand-written recursive descent + a small Pratt loop** for arithmetic precedence. No
18//! parser-generator dependency: the grammar is small and stable.
19//! - **Never panics on input.** Every failure path returns a [`ParseError`] carrying a byte offset
20//! and a one-line reason.
21//! - **Graceful degradation.** [`parse_or_unparsed`] never fails: a query the grammar does not yet
22//! model becomes [`MonitorQuery::Unparsed`] with the raw string preserved, so a consumer can
23//! always render *something*.
24//! - **Bounded recursion.** Pathologically nested input is rejected before the stack can grow.
25//! - **Pure.** No I/O, no global state; parsing is linear in the input length.
26//!
27//! # Dialects
28//!
29//! | Dialect | Shape | AST |
30//! | --- | --- | --- |
31//! | metric | `time-agg(window): expr op threshold` | [`MetricQuery`] |
32//! | search | `logs("…").rollup(…).last(…) op n` | [`SearchQuery`] |
33//! | service check | `"check".over(…).last(n).count_by_status()` | [`CheckQuery`] |
34//! | slo | `error_budget("id").over("7d") op n` | [`SloQuery`] |
35//! | composite | `123 && !456 \|\| 789` | [`CompositeExpr`] |
36//!
37//! # Examples
38//!
39//! ```
40//! use ddquery_core::{explain, parse, MonitorQuery};
41//!
42//! let query = "avg(last_5m):avg:system.cpu.user{env:production} > 90";
43//! let ast = parse(query).unwrap();
44//! assert!(matches!(ast, MonitorQuery::Metric(_)));
45//! println!("{}", explain(&ast));
46//! ```
47
48mod ast;
49mod cursor;
50mod error;
51mod explain;
52mod parser;
53
54pub use ast::{
55 ArithOp, BoolOp, ChangeKind, CheckQuery, CmpOp, CompositeExpr, Condition, Filter, FuncKind,
56 FuncParam, MetricExpr, MetricQuery, Modifier, MonitorQuery, MonitorRef, ParamValue, Scalar,
57 SearchQuery, SearchSource, Series, SloQuery, SpaceAgg, TimeAgg, Window,
58};
59pub use error::ParseError;
60pub use explain::{KeyVal, QuerySummary, explain, summarize};
61pub use parser::parse;
62
63/// Parse a query, degrading to [`MonitorQuery::Unparsed`] on failure.
64///
65/// This is the entry point for offline batch processing where a single
66/// unparsable query must never drop the surrounding record: the raw string,
67/// failure reason, and byte offset are preserved so a consumer can still render
68/// the original query verbatim.
69///
70/// # Examples
71///
72/// ```
73/// use ddquery_core::{parse_or_unparsed, MonitorQuery};
74///
75/// let q = parse_or_unparsed("this is not a valid query !!!");
76/// assert!(matches!(q, MonitorQuery::Unparsed { .. }));
77/// ```
78#[must_use]
79pub fn parse_or_unparsed(query: &str) -> MonitorQuery {
80 match parse(query) {
81 Ok(parsed) => parsed,
82 Err(err) => MonitorQuery::unparsed(query, &err),
83 }
84}