@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.
Files changed (157) hide show
  1. package/README.md +3 -1
  2. package/dist/alertmanager.d.ts.map +1 -1
  3. package/dist/collector-metrics.d.ts +58 -0
  4. package/dist/collector-metrics.d.ts.map +1 -0
  5. package/dist/composites/genai.d.ts +1 -1
  6. package/dist/composites/genai.d.ts.map +1 -1
  7. package/dist/composites/slo.d.ts +1 -1
  8. package/dist/composites/slo.d.ts.map +1 -1
  9. package/dist/import/embedded.d.ts +12 -3
  10. package/dist/import/embedded.d.ts.map +1 -1
  11. package/dist/import/generator.d.ts +14 -0
  12. package/dist/import/generator.d.ts.map +1 -1
  13. package/dist/import/parser.d.ts.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/init-templates.d.ts.map +1 -1
  17. package/dist/integrations.d.ts.map +1 -1
  18. package/dist/integrity.json +23 -7
  19. package/dist/lint/audit-catalog.d.ts +2 -0
  20. package/dist/lint/audit-catalog.d.ts.map +1 -1
  21. package/dist/lint/audit-lineage.d.ts +54 -0
  22. package/dist/lint/audit-lineage.d.ts.map +1 -0
  23. package/dist/lint/post-synth/index.d.ts.map +1 -1
  24. package/dist/lint/post-synth/prom-helpers.d.ts +9 -2
  25. package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -1
  26. package/dist/lint/post-synth/prom208.d.ts +1 -1
  27. package/dist/lint/post-synth/prom209.d.ts +1 -1
  28. package/dist/lint/post-synth/prom210.d.ts +8 -0
  29. package/dist/lint/post-synth/prom210.d.ts.map +1 -0
  30. package/dist/lint/post-synth/prom211.d.ts +12 -0
  31. package/dist/lint/post-synth/prom211.d.ts.map +1 -0
  32. package/dist/lint/post-synth/prom212.d.ts +9 -0
  33. package/dist/lint/post-synth/prom212.d.ts.map +1 -0
  34. package/dist/lint/post-synth/prom213.d.ts +11 -0
  35. package/dist/lint/post-synth/prom213.d.ts.map +1 -0
  36. package/dist/lint/post-synth/prom214.d.ts +9 -0
  37. package/dist/lint/post-synth/prom214.d.ts.map +1 -0
  38. package/dist/lint/post-synth/prom215.d.ts +9 -0
  39. package/dist/lint/post-synth/prom215.d.ts.map +1 -0
  40. package/dist/lint/post-synth/prom216.d.ts +10 -0
  41. package/dist/lint/post-synth/prom216.d.ts.map +1 -0
  42. package/dist/lint/post-synth/prom217.d.ts +9 -0
  43. package/dist/lint/post-synth/prom217.d.ts.map +1 -0
  44. package/dist/lint/post-synth/prom218.d.ts +10 -0
  45. package/dist/lint/post-synth/prom218.d.ts.map +1 -0
  46. package/dist/lint/post-synth/prom219.d.ts +9 -0
  47. package/dist/lint/post-synth/prom219.d.ts.map +1 -0
  48. package/dist/lint/post-synth/prom220.d.ts +9 -0
  49. package/dist/lint/post-synth/prom220.d.ts.map +1 -0
  50. package/dist/lint/post-synth/prom221.d.ts +10 -0
  51. package/dist/lint/post-synth/prom221.d.ts.map +1 -0
  52. package/dist/lint/post-synth/prom222.d.ts +9 -0
  53. package/dist/lint/post-synth/prom222.d.ts.map +1 -0
  54. package/dist/lint/post-synth/prom223.d.ts +10 -0
  55. package/dist/lint/post-synth/prom223.d.ts.map +1 -0
  56. package/dist/lint/post-synth/prom224.d.ts +9 -0
  57. package/dist/lint/post-synth/prom224.d.ts.map +1 -0
  58. package/dist/lint/post-synth/prom301.d.ts +14 -0
  59. package/dist/lint/post-synth/prom301.d.ts.map +1 -0
  60. package/dist/lint/rules/literal-credential.d.ts.map +1 -1
  61. package/dist/manifest.json +1 -1
  62. package/dist/okf/index.md +16 -0
  63. package/dist/okf/rules/PROM210.md +11 -0
  64. package/dist/okf/rules/PROM211.md +11 -0
  65. package/dist/okf/rules/PROM212.md +11 -0
  66. package/dist/okf/rules/PROM213.md +11 -0
  67. package/dist/okf/rules/PROM214.md +11 -0
  68. package/dist/okf/rules/PROM215.md +11 -0
  69. package/dist/okf/rules/PROM216.md +11 -0
  70. package/dist/okf/rules/PROM217.md +11 -0
  71. package/dist/okf/rules/PROM218.md +11 -0
  72. package/dist/okf/rules/PROM219.md +11 -0
  73. package/dist/okf/rules/PROM220.md +11 -0
  74. package/dist/okf/rules/PROM221.md +11 -0
  75. package/dist/okf/rules/PROM222.md +11 -0
  76. package/dist/okf/rules/PROM223.md +11 -0
  77. package/dist/okf/rules/PROM224.md +11 -0
  78. package/dist/okf/rules/PROM301.md +11 -0
  79. package/dist/plugin.d.ts.map +1 -1
  80. package/dist/promql-analysis.d.ts +82 -0
  81. package/dist/promql-analysis.d.ts.map +1 -0
  82. package/dist/rule-eval.d.ts.map +1 -1
  83. package/dist/rules/literal-credential.ts +4 -0
  84. package/dist/rules/prom-helpers.ts +41 -2
  85. package/dist/rules/prom208.ts +1 -1
  86. package/dist/rules/prom209.ts +1 -1
  87. package/dist/rules/prom210.ts +17 -0
  88. package/dist/rules/prom211.ts +21 -0
  89. package/dist/rules/prom212.ts +18 -0
  90. package/dist/rules/prom213.ts +20 -0
  91. package/dist/rules/prom214.ts +18 -0
  92. package/dist/rules/prom215.ts +18 -0
  93. package/dist/rules/prom216.ts +19 -0
  94. package/dist/rules/prom217.ts +18 -0
  95. package/dist/rules/prom218.ts +19 -0
  96. package/dist/rules/prom219.ts +18 -0
  97. package/dist/rules/prom220.ts +18 -0
  98. package/dist/rules/prom221.ts +19 -0
  99. package/dist/rules/prom222.ts +18 -0
  100. package/dist/rules/prom223.ts +19 -0
  101. package/dist/rules/prom224.ts +18 -0
  102. package/dist/rules/prom301.ts +23 -0
  103. package/dist/skill-defs.d.ts +1 -1
  104. package/dist/skill-defs.d.ts.map +1 -1
  105. package/dist/skills/chant-prometheus-alertmanager.md +3 -1
  106. package/dist/validate-config.d.ts +9 -3
  107. package/dist/validate-config.d.ts.map +1 -1
  108. package/dist/validate-integrations.d.ts +38 -0
  109. package/dist/validate-integrations.d.ts.map +1 -0
  110. package/dist/validate-security.d.ts +13 -0
  111. package/dist/validate-security.d.ts.map +1 -0
  112. package/package.json +3 -3
  113. package/src/collector-metrics.ts +262 -0
  114. package/src/composites/composites.test.ts +2 -5
  115. package/src/import/embedded.test.ts +52 -3
  116. package/src/import/embedded.ts +37 -5
  117. package/src/import/generator.ts +19 -1
  118. package/src/import/testdata/alertmanager-integrations.yml +5 -1
  119. package/src/index.ts +1 -0
  120. package/src/init-templates.test.ts +3 -1
  121. package/src/lint/audit-catalog.ts +115 -2
  122. package/src/lint/audit-lineage.test.ts +74 -0
  123. package/src/lint/audit-lineage.ts +97 -0
  124. package/src/lint/post-synth/checks.test.ts +257 -0
  125. package/src/lint/post-synth/index.ts +32 -0
  126. package/src/lint/post-synth/post-synth.test.ts +37 -1
  127. package/src/lint/post-synth/prom-helpers.ts +41 -2
  128. package/src/lint/post-synth/prom208.ts +1 -1
  129. package/src/lint/post-synth/prom209.ts +1 -1
  130. package/src/lint/post-synth/prom210.ts +17 -0
  131. package/src/lint/post-synth/prom211.ts +21 -0
  132. package/src/lint/post-synth/prom212.ts +18 -0
  133. package/src/lint/post-synth/prom213.ts +20 -0
  134. package/src/lint/post-synth/prom214.ts +18 -0
  135. package/src/lint/post-synth/prom215.ts +18 -0
  136. package/src/lint/post-synth/prom216.ts +19 -0
  137. package/src/lint/post-synth/prom217.ts +18 -0
  138. package/src/lint/post-synth/prom218.ts +19 -0
  139. package/src/lint/post-synth/prom219.ts +18 -0
  140. package/src/lint/post-synth/prom220.ts +18 -0
  141. package/src/lint/post-synth/prom221.ts +19 -0
  142. package/src/lint/post-synth/prom222.ts +18 -0
  143. package/src/lint/post-synth/prom223.ts +19 -0
  144. package/src/lint/post-synth/prom224.ts +18 -0
  145. package/src/lint/post-synth/prom301.test.ts +157 -0
  146. package/src/lint/post-synth/prom301.ts +23 -0
  147. package/src/lint/rules/literal-credential.ts +4 -0
  148. package/src/lint/rules/rules.test.ts +16 -0
  149. package/src/plugin.test.ts +23 -0
  150. package/src/plugin.ts +14 -3
  151. package/src/promql-analysis.ts +365 -0
  152. package/src/skills/chant-prometheus-alertmanager.md +3 -1
  153. package/src/testdata/integration-cases.ts +96 -0
  154. package/src/tools.test.ts +6 -0
  155. package/src/validate-config.ts +279 -33
  156. package/src/validate-integrations.ts +389 -0
  157. 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
- test.each(postSynthChecks.map((c) => [c.id, c] as const))("%s finds nothing", (_id, check) => {
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-PROM209) across every `alertmanager.yml` in the output. */
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 webhook timeout take durations such as 30s or 4h.
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
- * A webhook needs url or url_file, Slack an api_url (or the global one), PagerDuty a routing_key or service_key, and email a to address, a smarthost and a from address (or their global defaults).
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
+ };