@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
@@ -7,7 +7,7 @@
7
7
  * `PrometheusRule`'s groups, or on a file parsed by some other tool.
8
8
  */
9
9
 
10
- import { isValidDuration } from "./duration";
10
+ import { durationMs, formatDuration, isValidDuration } from "./duration";
11
11
  import { matcherMatches, parseMatchers, type Matcher } from "./matchers";
12
12
  import {
13
13
  isAlertingRuleConfig,
@@ -20,6 +20,20 @@ import {
20
20
  type RuleGroupConfig,
21
21
  } from "./model";
22
22
  import { checkPromql } from "./promql";
23
+ import {
24
+ hasCondition,
25
+ histogramProblems,
26
+ isMultiWindow,
27
+ keepsLabel,
28
+ nonCounterRates,
29
+ parsePromql,
30
+ readsOnlyOverTime,
31
+ regexProblems,
32
+ resultLabels,
33
+ selectors,
34
+ } from "./promql-analysis";
35
+ import { validateGlobalSettings, validateReceiverIntegrations } from "./validate-integrations";
36
+ import { validateGlobalSecurity, validateReceiverSecurity } from "./validate-security";
23
37
 
24
38
  export type PrometheusIssueCode =
25
39
  | "PROM101"
@@ -37,7 +51,22 @@ export type PrometheusIssueCode =
37
51
  | "PROM206"
38
52
  | "PROM207"
39
53
  | "PROM208"
40
- | "PROM209";
54
+ | "PROM209"
55
+ | "PROM210"
56
+ | "PROM211"
57
+ | "PROM212"
58
+ | "PROM213"
59
+ | "PROM214"
60
+ | "PROM215"
61
+ | "PROM216"
62
+ | "PROM217"
63
+ | "PROM218"
64
+ | "PROM219"
65
+ | "PROM220"
66
+ | "PROM221"
67
+ | "PROM222"
68
+ | "PROM223"
69
+ | "PROM224";
41
70
 
42
71
  export interface PrometheusIssue {
43
72
  code: PrometheusIssueCode;
@@ -64,7 +93,124 @@ function alertLabels(group: RuleGroupConfig, rule: AlertingRuleConfig): LabelSet
64
93
  return { ...(group.labels ?? {}), ...(rule.labels ?? {}) };
65
94
  }
66
95
 
67
- /** Check a rule file (PROM101-PROM107). */
96
+ /** The `$labels.x`, `.Labels.x` and `index $labels "x"` references in a template. */
97
+ function templateLabels(template: string): string[] {
98
+ const out = new Set<string>();
99
+ for (const re of [/\$labels\.([A-Za-z_][A-Za-z0-9_]*)/g, /\.Labels\.([A-Za-z_][A-Za-z0-9_]*)/g, /index\s+\$labels\s+"([^"]+)"/g]) {
100
+ for (const m of template.matchAll(re)) out.add(m[1]);
101
+ }
102
+ return [...out];
103
+ }
104
+
105
+ /** `level:metric:operations`: three or more non-empty parts separated by colons. */
106
+ const RECORD_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*(?::[a-zA-Z0-9_]+){2,}$/;
107
+
108
+ /** PROM215, PROM216 and PROM218: what any rule's expression can get wrong. */
109
+ function exprIssues(expr: string, subject: string): PrometheusIssue[] {
110
+ const issues: PrometheusIssue[] = [];
111
+ for (const { fn, name } of nonCounterRates(expr)) {
112
+ issues.push({
113
+ code: "PROM215",
114
+ severity: "warning",
115
+ subject,
116
+ message: `rule ${subject} takes ${fn}() of ${name}, whose name does not end in _total, _count, _sum or _bucket; ${fn}() reads only counters, so use delta() or deriv() for a gauge`,
117
+ });
118
+ }
119
+ for (const { call, problem } of histogramProblems(expr)) {
120
+ issues.push({
121
+ code: "PROM216",
122
+ severity: "warning",
123
+ subject,
124
+ message:
125
+ problem === "no-bucket"
126
+ ? `rule ${subject} ${call} reads a series without _bucket in its name; a classic histogram's quantile needs its _bucket series`
127
+ : `rule ${subject} ${call} aggregates away the le label; keep le in by (...), or out of without (...)`,
128
+ });
129
+ }
130
+ for (const { matcher, problem } of regexProblems(expr)) {
131
+ issues.push({
132
+ code: "PROM218",
133
+ severity: "warning",
134
+ subject,
135
+ message:
136
+ problem === "literal"
137
+ ? `rule ${subject} matcher ${matcher} has no regex metacharacters; use = or != instead`
138
+ : `rule ${subject} matcher ${matcher} is anchored; Prometheus anchors every regex, so drop the ^ and $`,
139
+ });
140
+ }
141
+ return issues;
142
+ }
143
+
144
+ /** PROM211, PROM213, PROM214 and PROM219: alerting rules. */
145
+ function alertIssues(group: RuleGroupConfig, rule: AlertingRuleConfig, subject: string): PrometheusIssue[] {
146
+ const issues: PrometheusIssue[] = [];
147
+ const expr = typeof rule.expr === "string" ? rule.expr : "";
148
+ // An expression that reads no series (`vector(1)`) is a deliberate always-firing
149
+ // alert, a dead man's switch; PROM211 and PROM213 leave it alone.
150
+ const heartbeat = parsePromql(expr) !== undefined && selectors(expr).length === 0;
151
+
152
+ // PROM211: no for, or for: 0s. An expression that already spans a window
153
+ // (every selector read through *_over_time, or the multi-window `and` of two
154
+ // conditions, where the short window does what for would) is left alone. A
155
+ // for that is not a duration is PROM103's.
156
+ const forMs = rule.for === undefined ? 0 : durationMs(str(rule.for));
157
+ if (forMs === 0 && !heartbeat && !readsOnlyOverTime(expr) && !isMultiWindow(expr)) {
158
+ issues.push({
159
+ code: "PROM211",
160
+ severity: "warning",
161
+ subject,
162
+ message: `alert ${subject} has ${rule.for === undefined ? "no for" : `for: ${str(rule.for)}`}, so one evaluation that matches fires it; set for to how long the condition must hold`,
163
+ });
164
+ }
165
+
166
+ // PROM213: no condition
167
+ if (!heartbeat && hasCondition(expr) === false) {
168
+ issues.push({
169
+ code: "PROM213",
170
+ severity: "warning",
171
+ subject,
172
+ message: `alert ${subject} expression has no comparison, so it fires for every series it returns; add a condition such as > 0`,
173
+ });
174
+ }
175
+
176
+ // PROM214: a template reads a label the expression aggregates away. A label
177
+ // the rule or its group sets is left alone.
178
+ const scope = resultLabels(expr);
179
+ if (scope) {
180
+ const fixed = alertLabels(group, rule);
181
+ const reported = new Set<string>();
182
+ for (const [key, value] of [...Object.entries(rule.annotations ?? {}), ...Object.entries(rule.labels ?? {})]) {
183
+ for (const label of templateLabels(str(value))) {
184
+ if (keepsLabel(scope, label) || label in fixed || reported.has(label)) continue;
185
+ reported.add(label);
186
+ issues.push({
187
+ code: "PROM214",
188
+ severity: "warning",
189
+ subject,
190
+ message: `alert ${subject} ${key} reads $labels.${label}, which the expression's by or without drops, so it renders empty`,
191
+ });
192
+ }
193
+ }
194
+ }
195
+
196
+ // PROM219: alertname set by hand
197
+ for (const [where, labels] of [
198
+ ["labels", rule.labels],
199
+ ["group labels", group.labels],
200
+ ] as const) {
201
+ if (labels && Object.prototype.hasOwnProperty.call(labels, "alertname")) {
202
+ issues.push({
203
+ code: "PROM219",
204
+ severity: "warning",
205
+ subject,
206
+ message: `alert ${subject} ${where} set alertname; Prometheus sets alertname to the rule's name and overwrites it`,
207
+ });
208
+ }
209
+ }
210
+ return issues;
211
+ }
212
+
213
+ /** Check a rule file (PROM101-PROM107, PROM211 and PROM213-PROM219). PROM212 is opt-in; see {@link validateRunbookUrls}. */
68
214
  export function validateRuleFile(file: RuleFileConfig): PrometheusIssue[] {
69
215
  const issues: PrometheusIssue[] = [];
70
216
  const groups = Array.isArray(file?.groups) ? file.groups : [];
@@ -179,7 +325,37 @@ export function validateRuleFile(file: RuleFileConfig): PrometheusIssue[] {
179
325
  message: `alert ${subject} has no summary or description annotation; a notification carries only its labels`,
180
326
  });
