@intentius/chant-lexicon-prometheus 0.124.31 → 0.124.33

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.
Files changed (114) hide show
  1. package/dist/alertmanager.js +61 -0
  2. package/dist/api/alertmanager.js +98 -0
  3. package/dist/api/apply.js +306 -0
  4. package/dist/api/bind.js +66 -0
  5. package/dist/api/client.js +110 -0
  6. package/dist/api/evaluated.js +86 -0
  7. package/dist/api/fake-servers.js +165 -0
  8. package/dist/api/index.js +17 -0
  9. package/dist/api/ruler.js +122 -0
  10. package/dist/build.js +240 -0
  11. package/dist/catalog.js +66 -0
  12. package/dist/codegen/docs-cli.js +3 -0
  13. package/dist/codegen/docs.js +105 -0
  14. package/dist/codegen/generate-cli.js +7 -0
  15. package/dist/codegen/generate.js +42 -0
  16. package/dist/codegen/package.js +28 -0
  17. package/dist/collector-metrics.js +225 -0
  18. package/dist/composites/alert-routing.js +210 -0
  19. package/dist/composites/catalog.js +344 -0
  20. package/dist/composites/genai.js +454 -0
  21. package/dist/composites/index.js +8 -0
  22. package/dist/composites/red-alerts.js +135 -0
  23. package/dist/composites/slo.js +353 -0
  24. package/dist/composites/watchdog.js +110 -0
  25. package/dist/config-model.js +52 -0
  26. package/dist/config.js +225 -0
  27. package/dist/describe-resources.js +299 -0
  28. package/dist/detect.js +12 -0
  29. package/dist/duration.js +54 -0
  30. package/dist/export-resources.js +163 -0
  31. package/dist/import/alertmanager-live.js +309 -0
  32. package/dist/import/embedded.js +100 -0
  33. package/dist/import/generator.js +683 -0
  34. package/dist/import/parser.js +428 -0
  35. package/dist/import/slo.js +207 -0
  36. package/dist/index.js +33 -0
  37. package/dist/init-templates.js +119 -0
  38. package/dist/integrations.js +33 -0
  39. package/dist/integrity.json +2 -2
  40. package/dist/lint/audit-catalog.js +60 -0
  41. package/dist/lint/audit-lineage.js +42 -0
  42. package/dist/lint/post-synth/index.js +68 -0
  43. package/dist/lint/post-synth/prom-helpers.js +157 -0
  44. package/dist/lint/post-synth/prom101.js +13 -0
  45. package/dist/lint/post-synth/prom102.js +13 -0
  46. package/dist/lint/post-synth/prom103.js +13 -0
  47. package/dist/lint/post-synth/prom104.js +13 -0
  48. package/dist/lint/post-synth/prom105.js +13 -0
  49. package/dist/lint/post-synth/prom106.js +13 -0
  50. package/dist/lint/post-synth/prom107.js +13 -0
  51. package/dist/lint/post-synth/prom201.js +13 -0
  52. package/dist/lint/post-synth/prom202.js +13 -0
  53. package/dist/lint/post-synth/prom203.js +13 -0
  54. package/dist/lint/post-synth/prom204.js +13 -0
  55. package/dist/lint/post-synth/prom205.js +13 -0
  56. package/dist/lint/post-synth/prom206.js +13 -0
  57. package/dist/lint/post-synth/prom207.js +13 -0
  58. package/dist/lint/post-synth/prom208.js +13 -0
  59. package/dist/lint/post-synth/prom209.js +13 -0
  60. package/dist/lint/post-synth/prom210.js +13 -0
  61. package/dist/lint/post-synth/prom211.js +17 -0
  62. package/dist/lint/post-synth/prom212.js +14 -0
  63. package/dist/lint/post-synth/prom213.js +16 -0
  64. package/dist/lint/post-synth/prom214.js +14 -0
  65. package/dist/lint/post-synth/prom215.js +14 -0
  66. package/dist/lint/post-synth/prom216.js +15 -0
  67. package/dist/lint/post-synth/prom217.js +14 -0
  68. package/dist/lint/post-synth/prom218.js +15 -0
  69. package/dist/lint/post-synth/prom219.js +14 -0
  70. package/dist/lint/post-synth/prom220.js +14 -0
  71. package/dist/lint/post-synth/prom221.js +15 -0
  72. package/dist/lint/post-synth/prom222.js +14 -0
  73. package/dist/lint/post-synth/prom223.js +15 -0
  74. package/dist/lint/post-synth/prom224.js +14 -0
  75. package/dist/lint/post-synth/prom225.js +15 -0
  76. package/dist/lint/post-synth/prom301.js +19 -0
  77. package/dist/lint/rules/index.js +8 -0
  78. package/dist/lint/rules/literal-credential.js +127 -0
  79. package/dist/lint/rules/prom-ast.js +65 -0
  80. package/dist/lint/rules/promql-literal.js +65 -0
  81. package/dist/lint/rules/slo-literal.js +79 -0
  82. package/dist/lsp/completions.js +11 -0
  83. package/dist/lsp/hover.js +21 -0
  84. package/dist/manifest.json +1 -1
  85. package/dist/matchers.js +131 -0
  86. package/dist/model.js +108 -0
  87. package/dist/op/activities/index.js +20 -0
  88. package/dist/op/activities/promtool.js +87 -0
  89. package/dist/op/activities/rule-audit.js +128 -0
  90. package/dist/op/activities/rules-loaded.js +73 -0
  91. package/dist/op/activities/silence.js +123 -0
  92. package/dist/op/activity-contracts.js +51 -0
  93. package/dist/op/audit-op.js +28 -0
  94. package/dist/op/builders.js +57 -0
  95. package/dist/op/slo-rule-tests.js +148 -0
  96. package/dist/package-cli.js +14 -0
  97. package/dist/pin.js +13 -0
  98. package/dist/plugin.js +143 -0
  99. package/dist/prometheus-config.js +38 -0
  100. package/dist/promql-analysis.js +343 -0
  101. package/dist/promql.js +44 -0
  102. package/dist/rule-eval.js +466 -0
  103. package/dist/rules.js +97 -0
  104. package/dist/serializer.js +59 -0
  105. package/dist/skill-defs.js +41 -0
  106. package/dist/tools.js +85 -0
  107. package/dist/validate-cli.js +7 -0
  108. package/dist/validate-config.js +556 -0
  109. package/dist/validate-integrations.js +388 -0
  110. package/dist/validate-relabel.js +52 -0
  111. package/dist/validate-security.js +135 -0
  112. package/dist/validate.js +69 -0
  113. package/package.json +10 -10
  114. package/src/codegen/package.ts +2 -2
