Add between/outside range predicate operators for custom alert rules (#3351) - #3371
Conversation
…3351) Custom alert rules could only compare a metric to a single scalar bar (gt/ge/lt/le). This adds the two range operators deferred from #3285: - between: breaches when the value is INSIDE the inclusive band [lower, upper] - outside: breaches when the value is OUTSIDE that band (value < lower OR value > upper) The band lives in NEW predicate fields (lowerBound/upperBound), never by overloading warn/critical -- overloading would break the "crossed the warning threshold" wording and the two-tier severity model. A predicate is therefore one shape OR the other, enforced as mutually exclusive in the validator: a scalar op requires warnThreshold and rejects bounds; a range op requires both numeric bounds with lower < upper and rejects warn/critical. Range ops are Warning-only in v1 (a two-tier band would need four bounds; a tracked follow-up). SeverityFor returns Warning for a breaching range op, and the delivered-alert body renders the band ("outside 10 - 100") with a null numeric threshold twin. Compare/SeverityFor/IsBreaching stay the single breach authority; the web editor swaps Warning/Critical inputs for Lower/Upper bounds when a range op is chosen, and the rule card renders "outside 10-100". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WejwpgWF5Xfm3fAmoEbFz4
| between the bounds — a range predicate carries lowerBound/upperBound and no warnThreshold. */ | ||
| if (pred.op === "between" || pred.op === "outside") { | ||
| if (pred.lowerBound == null || pred.upperBound == null) return null; | ||
| return pred.op + " " + pred.lowerBound + "–" + pred.upperBound; |
There was a problem hiding this comment.
Minor cosmetic ambiguity: bounds are rendered with an en dash and no surrounding spaces, so a negative bound reads badly — e.g. lowerBound: -20, upperBound: -5 renders as outside -20–-5, which is easy to misread as a single number or a different range. CustomAlertRuleDefinition.FiredThreshold deliberately uses a spaced " - " for the same band text in the delivered alert body; this chip could do the same (or add spaces around the en dash) for consistency and readability with negative bounds. Not a functional bug since this is a text: node (safe from injection either way), just a display nit worth a follow-up.
ReviewReviewed the
No correctness, security, or performance issues found beyond that cosmetic one. |
What
Implements the
between/outsiderange predicate operators for custom alert rules (#3351) — the range comparisons deferred from #3285.A rule's predicate can now test a metric against a two-sided band
[lowerBound, upperBound]in addition to the existing scalar bars (gt/ge/lt/le):outsidebreaches whenvalue < lowerBound OR value > upperBoundbetweenbreaches whenlowerBound <= value <= upperBound(inclusive bounds)Design
predicatefieldslowerBound/upperBound(nullable), never by overloadingwarnThreshold/criticalThreshold— overloading would break the "crossed the warning threshold" notification wording and the two-tier severity model.CustomAlertRuleDefinition.TryParse): a scalar op requireswarnThreshold(+ an optional more-extremecriticalThreshold) and rejects bounds; a range op requires both numeric bounds withlowerBound < upperBoundand rejects warn/critical.SeverityForreturns Warning for a breaching range op; a code comment marks the escalation follow-up.Compare/SeverityFor/IsBreachingremain the single breach authority. The delivered-alert body renders the band asoutside 10 - 100(ASCII, with a null numeric threshold twin) via a newFiredThresholdhelper the evaluator calls.Predicate JSON (range rule)
{ "metric": { "source": "wait_stats", "measure": "wait_time_ms", "aggregate": "sum", "hours": 1 }, "predicate": { "op": "outside", "lowerBound": 10, "upperBound": 100 } }Frontend
outside/betweenadded to the operator dropdown; the Warning/Critical inputs (and the help text) swap for Lower/Upper-bound inputs when a range op is chosen, mapped tolowerBound/upperBoundinmodelToDefinition/definitionToModel. The live/api/alerts/testpreview evaluates the serialized definition, so it works unchanged.outside 10–100/between 10–100.Tests
Darling.Tests.exeforCustomAlertRuleDefinitionTests+CustomAlertEvaluateNowTests+CustomAlertSeverityEscalationTests+CustomAlertTemplatesTests→ 65 passing, plusDarlingMcpCustomAlertToolsSurfaceTests→ 18 passing. Added: parser accepts between/outside; bounds required + numeric;lower < upper; scalar-vs-range mutual exclusion (both directions); breach logic at/inside/outside the band for both ops;SeverityFor= Warning;FiredThresholdround-trip (band text + null numeric twin, and the scalar bar); andClassifyTestValuewould-fire for range ops.Service + tests build
0 Warning(s) / 0 Error(s).Deferred
SeverityFor. Recommend a tracked issue.<trap for range ops — the scalar</<=-on-a-count guard (zero events vs stalled collector) is not mirrored to range ops; whether a given band is ambiguous depends on the specific bounds, so it needs a design decision rather than a mechanical mirror.🤖 Generated with Claude Code
https://claude.ai/code/session_01WejwpgWF5Xfm3fAmoEbFz4