181
327
  }
328
+ issues.push(...alertIssues(group, rule as AlertingRuleConfig, subject));
329
+ }
330
+
331
+ // PROM217: recording rule name
332
+ if (recording && !alerting && rule.record.trim() !== "" && !RECORD_NAME.test(rule.record)) {
333
+ issues.push({
334
+ code: "PROM217",
335
+ severity: "warning",
336
+ subject,
337
+ message: `recording rule ${subject} is not named level:metric:operations (e.g. job:http_requests:rate5m)`,
338
+ });
182
339
  }
340
+
341
+ if (checked.ok) issues.push(...exprIssues(expr as string, subject));
342
+ });
343
+ }
344
+ return issues;
345
+ }
346
+
347
+ /**
348
+ * PROM212: alerts without a `runbook_url` annotation. Not part of
349
+ * {@link validateRuleFile}, because the check is opt-in: the lint preset
350
+ * `all`, or a `lint.rules` entry for PROM212, turns it on.
351
+ */
352
+ export function validateRunbookUrls(file: RuleFileConfig): PrometheusIssue[] {
353
+ const issues: PrometheusIssue[] = [];
354
+ for (const group of Array.isArray(file?.groups) ? file.groups : []) {
355
+ (Array.isArray(group?.rules) ? group.rules : []).forEach((rule, i) => {
356
+ if (!isAlertingRuleConfig(rule) || rule.annotations?.runbook_url) return;
357
+ const subject = ruleLabel(group, i, rule);
358
+ issues.push({ code: "PROM212", severity: "warning", subject, message: `alert ${subject} has no runbook_url annotation` });
183
359
  });
184
360
  }
185
361
  return issues;
@@ -221,7 +397,83 @@ function visitRoutes(root: RouteConfig | undefined): RouteVisit[] {
221
397
 
222
398
  const AM_DURATIONS = ["group_wait", "group_interval", "repeat_interval"] as const;
223
399
 
224
- /** Check an `alertmanager.yml` on its own (PROM201, PROM203-PROM209). */
400
+ /** Alertmanager's defaults for a root route that sets neither (config/config.go). */
401
+ const DEFAULT_GROUP_INTERVAL_MS = 5 * 60_000;
402
+ const DEFAULT_REPEAT_INTERVAL_MS = 4 * 3_600_000;
403
+
404
+ /**
405
+ * PROM223: routes whose repeat_interval, as set or inherited, is shorter
406
+ * than their group_interval. Alertmanager only resends when a group is
407
+ * flushed, every group_interval, so the shorter repeat_interval never takes
408
+ * effect. Reported on the route that sets one of the two.
409
+ */
410
+ function repeatIntervalIssues(root: RouteConfig | undefined): PrometheusIssue[] {
411
+ const issues: PrometheusIssue[] = [];
412
+ const walk = (route: RouteConfig, path: string, groupMs: number, repeatMs: number) => {
413
+ const ownGroup = route.group_interval !== undefined ? durationMs(str(route.group_interval)) : groupMs;
414
+ const ownRepeat = route.repeat_interval !== undefined ? durationMs(str(route.repeat_interval)) : repeatMs;
415
+ if (ownGroup === undefined || ownRepeat === undefined) return; // PROM208
416
+ if ((route.group_interval !== undefined || route.repeat_interval !== undefined) && ownRepeat < ownGroup) {
417
+ issues.push({
418
+ code: "PROM223",
419
+ severity: "warning",
420
+ subject: path,
421
+ message: `${path} repeat_interval (${formatDuration(ownRepeat)}) is shorter than its group_interval (${formatDuration(ownGroup)}); notifications repeat no faster than group_interval`,
422
+ });
423
+ }
424
+ (Array.isArray(route.routes) ? route.routes : []).forEach((child, i) => walk(child, `${path}.routes[${i}]`, ownGroup, ownRepeat));
425
+ };
426
+ if (root && typeof root === "object") walk(root, "route", DEFAULT_GROUP_INTERVAL_MS, DEFAULT_REPEAT_INTERVAL_MS);
427
+ return issues;
428
+ }
429
+
430
+ function parsedList(entries: unknown): Matcher[] | undefined {
431
+ const out: Matcher[] = [];
432
+ for (const m of Array.isArray(entries) ? entries : []) {
433
+ const parsed = parseMatchers(str(m));
434
+ if (!parsed.ok) return undefined; // PROM206
435
+ out.push(...parsed.matchers);
436
+ }
437
+ return out;
438
+ }
439
+
440
+ /** A regex's alternatives when every one is a plain string, e.g. `a|b` gives `["a", "b"]`. */
441
+ function literalAlternatives(regex: string): string[] | undefined {
442
+ const parts = regex.split("|");
443
+ return parts.every((p) => !/[.+*?()[\]{}\\^$]/.test(p)) ? parts : undefined;
444
+ }
445
+
446
+ /**
447
+ * Whether some label set matches every matcher. A label whose matchers
448
+ * name values (`=`, or a regex of plain alternatives) is tried with those
449
+ * values; one with only negative or open-ended matchers is taken to have a
450
+ * value that passes.
451
+ */
452
+ function satisfiable(matchers: Matcher[]): boolean {
453
+ const byLabel = new Map<string, Matcher[]>();
454
+ for (const m of matchers) byLabel.set(m.name, [...(byLabel.get(m.name) ?? []), m]);
455
+ for (const [name, ms] of byLabel) {
456
+ const candidates = new Set<string>();
457
+ let bounded = false;
458
+ for (const m of ms) {
459
+ if (m.op === "=") {
460
+ candidates.add(m.value);
461
+ bounded = true;
462
+ } else if (m.op === "=~") {
463
+ const alts = literalAlternatives(m.value);
464
+ if (alts) {
465
+ alts.forEach((a) => candidates.add(a));
466
+ bounded = true;
467
+ }
468
+ }
469
+ }
470
+ if (!bounded) continue;
471
+ if (![...candidates].some((v) => ms.every((m) => matcherMatches(m, { [name]: v })))) return false;
472
+ }
473
+ return true;
474
+ }
475
+
476
+ /** Check an `alertmanager.yml` on its own (PROM201, PROM203-PROM210, PROM220-PROM224). */
225
477
  export function validateAlertmanagerConfig(config: AlertmanagerConfig): PrometheusIssue[] {
226
478
  const issues: PrometheusIssue[] = [];
227
479
  const receivers = Array.isArray(config?.receivers) ? config.receivers : [];
@@ -307,41 +559,35 @@ export function validateAlertmanagerConfig(config: AlertmanagerConfig): Promethe
307
559
  }
308
560
  });