@@ -0,0 +1,466 @@
1
+ /**
2
+ * A small rule-file evaluator, for tests over synthetic series.
3
+ *
4
+ * It evaluates recording and alerting rules the way Prometheus does, one
5
+ * evaluation step at a time: each group's rules in order, recording rule
6
+ * results written back as series the later rules (and later steps) read.
7
+ * The PromQL it understands is the subset the lexicon's generated rules use:
8
+ * number literals, vector and range selectors with `=`, `!=`, `=~` and `!~`
9
+ * matchers, `rate`, `increase`, `avg_over_time`, `sum_over_time`,
10
+ * `count_over_time`, `vector`, `histogram_quantile`, `sum` (with `by` or
11
+ * `without`), the arithmetic and comparison operators (one-to-one, or
12
+ * many-to-one with `group_left` and no extra labels) and `and`, `or` and
13
+ * `unless` (with `on` or `ignoring`). Anything else throws, so a test never
14
+ * passes by silently evaluating something it does not understand.
15
+ *
16
+ * `histogram_quantile` interpolates linearly inside the bucket the rank
17
+ * falls in, as Prometheus does for classic buckets, and returns the upper
18
+ * bound of the highest finite bucket when the rank falls in `+Inf`.
19
+ *
20
+ * `rate` is the increase between the first and last sample in the range
21
+ * over the time between them, without Prometheus's extrapolation to the
22
+ * range's edges. For two counters sampled together that changes neither's
23
+ * ratio, which is what an SLI is. `promtool test rules`, when installed,
24
+ * checks the same rules with Prometheus's own engine.
25
+ *
26
+ * Not exported from the package root; tests import it by path.
27
+ */
28
+ import { parser } from "@prometheus-io/lezer-promql";
29
+ import { durationMs } from "./duration.js";
30
+ import { isAlertingRuleConfig, isRecordingRuleConfig } from "./model.js";
31
+ const LOOKBACK_MS = 5 * 60000;
32
+ const RANGE_FUNCTIONS = new Set(["rate", "increase", "avg_over_time", "sum_over_time", "count_over_time"]);
33
+ function key(labels) {
34
+ return JSON.stringify(Object.entries(labels).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
35
+ }
36
+ function withoutName(labels) {
37
+ const { __name__: _, ...rest } = labels;
38
+ return rest;
39
+ }
40
+ function matches(labels, m) {
41
+ const v = labels[m.name] ?? "";
42
+ switch (m.op) {
43
+ case "=":
44
+ return v === m.value;
45
+ case "!=":
46
+ return v !== m.value;
47
+ case "=~":
48
+ return new RegExp(`^(?:${m.value})$`).test(v);
49
+ case "!~":
50
+ return !new RegExp(`^(?:${m.value})$`).test(v);
51
+ }
52
+ }
53
+ /** An in-memory series store with rule evaluation over it. */
54
+ export class RuleEvaluator {
55
+ constructor(groups) {
56
+ this.groups = groups;
57
+ this.series = new Map();
58
+ /** When each active alert started, per alerting rule. */
59
+ this.pending = new Map();
60
+ }
61
+ /** Append one sample; samples of a series must arrive in time order. */
62
+ add(labels, t, v) {
63
+ const k = key(labels);
64
+ let s = this.series.get(k);
65
+ if (!s) {
66
+ s = { labels: { ...labels }, samples: [] };
67
+ this.series.set(k, s);
68
+ }
69
+ s.samples.push({ t, v });
70
+ }
71
+ /** Evaluate every group at time `t` (milliseconds); returns the alerts firing at `t`. */
72
+ step(t) {
73
+ const firing = [];
74
+ for (const group of this.groups) {
75
+ const groupLabels = group.labels ?? {};
76
+ for (const rule of group.rules) {
77
+ const result = this.eval(rule.expr, t);
78
+ const elements = result.kind === "scalar" ? [{ labels: {}, value: result.value }] : result.elements;
79
+ if (isRecordingRuleConfig(rule)) {
80
+ for (const e of elements) {
81
+ if (Number.isNaN(e.value))
82
+ continue;
83
+ this.add({ ...withoutName(e.labels), ...groupLabels, ...(rule.labels ?? {}), __name__: rule.record }, t, e.value);
84
+ }
85
+ }
86
+ else if (isAlertingRuleConfig(rule)) {
87
+ const before = this.pending.get(rule) ?? new Map();
88
+ const now = new Map();
89
+ const holdMs = rule.for ? (durationMs(rule.for) ?? 0) : 0;
90
+ for (const e of elements) {
91
+ const labels = { ...withoutName(e.labels), ...groupLabels, ...(rule.labels ?? {}), alertname: rule.alert };
92
+ const k = key(labels);
93
+ const since = before.get(k) ?? t;
94
+ now.set(k, since);
95
+ if (t - since >= holdMs)
96
+ firing.push({ labels, value: e.value });
97
+ }
98
+ this.pending.set(rule, now);
99
+ }
100
+ }
101
+ }
102
+ return firing;
103
+ }
104
+ /** The latest value of each series matching `selector` at `t`, e.g. `slo:x{slo="a"}`. */
105
+ query(expr, t) {
106
+ const v = this.eval(expr, t);
107
+ return v.kind === "scalar" ? [{ labels: {}, value: v.value }] : v.elements;
108
+ }
109
+ eval(expr, t) {
110
+ const tree = parser.parse(expr);
111
+ const top = tree.topNode.firstChild;
112
+ if (!top)
113
+ throw new Error(`rule-eval: empty expression`);
114
+ return this.node(top, expr, t);
115
+ }
116
+ text(n, src) {
117
+ return src.slice(n.from, n.to);
118
+ }
119
+ matchers(sel, src) {
120
+ const out = [];
121
+ const ident = sel.getChild("Identifier");
122
+ if (ident)
123
+ out.push({ name: "__name__", op: "=", value: this.text(ident, src) });
124
+ const list = sel.getChild("LabelMatchers");
125
+ if (list) {
126
+ for (let c = list.firstChild; c; c = c.nextSibling) {
127
+ if (c.name !== "UnquotedLabelMatcher")
128
+ continue;
129
+ const name = this.text(c.getChild("LabelName"), src);
130
+ const op = this.text(c.getChild("MatchOp"), src);
131
+ const value = JSON.parse(this.text(c.getChild("StringLiteral"), src).replace(/^'|'$/g, '"'));
132
+ out.push({ name, op, value });
133
+ }
134
+ }
135
+ return out;
136
+ }
137
+ select(ms) {
138
+ return [...this.series.values()].filter((s) => ms.every((m) => matches(s.labels, m)));
139
+ }
140
+ /** Samples in (t - range, t]. */
141
+ window(s, t, rangeMs) {
142
+ const xs = s.samples;
143
+ let lo = 0;
144
+ let hi = xs.length;
145
+ while (lo < hi) {
146
+ const mid = (lo + hi) >> 1;
147
+ if (xs[mid].t <= t - rangeMs)
148
+ lo = mid + 1;
149
+ else
150
+ hi = mid;
151
+ }
152
+ const out = [];
153
+ for (let i = lo; i < xs.length && xs[i].t <= t; i++)
154
+ out.push(xs[i]);
155
+ return out;
156
+ }
157
+ node(n, src, t) {
158
+ switch (n.name) {
159
+ case "NumberDurationLiteral":
160
+ return { kind: "scalar", value: Number(this.text(n, src)) };
161
+ case "ParenExpr":
162
+ return this.node(n.firstChild, src, t);
163
+ case "VectorSelector": {
164
+ const elements = [];
165
+ for (const s of this.select(this.matchers(n, src))) {
166
+ const w = this.window(s, t, LOOKBACK_MS);
167
+ if (w.length > 0)
168
+ elements.push({ labels: { ...s.labels }, value: w[w.length - 1].v });
169
+ }
170
+ return { kind: "vector", elements };
171
+ }
172
+ case "FunctionCall":
173
+ return this.call(n, src, t);
174
+ case "AggregateExpr":
175
+ return this.aggregate(n, src, t);
176
+ case "BinaryExpr":
177
+ return this.binary(n, src, t);
178
+ default:
179
+ throw new Error(`rule-eval: ${n.name} (${this.text(n, src)}) is not supported`);
180
+ }
181
+ }
182
+ args(body) {
183
+ const out = [];
184
+ for (let c = body.firstChild; c; c = c.nextSibling) {
185
+ if (c.name !== "(" && c.name !== ")" && c.name !== ",")
186
+ out.push(c);
187
+ }
188
+ return out;
189
+ }
190
+ call(n, src, t) {
191
+ const fn = this.text(n.getChild("FunctionIdentifier"), src);
192
+ const [arg, second] = this.args(n.getChild("FunctionCallBody"));
193
+ if (fn === "histogram_quantile")
194
+ return this.histogramQuantile(arg, second, src, t);
195
+ if (fn === "vector") {
196
+ const v = this.node(arg, src, t);
197
+ if (v.kind !== "scalar")
198
+ throw new Error("rule-eval: vector() takes a scalar");
199
+ return { kind: "vector", elements: [{ labels: {}, value: v.value }] };
200
+ }
201
+ // Checked before any series is read, so an unsupported function throws even over no data.
202
+ if (!RANGE_FUNCTIONS.has(fn))
203
+ throw new Error(`rule-eval: function ${fn} is not supported`);
204
+ if (arg.name !== "MatrixSelector")
205
+ throw new Error(`rule-eval: ${fn}() needs a range selector`);
206
+ const rangeMs = durationMs(this.text(arg.getChild("DurationExpr"), src));
207
+ if (rangeMs === undefined)
208
+ throw new Error(`rule-eval: bad range in ${this.text(arg, src)}`);
209
+ const elements = [];
210
+ for (const s of this.select(this.matchers(arg.getChild("VectorSelector"), src))) {
211
+ const w = this.window(s, t, rangeMs);
212
+ let value;
213
+ switch (fn) {
214
+ case "rate":
215
+ case "increase": {
216
+ if (w.length < 2)
217
+ continue;
218
+ const inc = w[w.length - 1].v - w[0].v;
219
+ value = fn === "rate" ? inc / ((w[w.length - 1].t - w[0].t) / 1000) : (inc * rangeMs) / (w[w.length - 1].t - w[0].t);
220
+ break;
221
+ }
222
+ case "avg_over_time":
223
+ case "sum_over_time":
224
+ case "count_over_time": {
225
+ if (w.length === 0)
226
+ continue;
227
+ const sum = w.reduce((a, x) => a + x.v, 0);
228
+ value = fn === "sum_over_time" ? sum : fn === "count_over_time" ? w.length : sum / w.length;
229
+ break;
230
+ }
231
+ default:
232
+ throw new Error(`rule-eval: function ${fn} is not supported`);
233
+ }
234
+ elements.push({ labels: withoutName(s.labels), value });
235
+ }
236
+ return { kind: "vector", elements };
237
+ }
238
+ histogramQuantile(qNode, vNode, src, t) {
239
+ const q = this.node(qNode, src, t);
240
+ const v = this.node(vNode, src, t);
241
+ if (q.kind !== "scalar" || v.kind !== "vector")
242
+ throw new Error("rule-eval: histogram_quantile takes a scalar and a vector");
243
+ const groups = new Map();
244
+ for (const e of v.elements) {
245
+ const { le, ...rest } = withoutName(e.labels);
246
+ if (le === undefined)
247
+ continue;
248
+ const k = key(rest);
249
+ const g = groups.get(k) ?? { labels: rest, buckets: [] };
250
+ g.buckets.push({ le: le === "+Inf" ? Infinity : Number(le), count: e.value });
251
+ groups.set(k, g);
252
+ }
253
+ const elements = [];
254
+ for (const { labels, buckets } of groups.values()) {
255
+ buckets.sort((a, b) => a.le - b.le);
256
+ const last = buckets[buckets.length - 1];
257
+ if (!last || last.le !== Infinity)
258
+ continue;
259
+ const total = last.count;
260
+ let value;
261
+ if (q.value < 0)
262
+ value = -Infinity;
263
+ else if (q.value > 1)
264
+ value = Infinity;
265
+ else if (!(total > 0))
266
+ value = NaN;
267
+ else {
268
+ const rank = q.value * total;
269
+ const i = buckets.findIndex((b) => b.count >= rank);
270
+ if (buckets[i].le === Infinity)
271
+ value = buckets.length > 1 ? buckets[buckets.length - 2].le : NaN;
272
+ else {
273
+ const start = i === 0 ? 0 : buckets[i - 1].le;
274
+ const below = i === 0 ? 0 : buckets[i - 1].count;
275
+ const inBucket = buckets[i].count - below;
276
+ value = inBucket > 0 ? start + (buckets[i].le - start) * ((rank - below) / inBucket) : buckets[i].le;
277
+ }
278
+ }
279
+ elements.push({ labels, value });
280
+ }
281
+ return { kind: "vector", elements };
282
+ }
283
+ aggregate(n, src, t) {
284
+ const op = this.text(n.getChild("AggregateOp"), src);
285
+ if (op !== "sum")
286
+ throw new Error(`rule-eval: aggregation ${op} is not supported`);
287
+ const mod = n.getChild("AggregateModifier");
288
+ let by;
289
+ let without;
290
+ if (mod) {
291
+ const names = (mod.getChild("GroupingLabels")?.getChildren("LabelName") ?? []).map((l) => this.text(l, src));
292
+ if (mod.getChild("By"))
293
+ by = names;
294
+ else
295
+ without = names;
296
+ }
297
+ const [arg] = this.args(n.getChild("FunctionCallBody"));
298
+ const v = this.node(arg, src, t);
299
+ if (v.kind !== "vector")
300
+ throw new Error("rule-eval: sum() takes a vector");
301
+ const groups = new Map();
302
+ for (const e of v.elements) {
303
+ const base = withoutName(e.labels);
304
+ const labels = {};
305
+ if (by)
306
+ for (const l of by)
307
+ if (base[l] !== undefined)
308
+ labels[l] = base[l];
309
+ if (without)
310
+ for (const [k, val] of Object.entries(base))
311
+ if (!without.includes(k))
312
+ labels[k] = val;
313
+ const k = key(labels);
314
+ const g = groups.get(k);
315
+ if (g)
316
+ g.value += e.value;
317
+ else
318
+ groups.set(k, { labels, value: e.value });
319
+ }
320
+ return { kind: "vector", elements: [...groups.values()] };
321
+ }
322
+ binary(n, src, t) {
323
+ const parts = [];
324
+ for (let c = n.firstChild; c; c = c.nextSibling)
325
+ parts.push(c);
326
+ const lhsNode = parts[0];
327
+ const rhsNode = parts[parts.length - 1];
328
+ const opNode = parts[1];
329
+ const op = this.text(opNode, src);
330
+ const modifier = parts.find((p) => p.name === "MatchingModifierClause");
331
+ if (parts.some((p) => p.name === "BoolModifier"))
332
+ throw new Error("rule-eval: bool modifier is not supported");
333
+ let on;
334
+ let ignoring = [];
335
+ let groupLeft = false;
336
+ if (modifier) {
337
+ const grouping = modifier.getChild("GroupingLabels");
338
+ const names = (grouping?.getChildren("LabelName") ?? []).map((l) => this.text(l, src));
339
+ if (modifier.getChild("On"))
340
+ on = names;
341
+ else
342
+ ignoring = names;
343
+ if (modifier.getChild("GroupRight"))
344
+ throw new Error("rule-eval: group_right is not supported");
345
+ const left = modifier.getChild("GroupLeft");
346
+ if (left) {
347
+ // group_left(<labels>) copies labels from the one side; only the bare form is supported.
348
+ const after = left.nextSibling;
349
+ if (after && after.name === "GroupingLabels" && after !== grouping)
350
+ throw new Error("rule-eval: group_left with labels is not supported");
351
+ groupLeft = true;
352
+ }
353
+ }
354
+ const lhs = this.node(lhsNode, src, t);
355
+ const rhs = this.node(rhsNode, src, t);
356
+ const signature = (labels) => {
357
+ const base = withoutName(labels);
358
+ const picked = {};
359
+ for (const [k, v] of Object.entries(base)) {
360
+ if (on ? on.includes(k) : !ignoring.includes(k))
361
+ picked[k] = v;
362
+ }
363
+ return key(picked);
364
+ };
365
+ if (op === "and" || op === "or" || op === "unless") {
366
+ if (lhs.kind !== "vector" || rhs.kind !== "vector")
367
+ throw new Error(`rule-eval: ${op} takes two vectors`);
368
+ const rhsSigs = new Set(rhs.elements.map((e) => signature(e.labels)));
369
+ if (op === "and")
370
+ return { kind: "vector", elements: lhs.elements.filter((e) => rhsSigs.has(signature(e.labels))) };
371
+ if (op === "unless")
372
+ return { kind: "vector", elements: lhs.elements.filter((e) => !rhsSigs.has(signature(e.labels))) };
373
+ const lhsSigs = new Set(lhs.elements.map((e) => signature(e.labels)));
374
+ return { kind: "vector", elements: [...lhs.elements, ...rhs.elements.filter((e) => !lhsSigs.has(signature(e.labels)))] };
375
+ }
376
+ const comparison = [">", "<", ">=", "<=", "==", "!="].includes(op);
377
+ const apply = (a, b) => {
378
+ switch (op) {
379
+ case "+":
380
+ return a + b;
381
+ case "-":
382
+ return a - b;
383
+ case "*":
384
+ return a * b;
385
+ case "/":
386
+ return a / b;
387
+ case ">":
388
+ return a > b ? a : undefined;
389
+ case "<":
390
+ return a < b ? a : undefined;
391
+ case ">=":
392
+ return a >= b ? a : undefined;
393
+ case "<=":
394
+ return a <= b ? a : undefined;
395
+ case "==":
396
+ return a === b ? a : undefined;
397
+ case "!=":
398
+ return a !== b ? a : undefined;
399
+ default:
400
+ throw new Error(`rule-eval: operator ${op} is not supported`);
401
+ }
402
+ };
403
+ if (lhs.kind === "scalar" && rhs.kind === "scalar") {
404
+ if (comparison)
405
+ throw new Error("rule-eval: scalar comparison needs bool");
406
+ return { kind: "scalar", value: apply(lhs.value, rhs.value) };
407
+ }
408
+ const keepName = comparison;
409
+ if (lhs.kind === "vector" && rhs.kind === "scalar") {
410
+ const elements = [];
411
+ for (const e of lhs.elements) {
412
+ const v = apply(e.value, rhs.value);
413
+ if (v !== undefined)
414
+ elements.push({ labels: keepName ? e.labels : withoutName(e.labels), value: v });
415
+ }
416
+ return { kind: "vector", elements };
417
+ }
418
+ if (lhs.kind === "scalar" && rhs.kind === "vector") {
419
+ const elements = [];
420
+ for (const e of rhs.elements) {
421
+ const v = apply(lhs.value, e.value);
422
+ // A scalar-vector comparison filters the vector and keeps its sample value.
423
+ if (v !== undefined)
424
+ elements.push({ labels: keepName ? e.labels : withoutName(e.labels), value: comparison ? e.value : v });
425
+ }
426
+ return { kind: "vector", elements };
427
+ }
428
+ const l = lhs;
429
+ const r = rhs;
430
+ const bySig = new Map();
431
+ for (const e of r.elements) {
432
+ const s = signature(e.labels);
433
+ if (bySig.has(s))
434
+ throw new Error("rule-eval: many-to-many matching is not supported");
435
+ bySig.set(s, e);
436
+ }
437
+ if (!groupLeft) {
438
+ const seen = new Set();
439
+ for (const e of l.elements) {
440
+ const s = signature(e.labels);
441
+ if (seen.has(s))
442
+ throw new Error("rule-eval: many-to-one matching needs group_left");
443
+ seen.add(s);
444
+ }
445
+ }
446
+ const elements = [];
447
+ for (const e of l.elements) {
448
+ const other = bySig.get(signature(e.labels));
449
+ if (!other)
450
+ continue;
451
+ const v = apply(e.value, other.value);
452
+ if (v === undefined)
453
+ continue;
454
+ const labels = on && !keepName ? pick(withoutName(e.labels), on) : keepName ? e.labels : withoutName(e.labels);
455
+ elements.push({ labels, value: v });
456
+ }
457
+ return { kind: "vector", elements };
458
+ }
459
+ }
460
+ function pick(labels, names) {
461
+ const out = {};
462
+ for (const n of names)
463
+ if (labels[n] !== undefined)
464
+ out[n] = labels[n];
465
+ return out;
466
+ }
package/dist/rules.js ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `RuleGroup`: one group of recording and alerting rules.
3
+ *
4
+ * A group is the unit Prometheus evaluates together, on one interval, and
5
+ * the unit both destinations take: an entry under `groups:` in a rule file,
6
+ * and an entry in a Prometheus Operator `PrometheusRule`'s `spec.groups`. The
7
+ * same declaration renders to either; `ruleGroupConfig()` is the one
8
+ * conversion both go through.
9
+ *
10
+ * Rules are plain objects in the rule file's own shape, so a composite (the
11
+ * SLO declaration, say) can build them as data and hand a list to one or
12
+ * more groups.
13
+ */
14
+ import { createResource } from "@intentius/chant/runtime";
15
+ import { isAlertingRuleConfig, isRecordingRuleConfig, } from "./model.js";
16
+ export const RULE_GROUP_TYPE = "Prometheus::Rules::RuleGroup";
17
+ const RuleGroupBase = createResource(RULE_GROUP_TYPE, "prometheus", {});
18
+ /**
19
+ * A group of recording and alerting rules.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * export const api = new RuleGroup({
24
+ * name: "api",
25
+ * interval: "30s",
26
+ * rules: [
27
+ * { record: "job:http_requests:rate5m", expr: "sum by (job) (rate(http_requests_total[5m]))" },
28
+ * {
29
+ * alert: "ApiHighErrorRate",
30
+ * expr: 'sum(rate(http_requests_total{code=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.05',
31
+ * for: "10m",
32
+ * labels: { severity: "page" },
33
+ * annotations: { summary: "API 5xx ratio above 5%" },
34
+ * },
35
+ * ],
36
+ * });
37
+ * ```
38
+ */
39
+ export const RuleGroup = function (props) {
40
+ RuleGroupBase.call(this, props);
41
+ Object.defineProperty(this, "groupName", { value: props?.name, enumerable: false });
42
+ };
43
+ Object.defineProperty(RuleGroup, "name", { value: "RuleGroup" });
44
+ /** True when `value` is a declared `RuleGroup`. */
45
+ export function isRuleGroup(value) {
46
+ return typeof value === "object" && value !== null && value.entityType === RULE_GROUP_TYPE;
47
+ }
48
+ function labelSet(value) {
49
+ if (value === undefined || value === null)
50
+ return undefined;
51
+ const out = {};
52
+ for (const [k, v] of Object.entries(value)) {
53
+ if (v !== undefined)
54
+ out[k] = String(v);
55
+ }
56
+ return out;
57
+ }
58
+ function defined(obj) {
59
+ for (const k of Object.keys(obj))
60
+ if (obj[k] === undefined)
61
+ delete obj[k];
62
+ return obj;
63
+ }
64
+ /** A rule in canonical key order, `undefined` fields dropped. */
65
+ export function ruleConfig(rule) {
66
+ if (isRecordingRuleConfig(rule)) {
67
+ return defined({ record: rule.record, expr: rule.expr, labels: labelSet(rule.labels) });
68
+ }
69
+ if (isAlertingRuleConfig(rule)) {
70
+ return defined({
71
+ alert: rule.alert,
72
+ expr: rule.expr,
73
+ for: rule.for,
74
+ keep_firing_for: rule.keep_firing_for,
75
+ labels: labelSet(rule.labels),
76
+ annotations: labelSet(rule.annotations),
77
+ });
78
+ }
79
+ // Neither `record` nor `alert`: keep it as written, and let PROM105 name it.
80
+ return { ...rule };
81
+ }
82
+ /**
83
+ * The plain group a `RuleGroup` (or group props) renders to, in canonical key
84
+ * order. This is what goes under `groups:` in a rule file and into a
85
+ * `PrometheusRule`'s `spec.groups`.
86
+ */
87
+ export function ruleGroupConfig(group) {
88
+ const p = isRuleGroup(group) ? group.props : group;
89
+ return defined({
90
+ name: p.name,
91
+ interval: p.interval,
92
+ query_offset: p.query_offset,
93
+ limit: p.limit,
94
+ labels: labelSet(p.labels),
95
+ rules: (p.rules ?? []).map(ruleConfig),
96
+ });
97
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Prometheus serializer.
3
+ *
4
+ * Emits the files a Prometheus setup reads:
5
+ *
6
+ * - the rule file (`groups:`), from every `RuleGroup` in the build, the file
7
+ * `rule_files:` points at and `promtool check rules` reads;
8
+ * - `prometheus.yml`, from the `PrometheusConfig` and `ScrapeConfig`
9
+ * entities, the file `promtool check config` reads;
10
+ * - `alertmanager.yml`, from the `Route`, `Receiver`, `InhibitRule`,
11
+ * `TimeInterval` and `AlertmanagerSettings` entities, the file
12
+ * `amtool check-config` reads.
13
+ *
14
+ * The first of those a build produces, in that order, is the primary output
15
+ * and the others are written beside it: the rule file, then `prometheus.yml`
16
+ * (`PROMETHEUS_FILE`), then `alertmanager.yml`. A project that declares only
17
+ * rule groups, or only Alertmanager entities, sees no change. Rules inside a
18
+ * group and child routes keep the order they are written in, which is the
19
+ * order that matters to Prometheus and Alertmanager; groups, scrape jobs,
20
+ * receivers and time intervals, whose order means nothing, are sorted by name.
21
+ *
22
+ * None of these files has a metadata channel, so there is no ownership marker
23
+ * to stamp.
24
+ */
25
+ import { buildAlertmanagerConfig, buildPrometheusConfig, buildRuleFile, emitYaml } from "./build.js";
26
+ /** The filename `prometheus.yml` is written under when it sits beside a rule file or `alertmanager.yml`. */
27
+ export const PROMETHEUS_FILE = "prometheus.yml";
28
+ /** The filename `alertmanager.yml` is written under when it sits beside a rule file. */
29
+ export const ALERTMANAGER_FILE = "alertmanager.yml";
30
+ export const prometheusSerializer = {
31
+ name: "prometheus",
32
+ rulePrefix: "PROM",
33
+ serialize(entities, _outputs) {
34
+ const rules = buildRuleFile(entities);
35
+ const prom = buildPrometheusConfig(entities);
36
+ const am = buildAlertmanagerConfig(entities);
37
+ const warnings = [...prom.warnings, ...am.warnings];
38
+ // The files this build writes, in the order of the primary: the rule file,
39
+ // then prometheus.yml, then alertmanager.yml.
40
+ const out = [];
41
+ if (rules.groups.length > 0)
42
+ out.push(["rules", emitYaml(rules.config)]);
43
+ if (prom.count > 0)
44
+ out.push([PROMETHEUS_FILE, emitYaml(prom.config)]);
45
+ if (am.count > 0)
46
+ out.push([ALERTMANAGER_FILE, emitYaml(am.config)]);
47
+ if (out.length === 0)
48
+ return "";
49
+ if (out.length === 1) {
50
+ return warnings.length === 0 ? out[0][1] : { primary: out[0][1], warnings };
51
+ }
52
+ const [[, primary], ...rest] = out;
53
+ return {
54
+ primary,
55
+ files: Object.fromEntries(rest),
56
+ ...(warnings.length > 0 ? { warnings } : {}),
57
+ };
58
+ },
59
+ };
@@ -0,0 +1,41 @@
1
+ import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
2
+ /** The prometheus lexicon's AI skills, read from src/skills/. */
3
+ export const prometheusSkills = createSkillsLoader(import.meta.url, [
4
+ {
5
+ file: "chant-prometheus.md",
6
+ name: "chant-prometheus",
7
+ description: "Declare Prometheus recording and alerting rule groups as typed chant entities and build a rule file that passes promtool",
8
+ triggers: [
9
+ { type: "context", value: "prometheus rules" },
10
+ { type: "context", value: "alerting rule" },
11
+ { type: "context", value: "recording rule" },
12
+ ],
13
+ examples: [
14
+ {
15
+ title: "A recording rule and an alert on it",
16
+ output: "export const api = new RuleGroup({\n" +
17
+ ' name: "api",\n' +
18
+ " rules: [\n" +
19
+ ' { record: "job:http_errors:ratio5m", expr: \'sum by (job) (rate(http_requests_total{code=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m]))\' },\n' +
20
+ ' { alert: "ApiErrors", expr: "job:http_errors:ratio5m > 0.05", for: "10m", labels: { severity: "page" }, annotations: { summary: "5xx ratio above 5%" } },\n' +
21
+ " ],\n" +
22
+ "});",
23
+ },
24
+ ],
25
+ },
26
+ {
27
+ file: "chant-prometheus-alertmanager.md",
28
+ name: "chant-prometheus-alertmanager",
29
+ description: "Declare Alertmanager routing (routes, receivers, inhibit rules, time intervals) and keep every alert severity routed",
30
+ triggers: [
31
+ { type: "context", value: "alertmanager" },
32
+ { type: "context", value: "alert routing" },
33
+ ],
34
+ },
35
+ {
36
+ file: "chant-prometheus-kubernetes.md",
37
+ name: "chant-prometheus-kubernetes",
38
+ description: "Render the same RuleGroups into a Prometheus Operator PrometheusRule, or into ConfigMaps for a plain Prometheus and Alertmanager on Kubernetes",
39
+ triggers: [{ type: "context", value: "PrometheusRule" }],
40
+ },
41
+ ]);