Alert expressions¶
A small language for conditions over metric history. It is parsed by a hand-written
lexer and Pratt parser — not Python's ast — so its precedence can be the one that
reads correctly for alerting.
Grammar¶
rule := expression "=>" [level] [":" message]
expression := or_expr
or_expr := and_expr ("or" | "||" | "|" and_expr)*
and_expr := not_expr ("and" | "&&" | "&" not_expr)*
not_expr := ["not" | "!"] comparison
comparison := arithmetic (("<" | "<=" | ">" | ">=" | "==" | "!=") arithmetic)?
arithmetic := term (("+" | "-") term)*
term := factor (("*" | "/" | "%") factor)*
factor := number | metric | metric "[" window "]" | call | "(" expression ")"
window := integer | duration # 20 | 30s | 5m | 2h | 1d
Precedence is or < and < not < comparison < +- < */%.
Different from Python
Python parses a > 50 | b > 5 as a > (50 | b) > 5. Here | is a plain or,
so it means (a > 50) or (b > 5) — what it looks like.
Run et rules explain "<expr>" if in doubt; it prints the fully parenthesised
form.
Metric names¶
Bare names may contain letters, digits, _, ., @ and /, so the names ML code
actually uses need no ceremony:
. also resolves to /, so eval.acc finds eval/acc if no metric is literally
called eval.acc.
/ is part of a name unless it is spaced¶
A / joins the name when a name character follows it immediately. Division must
therefore be written with spaces:
| Written | Means |
|---|---|
a/b |
the metric a/b |
a / b |
a divided by b |
a /b, a/ b |
a divided by b (any space makes it division) |
loss/2 |
the metric loss/2 |
loss / 2 |
loss divided by 2 |
1/2 |
division — a name cannot start with a digit |
Quote anything else¶
A name with spaces or other characters must be quoted, with ", ' or `:
Quotes only ever produce a metric name; the language has no string literals.
to_source() renders with " (or another quote if the name contains one).
Windows¶
metric[window] selects the recent history of a metric.
| Form | Meaning |
|---|---|
loss[20] |
the last 20 points |
loss[30s] |
the last 30 seconds |
loss[5m], loss[2h], loss[1d] |
minutes, hours, days |
Aggregates require an explicit window, because the answer would otherwise depend silently on buffer size:
mean(loss) # error: mean() requires an explicit window
mean(loss[20]) # fine
diff(loss) # fine - diff has a sensible default of 2 points
Functions¶
| Category | Functions |
|---|---|
| Aggregate | mean std var median sum min max first last count |
| Change | diff(m[w]) rate(m[w]) pct_change(m[w]) |
| Trend | slope(m[w]) zscore(m[w]) ema(m[w], alpha) increasing(m[w]) decreasing(m[w]) |
| Predicate | isnan(m) isinf(m) has(m) stalled(m[w], eps) |
| Context | step() elapsed() age(m) no_data(duration) |
| Scalar | abs log exp sqrt floor ceil min max |
min and max are dual: min(loss[20]) is a rolling minimum, min(a, b) is the
smaller of two scalars. The argument types decide.
no_data(30s) takes a duration and asks whether anything has been committed
recently. age(loss) takes a metric and returns how long since it last appeared.
Three-valued logic¶
Any expression that cannot be answered evaluates to UNKNOWN — never an exception:
- a metric that was never logged
- fewer points than the window needs
NaNor infinity in the input- division by zero,
log(0),sqrt(-1)
UNKNOWN never fires a rule and never changes its state, so warm-up and sparse
metrics cannot produce false alarms. It propagates by Kleene rules:
| Expression | Result |
|---|---|
UNKNOWN or true |
true |
UNKNOWN or false |
UNKNOWN |
UNKNOWN and false |
false |
UNKNOWN and true |
UNKNOWN |
not UNKNOWN |
UNKNOWN |
Examples¶
"isnan(loss) or isinf(loss) => critical: non-finite loss"
"zscore(loss[50]) > 4 => error: loss spike {loss:.4f}"
"diff(loss[2]) > 50 => warning: sudden jump"
"stalled(loss[100]) and step() > 500 => warning: flat for 100 steps"
"mean(grad[20]) > 10 * mean(grad[500]) => warning: exploding gradients"
"increasing(eval.loss[5]) => warning: overfitting"
"pct_change(lr[2]) < -0.5 => info: lr dropped"
"no_data(10m) => error: training looks hung"
"age(eval.acc) > 3600 => warning: no eval for an hour"
"elapsed() > 86400 => info: running for a day"
"has(grad_norm) and grad_norm > 1e4 => error: gradient blow-up"
Python builder¶
The same language with editor completion:
from expr_tracker import M
M.loss > 5 # loss > 5
M.train.loss > 5 # train.loss > 5
M["odd name"] > 5 # `odd name` > 5
M.loss[50].zscore() > 4 # zscore(loss[50]) > 4
M.loss["5m"].mean() > 1 # mean(loss[5m]) > 1
(M.a > 1) & (M.b > 2) # and
(M.a > 1) | (M.b > 2) # or
~(M.a > 1) # not
M.loss * 2 + 1 > 0 # arithmetic, both operand orders
Use & | ~, not and or not — Python cannot overload the keywords.
Message templates¶
{step}, {time}, {run}, {project}, any metric on the current step, and
{expr} — the condition rendered with the values that were observed:
Format specifiers work ({loss:.4f}). An unknown placeholder is left as written
instead of raising.