@intentius/chant-lexicon-prometheus 0.96.0
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/README.md +58 -0
- package/dist/alertmanager.d.ts +81 -0
- package/dist/alertmanager.d.ts.map +1 -0
- package/dist/build.d.ts +33 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/catalog.d.ts +17 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +9 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +3 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +19 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +7 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/composites/catalog.d.ts +3 -0
- package/dist/composites/catalog.d.ts.map +1 -0
- package/dist/composites/index.d.ts +6 -0
- package/dist/composites/index.d.ts.map +1 -0
- package/dist/composites/slo.d.ts +205 -0
- package/dist/composites/slo.d.ts.map +1 -0
- package/dist/detect.d.ts +2 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/duration.d.ts +16 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/init-templates.d.ts +13 -0
- package/dist/init-templates.d.ts.map +1 -0
- package/dist/integrity.json +33 -0
- package/dist/lint/audit-catalog.d.ts +13 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/post-synth/prom-helpers.d.ts +33 -0
- package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/prom101.d.ts +8 -0
- package/dist/lint/post-synth/prom101.d.ts.map +1 -0
- package/dist/lint/post-synth/prom102.d.ts +8 -0
- package/dist/lint/post-synth/prom102.d.ts.map +1 -0
- package/dist/lint/post-synth/prom103.d.ts +8 -0
- package/dist/lint/post-synth/prom103.d.ts.map +1 -0
- package/dist/lint/post-synth/prom104.d.ts +8 -0
- package/dist/lint/post-synth/prom104.d.ts.map +1 -0
- package/dist/lint/post-synth/prom105.d.ts +8 -0
- package/dist/lint/post-synth/prom105.d.ts.map +1 -0
- package/dist/lint/post-synth/prom106.d.ts +8 -0
- package/dist/lint/post-synth/prom106.d.ts.map +1 -0
- package/dist/lint/post-synth/prom107.d.ts +8 -0
- package/dist/lint/post-synth/prom107.d.ts.map +1 -0
- package/dist/lint/post-synth/prom201.d.ts +8 -0
- package/dist/lint/post-synth/prom201.d.ts.map +1 -0
- package/dist/lint/post-synth/prom202.d.ts +8 -0
- package/dist/lint/post-synth/prom202.d.ts.map +1 -0
- package/dist/lint/post-synth/prom203.d.ts +8 -0
- package/dist/lint/post-synth/prom203.d.ts.map +1 -0
- package/dist/lint/post-synth/prom204.d.ts +8 -0
- package/dist/lint/post-synth/prom204.d.ts.map +1 -0
- package/dist/lint/post-synth/prom205.d.ts +8 -0
- package/dist/lint/post-synth/prom205.d.ts.map +1 -0
- package/dist/lint/post-synth/prom206.d.ts +8 -0
- package/dist/lint/post-synth/prom206.d.ts.map +1 -0
- package/dist/lint/post-synth/prom207.d.ts +8 -0
- package/dist/lint/post-synth/prom207.d.ts.map +1 -0
- package/dist/lint/post-synth/prom208.d.ts +8 -0
- package/dist/lint/post-synth/prom208.d.ts.map +1 -0
- package/dist/lint/post-synth/prom209.d.ts +8 -0
- package/dist/lint/post-synth/prom209.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +7 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/literal-credential.d.ts +18 -0
- package/dist/lint/rules/literal-credential.d.ts.map +1 -0
- package/dist/lint/rules/prom-ast.d.ts +14 -0
- package/dist/lint/rules/prom-ast.d.ts.map +1 -0
- package/dist/lint/rules/promql-literal.d.ts +11 -0
- package/dist/lint/rules/promql-literal.d.ts.map +1 -0
- package/dist/lint/rules/slo-literal.d.ts +13 -0
- package/dist/lint/rules/slo-literal.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +4 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +4 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/manifest.json +8 -0
- package/dist/matchers.d.ts +29 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/meta.json +32 -0
- package/dist/model.d.ts +262 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/okf/index.md +34 -0
- package/dist/okf/rules/PROM001.md +16 -0
- package/dist/okf/rules/PROM002.md +15 -0
- package/dist/okf/rules/PROM003.md +11 -0
- package/dist/okf/rules/PROM101.md +11 -0
- package/dist/okf/rules/PROM102.md +11 -0
- package/dist/okf/rules/PROM103.md +11 -0
- package/dist/okf/rules/PROM104.md +11 -0
- package/dist/okf/rules/PROM105.md +11 -0
- package/dist/okf/rules/PROM106.md +11 -0
- package/dist/okf/rules/PROM107.md +11 -0
- package/dist/okf/rules/PROM201.md +11 -0
- package/dist/okf/rules/PROM202.md +11 -0
- package/dist/okf/rules/PROM203.md +11 -0
- package/dist/okf/rules/PROM204.md +11 -0
- package/dist/okf/rules/PROM205.md +11 -0
- package/dist/okf/rules/PROM206.md +11 -0
- package/dist/okf/rules/PROM207.md +11 -0
- package/dist/okf/rules/PROM208.md +11 -0
- package/dist/okf/rules/PROM209.md +11 -0
- package/dist/okf/types/AlertmanagerSettings.md +13 -0
- package/dist/okf/types/InhibitRule.md +9 -0
- package/dist/okf/types/Receiver.md +13 -0
- package/dist/okf/types/Route.md +9 -0
- package/dist/okf/types/RuleGroup.md +13 -0
- package/dist/okf/types/TimeInterval.md +9 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/pin.d.ts +20 -0
- package/dist/pin.d.ts.map +1 -0
- package/dist/plugin.d.ts +10 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/promql.d.ts +25 -0
- package/dist/promql.d.ts.map +1 -0
- package/dist/rule-eval.d.ts +67 -0
- package/dist/rule-eval.d.ts.map +1 -0
- package/dist/rules/literal-credential.ts +84 -0
- package/dist/rules/prom-ast.ts +30 -0
- package/dist/rules/prom-helpers.ts +89 -0
- package/dist/rules/prom101.ts +17 -0
- package/dist/rules/prom102.ts +17 -0
- package/dist/rules/prom103.ts +17 -0
- package/dist/rules/prom104.ts +17 -0
- package/dist/rules/prom105.ts +17 -0
- package/dist/rules/prom106.ts +17 -0
- package/dist/rules/prom107.ts +17 -0
- package/dist/rules/prom201.ts +17 -0
- package/dist/rules/prom202.ts +17 -0
- package/dist/rules/prom203.ts +17 -0
- package/dist/rules/prom204.ts +17 -0
- package/dist/rules/prom205.ts +17 -0
- package/dist/rules/prom206.ts +17 -0
- package/dist/rules/prom207.ts +17 -0
- package/dist/rules/prom208.ts +17 -0
- package/dist/rules/prom209.ts +17 -0
- package/dist/rules/promql-literal.ts +55 -0
- package/dist/rules/slo-literal.ts +81 -0
- package/dist/rules.d.ts +59 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/serializer.d.ts +27 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skill-defs.d.ts +3 -0
- package/dist/skill-defs.d.ts.map +1 -0
- package/dist/skills/chant-prometheus-alertmanager.md +58 -0
- package/dist/skills/chant-prometheus-kubernetes.md +48 -0
- package/dist/skills/chant-prometheus.md +91 -0
- package/dist/tools.d.ts +30 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate-config.d.ts +35 -0
- package/dist/validate-config.d.ts.map +1 -0
- package/dist/validate.d.ts +11 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +77 -0
- package/src/alertmanager.ts +143 -0
- package/src/build.ts +199 -0
- package/src/catalog.ts +65 -0
- package/src/codegen/docs-cli.ts +4 -0
- package/src/codegen/docs.ts +104 -0
- package/src/codegen/generate-cli.ts +8 -0
- package/src/codegen/generate.ts +49 -0
- package/src/codegen/package.ts +41 -0
- package/src/composites/catalog.test.ts +37 -0
- package/src/composites/catalog.ts +65 -0
- package/src/composites/composites.test.ts +256 -0
- package/src/composites/index.ts +23 -0
- package/src/composites/slo-burn.test.ts +289 -0
- package/src/composites/slo.ts +471 -0
- package/src/detect.ts +12 -0
- package/src/duration.ts +51 -0
- package/src/generated/lexicon-prometheus.json +32 -0
- package/src/index.ts +97 -0
- package/src/init-templates.test.ts +29 -0
- package/src/init-templates.ts +76 -0
- package/src/lint/audit-catalog.ts +123 -0
- package/src/lint/post-synth/index.ts +37 -0
- package/src/lint/post-synth/post-synth.test.ts +256 -0
- package/src/lint/post-synth/prom-helpers.ts +89 -0
- package/src/lint/post-synth/prom101.ts +17 -0
- package/src/lint/post-synth/prom102.ts +17 -0
- package/src/lint/post-synth/prom103.ts +17 -0
- package/src/lint/post-synth/prom104.ts +17 -0
- package/src/lint/post-synth/prom105.ts +17 -0
- package/src/lint/post-synth/prom106.ts +17 -0
- package/src/lint/post-synth/prom107.ts +17 -0
- package/src/lint/post-synth/prom201.ts +17 -0
- package/src/lint/post-synth/prom202.ts +17 -0
- package/src/lint/post-synth/prom203.ts +17 -0
- package/src/lint/post-synth/prom204.ts +17 -0
- package/src/lint/post-synth/prom205.ts +17 -0
- package/src/lint/post-synth/prom206.ts +17 -0
- package/src/lint/post-synth/prom207.ts +17 -0
- package/src/lint/post-synth/prom208.ts +17 -0
- package/src/lint/post-synth/prom209.ts +17 -0
- package/src/lint/rules/index.ts +11 -0
- package/src/lint/rules/literal-credential.ts +84 -0
- package/src/lint/rules/prom-ast.ts +30 -0
- package/src/lint/rules/promql-literal.ts +55 -0
- package/src/lint/rules/rules.test.ts +114 -0
- package/src/lint/rules/slo-literal.ts +81 -0
- package/src/lsp/completions.test.ts +23 -0
- package/src/lsp/completions.ts +15 -0
- package/src/lsp/hover.test.ts +27 -0
- package/src/lsp/hover.ts +26 -0
- package/src/matchers.ts +132 -0
- package/src/model.test.ts +104 -0
- package/src/model.ts +290 -0
- package/src/package-cli.ts +17 -0
- package/src/pin.ts +13 -0
- package/src/plugin.test.ts +73 -0
- package/src/plugin.ts +129 -0
- package/src/promql.ts +46 -0
- package/src/rule-eval.ts +397 -0
- package/src/rules.ts +127 -0
- package/src/serializer.test.ts +179 -0
- package/src/serializer.ts +55 -0
- package/src/skill-defs.ts +43 -0
- package/src/skills/chant-prometheus-alertmanager.md +58 -0
- package/src/skills/chant-prometheus-kubernetes.md +48 -0
- package/src/skills/chant-prometheus.md +91 -0
- package/src/tools.test.ts +119 -0
- package/src/tools.ts +70 -0
- package/src/validate-cli.ts +7 -0
- package/src/validate-config.ts +400 -0
- package/src/validate.ts +66 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { LexiconPlugin } from "@intentius/chant/lexicon";
|
|
2
|
+
import type { CompletionContext, HoverContext } from "@intentius/chant/lsp/types";
|
|
3
|
+
import type { McpResourceContribution } from "@intentius/chant/mcp/types";
|
|
4
|
+
import { createDiffTool } from "@intentius/chant/lexicon-plugin-helpers";
|
|
5
|
+
import { prometheusSerializer } from "./serializer";
|
|
6
|
+
import { rules } from "./lint/rules";
|
|
7
|
+
import { postSynthChecks } from "./lint/post-synth";
|
|
8
|
+
import { prometheusAuditCatalog } from "./lint/audit-catalog";
|
|
9
|
+
import { completions } from "./lsp/completions";
|
|
10
|
+
import { hover } from "./lsp/hover";
|
|
11
|
+
import { detectTemplate } from "./detect";
|
|
12
|
+
import { initTemplates } from "./init-templates";
|
|
13
|
+
import { prometheusSkills } from "./skill-defs";
|
|
14
|
+
import { CATALOG } from "./catalog";
|
|
15
|
+
import { PROMETHEUS_PIN } from "./pin";
|
|
16
|
+
import { PROMQL_GRAMMAR } from "./promql";
|
|
17
|
+
import { compositeCatalog } from "./composites/catalog";
|
|
18
|
+
|
|
19
|
+
const catalogResource: McpResourceContribution = {
|
|
20
|
+
uri: "prometheus:resource-catalog",
|
|
21
|
+
name: "Prometheus entity catalog",
|
|
22
|
+
description: "The rule group and Alertmanager entities this lexicon types, with the Prometheus and Alertmanager releases they follow",
|
|
23
|
+
mimeType: "application/json",
|
|
24
|
+
async handler(): Promise<string> {
|
|
25
|
+
return JSON.stringify({ pin: PROMETHEUS_PIN, promql: PROMQL_GRAMMAR, entities: CATALOG });
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Prometheus lexicon plugin.
|
|
31
|
+
*
|
|
32
|
+
* Typed recording and alerting rule groups, serialized to a Prometheus rule
|
|
33
|
+
* file, and Alertmanager routes, receivers, inhibit rules and time
|
|
34
|
+
* intervals, serialized to `alertmanager.yml`.
|
|
35
|
+
*/
|
|
36
|
+
export const prometheusPlugin: LexiconPlugin = {
|
|
37
|
+
name: "prometheus",
|
|
38
|
+
serializer: prometheusSerializer,
|
|
39
|
+
|
|
40
|
+
// ── Required lifecycle methods ────────────────────────────────
|
|
41
|
+
|
|
42
|
+
async generate(options?: { verbose?: boolean }): Promise<void> {
|
|
43
|
+
const { generate, writeGeneratedFiles } = await import("./codegen/generate");
|
|
44
|
+
writeGeneratedFiles(await generate(options));
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async validate(_options?: { verbose?: boolean }): Promise<void> {
|
|
48
|
+
const { validate } = await import("./validate");
|
|
49
|
+
const { printValidationResult } = await import("@intentius/chant/codegen/validate");
|
|
50
|
+
printValidationResult(await validate());
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async coverage(_options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
|
|
54
|
+
const { prometheus, alertmanager } = PROMETHEUS_PIN;
|
|
55
|
+
console.error(`prometheus: ${CATALOG.length} entities`);
|
|
56
|
+
console.error(` rule file (${prometheus.source} ${prometheus.version}): RuleGroup, recording and alerting rules`);
|
|
57
|
+
console.error(
|
|
58
|
+
` alertmanager.yml (${alertmanager.source} ${alertmanager.version}): ` +
|
|
59
|
+
CATALOG.filter((c) => c.file === "alertmanager.yml").map((c) => c.className).join(", "),
|
|
60
|
+
);
|
|
61
|
+
console.error(" receivers typed: webhook, email, slack, pagerduty");
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
|
|
65
|
+
const { packageLexicon } = await import("./codegen/package");
|
|
66
|
+
const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
|
|
67
|
+
const { join, dirname } = await import("path");
|
|
68
|
+
const { fileURLToPath } = await import("url");
|
|
69
|
+
const { spec, stats } = await packageLexicon(options);
|
|
70
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
71
|
+
writeBundleSpec(spec, join(pkgDir, "dist"));
|
|
72
|
+
console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// ── Optional extensions ────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
lintRules() {
|
|
78
|
+
return rules;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
postSynthChecks() {
|
|
82
|
+
return postSynthChecks;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
auditCatalog() {
|
|
86
|
+
return prometheusAuditCatalog;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
skills: prometheusSkills,
|
|
90
|
+
|
|
91
|
+
composites() {
|
|
92
|
+
return compositeCatalog;
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
mcpTools() {
|
|
96
|
+
return [
|
|
97
|
+
createDiffTool(
|
|
98
|
+
prometheusSerializer,
|
|
99
|
+
"Compare the current rule file and alertmanager.yml output against the previous build",
|
|
100
|
+
"prometheus",
|
|
101
|
+
),
|
|
102
|
+
];
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
mcpResources() {
|
|
106
|
+
return [catalogResource];
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
detectTemplate(data: unknown) {
|
|
110
|
+
return detectTemplate(data);
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
initTemplates(template?: string) {
|
|
114
|
+
return initTemplates(template);
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
completionProvider(ctx: CompletionContext) {
|
|
118
|
+
return completions(ctx);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
hoverProvider(ctx: HoverContext) {
|
|
122
|
+
return hover(ctx);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async docs(options?: { verbose?: boolean }) {
|
|
126
|
+
const { generateDocs } = await import("./codegen/docs");
|
|
127
|
+
return generateDocs(options);
|
|
128
|
+
},
|
|
129
|
+
};
|
package/src/promql.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
|
|
12
|
+
import { parser } from "@prometheus-io/lezer-promql";
|
|
13
|
+
|
|
14
|
+
/** The grammar package and the exact version this lexicon checks PromQL with. */
|
|
15
|
+
export const PROMQL_GRAMMAR = Object.freeze({ source: "@prometheus-io/lezer-promql", version: "0.315.0" });
|
|
16
|
+
|
|
17
|
+
export type PromqlCheck = { ok: true } | { ok: false; position: number; message: string };
|
|
18
|
+
|
|
19
|
+
function describe(expr: string, at: number): string {
|
|
20
|
+
if (expr.trim() === "") return "the expression is empty";
|
|
21
|
+
if (at >= expr.length) return `the expression ends early (after "${expr.slice(Math.max(0, at - 20)).trim()}")`;
|
|
22
|
+
const before = expr.slice(Math.max(0, at - 20), at);
|
|
23
|
+
const after = expr.slice(at, at + 20);
|
|
24
|
+
return `syntax error at offset ${at}: "${before}" >>> "${after}"`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Check one PromQL expression's syntax. */
|
|
28
|
+
export function checkPromql(expr: string): PromqlCheck {
|
|
29
|
+
if (typeof expr !== "string" || expr.trim() === "") {
|
|
30
|
+
return { ok: false, position: 0, message: "the expression is empty" };
|
|
31
|
+
}
|
|
32
|
+
const tree = parser.parse(expr);
|
|
33
|
+
let errorAt: number | undefined;
|
|
34
|
+
tree.iterate({
|
|
35
|
+
enter: (node) => {
|
|
36
|
+
if (errorAt !== undefined) return false;
|
|
37
|
+
if (node.type.isError) {
|
|
38
|
+
errorAt = node.from;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
if (errorAt === undefined) return { ok: true };
|
|
45
|
+
return { ok: false, position: errorAt, message: describe(expr, errorAt) };
|
|
46
|
+
}
|
package/src/rule-eval.ts
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
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`, `sum` (with `by` or `without`), the
|
|
11
|
+
* arithmetic and comparison operators and `and`, `or` and `unless` (with
|
|
12
|
+
* `on` or `ignoring`). Anything else throws, so a test never passes by
|
|
13
|
+
* silently evaluating something it does not understand.
|
|
14
|
+
*
|
|
15
|
+
* `rate` is the increase between the first and last sample in the range
|
|
16
|
+
* over the time between them, without Prometheus's extrapolation to the
|
|
17
|
+
* range's edges. For two counters sampled together that changes neither's
|
|
18
|
+
* ratio, which is what an SLI is. `promtool test rules`, when installed,
|
|
19
|
+
* checks the same rules with Prometheus's own engine.
|
|
20
|
+
*
|
|
21
|
+
* Not exported from the package root; tests import it by path.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { parser } from "@prometheus-io/lezer-promql";
|
|
25
|
+
import { durationMs } from "./duration";
|
|
26
|
+
import { isAlertingRuleConfig, isRecordingRuleConfig, type LabelSet, type RuleGroupConfig } from "./model";
|
|
27
|
+
|
|
28
|
+
type SyntaxNode = ReturnType<typeof parser.parse>["topNode"];
|
|
29
|
+
|
|
30
|
+
export type Labels = Record<string, string>;
|
|
31
|
+
export interface Sample {
|
|
32
|
+
t: number;
|
|
33
|
+
v: number;
|
|
34
|
+
}
|
|
35
|
+
export interface Series {
|
|
36
|
+
labels: Labels;
|
|
37
|
+
samples: Sample[];
|
|
38
|
+
}
|
|
39
|
+
export interface VectorElement {
|
|
40
|
+
labels: Labels;
|
|
41
|
+
value: number;
|
|
42
|
+
}
|
|
43
|
+
type Value = { kind: "scalar"; value: number } | { kind: "vector"; elements: VectorElement[] };
|
|
44
|
+
|
|
45
|
+
/** One firing alert at one evaluation. */
|
|
46
|
+
export interface FiringAlert {
|
|
47
|
+
labels: Labels;
|
|
48
|
+
value: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const LOOKBACK_MS = 5 * 60_000;
|
|
52
|
+
|
|
53
|
+
function key(labels: Labels): string {
|
|
54
|
+
return JSON.stringify(Object.entries(labels).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function withoutName(labels: Labels): Labels {
|
|
58
|
+
const { __name__: _, ...rest } = labels;
|
|
59
|
+
return rest;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface Matcher {
|
|
63
|
+
name: string;
|
|
64
|
+
op: "=" | "!=" | "=~" | "!~";
|
|
65
|
+
value: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function matches(labels: Labels, m: Matcher): boolean {
|
|
69
|
+
const v = labels[m.name] ?? "";
|
|
70
|
+
switch (m.op) {
|
|
71
|
+
case "=":
|
|
72
|
+
return v === m.value;
|
|
73
|
+
case "!=":
|
|
74
|
+
return v !== m.value;
|
|
75
|
+
case "=~":
|
|
76
|
+
return new RegExp(`^(?:${m.value})$`).test(v);
|
|
77
|
+
case "!~":
|
|
78
|
+
return !new RegExp(`^(?:${m.value})$`).test(v);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** An in-memory series store with rule evaluation over it. */
|
|
83
|
+
export class RuleEvaluator {
|
|
84
|
+
private readonly series = new Map<string, Series>();
|
|
85
|
+
/** When each active alert started, per alerting rule. */
|
|
86
|
+
private readonly pending = new Map<object, Map<string, number>>();
|
|
87
|
+
|
|
88
|
+
constructor(private readonly groups: RuleGroupConfig[]) {}
|
|
89
|
+
|
|
90
|
+
/** Append one sample; samples of a series must arrive in time order. */
|
|
91
|
+
add(labels: Labels, t: number, v: number): void {
|
|
92
|
+
const k = key(labels);
|
|
93
|
+
let s = this.series.get(k);
|
|
94
|
+
if (!s) {
|
|
95
|
+
s = { labels: { ...labels }, samples: [] };
|
|
96
|
+
this.series.set(k, s);
|
|
97
|
+
}
|
|
98
|
+
s.samples.push({ t, v });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Evaluate every group at time `t` (milliseconds); returns the alerts firing at `t`. */
|
|
102
|
+
step(t: number): FiringAlert[] {
|
|
103
|
+
const firing: FiringAlert[] = [];
|
|
104
|
+
for (const group of this.groups) {
|
|
105
|
+
const groupLabels: LabelSet = group.labels ?? {};
|
|
106
|
+
for (const rule of group.rules) {
|
|
107
|
+
const result = this.eval(rule.expr, t);
|
|
108
|
+
const elements = result.kind === "scalar" ? [{ labels: {}, value: result.value }] : result.elements;
|
|
109
|
+
if (isRecordingRuleConfig(rule)) {
|
|
110
|
+
for (const e of elements) {
|
|
111
|
+
if (Number.isNaN(e.value)) continue;
|
|
112
|
+
this.add({ ...withoutName(e.labels), ...groupLabels, ...(rule.labels ?? {}), __name__: rule.record }, t, e.value);
|
|
113
|
+
}
|
|
114
|
+
} else if (isAlertingRuleConfig(rule)) {
|
|
115
|
+
const before = this.pending.get(rule) ?? new Map<string, number>();
|
|
116
|
+
const now = new Map<string, number>();
|
|
117
|
+
const holdMs = rule.for ? (durationMs(rule.for) ?? 0) : 0;
|
|
118
|
+
for (const e of elements) {
|
|
119
|
+
const labels = { ...withoutName(e.labels), ...groupLabels, ...(rule.labels ?? {}), alertname: rule.alert };
|
|
120
|
+
const k = key(labels);
|
|
121
|
+
const since = before.get(k) ?? t;
|
|
122
|
+
now.set(k, since);
|
|
123
|
+
if (t - since >= holdMs) firing.push({ labels, value: e.value });
|
|
124
|
+
}
|
|
125
|
+
this.pending.set(rule, now);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return firing;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The latest value of each series matching `selector` at `t`, e.g. `slo:x{slo="a"}`. */
|
|
133
|
+
query(expr: string, t: number): VectorElement[] {
|
|
134
|
+
const v = this.eval(expr, t);
|
|
135
|
+
return v.kind === "scalar" ? [{ labels: {}, value: v.value }] : v.elements;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private eval(expr: string, t: number): Value {
|
|
139
|
+
const tree = parser.parse(expr);
|
|
140
|
+
const top = tree.topNode.firstChild;
|
|
141
|
+
if (!top) throw new Error(`rule-eval: empty expression`);
|
|
142
|
+
return this.node(top, expr, t);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private text(n: SyntaxNode, src: string): string {
|
|
146
|
+
return src.slice(n.from, n.to);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private matchers(sel: SyntaxNode, src: string): Matcher[] {
|
|
150
|
+
const out: Matcher[] = [];
|
|
151
|
+
const ident = sel.getChild("Identifier");
|
|
152
|
+
if (ident) out.push({ name: "__name__", op: "=", value: this.text(ident, src) });
|
|
153
|
+
const list = sel.getChild("LabelMatchers");
|
|
154
|
+
if (list) {
|
|
155
|
+
for (let c = list.firstChild; c; c = c.nextSibling) {
|
|
156
|
+
if (c.name !== "UnquotedLabelMatcher") continue;
|
|
157
|
+
const name = this.text(c.getChild("LabelName")!, src);
|
|
158
|
+
const op = this.text(c.getChild("MatchOp")!, src) as Matcher["op"];
|
|
159
|
+
const value = JSON.parse(this.text(c.getChild("StringLiteral")!, src).replace(/^'|'$/g, '"')) as string;
|
|
160
|
+
out.push({ name, op, value });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private select(ms: Matcher[]): Series[] {
|
|
167
|
+
return [...this.series.values()].filter((s) => ms.every((m) => matches(s.labels, m)));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Samples in (t - range, t]. */
|
|
171
|
+
private window(s: Series, t: number, rangeMs: number): Sample[] {
|
|
172
|
+
const xs = s.samples;
|
|
173
|
+
let lo = 0;
|
|
174
|
+
let hi = xs.length;
|
|
175
|
+
while (lo < hi) {
|
|
176
|
+
const mid = (lo + hi) >> 1;
|
|
177
|
+
if (xs[mid].t <= t - rangeMs) lo = mid + 1;
|
|
178
|
+
else hi = mid;
|
|
179
|
+
}
|
|
180
|
+
const out: Sample[] = [];
|
|
181
|
+
for (let i = lo; i < xs.length && xs[i].t <= t; i++) out.push(xs[i]);
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private node(n: SyntaxNode, src: string, t: number): Value {
|
|
186
|
+
switch (n.name) {
|
|
187
|
+
case "NumberDurationLiteral":
|
|
188
|
+
return { kind: "scalar", value: Number(this.text(n, src)) };
|
|
189
|
+
case "ParenExpr":
|
|
190
|
+
return this.node(n.firstChild!, src, t);
|
|
191
|
+
case "VectorSelector": {
|
|
192
|
+
const elements: VectorElement[] = [];
|
|
193
|
+
for (const s of this.select(this.matchers(n, src))) {
|
|
194
|
+
const w = this.window(s, t, LOOKBACK_MS);
|
|
195
|
+
if (w.length > 0) elements.push({ labels: { ...s.labels }, value: w[w.length - 1].v });
|
|
196
|
+
}
|
|
197
|
+
return { kind: "vector", elements };
|
|
198
|
+
}
|
|
199
|
+
case "FunctionCall":
|
|
200
|
+
return this.call(n, src, t);
|
|
201
|
+
case "AggregateExpr":
|
|
202
|
+
return this.aggregate(n, src, t);
|
|
203
|
+
case "BinaryExpr":
|
|
204
|
+
return this.binary(n, src, t);
|
|
205
|
+
default:
|
|
206
|
+
throw new Error(`rule-eval: ${n.name} (${this.text(n, src)}) is not supported`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private args(body: SyntaxNode): SyntaxNode[] {
|
|
211
|
+
const out: SyntaxNode[] = [];
|
|
212
|
+
for (let c = body.firstChild; c; c = c.nextSibling) {
|
|
213
|
+
if (c.name !== "(" && c.name !== ")" && c.name !== ",") out.push(c);
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private call(n: SyntaxNode, src: string, t: number): Value {
|
|
219
|
+
const fn = this.text(n.getChild("FunctionIdentifier")!, src);
|
|
220
|
+
const [arg] = this.args(n.getChild("FunctionCallBody")!);
|
|
221
|
+
if (fn === "vector") {
|
|
222
|
+
const v = this.node(arg, src, t);
|
|
223
|
+
if (v.kind !== "scalar") throw new Error("rule-eval: vector() takes a scalar");
|
|
224
|
+
return { kind: "vector", elements: [{ labels: {}, value: v.value }] };
|
|
225
|
+
}
|
|
226
|
+
if (arg.name !== "MatrixSelector") throw new Error(`rule-eval: ${fn}() needs a range selector`);
|
|
227
|
+
const rangeMs = durationMs(this.text(arg.getChild("DurationExpr")!, src));
|
|
228
|
+
if (rangeMs === undefined) throw new Error(`rule-eval: bad range in ${this.text(arg, src)}`);
|
|
229
|
+
const elements: VectorElement[] = [];
|
|
230
|
+
for (const s of this.select(this.matchers(arg.getChild("VectorSelector")!, src))) {
|
|
231
|
+
const w = this.window(s, t, rangeMs);
|
|
232
|
+
let value: number;
|
|
233
|
+
switch (fn) {
|
|
234
|
+
case "rate":
|
|
235
|
+
case "increase": {
|
|
236
|
+
if (w.length < 2) continue;
|
|
237
|
+
const inc = w[w.length - 1].v - w[0].v;
|
|
238
|
+
value = fn === "rate" ? inc / ((w[w.length - 1].t - w[0].t) / 1000) : (inc * rangeMs) / (w[w.length - 1].t - w[0].t);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "avg_over_time":
|
|
242
|
+
case "sum_over_time":
|
|
243
|
+
case "count_over_time": {
|
|
244
|
+
if (w.length === 0) continue;
|
|
245
|
+
const sum = w.reduce((a, x) => a + x.v, 0);
|
|
246
|
+
value = fn === "sum_over_time" ? sum : fn === "count_over_time" ? w.length : sum / w.length;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
default:
|
|
250
|
+
throw new Error(`rule-eval: function ${fn} is not supported`);
|
|
251
|
+
}
|
|
252
|
+
elements.push({ labels: withoutName(s.labels), value });
|
|
253
|
+
}
|
|
254
|
+
return { kind: "vector", elements };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private aggregate(n: SyntaxNode, src: string, t: number): Value {
|
|
258
|
+
const op = this.text(n.getChild("AggregateOp")!, src);
|
|
259
|
+
if (op !== "sum") throw new Error(`rule-eval: aggregation ${op} is not supported`);
|
|
260
|
+
const mod = n.getChild("AggregateModifier");
|
|
261
|
+
let by: string[] | undefined;
|
|
262
|
+
let without: string[] | undefined;
|
|
263
|
+
if (mod) {
|
|
264
|
+
const names = (mod.getChild("GroupingLabels")?.getChildren("LabelName") ?? []).map((l) => this.text(l, src));
|
|
265
|
+
if (mod.getChild("By")) by = names;
|
|
266
|
+
else without = names;
|
|
267
|
+
}
|
|
268
|
+
const [arg] = this.args(n.getChild("FunctionCallBody")!);
|
|
269
|
+
const v = this.node(arg, src, t);
|
|
270
|
+
if (v.kind !== "vector") throw new Error("rule-eval: sum() takes a vector");
|
|
271
|
+
const groups = new Map<string, VectorElement>();
|
|
272
|
+
for (const e of v.elements) {
|
|
273
|
+
const base = withoutName(e.labels);
|
|
274
|
+
const labels: Labels = {};
|
|
275
|
+
if (by) for (const l of by) if (base[l] !== undefined) labels[l] = base[l];
|
|
276
|
+
if (without) for (const [k, val] of Object.entries(base)) if (!without.includes(k)) labels[k] = val;
|
|
277
|
+
const k = key(labels);
|
|
278
|
+
const g = groups.get(k);
|
|
279
|
+
if (g) g.value += e.value;
|
|
280
|
+
else groups.set(k, { labels, value: e.value });
|
|
281
|
+
}
|
|
282
|
+
return { kind: "vector", elements: [...groups.values()] };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private binary(n: SyntaxNode, src: string, t: number): Value {
|
|
286
|
+
const parts: SyntaxNode[] = [];
|
|
287
|
+
for (let c = n.firstChild; c; c = c.nextSibling) parts.push(c);
|
|
288
|
+
const lhsNode = parts[0];
|
|
289
|
+
const rhsNode = parts[parts.length - 1];
|
|
290
|
+
const opNode = parts[1];
|
|
291
|
+
const op = this.text(opNode, src);
|
|
292
|
+
const modifier = parts.find((p) => p.name === "MatchingModifierClause");
|
|
293
|
+
if (parts.some((p) => p.name === "BoolModifier")) throw new Error("rule-eval: bool modifier is not supported");
|
|
294
|
+
let on: string[] | undefined;
|
|
295
|
+
let ignoring: string[] = [];
|
|
296
|
+
if (modifier) {
|
|
297
|
+
const names = (modifier.getChild("GroupingLabels")?.getChildren("LabelName") ?? []).map((l) => this.text(l, src));
|
|
298
|
+
if (modifier.getChild("On")) on = names;
|
|
299
|
+
else ignoring = names;
|
|
300
|
+
if (modifier.getChild("GroupLeft") || modifier.getChild("GroupRight")) throw new Error("rule-eval: group_left/right is not supported");
|
|
301
|
+
}
|
|
302
|
+
const lhs = this.node(lhsNode, src, t);
|
|
303
|
+
const rhs = this.node(rhsNode, src, t);
|
|
304
|
+
const signature = (labels: Labels): string => {
|
|
305
|
+
const base = withoutName(labels);
|
|
306
|
+
const picked: Labels = {};
|
|
307
|
+
for (const [k, v] of Object.entries(base)) {
|
|
308
|
+
if (on ? on.includes(k) : !ignoring.includes(k)) picked[k] = v;
|
|
309
|
+
}
|
|
310
|
+
return key(picked);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (op === "and" || op === "or" || op === "unless") {
|
|
314
|
+
if (lhs.kind !== "vector" || rhs.kind !== "vector") throw new Error(`rule-eval: ${op} takes two vectors`);
|
|
315
|
+
const rhsSigs = new Set(rhs.elements.map((e) => signature(e.labels)));
|
|
316
|
+
if (op === "and") return { kind: "vector", elements: lhs.elements.filter((e) => rhsSigs.has(signature(e.labels))) };
|
|
317
|
+
if (op === "unless") return { kind: "vector", elements: lhs.elements.filter((e) => !rhsSigs.has(signature(e.labels))) };
|
|
318
|
+
const lhsSigs = new Set(lhs.elements.map((e) => signature(e.labels)));
|
|
319
|
+
return { kind: "vector", elements: [...lhs.elements, ...rhs.elements.filter((e) => !lhsSigs.has(signature(e.labels)))] };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const comparison = [">", "<", ">=", "<=", "==", "!="].includes(op);
|
|
323
|
+
const apply = (a: number, b: number): number | undefined => {
|
|
324
|
+
switch (op) {
|
|
325
|
+
case "+":
|
|
326
|
+
return a + b;
|
|
327
|
+
case "-":
|
|
328
|
+
return a - b;
|
|
329
|
+
case "*":
|
|
330
|
+
return a * b;
|
|
331
|
+
case "/":
|
|
332
|
+
return a / b;
|
|
333
|
+
case ">":
|
|
334
|
+
return a > b ? a : undefined;
|
|
335
|
+
case "<":
|
|
336
|
+
return a < b ? a : undefined;
|
|
337
|
+
case ">=":
|
|
338
|
+
return a >= b ? a : undefined;
|
|
339
|
+
case "<=":
|
|
340
|
+
return a <= b ? a : undefined;
|
|
341
|
+
case "==":
|
|
342
|
+
return a === b ? a : undefined;
|
|
343
|
+
case "!=":
|
|
344
|
+
return a !== b ? a : undefined;
|
|
345
|
+
default:
|
|
346
|
+
throw new Error(`rule-eval: operator ${op} is not supported`);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
if (lhs.kind === "scalar" && rhs.kind === "scalar") {
|
|
351
|
+
if (comparison) throw new Error("rule-eval: scalar comparison needs bool");
|
|
352
|
+
return { kind: "scalar", value: apply(lhs.value, rhs.value)! };
|
|
353
|
+
}
|
|
354
|
+
const keepName = comparison;
|
|
355
|
+
if (lhs.kind === "vector" && rhs.kind === "scalar") {
|
|
356
|
+
const elements: VectorElement[] = [];
|
|
357
|
+
for (const e of lhs.elements) {
|
|
358
|
+
const v = apply(e.value, rhs.value);
|
|
359
|
+
if (v !== undefined) elements.push({ labels: keepName ? e.labels : withoutName(e.labels), value: v });
|
|
360
|
+
}
|
|
361
|
+
return { kind: "vector", elements };
|
|
362
|
+
}
|
|
363
|
+
if (lhs.kind === "scalar" && rhs.kind === "vector") {
|
|
364
|
+
const elements: VectorElement[] = [];
|
|
365
|
+
for (const e of rhs.elements) {
|
|
366
|
+
const v = apply(lhs.value, e.value);
|
|
367
|
+
// A scalar-vector comparison filters the vector and keeps its sample value.
|
|
368
|
+
if (v !== undefined) elements.push({ labels: keepName ? e.labels : withoutName(e.labels), value: comparison ? e.value : v });
|
|
369
|
+
}
|
|
370
|
+
return { kind: "vector", elements };
|
|
371
|
+
}
|
|
372
|
+
const l = lhs as { elements: VectorElement[] };
|
|
373
|
+
const r = rhs as { elements: VectorElement[] };
|
|
374
|
+
const bySig = new Map<string, VectorElement>();
|
|
375
|
+
for (const e of r.elements) {
|
|
376
|
+
const s = signature(e.labels);
|
|
377
|
+
if (bySig.has(s)) throw new Error("rule-eval: many-to-many matching is not supported");
|
|
378
|
+
bySig.set(s, e);
|
|
379
|
+
}
|
|
380
|
+
const elements: VectorElement[] = [];
|
|
381
|
+
for (const e of l.elements) {
|
|
382
|
+
const other = bySig.get(signature(e.labels));
|
|
383
|
+
if (!other) continue;
|
|
384
|
+
const v = apply(e.value, other.value);
|
|
385
|
+
if (v === undefined) continue;
|
|
386
|
+
const labels = on && !keepName ? pick(withoutName(e.labels), on) : keepName ? e.labels : withoutName(e.labels);
|
|
387
|
+
elements.push({ labels, value: v });
|
|
388
|
+
}
|
|
389
|
+
return { kind: "vector", elements };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function pick(labels: Labels, names: string[]): Labels {
|
|
394
|
+
const out: Labels = {};
|
|
395
|
+
for (const n of names) if (labels[n] !== undefined) out[n] = labels[n];
|
|
396
|
+
return out;
|
|
397
|
+
}
|