@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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { build } from "@intentius/chant/build";
|
|
6
|
+
import { lintCommand } from "@intentius/chant/cli/commands/lint";
|
|
7
|
+
import { initTemplates } from "./init-templates";
|
|
8
|
+
import { prometheusSerializer } from "./serializer";
|
|
9
|
+
import { postSynthChecks } from "./lint/post-synth";
|
|
10
|
+
import { runPostSynthChecks } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
|
|
12
|
+
describe("init templates", () => {
|
|
13
|
+
test.each([undefined, "rules", "slo-style"])("%s builds, lints clean and passes every check", async (name) => {
|
|
14
|
+
const dir = mkdtempSync(join(import.meta.dirname, "..", ".init-template-"));
|
|
15
|
+
try {
|
|
16
|
+
mkdirSync(join(dir, "src"));
|
|
17
|
+
for (const [file, text] of Object.entries(initTemplates(name).src)) writeFileSync(join(dir, "src", file), text);
|
|
18
|
+
const result = await build(join(dir, "src"), [prometheusSerializer]);
|
|
19
|
+
expect(result.errors).toEqual([]);
|
|
20
|
+
expect(result.outputs.get("prometheus")).toBeTruthy();
|
|
21
|
+
const diags = runPostSynthChecks(postSynthChecks, result);
|
|
22
|
+
expect(diags).toEqual([]);
|
|
23
|
+
const lint = await lintCommand({ path: join(dir, "src"), format: "stylish", fix: false });
|
|
24
|
+
expect(lint.errorCount + lint.warningCount, lint.output).toBe(0);
|
|
25
|
+
} finally {
|
|
26
|
+
rmSync(dir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant init --lexicon prometheus` scaffolds.
|
|
3
|
+
*
|
|
4
|
+
* - default: one rule group and the Alertmanager routing for it, in one build
|
|
5
|
+
* root so PROM202 checks every severity is routed.
|
|
6
|
+
* - `rules`: rule groups only, for a setup whose Alertmanager config lives
|
|
7
|
+
* elsewhere.
|
|
8
|
+
* - `slo-style`: a recording rule per window and alerts on two severities,
|
|
9
|
+
* the shape SLO burn-rate rules take.
|
|
10
|
+
*/
|
|
11
|
+
import type { InitTemplateSet } from "@intentius/chant/lexicon";
|
|
12
|
+
|
|
13
|
+
const RULES = `import { RuleGroup, type Rule } from "@intentius/chant-lexicon-prometheus";
|
|
14
|
+
|
|
15
|
+
const rules: Rule[] = [
|
|
16
|
+
{
|
|
17
|
+
record: "job:http_errors:ratio5m",
|
|
18
|
+
expr: 'sum by (job) (rate(http_requests_total{code=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m]))',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
alert: "HttpErrorRatioHigh",
|
|
22
|
+
expr: "job:http_errors:ratio5m > 0.05",
|
|
23
|
+
for: "10m",
|
|
24
|
+
labels: { severity: "page" },
|
|
25
|
+
annotations: { summary: "{{ $labels.job }} is failing over 5% of requests" },
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const api = new RuleGroup({ name: "api", interval: "30s", rules });
|
|
30
|
+
|
|
31
|
+
export { api };
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const ALERTMANAGER = `import { Receiver, Route, type RouteProps, type WebhookConfig } from "@intentius/chant-lexicon-prometheus";
|
|
35
|
+
|
|
36
|
+
const hook: WebhookConfig[] = [{ url: "http://alert-sink:8080/" }];
|
|
37
|
+
const oncall = new Receiver({ name: "oncall", webhook_configs: hook });
|
|
38
|
+
const fallback = new Receiver({ name: "default" });
|
|
39
|
+
|
|
40
|
+
const children: RouteProps[] = [{ matchers: ['severity="page"'], receiver: oncall }];
|
|
41
|
+
const byAlert = ["alertname", "job"];
|
|
42
|
+
const root = new Route({ receiver: fallback, group_by: byAlert, routes: children });
|
|
43
|
+
|
|
44
|
+
export { oncall, fallback, root };
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const SLO_STYLE = `import { RuleGroup, type Rule } from "@intentius/chant-lexicon-prometheus";
|
|
48
|
+
|
|
49
|
+
// Error ratio over two windows, and an alert that needs both to agree.
|
|
50
|
+
const windows = ["5m", "1h"];
|
|
51
|
+
|
|
52
|
+
const recording: Rule[] = windows.map((w) => ({
|
|
53
|
+
record: \`job:http_errors:ratio\${w}\`,
|
|
54
|
+
expr: \`sum by (job) (rate(http_requests_total{code=~"5.."}[\${w}])) / sum by (job) (rate(http_requests_total[\${w}]))\`,
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const alerting: Rule[] = [
|
|
58
|
+
{
|
|
59
|
+
alert: "ErrorBudgetBurn",
|
|
60
|
+
expr: "job:http_errors:ratio1h > (14.4 * 0.001) and job:http_errors:ratio5m > (14.4 * 0.001)",
|
|
61
|
+
labels: { severity: "page" },
|
|
62
|
+
annotations: { summary: "{{ $labels.job }} is burning its error budget 14.4x too fast" },
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const errorRatios = new RuleGroup({ name: "error-ratios", rules: recording });
|
|
67
|
+
const burnAlerts = new RuleGroup({ name: "burn-alerts", rules: alerting });
|
|
68
|
+
|
|
69
|
+
export { errorRatios, burnAlerts };
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
export function initTemplates(template?: string): InitTemplateSet {
|
|
73
|
+
if (template === "rules") return { src: { "rules.ts": RULES } };
|
|
74
|
+
if (template === "slo-style") return { src: { "rules.ts": SLO_STYLE } };
|
|
75
|
+
return { src: { "rules.ts": RULES, "alertmanager.ts": ALERTMANAGER } };
|
|
76
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prometheus lexicon's chant audit catalog, contributed via
|
|
3
|
+
* `prometheusPlugin.auditCatalog()` (#687, #1346).
|
|
4
|
+
*
|
|
5
|
+
* Every post-synth check reads the emitted rule file or `alertmanager.yml`
|
|
6
|
+
* (or any output document shaped like one), so they are all `yamlBased` and
|
|
7
|
+
* fire on an audit of standalone files too. The three source-level lint rules
|
|
8
|
+
* read TypeScript, so they are constructed with `yamlBased: false`; they are
|
|
9
|
+
* listed for a reader who meets them in a lint report.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { auditRule, type RuleMeta } from "@intentius/chant/audit/catalog";
|
|
13
|
+
|
|
14
|
+
function sourceRule(id: string, category: RuleMeta["category"], title: string, remediation: string): RuleMeta {
|
|
15
|
+
return { id, tier: "merge-worthy", fixKind: "guidance", category, title, remediation, yamlBased: false };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function outputRule(
|
|
19
|
+
id: string,
|
|
20
|
+
tier: RuleMeta["tier"],
|
|
21
|
+
category: NonNullable<RuleMeta["category"]>,
|
|
22
|
+
title: string,
|
|
23
|
+
remediation: string,
|
|
24
|
+
): RuleMeta {
|
|
25
|
+
return auditRule(id, tier, "guidance", title, remediation, { category });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const prometheusAuditCatalog: Record<string, RuleMeta> = {
|
|
29
|
+
PROM001: sourceRule(
|
|
30
|
+
"PROM001",
|
|
31
|
+
"security",
|
|
32
|
+
"Alertmanager credential declared as a literal",
|
|
33
|
+
"Mount the secret as a file and set the field's *_file sibling (api_url_file, routing_key_file, auth_password_file, credentials_file) instead.",
|
|
34
|
+
),
|
|
35
|
+
PROM002: sourceRule(
|
|
36
|
+
"PROM002",
|
|
37
|
+
"correctness",
|
|
38
|
+
"Literal PromQL expression in a RuleGroup does not parse",
|
|
39
|
+
"Fix the expression at the offset the message names.",
|
|
40
|
+
),
|
|
41
|
+
PROM003: sourceRule(
|
|
42
|
+
"PROM003",
|
|
43
|
+
"correctness",
|
|
44
|
+
"Slo objective, window or SLI expression is invalid",
|
|
45
|
+
"Set objective strictly between 0 and 1, window to a Prometheus duration such as 28d, and write each SLI expression as PromQL with {{window}} where the range goes.",
|
|
46
|
+
),
|
|
47
|
+
PROM101: outputRule("PROM101", "merge-worthy", "correctness", "Rule group name repeated in a rule file", "Give every group in the rule file its own name."),
|
|
48
|
+
PROM102: outputRule(
|
|
49
|
+
"PROM102",
|
|
50
|
+
"report-only",
|
|
51
|
+
"correctness",
|
|
52
|
+
"Two rules share a name and label set",
|
|
53
|
+
"Rename one rule, or give the two different labels (for alerts, usually a different severity).",
|
|
54
|
+
),
|
|
55
|
+
PROM103: outputRule(
|
|
56
|
+
"PROM103",
|
|
57
|
+
"merge-worthy",
|
|
58
|
+
"correctness",
|
|
59
|
+
"Rule or group duration is not a Prometheus duration",
|
|
60
|
+
"Write durations as <number><unit> terms in descending order, e.g. 30s, 5m, 1h30m.",
|
|
61
|
+
),
|
|
62
|
+
PROM104: outputRule("PROM104", "merge-worthy", "correctness", "Rule expression is not valid PromQL", "Fix the expression at the offset the message names."),
|
|
63
|
+
PROM105: outputRule(
|
|
64
|
+
"PROM105",
|
|
65
|
+
"merge-worthy",
|
|
66
|
+
"correctness",
|
|
67
|
+
"Rule or group is malformed",
|
|
68
|
+
"Set exactly one of record or alert with a non-empty name; keep for, keep_firing_for and annotations to alerting rules.",
|
|
69
|
+
),
|
|
70
|
+
PROM106: outputRule(
|
|
71
|
+
"PROM106",
|
|
72
|
+
"report-only",
|
|
73
|
+
"best-practice",
|
|
74
|
+
"Alerting rule has no severity label",
|
|
75
|
+
"Add a severity label (on the rule or the group) that an Alertmanager route matches.",
|
|
76
|
+
),
|
|
77
|
+
PROM107: outputRule(
|
|
78
|
+
"PROM107",
|
|
79
|
+
"report-only",
|
|
80
|
+
"best-practice",
|
|
81
|
+
"Alerting rule has no summary or description",
|
|
82
|
+
"Add a summary annotation saying what is wrong, and a description or runbook_url saying what to do.",
|
|
83
|
+
),
|
|
84
|
+
PROM201: outputRule("PROM201", "merge-worthy", "correctness", "Route sends to an undeclared receiver", "Declare the receiver, or reference the declared Receiver entity instead of a name string."),
|
|
85
|
+
PROM202: outputRule(
|
|
86
|
+
"PROM202",
|
|
87
|
+
"merge-worthy",
|
|
88
|
+
"correctness",
|
|
89
|
+
"Alert severity not routed by any route",
|
|
90
|
+
"Add a route matching the severity (e.g. severity=\"page\") below the root, or change the alert's severity to one that is routed.",
|
|
91
|
+
),
|
|
92
|
+
PROM203: outputRule("PROM203", "merge-worthy", "correctness", "Receiver or time interval name repeated", "Give every receiver and every time interval its own name."),
|
|
93
|
+
PROM204: outputRule(
|
|
94
|
+
"PROM204",
|
|
95
|
+
"merge-worthy",
|
|
96
|
+
"correctness",
|
|
97
|
+
"Route names an undeclared time interval",
|
|
98
|
+
"Declare the TimeInterval, or reference the declared entity instead of a name string.",
|
|
99
|
+
),
|
|
100
|
+
PROM205: outputRule(
|
|
101
|
+
"PROM205",
|
|
102
|
+
"merge-worthy",
|
|
103
|
+
"correctness",
|
|
104
|
+
"Root route missing, without a receiver, or with matchers",
|
|
105
|
+
"Declare exactly one root Route with a receiver and no matchers, and nest every other route under it.",
|
|
106
|
+
),
|
|
107
|
+
PROM206: outputRule("PROM206", "merge-worthy", "correctness", "Matcher does not parse", 'Write matchers as label, operator and quoted value, e.g. severity="page" or team=~"db|infra".'),
|
|
108
|
+
PROM207: outputRule("PROM207", "report-only", "best-practice", "Receiver declared but never routed to", "Route to the receiver, or remove it."),
|
|
109
|
+
PROM208: outputRule(
|
|
110
|
+
"PROM208",
|
|
111
|
+
"merge-worthy",
|
|
112
|
+
"correctness",
|
|
113
|
+
"Alertmanager duration is not a duration",
|
|
114
|
+
"Write durations as <number><unit> terms in descending order, e.g. 30s, 5m, 4h.",
|
|
115
|
+
),
|
|
116
|
+
PROM209: outputRule(
|
|
117
|
+
"PROM209",
|
|
118
|
+
"merge-worthy",
|
|
119
|
+
"correctness",
|
|
120
|
+
"Receiver integration missing its destination or credential",
|
|
121
|
+
"Set the integration's url, api_url, routing_key or to/smarthost/from (or their *_file and global equivalents).",
|
|
122
|
+
),
|
|
123
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Code generated by chant generate. DO NOT EDIT.
|
|
2
|
+
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { prom101 } from "./prom101";
|
|
4
|
+
import { prom102 } from "./prom102";
|
|
5
|
+
import { prom103 } from "./prom103";
|
|
6
|
+
import { prom104 } from "./prom104";
|
|
7
|
+
import { prom105 } from "./prom105";
|
|
8
|
+
import { prom106 } from "./prom106";
|
|
9
|
+
import { prom107 } from "./prom107";
|
|
10
|
+
import { prom201 } from "./prom201";
|
|
11
|
+
import { prom202 } from "./prom202";
|
|
12
|
+
import { prom203 } from "./prom203";
|
|
13
|
+
import { prom204 } from "./prom204";
|
|
14
|
+
import { prom205 } from "./prom205";
|
|
15
|
+
import { prom206 } from "./prom206";
|
|
16
|
+
import { prom207 } from "./prom207";
|
|
17
|
+
import { prom208 } from "./prom208";
|
|
18
|
+
import { prom209 } from "./prom209";
|
|
19
|
+
|
|
20
|
+
export const postSynthChecks: PostSynthCheck[] = [
|
|
21
|
+
prom101,
|
|
22
|
+
prom102,
|
|
23
|
+
prom103,
|
|
24
|
+
prom104,
|
|
25
|
+
prom105,
|
|
26
|
+
prom106,
|
|
27
|
+
prom107,
|
|
28
|
+
prom201,
|
|
29
|
+
prom202,
|
|
30
|
+
prom203,
|
|
31
|
+
prom204,
|
|
32
|
+
prom205,
|
|
33
|
+
prom206,
|
|
34
|
+
prom207,
|
|
35
|
+
prom208,
|
|
36
|
+
prom209,
|
|
37
|
+
];
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { makePostSynthCtx, makePostSynthCtxFromFiles } from "@intentius/chant-test-utils";
|
|
3
|
+
import { postSynthChecks } from "./index";
|
|
4
|
+
import { prom101 } from "./prom101";
|
|
5
|
+
import { prom102 } from "./prom102";
|
|
6
|
+
import { prom103 } from "./prom103";
|
|
7
|
+
import { prom104 } from "./prom104";
|
|
8
|
+
import { prom105 } from "./prom105";
|
|
9
|
+
import { prom106 } from "./prom106";
|
|
10
|
+
import { prom107 } from "./prom107";
|
|
11
|
+
import { prom201 } from "./prom201";
|
|
12
|
+
import { prom202 } from "./prom202";
|
|
13
|
+
import { prom203 } from "./prom203";
|
|
14
|
+
import { prom204 } from "./prom204";
|
|
15
|
+
import { prom205 } from "./prom205";
|
|
16
|
+
import { prom206 } from "./prom206";
|
|
17
|
+
import { prom207 } from "./prom207";
|
|
18
|
+
import { prom208 } from "./prom208";
|
|
19
|
+
import { prom209 } from "./prom209";
|
|
20
|
+
|
|
21
|
+
const RULES = `groups:
|
|
22
|
+
- name: api
|
|
23
|
+
interval: 30s
|
|
24
|
+
rules:
|
|
25
|
+
- record: job:http_errors:ratio5m
|
|
26
|
+
expr: sum by (job) (rate(http_requests_total{code=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m]))
|
|
27
|
+
- alert: ApiErrors
|
|
28
|
+
expr: job:http_errors:ratio5m > 0.05
|
|
29
|
+
for: 10m
|
|
30
|
+
labels:
|
|
31
|
+
severity: page
|
|
32
|
+
annotations:
|
|
33
|
+
summary: errors
|
|
34
|
+
- alert: ApiErrors
|
|
35
|
+
expr: job:http_errors:ratio5m > 0.01
|
|
36
|
+
for: 1h
|
|
37
|
+
labels:
|
|
38
|
+
severity: ticket
|
|
39
|
+
annotations:
|
|
40
|
+
summary: errors
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const AM = `global:
|
|
44
|
+
resolve_timeout: 5m
|
|
45
|
+
route:
|
|
46
|
+
receiver: default
|
|
47
|
+
group_by: [alertname]
|
|
48
|
+
routes:
|
|
49
|
+
- receiver: oncall
|
|
50
|
+
matchers: ['severity="page"']
|
|
51
|
+
- receiver: default
|
|
52
|
+
matchers: ['severity=~"ticket|info"']
|
|
53
|
+
mute_time_intervals: [weekend]
|
|
54
|
+
inhibit_rules:
|
|
55
|
+
- source_matchers: ['severity="page"']
|
|
56
|
+
target_matchers: ['severity="ticket"']
|
|
57
|
+
equal: [alertname]
|
|
58
|
+
receivers:
|
|
59
|
+
- name: default
|
|
60
|
+
webhook_configs:
|
|
61
|
+
- url: "http://sink:8080/"
|
|
62
|
+
- name: oncall
|
|
63
|
+
pagerduty_configs:
|
|
64
|
+
- routing_key_file: /etc/pd
|
|
65
|
+
time_intervals:
|
|
66
|
+
- name: weekend
|
|
67
|
+
time_intervals:
|
|
68
|
+
- weekdays: [saturday, sunday]
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
const both = (rules = RULES, am = AM) => makePostSynthCtxFromFiles("prometheus", { "alertmanager.yml": am }, rules);
|
|
72
|
+
const rulesOnly = (rules: string) => makePostSynthCtx("prometheus", rules);
|
|
73
|
+
const amOnly = (am: string) => makePostSynthCtx("prometheus", am);
|
|
74
|
+
|
|
75
|
+
describe("a clean rule file and alertmanager.yml", () => {
|
|
76
|
+
test.each(postSynthChecks.map((c) => [c.id, c] as const))("%s finds nothing", (_id, check) => {
|
|
77
|
+
expect(check.check(both())).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("rule file checks", () => {
|
|
82
|
+
test("PROM101 repeated group name", () => {
|
|
83
|
+
const diags = prom101.check(rulesOnly(`${RULES} - name: api\n rules: []\n`));
|
|
84
|
+
expect(diags).toHaveLength(1);
|
|
85
|
+
expect(diags[0]).toMatchObject({ checkId: "PROM101", severity: "error", entity: "api" });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("PROM102 same name and labels, but not same name with a different severity", () => {
|
|
89
|
+
const dup = RULES.replace("severity: ticket", "severity: page");
|
|
90
|
+
expect(prom102.check(rulesOnly(dup)).map((d) => d.entity)).toEqual(["api/ApiErrors"]);
|
|
91
|
+
expect(prom102.check(rulesOnly(RULES))).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("PROM102 counts group labels", () => {
|
|
95
|
+
const text = `groups:
|
|
96
|
+
- name: a
|
|
97
|
+
labels: {team: x}
|
|
98
|
+
rules:
|
|
99
|
+
- record: r
|
|
100
|
+
expr: "1"
|
|
101
|
+
- name: b
|
|
102
|
+
rules:
|
|
103
|
+
- record: r
|
|
104
|
+
expr: "1"
|
|
105
|
+
labels: {team: x}
|
|
106
|
+
`;
|
|
107
|
+
expect(prom102.check(rulesOnly(text))).toHaveLength(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("PROM103 bad durations on the group and the rule", () => {
|
|
111
|
+
const bad = RULES.replace("interval: 30s", "interval: 30 seconds").replace("for: 10m", "for: 1.5h");
|
|
112
|
+
expect(prom103.check(rulesOnly(bad)).map((d) => d.message)).toEqual([
|
|
113
|
+
'group "api" interval "30 seconds" is not a Prometheus duration (e.g. 30s, 1m, 1h30m)',
|
|
114
|
+
'rule api/ApiErrors for "1.5h" is not a Prometheus duration (e.g. 5m, 1h)',
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("PROM104 PromQL syntax", () => {
|
|
119
|
+
const bad = RULES.replace("expr: job:http_errors:ratio5m > 0.05", "expr: job:http_errors:ratio5m >");
|
|
120
|
+
const diags = prom104.check(rulesOnly(bad));
|
|
121
|
+
expect(diags).toHaveLength(1);
|
|
122
|
+
expect(diags[0].entity).toBe("api/ApiErrors");
|
|
123
|
+
expect(diags[0].message).toContain("ends early");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("PROM105 malformed rules", () => {
|
|
127
|
+
const text = `groups:
|
|
128
|
+
- name: g
|
|
129
|
+
rules:
|
|
130
|
+
- record: a:b
|
|
131
|
+
expr: "1"
|
|
132
|
+
for: 5m
|
|
133
|
+
- expr: "1"
|
|
134
|
+
- record: x
|
|
135
|
+
alert: y
|
|
136
|
+
expr: "1"
|
|
137
|
+
`;
|
|
138
|
+
const diags = prom105.check(rulesOnly(text));
|
|
139
|
+
expect(diags.map((d) => d.message)).toEqual([
|
|
140
|
+
"recording rule g/a:b sets for, which only alerting rules take",
|
|
141
|
+
"rule g/rule 2 sets neither record nor alert",
|
|
142
|
+
"rule g/x sets both record and alert; a rule is one or the other",
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("PROM106 no severity, unless the group carries one", () => {
|
|
147
|
+
const noSev = RULES.replace(" labels:\n severity: page\n", "");
|
|
148
|
+
expect(prom106.check(rulesOnly(noSev)).map((d) => d.entity)).toEqual(["api/ApiErrors"]);
|
|
149
|
+
const groupSev = noSev.replace(" interval: 30s\n", " interval: 30s\n labels:\n severity: page\n");
|
|
150
|
+
expect(prom106.check(rulesOnly(groupSev))).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("PROM107 no summary or description", () => {
|
|
154
|
+
const bare = RULES.replace(" annotations:\n summary: errors\n", "");
|
|
155
|
+
expect(prom107.check(rulesOnly(bare))).toHaveLength(1);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("alertmanager checks", () => {
|
|
160
|
+
test("PROM201 route to an undeclared receiver", () => {
|
|
161
|
+
const diags = prom201.check(amOnly(AM.replace(" - receiver: oncall", " - receiver: on-call")));
|
|
162
|
+
expect(diags).toHaveLength(1);
|
|
163
|
+
expect(diags[0].message).toContain('receiver "on-call"');
|
|
164
|
+
expect(diags[0].entity).toBe("route.routes[0]");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("PROM202 an alert severity no route matches", () => {
|
|
168
|
+
const rules = RULES.replace("severity: ticket", "severity: warning");
|
|
169
|
+
const diags = prom202.check(both(rules));
|
|
170
|
+
expect(diags).toHaveLength(1);
|
|
171
|
+
expect(diags[0]).toMatchObject({ checkId: "PROM202", severity: "warning", entity: "warning" });
|
|
172
|
+
expect(diags[0].message).toContain("api/ApiErrors");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("PROM202 reads regexes and negative matchers, and nested routes", () => {
|
|
176
|
+
const am = AM.replace(` - receiver: default
|
|
177
|
+
matchers: ['severity=~"ticket|info"']
|
|
178
|
+
mute_time_intervals: [weekend]`, ` - receiver: default
|
|
179
|
+
matchers: ['team="x"']
|
|
180
|
+
routes:
|
|
181
|
+
- receiver: default
|
|
182
|
+
matchers: ['severity!="page"']`);
|
|
183
|
+
expect(prom202.check(both(RULES, am))).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("PROM202 is silent without rules or without an alertmanager config (chant #1939)", () => {
|
|
187
|
+
expect(prom202.check(rulesOnly(RULES.replace("severity: ticket", "severity: nope")))).toEqual([]);
|
|
188
|
+
expect(prom202.check(amOnly(AM))).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("PROM203 repeated receiver and interval names", () => {
|
|
192
|
+
const dup = AM.replace("time_intervals:\n - name: weekend", "time_intervals:\n - name: weekend\n time_intervals: []\n - name: weekend") + " - name: default\n";
|
|
193
|
+
const am = dup.replace("receivers:\n", "receivers:\n - name: oncall\n");
|
|
194
|
+
const diags = prom203.check(amOnly(am));
|
|
195
|
+
expect(diags.map((d) => d.entity).sort()).toEqual(["oncall", "weekend"]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("PROM204 an undeclared time interval", () => {
|
|
199
|
+
const diags = prom204.check(amOnly(AM.replace("mute_time_intervals: [weekend]", "mute_time_intervals: [weekends]")));
|
|
200
|
+
expect(diags).toHaveLength(1);
|
|
201
|
+
expect(diags[0].message).toContain('"weekends"');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("PROM205 root route shape", () => {
|
|
205
|
+
expect(prom205.check(amOnly("receivers:\n - name: a\n")).map((d) => d.message)[0]).toContain("no root route");
|
|
206
|
+
expect(prom205.check(amOnly(AM.replace(" receiver: default\n group_by", " group_by")))[0].message).toContain("no receiver");
|
|
207
|
+
expect(prom205.check(amOnly(AM.replace(" group_by: [alertname]", " group_by: [alertname]\n matchers: ['a=\"b\"']")))[0].message).toContain(
|
|
208
|
+
"has matchers",
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("PROM206 unparseable matchers in routes and inhibit rules", () => {
|
|
213
|
+
const bad = AM.replace(`matchers: ['severity="page"']`, `matchers: ['severity']`).replace(
|
|
214
|
+
`target_matchers: ['severity="ticket"']`,
|
|
215
|
+
`target_matchers: ['severity=~"("']`,
|
|
216
|
+
);
|
|
217
|
+
expect(prom206.check(amOnly(bad)).map((d) => d.entity)).toEqual(["route.routes[0]", "inhibit_rules[0]"]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("PROM207 an unused receiver", () => {
|
|
221
|
+
const diags = prom207.check(amOnly(`${AM.replace("\ntime_intervals:", "\n - name: spare\ntime_intervals:")}`));
|
|
222
|
+
expect(diags.map((d) => d.entity)).toEqual(["spare"]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("PROM208 bad Alertmanager durations", () => {
|
|
226
|
+
const bad = AM.replace("resolve_timeout: 5m", "resolve_timeout: five").replace("group_by: [alertname]", "group_by: [alertname]\n group_wait: 30sec");
|
|
227
|
+
expect(prom208.check(amOnly(bad)).map((d) => d.entity).sort()).toEqual(["global", "route"]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("PROM209 integrations missing their destination", () => {
|
|
231
|
+
const am = `route:
|
|
232
|
+
receiver: all
|
|
233
|
+
receivers:
|
|
234
|
+
- name: all
|
|
235
|
+
webhook_configs: [{}]
|
|
236
|
+
slack_configs: [{channel: "#a"}]
|
|
237
|
+
pagerduty_configs: [{}]
|
|
238
|
+
email_configs: [{to: a@b.c}]
|
|
239
|
+
`;
|
|
240
|
+
const msgs = prom209.check(amOnly(am)).map((d) => d.message);
|
|
241
|
+
expect(msgs).toHaveLength(5);
|
|
242
|
+
expect(msgs.join("\n")).toContain("smarthost");
|
|
243
|
+
const withGlobals = `global:\n slack_api_url_file: /etc/slack\n smtp_smarthost: "smtp:25"\n smtp_from: am@b.c\n${am}`;
|
|
244
|
+
expect(prom209.check(amOnly(withGlobals)).map((d) => d.message)).toHaveLength(2);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("documents from other lexicons", () => {
|
|
249
|
+
test("a k8s manifest and a collector config are not read as ours", () => {
|
|
250
|
+
const ctx = makePostSynthCtx(
|
|
251
|
+
"k8s",
|
|
252
|
+
`apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: x\n---\nreceivers:\n otlp: {}\nservice:\n pipelines: {}\n`,
|
|
253
|
+
);
|
|
254
|
+
for (const check of postSynthChecks) expect(check.check(ctx)).toEqual([]);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plumbing for the prometheus post-synth checks: find the rule files
|
|
3
|
+
* and `alertmanager.yml` documents in a build's output, and run the
|
|
4
|
+
* plain-function checks over them.
|
|
5
|
+
*
|
|
6
|
+
* Documents are recognized by shape, not by which lexicon emitted them, so a
|
|
7
|
+
* rule file that reaches the output some other way (a hand-written YAML
|
|
8
|
+
* sidecar) is checked too. `PrometheusRule` manifests are not read here;
|
|
9
|
+
* their groups are the same `RuleGroup`s and are checked where they are
|
|
10
|
+
* declared.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
14
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
15
|
+
import { loadAll } from "js-yaml";
|
|
16
|
+
import { looksLikeAlertmanagerConfig, looksLikeRuleFile, type AlertmanagerConfig, type RuleFileConfig } from "../../model";
|
|
17
|
+
import {
|
|
18
|
+
validateAlertmanagerConfig,
|
|
19
|
+
validateRuleFile,
|
|
20
|
+
validateSeverityRouting,
|
|
21
|
+
type PrometheusIssue,
|
|
22
|
+
type PrometheusIssueCode,
|
|
23
|
+
} from "../../validate-config";
|
|
24
|
+
|
|
25
|
+
export interface FoundDocs {
|
|
26
|
+
ruleFiles: Array<{ source: string; config: RuleFileConfig }>;
|
|
27
|
+
alertmanager: Array<{ source: string; config: AlertmanagerConfig }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Every rule file and Alertmanager config in the output. Parsed with js-yaml rather than `ctx.docs`, which keeps block scalars and flow lists as strings. */
|
|
31
|
+
export function prometheusDocs(ctx: PostSynthContext): FoundDocs {
|
|
32
|
+
const found: FoundDocs = { ruleFiles: [], alertmanager: [] };
|
|
33
|
+
for (const [lexicon, output] of ctx.outputs) {
|
|
34
|
+
const texts: Array<[string, string]> =
|
|
35
|
+
typeof output === "string"
|
|
36
|
+
? [[lexicon, output]]
|
|
37
|
+
: [[lexicon, (output as SerializerResult).primary], ...Object.entries((output as SerializerResult).files ?? {})];
|
|
38
|
+
for (const [source, text] of texts) {
|
|
39
|
+
if (!text) continue;
|
|
40
|
+
let docs: unknown[];
|
|
41
|
+
try {
|
|
42
|
+
docs = loadAll(text);
|
|
43
|
+
} catch {
|
|
44
|
+
continue; // not YAML, or not ours
|
|
45
|
+
}
|
|
46
|
+
for (const doc of docs) {
|
|
47
|
+
if (looksLikeRuleFile(doc)) found.ruleFiles.push({ source, config: doc });
|
|
48
|
+
else if (looksLikeAlertmanagerConfig(doc)) found.alertmanager.push({ source, config: doc });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toDiagnostic(issue: PrometheusIssue, source: string): PostSynthDiagnostic {
|
|
56
|
+
return {
|
|
57
|
+
checkId: issue.code,
|
|
58
|
+
severity: issue.severity,
|
|
59
|
+
message: source && source !== "prometheus" ? `${source}: ${issue.message}` : issue.message,
|
|
60
|
+
...(issue.subject ? { entity: issue.subject } : {}),
|
|
61
|
+
lexicon: "prometheus",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Diagnostics for one rule-file code (PROM101-PROM107) across every rule file in the output. */
|
|
66
|
+
export function ruleFileDiagnostics(ctx: PostSynthContext, code: PrometheusIssueCode): PostSynthDiagnostic[] {
|
|
67
|
+
return prometheusDocs(ctx).ruleFiles.flatMap(({ source, config }) =>
|
|
68
|
+
validateRuleFile(config)
|
|
69
|
+
.filter((i) => i.code === code)
|
|
70
|
+
.map((i) => toDiagnostic(i, source)),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Diagnostics for one Alertmanager code (PROM201, PROM203-PROM209) across every `alertmanager.yml` in the output. */
|
|
75
|
+
export function alertmanagerDiagnostics(ctx: PostSynthContext, code: PrometheusIssueCode): PostSynthDiagnostic[] {
|
|
76
|
+
return prometheusDocs(ctx).alertmanager.flatMap(({ source, config }) =>
|
|
77
|
+
validateAlertmanagerConfig(config)
|
|
78
|
+
.filter((i) => i.code === code)
|
|
79
|
+
.map((i) => toDiagnostic(i, source)),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** PROM202: joins every rule file in the output against every Alertmanager config in it. Silent when either is absent. */
|
|
84
|
+
export function routingDiagnostics(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
85
|
+
const { ruleFiles, alertmanager } = prometheusDocs(ctx);
|
|
86
|
+
if (ruleFiles.length === 0) return [];
|
|
87
|
+
const files = ruleFiles.map((r) => r.config);
|
|
88
|
+
return alertmanager.flatMap(({ source, config }) => validateSeverityRouting(files, config).map((i) => toDiagnostic(i, source)));
|
|
89
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROM101: Two rule groups in one rule file share a name
|
|
3
|
+
*
|
|
4
|
+
* Prometheus refuses to load a rule file whose group names repeat.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { ruleFileDiagnostics } from "./prom-helpers";
|
|
9
|
+
|
|
10
|
+
export const prom101: PostSynthCheck = {
|
|
11
|
+
id: "PROM101",
|
|
12
|
+
description: "Two rule groups in one rule file share a name",
|
|
13
|
+
|
|
14
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
15
|
+
return ruleFileDiagnostics(ctx, "PROM101");
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROM102: Two rules share a name and label set
|
|
3
|
+
*
|
|
4
|
+
* Two recording rules (or two alerts) with the same name and labels produce the same series and overwrite each other. This follows promtool's duplicate-rules lint: alerts that share a name and differ by a label, such as severity, are fine.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { ruleFileDiagnostics } from "./prom-helpers";
|
|
9
|
+
|
|
10
|
+
export const prom102: PostSynthCheck = {
|
|
11
|
+
id: "PROM102",
|
|
12
|
+
description: "Two rules share a name and label set",
|
|
13
|
+
|
|
14
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
15
|
+
return ruleFileDiagnostics(ctx, "PROM102");
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROM103: A rule or group duration is not a Prometheus duration
|
|
3
|
+
*
|
|
4
|
+
* for, keep_firing_for, interval and query_offset take durations such as 30s, 5m or 1h30m. Prometheus refuses the rule file otherwise.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { ruleFileDiagnostics } from "./prom-helpers";
|
|
9
|
+
|
|
10
|
+
export const prom103: PostSynthCheck = {
|
|
11
|
+
id: "PROM103",
|
|
12
|
+
description: "A rule or group duration is not a Prometheus duration",
|
|
13
|
+
|
|
14
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
15
|
+
return ruleFileDiagnostics(ctx, "PROM103");
|
|
16
|
+
},
|
|
17
|
+
};
|