309
561
 
310
- if (global.resolve_timeout !== undefined && !isValidDuration(global.resolve_timeout)) {
311
- issues.push({ code: "PROM208", severity: "error", subject: "global", message: `global.resolve_timeout "${str(global.resolve_timeout)}" is not a duration` });
312
- }
562
+ // PROM223: repeat_interval under group_interval
563
+ issues.push(...repeatIntervalIssues(root));
564
+
565
+ // PROM224: an inhibit rule an alert can be both source and target of, with no equal
566
+ (Array.isArray(config?.inhibit_rules) ? config.inhibit_rules : []).forEach((rule, i) => {
567
+ if (Array.isArray(rule?.equal) && rule.equal.length > 0) return;
568
+ const source = parsedList(rule?.source_matchers);
569
+ const target = parsedList(rule?.target_matchers);
570
+ if (!source || !target || !satisfiable([...source, ...target])) return;
571
+ issues.push({
572
+ code: "PROM224",
573
+ severity: "warning",
574
+ subject: `inhibit_rules[${i}]`,
575
+ message: `inhibit_rules[${i}]: one alert can match both source_matchers and target_matchers, and there is no equal, so any such alert firing mutes every other; list the labels the two must share under equal`,
576
+ });
577
+ });
578
+
579
+ // PROM208, PROM210: global settings; PROM220: global TLS
580
+ issues.push(...validateGlobalSettings(global));
581
+ issues.push(...validateGlobalSecurity(global));
313
582
 
