@intentius/chant-lexicon-prometheus 0.124.31 → 0.124.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/alertmanager.js +61 -0
- package/dist/api/alertmanager.js +98 -0
- package/dist/api/apply.js +306 -0
- package/dist/api/bind.js +66 -0
- package/dist/api/client.js +110 -0
- package/dist/api/evaluated.js +86 -0
- package/dist/api/fake-servers.js +165 -0
- package/dist/api/index.js +17 -0
- package/dist/api/ruler.js +122 -0
- package/dist/build.js +240 -0
- package/dist/catalog.js +66 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +105 -0
- package/dist/codegen/generate-cli.js +7 -0
- package/dist/codegen/generate.js +42 -0
- package/dist/codegen/package.js +28 -0
- package/dist/collector-metrics.js +225 -0
- package/dist/composites/alert-routing.js +210 -0
- package/dist/composites/catalog.js +344 -0
- package/dist/composites/genai.js +454 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/red-alerts.js +135 -0
- package/dist/composites/slo.js +353 -0
- package/dist/composites/watchdog.js +110 -0
- package/dist/config-model.js +52 -0
- package/dist/config.js +225 -0
- package/dist/describe-resources.js +299 -0
- package/dist/detect.js +12 -0
- package/dist/duration.js +54 -0
- package/dist/export-resources.js +163 -0
- package/dist/import/alertmanager-live.js +309 -0
- package/dist/import/embedded.js +100 -0
- package/dist/import/generator.js +683 -0
- package/dist/import/parser.js +428 -0
- package/dist/import/slo.js +207 -0
- package/dist/index.js +33 -0
- package/dist/init-templates.js +119 -0
- package/dist/integrations.js +33 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +60 -0
- package/dist/lint/audit-lineage.js +42 -0
- package/dist/lint/post-synth/index.js +68 -0
- package/dist/lint/post-synth/prom-helpers.js +157 -0
- package/dist/lint/post-synth/prom101.js +13 -0
- package/dist/lint/post-synth/prom102.js +13 -0
- package/dist/lint/post-synth/prom103.js +13 -0
- package/dist/lint/post-synth/prom104.js +13 -0
- package/dist/lint/post-synth/prom105.js +13 -0
- package/dist/lint/post-synth/prom106.js +13 -0
- package/dist/lint/post-synth/prom107.js +13 -0
- package/dist/lint/post-synth/prom201.js +13 -0
- package/dist/lint/post-synth/prom202.js +13 -0
- package/dist/lint/post-synth/prom203.js +13 -0
- package/dist/lint/post-synth/prom204.js +13 -0
- package/dist/lint/post-synth/prom205.js +13 -0
- package/dist/lint/post-synth/prom206.js +13 -0
- package/dist/lint/post-synth/prom207.js +13 -0
- package/dist/lint/post-synth/prom208.js +13 -0
- package/dist/lint/post-synth/prom209.js +13 -0
- package/dist/lint/post-synth/prom210.js +13 -0
- package/dist/lint/post-synth/prom211.js +17 -0
- package/dist/lint/post-synth/prom212.js +14 -0
- package/dist/lint/post-synth/prom213.js +16 -0
- package/dist/lint/post-synth/prom214.js +14 -0
- package/dist/lint/post-synth/prom215.js +14 -0
- package/dist/lint/post-synth/prom216.js +15 -0
- package/dist/lint/post-synth/prom217.js +14 -0
- package/dist/lint/post-synth/prom218.js +15 -0
- package/dist/lint/post-synth/prom219.js +14 -0
- package/dist/lint/post-synth/prom220.js +14 -0
- package/dist/lint/post-synth/prom221.js +15 -0
- package/dist/lint/post-synth/prom222.js +14 -0
- package/dist/lint/post-synth/prom223.js +15 -0
- package/dist/lint/post-synth/prom224.js +14 -0
- package/dist/lint/post-synth/prom225.js +15 -0
- package/dist/lint/post-synth/prom301.js +19 -0
- package/dist/lint/rules/index.js +8 -0
- package/dist/lint/rules/literal-credential.js +127 -0
- package/dist/lint/rules/prom-ast.js +65 -0
- package/dist/lint/rules/promql-literal.js +65 -0
- package/dist/lint/rules/slo-literal.js +79 -0
- package/dist/lsp/completions.js +11 -0
- package/dist/lsp/hover.js +21 -0
- package/dist/manifest.json +1 -1
- package/dist/matchers.js +131 -0
- package/dist/model.js +108 -0
- package/dist/op/activities/index.js +20 -0
- package/dist/op/activities/promtool.js +87 -0
- package/dist/op/activities/rule-audit.js +128 -0
- package/dist/op/activities/rules-loaded.js +73 -0
- package/dist/op/activities/silence.js +123 -0
- package/dist/op/activity-contracts.js +51 -0
- package/dist/op/audit-op.js +28 -0
- package/dist/op/builders.js +57 -0
- package/dist/op/slo-rule-tests.js +148 -0
- package/dist/package-cli.js +14 -0
- package/dist/pin.js +13 -0
- package/dist/plugin.js +143 -0
- package/dist/prometheus-config.js +38 -0
- package/dist/promql-analysis.js +343 -0
- package/dist/promql.js +44 -0
- package/dist/rule-eval.js +466 -0
- package/dist/rules.js +97 -0
- package/dist/serializer.js +59 -0
- package/dist/skill-defs.js +41 -0
- package/dist/tools.js +85 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate-config.js +556 -0
- package/dist/validate-integrations.js +388 -0
- package/dist/validate-relabel.js +52 -0
- package/dist/validate-security.js +135 -0
- package/dist/validate.js +69 -0
- package/package.json +10 -10
- package/src/codegen/package.ts +2 -2
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a parsed PromQL expression: the selectors it reads, the labels its
|
|
3
|
+
* result keeps, and whether it filters with a comparison. The rule-file
|
|
4
|
+
* checks PROM211-PROM219 (./validate-config.ts) are built on these.
|
|
5
|
+
*
|
|
6
|
+
* The tree is the one `checkPromql` (./promql.ts) parses with
|
|
7
|
+
* `@prometheus-io/lezer-promql`. Every function here returns nothing for an
|
|
8
|
+
* expression that does not parse; PROM104 reports that.
|
|
9
|
+
*/
|
|
10
|
+
import { parser } from "@prometheus-io/lezer-promql";
|
|
11
|
+
/** The expression under the tree's `PromQL` root, or undefined when it does not parse. */
|
|
12
|
+
export function parsePromql(expr) {
|
|
13
|
+
if (typeof expr !== "string" || expr.trim() === "")
|
|
14
|
+
return undefined;
|
|
15
|
+
const tree = parser.parse(expr);
|
|
16
|
+
let broken = false;
|
|
17
|
+
tree.iterate({
|
|
18
|
+
enter: (n) => {
|
|
19
|
+
if (n.type.isError)
|
|
20
|
+
broken = true;
|
|
21
|
+
return broken ? false : undefined;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
return broken ? undefined : (tree.topNode.firstChild ?? undefined);
|
|
25
|
+
}
|
|
26
|
+
function text(n, src) {
|
|
27
|
+
return src.slice(n.from, n.to);
|
|
28
|
+
}
|
|
29
|
+
/** The value of a PromQL string literal: double- or single-quoted with escapes, or a raw backtick string. */
|
|
30
|
+
export function stringValue(literal) {
|
|
31
|
+
if (literal.startsWith("`"))
|
|
32
|
+
return literal.slice(1, -1);
|
|
33
|
+
const body = literal.slice(1, -1);
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(`"${literal.startsWith("'") ? body.replace(/\\'/g, "'").replace(/"/g, '\\"') : body}"`);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return body;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function children(n) {
|
|
42
|
+
const out = [];
|
|
43
|
+
for (let c = n.firstChild; c; c = c.nextSibling)
|
|
44
|
+
out.push(c);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** The arguments of a function call or aggregation body, without the brackets and commas. */
|
|
48
|
+
function args(body) {
|
|
49
|
+
return body ? children(body).filter((c) => c.name !== "(" && c.name !== ")" && c.name !== ",") : [];
|
|
50
|
+
}
|
|
51
|
+
function unwrap(n) {
|
|
52
|
+
let cur = n;
|
|
53
|
+
while (cur.name === "ParenExpr" && cur.firstChild)
|
|
54
|
+
cur = children(cur).find((c) => c.name !== "(" && c.name !== ")") ?? cur;
|
|
55
|
+
return cur;
|
|
56
|
+
}
|
|
57
|
+
function selectorOf(n, src, within) {
|
|
58
|
+
const matchers = [];
|
|
59
|
+
const list = n.getChild("LabelMatchers");
|
|
60
|
+
for (const c of list ? children(list) : []) {
|
|
61
|
+
if (c.name !== "UnquotedLabelMatcher" && c.name !== "QuotedLabelMatcher")
|
|
62
|
+
continue;
|
|
63
|
+
const label = c.getChild("LabelName") ?? c.getChild("QuotedLabelName");
|
|
64
|
+
const op = c.getChild("MatchOp");
|
|
65
|
+
const value = c.getChild("StringLiteral");
|
|
66
|
+
if (!label || !op || !value)
|
|
67
|
+
continue;
|
|
68
|
+
const name = label.name === "QuotedLabelName" ? stringValue(text(label, src)) : text(label, src);
|
|
69
|
+
matchers.push({ name, op: text(op, src), value: stringValue(text(value, src)) });
|
|
70
|
+
}
|
|
71
|
+
const ident = n.getChild("Identifier");
|
|
72
|
+
const byName = matchers.find((m) => m.name === "__name__" && m.op === "=");
|
|
73
|
+
const name = ident ? text(ident, src) : byName?.value;
|
|
74
|
+
return { name, matchers, text: text(n, src), within };
|
|
75
|
+
}
|
|
76
|
+
/** The name of a function call or aggregation node, e.g. `rate` or `sum`. */
|
|
77
|
+
function callName(n, src) {
|
|
78
|
+
if (n.name === "FunctionCall") {
|
|
79
|
+
const id = n.getChild("FunctionIdentifier");
|
|
80
|
+
return id ? text(id, src) : undefined;
|
|
81
|
+
}
|
|
82
|
+
if (n.name === "AggregateExpr") {
|
|
83
|
+
const op = n.getChild("AggregateOp");
|
|
84
|
+
return op ? text(op, src) : undefined;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
/** Every vector selector in the expression, with the functions around it. */
|
|
89
|
+
export function selectors(expr) {
|
|
90
|
+
const top = parsePromql(expr);
|
|
91
|
+
if (!top)
|
|
92
|
+
return [];
|
|
93
|
+
const out = [];
|
|
94
|
+
const walk = (n, within) => {
|
|
95
|
+
if (n.name === "VectorSelector") {
|
|
96
|
+
out.push(selectorOf(n, expr, within));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const fn = callName(n, expr);
|
|
100
|
+
const inner = fn ? [fn, ...within] : within;
|
|
101
|
+
for (const c of children(n))
|
|
102
|
+
walk(c, inner);
|
|
103
|
+
};
|
|
104
|
+
walk(top, []);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
const COMPARISON = new Set(["Gtr", "Lss", "Gte", "Lte", "Eql", "Neq"]);
|
|
108
|
+
function binaryParts(n) {
|
|
109
|
+
const parts = children(n);
|
|
110
|
+
return {
|
|
111
|
+
lhs: parts[0],
|
|
112
|
+
op: parts[1],
|
|
113
|
+
rhs: parts[parts.length - 1],
|
|
114
|
+
bool: parts.some((p) => p.name === "BoolModifier"),
|
|
115
|
+
modifier: parts.find((p) => p.name === "MatchingModifierClause"),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Functions whose result is itself a condition: they return a series only when something is missing. */
|
|
119
|
+
const CONDITION_FUNCTIONS = new Set(["absent", "absent_over_time"]);
|
|
120
|
+
/**
|
|
121
|
+
* Whether the expression filters its result: a comparison without `bool`,
|
|
122
|
+
* `absent()`/`absent_over_time()`, or `unless`, anywhere a series can pass
|
|
123
|
+
* through to the result. An expression without one returns every series it
|
|
124
|
+
* reads, so an alert on it fires for all of them (pint alerts/comparison).
|
|
125
|
+
* `or` needs a condition on both sides, since either side alone reaches the
|
|
126
|
+
* result.
|
|
127
|
+
*/
|
|
128
|
+
export function hasCondition(expr) {
|
|
129
|
+
const top = parsePromql(expr);
|
|
130
|
+
if (!top)
|
|
131
|
+
return undefined;
|
|
132
|
+
const walk = (raw) => {
|
|
133
|
+
const n = unwrap(raw);
|
|
134
|
+
switch (n.name) {
|
|
135
|
+
case "BinaryExpr": {
|
|
136
|
+
const { lhs, op, rhs, bool } = binaryParts(n);
|
|
137
|
+
if (COMPARISON.has(op.name))
|
|
138
|
+
return !bool || walk(lhs) || walk(rhs);
|
|
139
|
+
if (op.name === "Unless")
|
|
140
|
+
return true;
|
|
141
|
+
if (op.name === "Or")
|
|
142
|
+
return walk(lhs) && walk(rhs);
|
|
143
|
+
return walk(lhs) || walk(rhs);
|
|
144
|
+
}
|
|
145
|
+
case "FunctionCall": {
|
|
146
|
+
const fn = callName(n, expr) ?? "";
|
|
147
|
+
if (CONDITION_FUNCTIONS.has(fn))
|
|
148
|
+
return true;
|
|
149
|
+
return args(n.getChild("FunctionCallBody")).some(walk);
|
|
150
|
+
}
|
|
151
|
+
case "AggregateExpr":
|
|
152
|
+
return args(n.getChild("FunctionCallBody")).some(walk);
|
|
153
|
+
case "UnaryExpr":
|
|
154
|
+
case "OffsetExpr":
|
|
155
|
+
case "StepInvariantExpr":
|
|
156
|
+
return children(n).some(walk);
|
|
157
|
+
case "SubqueryExpr":
|
|
158
|
+
case "MatrixSelector":
|
|
159
|
+
return n.firstChild ? walk(n.firstChild) : false;
|
|
160
|
+
default:
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
return walk(top);
|
|
165
|
+
}
|
|
166
|
+
const KEEPS_INPUT_LABELS = new Set(["topk", "bottomk", "limitk", "limit_ratio"]);
|
|
167
|
+
const NO_LABELS = new Set(["vector", "time", "scalar", "pi"]);
|
|
168
|
+
function grouping(mod, src) {
|
|
169
|
+
if (!mod)
|
|
170
|
+
return undefined;
|
|
171
|
+
const labels = (mod.getChild("GroupingLabels")?.getChildren("LabelName") ?? []).map((l) => text(l, src));
|
|
172
|
+
return { by: !!mod.getChild("By"), labels };
|
|
173
|
+
}
|
|
174
|
+
function minus(scope, drop) {
|
|
175
|
+
if (!scope)
|
|
176
|
+
return { except: new Set(drop) };
|
|
177
|
+
if ("only" in scope)
|
|
178
|
+
return { only: new Set([...scope.only].filter((l) => !drop.includes(l))) };
|
|
179
|
+
return { except: new Set([...scope.except, ...drop]) };
|
|
180
|
+
}
|
|
181
|
+
function plus(scope, add) {
|
|
182
|
+
if (!scope)
|
|
183
|
+
return undefined;
|
|
184
|
+
if ("only" in scope)
|
|
185
|
+
return { only: new Set([...scope.only, ...add]) };
|
|
186
|
+
return { except: new Set([...scope.except].filter((l) => !add.includes(l))) };
|
|
187
|
+
}
|
|
188
|
+
/** The labels the expression's result keeps; see {@link LabelScope}. */
|
|
189
|
+
export function resultLabels(expr) {
|
|
190
|
+
const top = parsePromql(expr);
|
|
191
|
+
if (!top)
|
|
192
|
+
return undefined;
|
|
193
|
+
const walk = (raw) => {
|
|
194
|
+
const n = unwrap(raw);
|
|
195
|
+
switch (n.name) {
|
|
196
|
+
case "NumberDurationLiteral":
|
|
197
|
+
case "StringLiteral":
|
|
198
|
+
return { only: new Set() };
|
|
199
|
+
case "AggregateExpr": {
|
|
200
|
+
const op = callName(n, expr) ?? "";
|
|
201
|
+
const body = args(n.getChild("FunctionCallBody"));
|
|
202
|
+
const last = body[body.length - 1];
|
|
203
|
+
const inner = last ? walk(last) : undefined;
|
|
204
|
+
if (KEEPS_INPUT_LABELS.has(op))
|
|
205
|
+
return inner;
|
|
206
|
+
if (op === "count_values")
|
|
207
|
+
return undefined;
|
|
208
|
+
const g = grouping(n.getChild("AggregateModifier"), expr);
|
|
209
|
+
if (!g)
|
|
210
|
+
return { only: new Set() };
|
|
211
|
+
return g.by ? { only: new Set(g.labels) } : minus(inner, g.labels);
|
|
212
|
+
}
|
|
213
|
+
case "FunctionCall": {
|
|
214
|
+
const fn = callName(n, expr) ?? "";
|
|
215
|
+
if (NO_LABELS.has(fn))
|
|
216
|
+
return { only: new Set() };
|
|
217
|
+
const a = args(n.getChild("FunctionCallBody"));
|
|
218
|
+
if (fn === "histogram_quantile" || fn === "histogram_fraction")
|
|
219
|
+
return minus(walk(a[a.length - 1]), ["le"]);
|
|
220
|
+
if (fn === "label_replace" || fn === "label_join") {
|
|
221
|
+
const dst = a[1]?.name === "StringLiteral" ? stringValue(text(a[1], expr)) : undefined;
|
|
222
|
+
return dst === undefined ? undefined : plus(walk(a[0]), [dst]);
|
|
223
|
+
}
|
|
224
|
+
if (CONDITION_FUNCTIONS.has(fn))
|
|
225
|
+
return undefined;
|
|
226
|
+
const vector = a.find((x) => x.name !== "NumberDurationLiteral" && x.name !== "StringLiteral");
|
|
227
|
+
return vector ? walk(vector) : { only: new Set() };
|
|
228
|
+
}
|
|
229
|
+
case "BinaryExpr": {
|
|
230
|
+
const { lhs, op, rhs, modifier } = binaryParts(n);
|
|
231
|
+
const l = walk(lhs);
|
|
232
|
+
const r = walk(rhs);
|
|
233
|
+
const lScalar = unwrap(lhs).name === "NumberDurationLiteral";
|
|
234
|
+
const rScalar = unwrap(rhs).name === "NumberDurationLiteral";
|
|
235
|
+
if (lScalar && !rScalar)
|
|
236
|
+
return r;
|
|
237
|
+
if (rScalar)
|
|
238
|
+
return l;
|
|
239
|
+
if (op.name === "And" || op.name === "Unless")
|
|
240
|
+
return l;
|
|
241
|
+
if (op.name === "Or") {
|
|
242
|
+
if (l && r && "only" in l && "only" in r)
|
|
243
|
+
return { only: new Set([...l.only, ...r.only]) };
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
if (!modifier)
|
|
247
|
+
return l;
|
|
248
|
+
const on = !!modifier.getChild("On");
|
|
249
|
+
const groupLabels = modifier.getChildren("GroupingLabels").map((g) => g.getChildren("LabelName").map((x) => text(x, expr)));
|
|
250
|
+
const matching = groupLabels[0] ?? [];
|
|
251
|
+
const extra = groupLabels[1] ?? [];
|
|
252
|
+
if (modifier.getChild("GroupLeft"))
|
|
253
|
+
return plus(l, extra);
|
|
254
|
+
if (modifier.getChild("GroupRight"))
|
|
255
|
+
return plus(r, extra);
|
|
256
|
+
// One-to-one: `on` keeps only the matching labels, `ignoring` drops them.
|
|
257
|
+
return on ? { only: new Set(matching.filter((x) => !l || ("only" in l ? l.only.has(x) : !l.except.has(x)))) } : minus(l, matching);
|
|
258
|
+
}
|
|
259
|
+
case "UnaryExpr":
|
|
260
|
+
case "OffsetExpr":
|
|
261
|
+
case "StepInvariantExpr":
|
|
262
|
+
return walk(children(n).find((c) => c.name !== "UnaryOp" && c.name !== "Offset" && c.name !== "OffsetDurationExpr") ?? n.firstChild);
|
|
263
|
+
default:
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
return walk(top);
|
|
268
|
+
}
|
|
269
|
+
/** Whether a label survives a scope. An undefined scope keeps every label. */
|
|
270
|
+
export function keepsLabel(scope, label) {
|
|
271
|
+
if (!scope)
|
|
272
|
+
return true;
|
|
273
|
+
return "only" in scope ? scope.only.has(label) : !scope.except.has(label);
|
|
274
|
+
}
|
|
275
|
+
/** `histogram_quantile` calls over a selector with no `_bucket` in its name, or an aggregation that drops `le`. */
|
|
276
|
+
export function histogramProblems(expr) {
|
|
277
|
+
const top = parsePromql(expr);
|
|
278
|
+
if (!top)
|
|
279
|
+
return [];
|
|
280
|
+
const out = [];
|
|
281
|
+
const walk = (n) => {
|
|
282
|
+
if (n.name === "FunctionCall" && callName(n, expr) === "histogram_quantile") {
|
|
283
|
+
const a = args(n.getChild("FunctionCallBody"));
|
|
284
|
+
const histogram = a[1];
|
|
285
|
+
if (histogram) {
|
|
286
|
+
const call = text(n, expr);
|
|
287
|
+
const names = selectors(text(histogram, expr)).flatMap((s) => (s.name ? [s.name] : []));
|
|
288
|
+
if (names.some((x) => !x.includes("_bucket")))
|
|
289
|
+
out.push({ call, problem: "no-bucket" });
|
|
290
|
+
else if (!keepsLabel(resultLabels(text(histogram, expr)), "le"))
|
|
291
|
+
out.push({ call, problem: "no-le" });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (const c of children(n))
|
|
295
|
+
walk(c);
|
|
296
|
+
};
|
|
297
|
+
walk(top);
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
const COUNTER_FUNCTIONS = new Set(["rate", "irate", "increase"]);
|
|
301
|
+
const COUNTER_SUFFIX = /_(total|count|sum|bucket)$/;
|
|
302
|
+
/** `rate`, `irate` or `increase` read straight from a named selector whose name does not end in a counter suffix. */
|
|
303
|
+
export function nonCounterRates(expr) {
|
|
304
|
+
return selectors(expr).flatMap((s) => {
|
|
305
|
+
const fn = s.within[0];
|
|
306
|
+
if (!fn || !COUNTER_FUNCTIONS.has(fn) || !s.name || COUNTER_SUFFIX.test(s.name))
|
|
307
|
+
return [];
|
|
308
|
+
return [{ fn, name: s.name }];
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const REGEX_META = /[.+*?()[\]{}|\\^$]/;
|
|
312
|
+
/** The `=~` and `!~` matchers in an expression that need no regex, or that are anchored (pint promql/regexp). */
|
|
313
|
+
export function regexProblems(expr) {
|
|
314
|
+
const out = [];
|
|
315
|
+
for (const s of selectors(expr)) {
|
|
316
|
+
for (const m of s.matchers) {
|
|
317
|
+
if (m.op !== "=~" && m.op !== "!~")
|
|
318
|
+
continue;
|
|
319
|
+
const shown = `${m.name}${m.op}${JSON.stringify(m.value)}`;
|
|
320
|
+
if (!REGEX_META.test(m.value))
|
|
321
|
+
out.push({ matcher: shown, problem: "literal" });
|
|
322
|
+
else if (m.value.startsWith("^") || (m.value.endsWith("$") && !m.value.endsWith("\\$")))
|
|
323
|
+
out.push({ matcher: shown, problem: "anchored" });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
/** Whether every selector in the expression is read through a `*_over_time` function, so the expression already spans a window. */
|
|
329
|
+
export function readsOnlyOverTime(expr) {
|
|
330
|
+
const all = selectors(expr);
|
|
331
|
+
return all.length > 0 && all.every((s) => s.within.some((fn) => fn.endsWith("_over_time")));
|
|
332
|
+
}
|
|
333
|
+
/** Whether the expression is two conditions joined by `and`: the multi-window form, where the short window does what `for` would. */
|
|
334
|
+
export function isMultiWindow(expr) {
|
|
335
|
+
const top = parsePromql(expr);
|
|
336
|
+
if (!top)
|
|
337
|
+
return false;
|
|
338
|
+
const n = unwrap(top);
|
|
339
|
+
if (n.name !== "BinaryExpr")
|
|
340
|
+
return false;
|
|
341
|
+
const { lhs, op, rhs } = binaryParts(n);
|
|
342
|
+
return op.name === "And" && hasCondition(text(lhs, expr)) === true && hasCondition(text(rhs, expr)) === true;
|
|
343
|
+
}
|
package/dist/promql.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromQL syntax checking, with the grammar the Prometheus project publishes
|
|
3
|
+
* (`@prometheus-io/lezer-promql`, the parser behind the Prometheus web UI's
|
|
4
|
+
* editor).
|
|
5
|
+
*
|
|
6
|
+
* This is a syntax check: unbalanced brackets, a bad duration in a range
|
|
7
|
+
* selector, an unknown function, two selectors with no operator between
|
|
8
|
+
* them. It does not type-check (`rate` over an instant vector parses), which
|
|
9
|
+
* `promtool check rules` does when it is available (see `tools.ts`).
|
|
10
|
+
*/
|
|
11
|
+
import { parser } from "@prometheus-io/lezer-promql";
|
|
12
|
+
/** The grammar package and the exact version this lexicon checks PromQL with. */
|
|
13
|
+
export const PROMQL_GRAMMAR = Object.freeze({ source: "@prometheus-io/lezer-promql", version: "0.315.0" });
|
|
14
|
+
function describe(expr, at) {
|
|
15
|
+
if (expr.trim() === "")
|
|
16
|
+
return "the expression is empty";
|
|
17
|
+
if (at >= expr.length)
|
|
18
|
+
return `the expression ends early (after "${expr.slice(Math.max(0, at - 20)).trim()}")`;
|
|
19
|
+
const before = expr.slice(Math.max(0, at - 20), at);
|
|
20
|
+
const after = expr.slice(at, at + 20);
|
|
21
|
+
return `syntax error at offset ${at}: "${before}" >>> "${after}"`;
|
|
22
|
+
}
|
|
23
|
+
/** Check one PromQL expression's syntax. */
|
|
24
|
+
export function checkPromql(expr) {
|
|
25
|
+
if (typeof expr !== "string" || expr.trim() === "") {
|
|
26
|
+
return { ok: false, position: 0, message: "the expression is empty" };
|
|
27
|
+
}
|
|
28
|
+
const tree = parser.parse(expr);
|
|
29
|
+
let errorAt;
|
|
30
|
+
tree.iterate({
|
|
31
|
+
enter: (node) => {
|
|
32
|
+
if (errorAt !== undefined)
|
|
33
|
+
return false;
|
|
34
|
+
if (node.type.isError) {
|
|
35
|
+
errorAt = node.from;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (errorAt === undefined)
|
|
42
|
+
return { ok: true };
|
|
43
|
+
return { ok: false, position: errorAt, message: describe(expr, errorAt) };
|
|
44
|
+
}
|