@intentius/chant-lexicon-prometheus 0.101.0 → 0.104.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 +3 -1
- package/dist/alertmanager.d.ts.map +1 -1
- package/dist/collector-metrics.d.ts +58 -0
- package/dist/collector-metrics.d.ts.map +1 -0
- package/dist/composites/genai.d.ts +1 -1
- package/dist/composites/genai.d.ts.map +1 -1
- package/dist/composites/slo.d.ts +1 -1
- package/dist/composites/slo.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +12 -3
- package/dist/import/embedded.d.ts.map +1 -1
- package/dist/import/generator.d.ts +14 -0
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/init-templates.d.ts.map +1 -1
- package/dist/integrations.d.ts.map +1 -1
- package/dist/integrity.json +23 -7
- package/dist/lint/audit-catalog.d.ts +2 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +54 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/prom-helpers.d.ts +9 -2
- package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/prom208.d.ts +1 -1
- package/dist/lint/post-synth/prom209.d.ts +1 -1
- package/dist/lint/post-synth/prom210.d.ts +8 -0
- package/dist/lint/post-synth/prom210.d.ts.map +1 -0
- package/dist/lint/post-synth/prom211.d.ts +12 -0
- package/dist/lint/post-synth/prom211.d.ts.map +1 -0
- package/dist/lint/post-synth/prom212.d.ts +9 -0
- package/dist/lint/post-synth/prom212.d.ts.map +1 -0
- package/dist/lint/post-synth/prom213.d.ts +11 -0
- package/dist/lint/post-synth/prom213.d.ts.map +1 -0
- package/dist/lint/post-synth/prom214.d.ts +9 -0
- package/dist/lint/post-synth/prom214.d.ts.map +1 -0
- package/dist/lint/post-synth/prom215.d.ts +9 -0
- package/dist/lint/post-synth/prom215.d.ts.map +1 -0
- package/dist/lint/post-synth/prom216.d.ts +10 -0
- package/dist/lint/post-synth/prom216.d.ts.map +1 -0
- package/dist/lint/post-synth/prom217.d.ts +9 -0
- package/dist/lint/post-synth/prom217.d.ts.map +1 -0
- package/dist/lint/post-synth/prom218.d.ts +10 -0
- package/dist/lint/post-synth/prom218.d.ts.map +1 -0
- package/dist/lint/post-synth/prom219.d.ts +9 -0
- package/dist/lint/post-synth/prom219.d.ts.map +1 -0
- package/dist/lint/post-synth/prom220.d.ts +9 -0
- package/dist/lint/post-synth/prom220.d.ts.map +1 -0
- package/dist/lint/post-synth/prom221.d.ts +10 -0
- package/dist/lint/post-synth/prom221.d.ts.map +1 -0
- package/dist/lint/post-synth/prom222.d.ts +9 -0
- package/dist/lint/post-synth/prom222.d.ts.map +1 -0
- package/dist/lint/post-synth/prom223.d.ts +10 -0
- package/dist/lint/post-synth/prom223.d.ts.map +1 -0
- package/dist/lint/post-synth/prom224.d.ts +9 -0
- package/dist/lint/post-synth/prom224.d.ts.map +1 -0
- package/dist/lint/post-synth/prom301.d.ts +14 -0
- package/dist/lint/post-synth/prom301.d.ts.map +1 -0
- package/dist/lint/rules/literal-credential.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +16 -0
- package/dist/okf/rules/PROM210.md +11 -0
- package/dist/okf/rules/PROM211.md +11 -0
- package/dist/okf/rules/PROM212.md +11 -0
- package/dist/okf/rules/PROM213.md +11 -0
- package/dist/okf/rules/PROM214.md +11 -0
- package/dist/okf/rules/PROM215.md +11 -0
- package/dist/okf/rules/PROM216.md +11 -0
- package/dist/okf/rules/PROM217.md +11 -0
- package/dist/okf/rules/PROM218.md +11 -0
- package/dist/okf/rules/PROM219.md +11 -0
- package/dist/okf/rules/PROM220.md +11 -0
- package/dist/okf/rules/PROM221.md +11 -0
- package/dist/okf/rules/PROM222.md +11 -0
- package/dist/okf/rules/PROM223.md +11 -0
- package/dist/okf/rules/PROM224.md +11 -0
- package/dist/okf/rules/PROM301.md +11 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/promql-analysis.d.ts +82 -0
- package/dist/promql-analysis.d.ts.map +1 -0
- package/dist/rule-eval.d.ts.map +1 -1
- package/dist/rules/literal-credential.ts +4 -0
- package/dist/rules/prom-helpers.ts +41 -2
- package/dist/rules/prom208.ts +1 -1
- package/dist/rules/prom209.ts +1 -1
- package/dist/rules/prom210.ts +17 -0
- package/dist/rules/prom211.ts +21 -0
- package/dist/rules/prom212.ts +18 -0
- package/dist/rules/prom213.ts +20 -0
- package/dist/rules/prom214.ts +18 -0
- package/dist/rules/prom215.ts +18 -0
- package/dist/rules/prom216.ts +19 -0
- package/dist/rules/prom217.ts +18 -0
- package/dist/rules/prom218.ts +19 -0
- package/dist/rules/prom219.ts +18 -0
- package/dist/rules/prom220.ts +18 -0
- package/dist/rules/prom221.ts +19 -0
- package/dist/rules/prom222.ts +18 -0
- package/dist/rules/prom223.ts +19 -0
- package/dist/rules/prom224.ts +18 -0
- package/dist/rules/prom301.ts +23 -0
- package/dist/skill-defs.d.ts +1 -1
- package/dist/skill-defs.d.ts.map +1 -1
- package/dist/skills/chant-prometheus-alertmanager.md +3 -1
- package/dist/validate-config.d.ts +9 -3
- package/dist/validate-config.d.ts.map +1 -1
- package/dist/validate-integrations.d.ts +38 -0
- package/dist/validate-integrations.d.ts.map +1 -0
- package/dist/validate-security.d.ts +13 -0
- package/dist/validate-security.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/collector-metrics.ts +262 -0
- package/src/composites/composites.test.ts +2 -5
- package/src/import/embedded.test.ts +52 -3
- package/src/import/embedded.ts +37 -5
- package/src/import/generator.ts +19 -1
- package/src/import/testdata/alertmanager-integrations.yml +5 -1
- package/src/index.ts +1 -0
- package/src/init-templates.test.ts +3 -1
- package/src/lint/audit-catalog.ts +115 -2
- package/src/lint/audit-lineage.test.ts +74 -0
- package/src/lint/audit-lineage.ts +97 -0
- package/src/lint/post-synth/checks.test.ts +257 -0
- package/src/lint/post-synth/index.ts +32 -0
- package/src/lint/post-synth/post-synth.test.ts +37 -1
- package/src/lint/post-synth/prom-helpers.ts +41 -2
- package/src/lint/post-synth/prom208.ts +1 -1
- package/src/lint/post-synth/prom209.ts +1 -1
- package/src/lint/post-synth/prom210.ts +17 -0
- package/src/lint/post-synth/prom211.ts +21 -0
- package/src/lint/post-synth/prom212.ts +18 -0
- package/src/lint/post-synth/prom213.ts +20 -0
- package/src/lint/post-synth/prom214.ts +18 -0
- package/src/lint/post-synth/prom215.ts +18 -0
- package/src/lint/post-synth/prom216.ts +19 -0
- package/src/lint/post-synth/prom217.ts +18 -0
- package/src/lint/post-synth/prom218.ts +19 -0
- package/src/lint/post-synth/prom219.ts +18 -0
- package/src/lint/post-synth/prom220.ts +18 -0
- package/src/lint/post-synth/prom221.ts +19 -0
- package/src/lint/post-synth/prom222.ts +18 -0
- package/src/lint/post-synth/prom223.ts +19 -0
- package/src/lint/post-synth/prom224.ts +18 -0
- package/src/lint/post-synth/prom301.test.ts +157 -0
- package/src/lint/post-synth/prom301.ts +23 -0
- package/src/lint/rules/literal-credential.ts +4 -0
- package/src/lint/rules/rules.test.ts +16 -0
- package/src/plugin.test.ts +23 -0
- package/src/plugin.ts +14 -3
- package/src/promql-analysis.ts +365 -0
- package/src/skills/chant-prometheus-alertmanager.md +3 -1
- package/src/testdata/integration-cases.ts +96 -0
- package/src/tools.test.ts +6 -0
- package/src/validate-config.ts +279 -33
- package/src/validate-integrations.ts +389 -0
- package/src/validate-security.ts +143 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { prometheusAuditCatalog } from "./audit-catalog";
|
|
3
|
+
|
|
4
|
+
describe("prometheus audit lineage (#2916, #3136)", () => {
|
|
5
|
+
const credits = (id: string) => (prometheusAuditCatalog[id].lineage ?? []).map((l) => `${l.tool}:${l.rule ?? ""}:${l.relation}`);
|
|
6
|
+
|
|
7
|
+
test("credits promtool where rulefmt rejects the same file", () => {
|
|
8
|
+
for (const id of ["PROM101", "PROM103", "PROM104", "PROM105"]) {
|
|
9
|
+
expect(credits(id), id).toContain("promtool::equivalent");
|
|
10
|
+
}
|
|
11
|
+
expect(credits("PROM002")).toContain("promtool::equivalent");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("credits pint's named checks", () => {
|
|
15
|
+
expect(credits("PROM002")).toEqual(["promtool::equivalent", "pint:promql/syntax:equivalent"]);
|
|
16
|
+
expect(credits("PROM102")).toEqual(["pint:rule/duplicate:overlaps"]);
|
|
17
|
+
expect(credits("PROM103")).toEqual(["promtool::equivalent", "pint:alerts/for:overlaps"]);
|
|
18
|
+
expect(credits("PROM104")).toEqual(["promtool::equivalent", "pint:promql/syntax:equivalent"]);
|
|
19
|
+
expect(credits("PROM106")).toEqual(["pint:rule/label:overlaps"]);
|
|
20
|
+
expect(credits("PROM107")).toEqual(["pint:alerts/annotation:overlaps"]);
|
|
21
|
+
expect(credits("PROM211")).toEqual(["pint:rule/for:overlaps"]);
|
|
22
|
+
expect(credits("PROM212")).toEqual(["pint:alerts/annotation:overlaps"]);
|
|
23
|
+
expect(credits("PROM213")).toEqual(["pint:alerts/comparison:equivalent"]);
|
|
24
|
+
expect(credits("PROM214")).toEqual(["pint:alerts/template:overlaps"]);
|
|
25
|
+
expect(credits("PROM215")).toEqual(["pint:promql/rate:overlaps"]);
|
|
26
|
+
expect(credits("PROM217")).toEqual(["pint:rule/name:overlaps"]);
|
|
27
|
+
expect(credits("PROM218")).toEqual(["pint:promql/regexp:equivalent"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("credits amtool check-config where Alertmanager rejects the same config", () => {
|
|
31
|
+
for (const id of ["PROM201", "PROM203", "PROM204", "PROM205", "PROM206", "PROM208"]) {
|
|
32
|
+
expect(credits(id), id).toEqual(["amtool::equivalent"]);
|
|
33
|
+
}
|
|
34
|
+
expect(credits("PROM209")).toEqual(["amtool::overlaps"]);
|
|
35
|
+
expect(credits("PROM210")).toEqual(["amtool::overlaps"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("rules no upstream tool checks carry no credit", () => {
|
|
39
|
+
for (const id of ["PROM001", "PROM003", "PROM202", "PROM207", "PROM216", "PROM219", "PROM220", "PROM221", "PROM222", "PROM223", "PROM224", "PROM301"]) {
|
|
40
|
+
expect(prometheusAuditCatalog[id].lineage, id).toBeUndefined();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("every rule is either credited above or deliberately left uncredited", () => {
|
|
45
|
+
const credited = Object.keys(prometheusAuditCatalog).filter((id) => prometheusAuditCatalog[id].lineage !== undefined);
|
|
46
|
+
expect(credited.sort()).toEqual(
|
|
47
|
+
[
|
|
48
|
+
"PROM002",
|
|
49
|
+
"PROM101",
|
|
50
|
+
"PROM102",
|
|
51
|
+
"PROM103",
|
|
52
|
+
"PROM104",
|
|
53
|
+
"PROM105",
|
|
54
|
+
"PROM106",
|
|
55
|
+
"PROM107",
|
|
56
|
+
"PROM201",
|
|
57
|
+
"PROM203",
|
|
58
|
+
"PROM204",
|
|
59
|
+
"PROM205",
|
|
60
|
+
"PROM206",
|
|
61
|
+
"PROM208",
|
|
62
|
+
"PROM209",
|
|
63
|
+
"PROM210",
|
|
64
|
+
"PROM211",
|
|
65
|
+
"PROM212",
|
|
66
|
+
"PROM213",
|
|
67
|
+
"PROM214",
|
|
68
|
+
"PROM215",
|
|
69
|
+
"PROM217",
|
|
70
|
+
"PROM218",
|
|
71
|
+
].sort(),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prior art for the prometheus lexicon's audit rules: the tools whose checks
|
|
3
|
+
* cover the same condition, credited per rule. See
|
|
4
|
+
* packages/core/src/audit/prior-art.ts for the registry, the relation
|
|
5
|
+
* vocabulary, and why this is credit rather than authority. Kept by hand.
|
|
6
|
+
*
|
|
7
|
+
* Mapped against each tool's own check list (#2916):
|
|
8
|
+
*
|
|
9
|
+
* - promtool check rules parses a rule file with Prometheus's rulefmt
|
|
10
|
+
* package (model/rulefmt/rulefmt.go): repeated group names, record/alert
|
|
11
|
+
* exclusivity, annotations/for/keep_firing_for on recording rules, durations,
|
|
12
|
+
* and PromQL syntax. It has no per-rule ids, so those credits omit `rule`.
|
|
13
|
+
* - amtool check-config loads an Alertmanager config with the same code the
|
|
14
|
+
* server uses (config/config.go): undefined receivers and time intervals,
|
|
15
|
+
* non-unique names, a root route that is missing, has no receiver or has
|
|
16
|
+
* matchers, matcher syntax, durations, missing integration destinations and
|
|
17
|
+
* credentials, and "at most one of X and X_file". No per-rule ids either.
|
|
18
|
+
* - pint (cloudflare/pint, docs/checks/) is a rule linter with named checks.
|
|
19
|
+
* promql/syntax, rule/duplicate, rule/label, alerts/annotation and
|
|
20
|
+
* alerts/for map onto chant rules. rule/label and alerts/annotation are
|
|
21
|
+
* configurable "this label or annotation must exist" checks that pint ships
|
|
22
|
+
* with nothing enabled, so they overlap PROM106 and PROM107 and no more.
|
|
23
|
+
*
|
|
24
|
+
* The PROM211-PROM219 rule-file checks (#3363), against the same list:
|
|
25
|
+
* - alerts/comparison and promql/regexp are the same checks as PROM213 and
|
|
26
|
+
* PROM218, so those are equivalent.
|
|
27
|
+
* - rule/for enforces a configured minimum `for`, and alerts/annotation a
|
|
28
|
+
* configured annotation; with `for` required and runbook_url named they
|
|
29
|
+
* are PROM211 and PROM212. alerts/for is not credited: it rejects bad
|
|
30
|
+
* values and a redundant `for: 0s`, not a missing `for`.
|
|
31
|
+
* - alerts/template covers PROM214's dropped label and checks template
|
|
32
|
+
* syntax besides. promql/rate reads the metric type from a live
|
|
33
|
+
* Prometheus's metadata; PROM215 has only the name to go on. rule/name
|
|
34
|
+
* is a configured name regex; PROM217 fixes it to level:metric:operations.
|
|
35
|
+
* All three overlap.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately without a credit from these tools:
|
|
38
|
+
* - PROM001 and PROM003: a literal credential in TypeScript source, and the
|
|
39
|
+
* Slo objective/window model. Neither tool reads either.
|
|
40
|
+
* - PROM202 (severity not routed) and PROM207 (receiver never routed to):
|
|
41
|
+
* check-config accepts both, and `amtool config routes test` answers a
|
|
42
|
+
* different question (where one given label set goes).
|
|
43
|
+
* - PROM216 (histogram_quantile without _bucket or le) and PROM219
|
|
44
|
+
* (alertname set by hand): no pint check reports either, and promtool's
|
|
45
|
+
* rulefmt accepts both.
|
|
46
|
+
* - PROM220-PROM224: check-config accepts insecure_skip_verify, SMTP auth
|
|
47
|
+
* without TLS, credentials over http://, a repeat_interval under
|
|
48
|
+
* group_interval, and an inhibit rule without equal.
|
|
49
|
+
* - PROM301: no tool here reads a rule file together with the collector
|
|
50
|
+
* config that produces its metrics.
|
|
51
|
+
*/
|
|
52
|
+
import type { Lineage } from "@intentius/chant/audit/catalog";
|
|
53
|
+
|
|
54
|
+
const RULEFMT = "https://github.com/prometheus/prometheus/blob/main/model/rulefmt/rulefmt.go";
|
|
55
|
+
const AM_CONFIG = "https://github.com/prometheus/alertmanager/blob/main/config/config.go";
|
|
56
|
+
const PINT = "https://github.com/cloudflare/pint/blob/main/docs/checks";
|
|
57
|
+
|
|
58
|
+
const amtool = (relation: Lineage["relation"]): Lineage => ({
|
|
59
|
+
tool: "amtool",
|
|
60
|
+
url: AM_CONFIG,
|
|
61
|
+
relation,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const prometheusAuditLineage: Record<string, Lineage[]> = {
|
|
65
|
+
PROM002: [
|
|
66
|
+
{ tool: "promtool", url: RULEFMT, relation: "equivalent" },
|
|
67
|
+
{ tool: "pint", rule: "promql/syntax", url: `${PINT}/promql/syntax.md`, relation: "equivalent" },
|
|
68
|
+
],
|
|
69
|
+
PROM101: [{ tool: "promtool", url: RULEFMT, relation: "equivalent" }],
|
|
70
|
+
PROM102: [{ tool: "pint", rule: "rule/duplicate", url: `${PINT}/rule/duplicate.md`, relation: "overlaps" }],
|
|
71
|
+
PROM103: [
|
|
72
|
+
{ tool: "promtool", url: RULEFMT, relation: "equivalent" },
|
|
73
|
+
{ tool: "pint", rule: "alerts/for", url: `${PINT}/alerts/for.md`, relation: "overlaps" },
|
|
74
|
+
],
|
|
75
|
+
PROM104: [
|
|
76
|
+
{ tool: "promtool", url: RULEFMT, relation: "equivalent" },
|
|
77
|
+
{ tool: "pint", rule: "promql/syntax", url: `${PINT}/promql/syntax.md`, relation: "equivalent" },
|
|
78
|
+
],
|
|
79
|
+
PROM105: [{ tool: "promtool", url: RULEFMT, relation: "equivalent" }],
|
|
80
|
+
PROM106: [{ tool: "pint", rule: "rule/label", url: `${PINT}/rule/label.md`, relation: "overlaps" }],
|
|
81
|
+
PROM107: [{ tool: "pint", rule: "alerts/annotation", url: `${PINT}/alerts/annotation.md`, relation: "overlaps" }],
|
|
82
|
+
PROM201: [amtool("equivalent")],
|
|
83
|
+
PROM203: [amtool("equivalent")],
|
|
84
|
+
PROM204: [amtool("equivalent")],
|
|
85
|
+
PROM205: [amtool("equivalent")],
|
|
86
|
+
PROM206: [amtool("equivalent")],
|
|
87
|
+
PROM208: [amtool("equivalent")],
|
|
88
|
+
PROM209: [amtool("overlaps")],
|
|
89
|
+
PROM210: [amtool("overlaps")],
|
|
90
|
+
PROM211: [{ tool: "pint", rule: "rule/for", url: `${PINT}/rule/for.md`, relation: "overlaps" }],
|
|
91
|
+
PROM212: [{ tool: "pint", rule: "alerts/annotation", url: `${PINT}/alerts/annotation.md`, relation: "overlaps" }],
|
|
92
|
+
PROM213: [{ tool: "pint", rule: "alerts/comparison", url: `${PINT}/alerts/comparison.md`, relation: "equivalent" }],
|
|
93
|
+
PROM214: [{ tool: "pint", rule: "alerts/template", url: `${PINT}/alerts/template.md`, relation: "overlaps" }],
|
|
94
|
+
PROM215: [{ tool: "pint", rule: "promql/rate", url: `${PINT}/promql/rate.md`, relation: "overlaps" }],
|
|
95
|
+
PROM217: [{ tool: "pint", rule: "rule/name", url: `${PINT}/rule/name.md`, relation: "overlaps" }],
|
|
96
|
+
PROM218: [{ tool: "pint", rule: "promql/regexp", url: `${PINT}/promql/regexp.md`, relation: "equivalent" }],
|
|
97
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROM211-PROM224 (#3363): one failing and one passing case each, and the
|
|
3
|
+
* lexicon's own composites, init templates and examples reporting none.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { readdirSync } from "fs";
|
|
8
|
+
import { makePostSynthCtx } from "@intentius/chant-test-utils";
|
|
9
|
+
import { build } from "@intentius/chant/build";
|
|
10
|
+
import { runPostSynthChecks } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { genAiMetrics } from "@intentius/chant-lexicon-otel/genai";
|
|
12
|
+
import { postSynthChecks } from "./index";
|
|
13
|
+
import { OPT_IN_CHECKS } from "../audit-catalog";
|
|
14
|
+
import { prometheusSerializer } from "../../serializer";
|
|
15
|
+
import { emitYaml } from "../../build";
|
|
16
|
+
import { ruleGroupConfig } from "../../rules";
|
|
17
|
+
import { GenAiRules, type GenAiAlerting } from "../../composites/genai";
|
|
18
|
+
import { Slo } from "../../composites/slo";
|
|
19
|
+
import type { RuleGroupConfig } from "../../model";
|
|
20
|
+
|
|
21
|
+
const NEW_CHECKS = postSynthChecks.filter((c) => c.id >= "PROM211" && c.id <= "PROM224");
|
|
22
|
+
|
|
23
|
+
/** The PROM211-PROM224 findings on a document, by check id. */
|
|
24
|
+
function found(text: string, ids?: string[]): string[] {
|
|
25
|
+
const ctx = makePostSynthCtx("prometheus", text);
|
|
26
|
+
return NEW_CHECKS.filter((c) => !ids || ids.includes(c.id))
|
|
27
|
+
.flatMap((c) => c.check(ctx))
|
|
28
|
+
.map((d) => d.checkId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A rule file of one group holding `rule`, as YAML. */
|
|
32
|
+
function rules(rule: Record<string, unknown>, groupLabels?: Record<string, string>): string {
|
|
33
|
+
return JSON.stringify({ groups: [{ name: "g", ...(groupLabels ? { labels: groupLabels } : {}), rules: [rule] }] });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** An alert that passes every check but the one a case varies. */
|
|
37
|
+
function alert(over: Record<string, unknown> = {}): string {
|
|
38
|
+
return rules({
|
|
39
|
+
alert: "A",
|
|
40
|
+
expr: "sum by (job) (rate(errors_total[5m])) > 1",
|
|
41
|
+
for: "5m",
|
|
42
|
+
labels: { severity: "page" },
|
|
43
|
+
annotations: { summary: "errors on {{ $labels.job }}", runbook_url: "https://runbooks/a" },
|
|
44
|
+
...over,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const record = (name: string, expr: string) => rules({ record: name, expr });
|
|
49
|
+
|
|
50
|
+
const am = (over: Record<string, unknown> = {}) =>
|
|
51
|
+
JSON.stringify({ route: { receiver: "r" }, receivers: [{ name: "r", webhook_configs: [{ url: "https://hook" }] }], ...over });
|
|
52
|
+
|
|
53
|
+
describe("the rule-file checks", () => {
|
|
54
|
+
test("the baseline alert and recording rule report nothing", () => {
|
|
55
|
+
expect(found(alert())).toEqual([]);
|
|
56
|
+
expect(found(record("job:errors:rate5m", "sum by (job) (rate(errors_total[5m]))"))).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("PROM211 no for, or for: 0s", () => {
|
|
60
|
+
expect(found(alert({ for: undefined }))).toEqual(["PROM211"]);
|
|
61
|
+
expect(found(alert({ for: "0s" }))).toEqual(["PROM211"]);
|
|
62
|
+
expect(found(alert({ for: "1m" }))).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("PROM211 leaves alone an expression over a window, a multi-window and, and a heartbeat", () => {
|
|
66
|
+
expect(found(alert({ for: undefined, expr: "avg_over_time(job:errors:rate5m[1h]) > 1" }))).toEqual([]);
|
|
67
|
+
expect(found(alert({ for: undefined, expr: "(job:errors:rate1h > 1) and (job:errors:rate5m > 1)" }))).toEqual([]);
|
|
68
|
+
expect(found(alert({ for: undefined, expr: "vector(1)" }), ["PROM211", "PROM213"])).toEqual([]);
|
|
69
|
+
// A for that is not a duration is PROM103's alone.
|
|
70
|
+
expect(found(alert({ for: "1.5h" }))).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("PROM212 is the only finding on an alert with no runbook_url", () => {
|
|
74
|
+
expect(found(alert({ annotations: { summary: "x" } }))).toEqual(["PROM212"]);
|
|
75
|
+
expect(found(record("job:errors:rate5m", "sum by (job) (rate(errors_total[5m]))"), ["PROM212"])).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("PROM213 no comparison", () => {
|
|
79
|
+
expect(found(alert({ expr: "sum by (job) (rate(errors_total[5m]))" }))).toEqual(["PROM213"]);
|
|
80
|
+
expect(found(alert({ expr: "sum by (job) (rate(errors_total[5m])) > bool 1" }))).toEqual(["PROM213"]);
|
|
81
|
+
expect(found(alert({ expr: "job:errors:rate5m > 1 or job:errors:rate1h" }))).toEqual(["PROM213"]);
|
|
82
|
+
expect(found(alert({ expr: "absent(up{job=\"api\"})" }))).toEqual([]);
|
|
83
|
+
expect(found(alert({ expr: "up{job=\"api\"} unless on (instance) maintenance" }))).toEqual([]);
|
|
84
|
+
expect(found(alert({ expr: "sum by (job) (up == 0)" }))).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("PROM214 a template label the aggregation drops", () => {
|
|
88
|
+
const reads = (expr: string) => alert({ expr, annotations: { summary: "{{ $labels.instance }} on {{ .Labels.job }}", runbook_url: "u" } });
|
|
89
|
+
expect(found(reads("sum by (job) (up) == 0"))).toEqual(["PROM214"]);
|
|
90
|
+
expect(found(reads("sum without (instance) (up) == 0"))).toEqual(["PROM214"]);
|
|
91
|
+
expect(found(reads("sum by (job, instance) (up) == 0"))).toEqual([]);
|
|
92
|
+
expect(found(reads("up == 0"))).toEqual([]);
|
|
93
|
+
expect(found(reads("sum without (pod) (up) == 0"))).toEqual([]);
|
|
94
|
+
// One-to-one matching on (job) keeps only job.
|
|
95
|
+
expect(found(reads("sum by (job, instance) (up) / on (job) sum by (job) (up) < 0.5"))).toEqual(["PROM214"]);
|
|
96
|
+
// A label the rule sets itself is left alone.
|
|
97
|
+
expect(found(alert({ expr: "sum(up) == 0", labels: { severity: "page", instance: "all" }, annotations: { summary: "{{ $labels.instance }}", runbook_url: "u" } }))).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("PROM215 rate over a name that is not a counter's", () => {
|
|
101
|
+
expect(found(record("job:mem:rate5m", "sum by (job) (rate(process_resident_memory_bytes[5m]))"))).toEqual(["PROM215"]);
|
|
102
|
+
expect(found(record("job:mem:rate5m", "sum by (job) (increase(queue_depth[5m]))"))).toEqual(["PROM215"]);
|
|
103
|
+
for (const name of ["errors_total", "req_seconds_count", "req_seconds_sum", "req_seconds_bucket"]) {
|
|
104
|
+
expect(found(record("job:x:rate5m", `sum by (job) (rate(${name}[5m]))`))).toEqual([]);
|
|
105
|
+
}
|
|
106
|
+
expect(found(record("job:mem:deriv5m", "sum by (job) (deriv(process_resident_memory_bytes[5m]))"))).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("PROM216 histogram_quantile without _bucket or without le", () => {
|
|
110
|
+
expect(found(record("job:lat:p95", "histogram_quantile(0.95, sum by (job) (rate(req_seconds_bucket[5m])))"))).toEqual(["PROM216"]);
|
|
111
|
+
expect(found(record("job:lat:p95", "histogram_quantile(0.95, sum without (le) (rate(req_seconds_bucket[5m])))"))).toEqual(["PROM216"]);
|
|
112
|
+
expect(found(record("job:lat:p95", "histogram_quantile(0.95, sum by (job, le) (rate(req_seconds_count[5m])))"))).toEqual(["PROM216"]);
|
|
113
|
+
expect(found(record("job:lat:p95", "histogram_quantile(0.95, sum by (job, le) (rate(req_seconds_bucket[5m])))"))).toEqual([]);
|
|
114
|
+
expect(found(record("job:lat:p95", "histogram_quantile(0.95, rate(req_seconds_bucket[5m]))"))).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("PROM217 recording rule names", () => {
|
|
118
|
+
expect(found(record("errors_rate", "sum(rate(errors_total[5m]))"))).toEqual(["PROM217"]);
|
|
119
|
+
expect(found(record("job:errors", "sum by (job) (rate(errors_total[5m]))"))).toEqual(["PROM217"]);
|
|
120
|
+
expect(found(record("job::rate5m", "sum by (job) (rate(errors_total[5m]))"))).toEqual(["PROM217"]);
|
|
121
|
+
expect(found(record("instance_path:requests:rate5m", "sum by (instance, path) (rate(requests_total[5m]))"))).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("PROM218 a regex that needs none, or is anchored", () => {
|
|
125
|
+
expect(found(record("job:e:rate5m", 'sum by (job) (rate(errors_total{job=~"api"}[5m]))'))).toEqual(["PROM218"]);
|
|
126
|
+
expect(found(record("job:e:rate5m", 'sum by (job) (rate(errors_total{job!~"api"}[5m]))'))).toEqual(["PROM218"]);
|
|
127
|
+
expect(found(record("job:e:rate5m", 'sum by (job) (rate(errors_total{job=~"^api.*"}[5m]))'))).toEqual(["PROM218"]);
|
|
128
|
+
expect(found(record("job:e:rate5m", 'sum by (job) (rate(errors_total{job=~"api.*$"}[5m]))'))).toEqual(["PROM218"]);
|
|
129
|
+
expect(found(record("job:e:rate5m", 'sum by (job) (rate(errors_total{job=~"api|web", code=~"5.."}[5m]))'))).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("PROM219 alertname set on the rule or the group", () => {
|
|
133
|
+
expect(found(alert({ labels: { severity: "page", alertname: "B" } }))).toEqual(["PROM219"]);
|
|
134
|
+
expect(found(rules(JSON.parse(alert()).groups[0].rules[0], { alertname: "B" }))).toEqual(["PROM219"]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("an expression that does not parse is PROM104's alone", () => {
|
|
138
|
+
expect(found(alert({ expr: "sum by (job) (rate(x[5m]) >" }))).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("the alertmanager.yml checks", () => {
|
|
143
|
+
test("the baseline config reports nothing", () => {
|
|
144
|
+
expect(found(am())).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("PROM220 insecure_skip_verify on a receiver or in global", () => {
|
|
148
|
+
const insecure = { url: "https://hook", http_config: { tls_config: { insecure_skip_verify: true } } };
|
|
149
|
+
expect(found(am({ receivers: [{ name: "r", webhook_configs: [insecure] }] }))).toEqual(["PROM220"]);
|
|
150
|
+
expect(found(am({ global: { http_config: { tls_config: { insecure_skip_verify: true } } } }))).toEqual(["PROM220"]);
|
|
151
|
+
expect(found(am({ receivers: [{ name: "r", email_configs: [{ to: "a@b.c", smarthost: "s:25", from: "x@b.c", tls_config: { insecure_skip_verify: true } }] }] }))).toEqual(["PROM220"]);
|
|
152
|
+
expect(found(am({ receivers: [{ name: "r", webhook_configs: [{ url: "https://hook", http_config: { tls_config: { insecure_skip_verify: false } } }] }] }))).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("PROM221 SMTP auth with require_tls false", () => {
|
|
156
|
+
const email = (entry: Record<string, unknown>, global: Record<string, unknown> = {}) =>
|
|
157
|
+
am({ global, receivers: [{ name: "r", email_configs: [{ to: "a@b.c", smarthost: "s:25", from: "x@b.c", ...entry }] }] });
|
|
158
|
+
expect(found(email({ require_tls: false, auth_username: "u", auth_password_file: "/p" }))).toEqual(["PROM221"]);
|
|
159
|
+
expect(found(email({ auth_username: "u" }, { smtp_require_tls: false, smtp_auth_password_file: "/p" }))).toEqual(["PROM221"]);
|
|
160
|
+
expect(found(email({ require_tls: false }))).toEqual([]);
|
|
161
|
+
expect(found(email({ auth_password_file: "/p" }))).toEqual([]);
|
|
162
|
+
expect(found(email({ require_tls: false, auth_password_file: "/p", force_implicit_tls: true }))).toEqual([]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("PROM222 credentials sent to an http:// URL", () => {
|
|
166
|
+
const hook = (entry: Record<string, unknown>, global: Record<string, unknown> = {}) => am({ global, receivers: [{ name: "r", webhook_configs: [entry] }] });
|
|
167
|
+
expect(found(hook({ url: "http://user:pass@hook/" }))).toEqual(["PROM222"]);
|
|
168
|
+
expect(found(hook({ url: "http://hook/", http_config: { bearer_token_file: "/t" } }))).toEqual(["PROM222"]);
|
|
169
|
+
expect(found(hook({ url: "http://hook/" }, { http_config: { basic_auth: { username: "u", password_file: "/p" } } }))).toEqual(["PROM222"]);
|
|
170
|
+
expect(found(hook({ url: "http://hook/" }))).toEqual([]);
|
|
171
|
+
expect(found(hook({ url: "https://hook/", http_config: { bearer_token_file: "/t" } }))).toEqual([]);
|
|
172
|
+
const opsgenie = (global: Record<string, unknown>) => am({ global, receivers: [{ name: "r", opsgenie_configs: [{ api_key_file: "/k" }] }] });
|
|
173
|
+
expect(found(opsgenie({ opsgenie_api_url: "http://opsgenie.internal/" }))).toEqual(["PROM222"]);
|
|
174
|
+
expect(found(opsgenie({}))).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("PROM223 repeat_interval under group_interval, inherited down the tree", () => {
|
|
178
|
+
expect(found(am({ route: { receiver: "r", group_interval: "10m", repeat_interval: "5m" } }))).toEqual(["PROM223"]);
|
|
179
|
+
expect(found(am({ route: { receiver: "r", repeat_interval: "1m" } }))).toEqual(["PROM223"]);
|
|
180
|
+
expect(found(am({ route: { receiver: "r", group_interval: "1h", routes: [{ receiver: "r", repeat_interval: "30m" }] } }))).toEqual(["PROM223"]);
|
|
181
|
+
expect(found(am({ route: { receiver: "r", group_interval: "5m", repeat_interval: "1h" } }))).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("PROM224 an inhibit rule whose sides can match one alert, with no equal", () => {
|
|
185
|
+
const inhibit = (rule: Record<string, unknown>) => am({ inhibit_rules: [rule] });
|
|
186
|
+
expect(found(inhibit({ source_matchers: ['severity="page"'], target_matchers: ['team="db"'] }))).toEqual(["PROM224"]);
|
|
187
|
+
expect(found(inhibit({ source_matchers: ['severity=~"page|ticket"'], target_matchers: ['severity="ticket"'] }))).toEqual(["PROM224"]);
|
|
188
|
+
expect(found(inhibit({ source_matchers: ['severity="page"'], target_matchers: ['team="db"'], equal: ["alertname"] }))).toEqual([]);
|
|
189
|
+
expect(found(inhibit({ source_matchers: ['severity="page"'], target_matchers: ['severity="ticket"'] }))).toEqual([]);
|
|
190
|
+
expect(found(inhibit({ source_matchers: ['severity=~"critical"'], target_matchers: ['severity=~"warning|info"'] }))).toEqual([]);
|
|
191
|
+
expect(found(inhibit({ source_matchers: ['severity="page"'], target_matchers: ['severity!="page"'] }))).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/** Every finding the `recommended` preset reports (PROM212, opt-in, is left out). */
|
|
196
|
+
function recommended(text: string): string[] {
|
|
197
|
+
const ctx = makePostSynthCtx("prometheus", text);
|
|
198
|
+
return postSynthChecks
|
|
199
|
+
.filter((c) => !OPT_IN_CHECKS.has(c.id))
|
|
200
|
+
.flatMap((c) => c.check(ctx))
|
|
201
|
+
.map((d) => `${d.checkId} ${d.message}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const groupYaml = (group: RuleGroupConfig) => emitYaml({ groups: [group] });
|
|
205
|
+
|
|
206
|
+
describe("the lexicon's own rules report none of the checks", () => {
|
|
207
|
+
const prices = [{ provider: "anthropic", model: "m1", inputPerMTok: 3, outputPerMTok: 15, currency: "USD", source: "https://example.com", asOf: "2026-09-29" }];
|
|
208
|
+
const alerts: GenAiAlerting = {
|
|
209
|
+
errorRatio: true,
|
|
210
|
+
latency: true,
|
|
211
|
+
toolErrorRatio: true,
|
|
212
|
+
budgets: [
|
|
213
|
+
{ amount: 5, currency: "USD", per: "hour" },
|
|
214
|
+
{ amount: 50, currency: "USD", per: "day" },
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
test.each([
|
|
219
|
+
["spans", genAiMetrics()],
|
|
220
|
+
["client", genAiMetrics({ clientMetrics: "derive" })],
|
|
221
|
+
] as const)("GenAiRules over the %s metrics, with and without its alerts", (_label, genAi) => {
|
|
222
|
+
expect(recommended(groupYaml(ruleGroupConfig(GenAiRules({ genAi, prices }).rules)))).toEqual([]);
|
|
223
|
+
expect(recommended(groupYaml(ruleGroupConfig(GenAiRules({ genAi, prices, alerts }).rules)))).toEqual([]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("Slo, with each SLI form and with tickets off", () => {
|
|
227
|
+
const calls = "traces_span_metrics_calls_total";
|
|
228
|
+
const slos = [
|
|
229
|
+
Slo({
|
|
230
|
+
name: "a",
|
|
231
|
+
objective: 0.995,
|
|
232
|
+
window: "28d",
|
|
233
|
+
sli: { good: `sum(rate(${calls}{status_code!="STATUS_CODE_ERROR"}[{{window}}]))`, total: `sum(rate(${calls}[{{window}}]))` },
|
|
234
|
+
}),
|
|
235
|
+
Slo({
|
|
236
|
+
name: "b",
|
|
237
|
+
objective: 0.999,
|
|
238
|
+
window: "30d",
|
|
239
|
+
sli: { errors: `sum(rate(${calls}{status_code="STATUS_CODE_ERROR"}[{{window}}]))`, total: `sum(rate(${calls}[{{window}}]))` },
|
|
240
|
+
alerting: { ticket: false },
|
|
241
|
+
}),
|
|
242
|
+
];
|
|
243
|
+
for (const slo of slos) expect(recommended(groupYaml(ruleGroupConfig(slo.rules)))).toEqual([]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const examplesDir = join(import.meta.dirname, "..", "..", "..", "examples");
|
|
247
|
+
const examples = readdirSync(examplesDir, { withFileTypes: true })
|
|
248
|
+
.filter((d) => d.isDirectory())
|
|
249
|
+
.map((d) => d.name);
|
|
250
|
+
|
|
251
|
+
test.each(examples)("the %s example", async (name) => {
|
|
252
|
+
const result = await build(join(examplesDir, name, "src"), [prometheusSerializer]);
|
|
253
|
+
expect(result.errors).toEqual([]);
|
|
254
|
+
const diags = runPostSynthChecks(postSynthChecks, result).filter((d) => !OPT_IN_CHECKS.has(d.checkId));
|
|
255
|
+
expect(diags).toEqual([]);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -16,6 +16,22 @@ import { prom206 } from "./prom206";
|
|
|
16
16
|
import { prom207 } from "./prom207";
|
|
17
17
|
import { prom208 } from "./prom208";
|
|
18
18
|
import { prom209 } from "./prom209";
|
|
19
|
+
import { prom210 } from "./prom210";
|
|
20
|
+
import { prom211 } from "./prom211";
|
|
21
|
+
import { prom212 } from "./prom212";
|
|
22
|
+
import { prom213 } from "./prom213";
|
|
23
|
+
import { prom214 } from "./prom214";
|
|
24
|
+
import { prom215 } from "./prom215";
|
|
25
|
+
import { prom216 } from "./prom216";
|
|
26
|
+
import { prom217 } from "./prom217";
|
|
27
|
+
import { prom218 } from "./prom218";
|
|
28
|
+
import { prom219 } from "./prom219";
|
|
29
|
+
import { prom220 } from "./prom220";
|
|
30
|
+
import { prom221 } from "./prom221";
|
|
31
|
+
import { prom222 } from "./prom222";
|
|
32
|
+
import { prom223 } from "./prom223";
|
|
33
|
+
import { prom224 } from "./prom224";
|
|
34
|
+
import { prom301 } from "./prom301";
|
|
19
35
|
|
|
20
36
|
export const postSynthChecks: PostSynthCheck[] = [
|
|
21
37
|
prom101,
|
|
@@ -34,4 +50,20 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
34
50
|
prom207,
|
|
35
51
|
prom208,
|
|
36
52
|
prom209,
|
|
53
|
+
prom210,
|
|
54
|
+
prom211,
|
|
55
|
+
prom212,
|
|
56
|
+
prom213,
|
|
57
|
+
prom214,
|
|
58
|
+
prom215,
|
|
59
|
+
prom216,
|
|
60
|
+
prom217,
|
|
61
|
+
prom218,
|
|
62
|
+
prom219,
|
|
63
|
+
prom220,
|
|
64
|
+
prom221,
|
|
65
|
+
prom222,
|
|
66
|
+
prom223,
|
|
67
|
+
prom224,
|
|
68
|
+
prom301,
|
|
37
69
|
];
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
2
|
import { makePostSynthCtx, makePostSynthCtxFromFiles } from "@intentius/chant-test-utils";
|
|
3
3
|
import { postSynthChecks } from "./index";
|
|
4
|
+
import { OPT_IN_CHECKS } from "../audit-catalog";
|
|
5
|
+
import { RECEIVER_INTEGRATION_TYPES } from "../../integrations";
|
|
6
|
+
import { INTEGRATION_CASES, SLACK_APP_URL, amWith } from "../../testdata/integration-cases";
|
|
4
7
|
import { prom101 } from "./prom101";
|
|
5
8
|
import { prom102 } from "./prom102";
|
|
6
9
|
import { prom103 } from "./prom103";
|
|
@@ -17,6 +20,7 @@ import { prom206 } from "./prom206";
|
|
|
17
20
|
import { prom207 } from "./prom207";
|
|
18
21
|
import { prom208 } from "./prom208";
|
|
19
22
|
import { prom209 } from "./prom209";
|
|
23
|
+
import { prom210 } from "./prom210";
|
|
20
24
|
|
|
21
25
|
const RULES = `groups:
|
|
22
26
|
- name: api
|
|
@@ -73,7 +77,8 @@ const rulesOnly = (rules: string) => makePostSynthCtx("prometheus", rules);
|
|
|
73
77
|
const amOnly = (am: string) => makePostSynthCtx("prometheus", am);
|
|
74
78
|
|
|
75
79
|
describe("a clean rule file and alertmanager.yml", () => {
|
|
76
|
-
|
|
80
|
+
// PROM212 (no runbook_url) is opt-in and RULES carries none; see checks.test.ts.
|
|
81
|
+
test.each(postSynthChecks.filter((c) => !OPT_IN_CHECKS.has(c.id)).map((c) => [c.id, c] as const))("%s finds nothing", (_id, check) => {
|
|
77
82
|
expect(check.check(both())).toEqual([]);
|
|
78
83
|
});
|
|
79
84
|
});
|
|
@@ -245,6 +250,37 @@ receivers:
|
|
|
245
250
|
});
|
|
246
251
|
});
|
|
247
252
|
|
|
253
|
+
const integrationCodes = (text: string) =>
|
|
254
|
+
[prom208, prom209, prom210].flatMap((c) => c.check(amOnly(text))).map((d) => d.checkId).sort();
|
|
255
|
+
|
|
256
|
+
describe("PROM208-PROM210 on every Alertmanager integration", () => {
|
|
257
|
+
test.each(INTEGRATION_CASES)("%s", (_label, key, entry, global, codes) => {
|
|
258
|
+
expect(integrationCodes(amWith(key, entry, global))).toEqual(codes);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("every integration has a passing and a failing case", () => {
|
|
262
|
+
for (const key of Object.keys(RECEIVER_INTEGRATION_TYPES)) {
|
|
263
|
+
const cases = INTEGRATION_CASES.filter((c) => c[1] === key);
|
|
264
|
+
expect(cases.some((c) => c[4].length === 0), `${key} passing`).toBe(true);
|
|
265
|
+
expect(cases.some((c) => c[4].length > 0), `${key} failing`).toBe(true);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("findings name the receiver and the entry", () => {
|
|
270
|
+
const [d] = prom209.check(amOnly(amWith("opsgenie_configs", {})));
|
|
271
|
+
expect(d).toMatchObject({ checkId: "PROM209", severity: "error", entity: "r" });
|
|
272
|
+
expect(d.message).toContain('receiver "r" opsgenie_configs[0] has no api_key');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("PROM210 and PROM208 in global", () => {
|
|
276
|
+
const am = (global: Record<string, unknown>) => JSON.stringify({ global, route: { receiver: "r" }, receivers: [{ name: "r" }] });
|
|
277
|
+
expect(integrationCodes(am({ slack_app_token_file: "/t", slack_api_url: "https://hooks.slack.com/x" }))).toEqual(["PROM210"]);
|
|
278
|
+
expect(integrationCodes(am({ slack_app_token_file: "/t", slack_api_url: SLACK_APP_URL }))).toEqual([]);
|
|
279
|
+
expect(integrationCodes(am({ smtp_auth_password: "x", smtp_auth_password_file: "/p", smtp_smarthost: "smtp" }))).toEqual(["PROM210", "PROM210"]);
|
|
280
|
+
expect(integrationCodes(am({ resolve_timeout: "1.5m" }))).toEqual(["PROM208"]);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
248
284
|
describe("documents from other lexicons", () => {
|
|
249
285
|
test("a k8s manifest and a collector config are not read as ours", () => {
|
|
250
286
|
const ctx = makePostSynthCtx(
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
14
14
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
15
15
|
import { loadAll } from "js-yaml";
|
|
16
|
+
import { buildCollectorConfigs, collectorMetricIssues, collectorMetrics } from "../../collector-metrics";
|
|
16
17
|
import { looksLikeAlertmanagerConfig, looksLikeRuleFile, type AlertmanagerConfig, type RuleFileConfig } from "../../model";
|
|
17
18
|
import {
|
|
18
19
|
validateAlertmanagerConfig,
|
|
19
20
|
validateRuleFile,
|
|
21
|
+
validateRunbookUrls,
|
|
20
22
|
validateSeverityRouting,
|
|
21
23
|
type PrometheusIssue,
|
|
22
24
|
type PrometheusIssueCode,
|
|
@@ -62,7 +64,7 @@ function toDiagnostic(issue: PrometheusIssue, source: string): PostSynthDiagnost
|
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
/** Diagnostics for one rule-file code (PROM101-PROM107) across every rule file in the output. */
|
|
67
|
+
/** Diagnostics for one rule-file code (PROM101-PROM107, PROM211, PROM213-PROM219) across every rule file in the output. */
|
|
66
68
|
export function ruleFileDiagnostics(ctx: PostSynthContext, code: PrometheusIssueCode): PostSynthDiagnostic[] {
|
|
67
69
|
return prometheusDocs(ctx).ruleFiles.flatMap(({ source, config }) =>
|
|
68
70
|
validateRuleFile(config)
|
|
@@ -71,7 +73,7 @@ export function ruleFileDiagnostics(ctx: PostSynthContext, code: PrometheusIssue
|
|
|
71
73
|
);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
/** Diagnostics for one Alertmanager code (PROM201, PROM203-
|
|
76
|
+
/** Diagnostics for one Alertmanager code (PROM201, PROM203-PROM210, PROM220-PROM224) across every `alertmanager.yml` in the output. */
|
|
75
77
|
export function alertmanagerDiagnostics(ctx: PostSynthContext, code: PrometheusIssueCode): PostSynthDiagnostic[] {
|
|
76
78
|
return prometheusDocs(ctx).alertmanager.flatMap(({ source, config }) =>
|
|
77
79
|
validateAlertmanagerConfig(config)
|
|
@@ -80,6 +82,43 @@ export function alertmanagerDiagnostics(ctx: PostSynthContext, code: PrometheusI
|
|
|
80
82
|
);
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/** PROM212 across every rule file in the output. Opt-in: the lexicon's `recommended` lint preset leaves it out (see ../../plugin.ts). */
|
|
86
|
+
export function runbookDiagnostics(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
87
|
+
return prometheusDocs(ctx).ruleFiles.flatMap(({ source, config }) => validateRunbookUrls(config).map((i) => toDiagnostic(i, source)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* PROM301: every rule expression in the output against the metrics the
|
|
92
|
+
* build's collector configs emit. Silent when the build has none.
|
|
93
|
+
*/
|
|
94
|
+
export function collectorMetricDiagnostics(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
95
|
+
const { ruleFiles } = prometheusDocs(ctx);
|
|
96
|
+
if (ruleFiles.length === 0) return [];
|
|
97
|
+
const emitted = collectorMetrics(buildCollectorConfigs(ctx));
|
|
98
|
+
if (!emitted) return [];
|
|
99
|
+
const out: PostSynthDiagnostic[] = [];
|
|
100
|
+
for (const { source, config } of ruleFiles) {
|
|
101
|
+
for (const group of Array.isArray(config?.groups) ? config.groups : []) {
|
|
102
|
+
for (const rule of Array.isArray(group?.rules) ? group.rules : []) {
|
|
103
|
+
const r = rule as unknown as Record<string, unknown>;
|
|
104
|
+
if (typeof r.expr !== "string") continue;
|
|
105
|
+
const subject = `${group.name}/${typeof r.record === "string" ? r.record : String(r.alert ?? "")}`;
|
|
106
|
+
for (const issue of collectorMetricIssues(r.expr, emitted)) {
|
|
107
|
+
const message = `rule ${subject} ${issue.message}`;
|
|
108
|
+
out.push({
|
|
109
|
+
checkId: "PROM301",
|
|
110
|
+
severity: "warning",
|
|
111
|
+
message: source && source !== "prometheus" ? `${source}: ${message}` : message,
|
|
112
|
+
entity: subject,
|
|
113
|
+
lexicon: "prometheus",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
83
122
|
/** PROM202: joins every rule file in the output against every Alertmanager config in it. Silent when either is absent. */
|
|
84
123
|
export function routingDiagnostics(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
85
124
|
const { ruleFiles, alertmanager } = prometheusDocs(ctx);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PROM208: An Alertmanager duration is not a duration
|
|
3
3
|
*
|
|
4
|
-
* group_wait, group_interval, repeat_interval, resolve_timeout and
|
|
4
|
+
* group_wait, group_interval, repeat_interval, resolve_timeout and Jira reopen_duration take Prometheus durations (30s, 4h, 1d); the timeout of webhook, Slack, PagerDuty and incident.io, and Pushover retry, expire and ttl take Go durations (10s, 1m30s, 500ms).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PROM209: A receiver integration is missing its destination or credential
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Every one of Alertmanager's 18 integrations is checked for the destination, credential and required fields its config validation asks for, with the global defaults it falls back to: e.g. a webhook url, an Opsgenie api_key (or global.opsgenie_api_key), a Telegram chat_id and bot token, a Webex room_id and authorization, an SNS target, a Jira project and issue_type.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROM210: A receiver integration or global setting is one Alertmanager rejects
|
|
3
|
+
*
|
|
4
|
+
* Two settings that exclude each other (a value and its *_file, a Slack api_url and app_token, Pushover html and monospace, two SNS targets), or a value outside the allowed set (WeChat message_type, Telegram parse_mode, Jira api_type, an Opsgenie responder type, a VictorOps reserved custom field, a smarthost that is not host:port, a Slack or Mattermost field without title and value).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { alertmanagerDiagnostics } from "./prom-helpers";
|
|
9
|
+
|
|
10
|
+
export const prom210: PostSynthCheck = {
|
|
11
|
+
id: "PROM210",
|
|
12
|
+
description: "A receiver integration or global setting is one Alertmanager rejects",
|
|
13
|
+
|
|
14
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
15
|
+
return alertmanagerDiagnostics(ctx, "PROM210");
|
|
16
|
+
},
|
|
17
|
+
};
|