314
- // PROM207: unused receivers; PROM209: incomplete integrations
583
+ // PROM207: unused receivers; PROM208-PROM210: integrations (./validate-integrations.ts)
315
584
  for (const r of receivers) {
316
585
  const name = str(r?.name ?? "");
317
586
  if (!usedReceivers.has(name)) {
318
587
  issues.push({ code: "PROM207", severity: "warning", subject: name, message: `receiver "${name}" is declared but no route sends to it` });
319
588
  }
320
- for (const [i, w] of (r.webhook_configs ?? []).entries()) {
321
- if (!w.url && !w.url_file) issues.push({ code: "PROM209", severity: "error", subject: name, message: `receiver "${name}" webhook_configs[${i}] has no url or url_file` });
322
- if (w.timeout !== undefined && !isValidDuration(w.timeout)) {
323
- issues.push({ code: "PROM208", severity: "error", subject: name, message: `receiver "${name}" webhook_configs[${i}].timeout "${str(w.timeout)}" is not a duration` });
324
- }
325
- }
326
- for (const [i, s] of (r.slack_configs ?? []).entries()) {
327
- if (!s.api_url && !s.api_url_file && !global.slack_api_url && !global.slack_api_url_file) {
328
- issues.push({ code: "PROM209", severity: "error", subject: name, message: `receiver "${name}" slack_configs[${i}] has no api_url or api_url_file, and global sets no slack_api_url` });
329
- }
330
- }
331
- for (const [i, p] of (r.pagerduty_configs ?? []).entries()) {
332
- if (!p.routing_key && !p.routing_key_file && !p.service_key && !p.service_key_file) {
333
- issues.push({ code: "PROM209", severity: "error", subject: name, message: `receiver "${name}" pagerduty_configs[${i}] has no routing_key(_file) or service_key(_file)` });
334
- }
335
- }
336
- for (const [i, e] of (r.email_configs ?? []).entries()) {
337
- if (!e.to) issues.push({ code: "PROM209", severity: "error", subject: name, message: `receiver "${name}" email_configs[${i}] has no to address` });
338
- if (!e.smarthost && !global.smtp_smarthost) {
339
- issues.push({ code: "PROM209", severity: "error", subject: name, message: `receiver "${name}" email_configs[${i}] has no smarthost, and global sets no smtp_smarthost` });
340
- }
341
- if (!e.from && !global.smtp_from) {
342
- issues.push({ code: "PROM209", severity: "error", subject: name, message: `receiver "${name}" email_configs[${i}] has no from address, and global sets no smtp_from` });
343
- }
344
- }
589
+ issues.push(...validateReceiverIntegrations(r, global));
590
+ issues.push(...validateReceiverSecurity(r, global));
345
591
  // A receiver with no integrations at all is valid: it is how Alertmanager drops alerts.
346
592
  }
347
593