@intentius/chant-lexicon-prometheus 0.124.31 → 0.124.32

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 (114) hide show
  1. package/dist/alertmanager.js +61 -0
  2. package/dist/api/alertmanager.js +98 -0
  3. package/dist/api/apply.js +306 -0
  4. package/dist/api/bind.js +66 -0
  5. package/dist/api/client.js +110 -0
  6. package/dist/api/evaluated.js +86 -0
  7. package/dist/api/fake-servers.js +165 -0
  8. package/dist/api/index.js +17 -0
  9. package/dist/api/ruler.js +122 -0
  10. package/dist/build.js +240 -0
  11. package/dist/catalog.js +66 -0
  12. package/dist/codegen/docs-cli.js +3 -0
  13. package/dist/codegen/docs.js +105 -0
  14. package/dist/codegen/generate-cli.js +7 -0
  15. package/dist/codegen/generate.js +42 -0
  16. package/dist/codegen/package.js +28 -0
  17. package/dist/collector-metrics.js +225 -0
  18. package/dist/composites/alert-routing.js +210 -0
  19. package/dist/composites/catalog.js +344 -0
  20. package/dist/composites/genai.js +454 -0
  21. package/dist/composites/index.js +8 -0
  22. package/dist/composites/red-alerts.js +135 -0
  23. package/dist/composites/slo.js +353 -0
  24. package/dist/composites/watchdog.js +110 -0
  25. package/dist/config-model.js +52 -0
  26. package/dist/config.js +225 -0
  27. package/dist/describe-resources.js +299 -0
  28. package/dist/detect.js +12 -0
  29. package/dist/duration.js +54 -0
  30. package/dist/export-resources.js +163 -0
  31. package/dist/import/alertmanager-live.js +309 -0
  32. package/dist/import/embedded.js +100 -0
  33. package/dist/import/generator.js +683 -0
  34. package/dist/import/parser.js +428 -0
  35. package/dist/import/slo.js +207 -0
  36. package/dist/index.js +33 -0
  37. package/dist/init-templates.js +119 -0
  38. package/dist/integrations.js +33 -0
  39. package/dist/integrity.json +2 -2
  40. package/dist/lint/audit-catalog.js +60 -0
  41. package/dist/lint/audit-lineage.js +42 -0
  42. package/dist/lint/post-synth/index.js +68 -0
  43. package/dist/lint/post-synth/prom-helpers.js +157 -0
  44. package/dist/lint/post-synth/prom101.js +13 -0
  45. package/dist/lint/post-synth/prom102.js +13 -0
  46. package/dist/lint/post-synth/prom103.js +13 -0
  47. package/dist/lint/post-synth/prom104.js +13 -0
  48. package/dist/lint/post-synth/prom105.js +13 -0
  49. package/dist/lint/post-synth/prom106.js +13 -0
  50. package/dist/lint/post-synth/prom107.js +13 -0
  51. package/dist/lint/post-synth/prom201.js +13 -0
  52. package/dist/lint/post-synth/prom202.js +13 -0
  53. package/dist/lint/post-synth/prom203.js +13 -0
  54. package/dist/lint/post-synth/prom204.js +13 -0
  55. package/dist/lint/post-synth/prom205.js +13 -0
  56. package/dist/lint/post-synth/prom206.js +13 -0
  57. package/dist/lint/post-synth/prom207.js +13 -0
  58. package/dist/lint/post-synth/prom208.js +13 -0
  59. package/dist/lint/post-synth/prom209.js +13 -0
  60. package/dist/lint/post-synth/prom210.js +13 -0
  61. package/dist/lint/post-synth/prom211.js +17 -0
  62. package/dist/lint/post-synth/prom212.js +14 -0
  63. package/dist/lint/post-synth/prom213.js +16 -0
  64. package/dist/lint/post-synth/prom214.js +14 -0
  65. package/dist/lint/post-synth/prom215.js +14 -0
  66. package/dist/lint/post-synth/prom216.js +15 -0
  67. package/dist/lint/post-synth/prom217.js +14 -0
  68. package/dist/lint/post-synth/prom218.js +15 -0
  69. package/dist/lint/post-synth/prom219.js +14 -0
  70. package/dist/lint/post-synth/prom220.js +14 -0
  71. package/dist/lint/post-synth/prom221.js +15 -0
  72. package/dist/lint/post-synth/prom222.js +14 -0
  73. package/dist/lint/post-synth/prom223.js +15 -0
  74. package/dist/lint/post-synth/prom224.js +14 -0
  75. package/dist/lint/post-synth/prom225.js +15 -0
  76. package/dist/lint/post-synth/prom301.js +19 -0
  77. package/dist/lint/rules/index.js +8 -0
  78. package/dist/lint/rules/literal-credential.js +127 -0
  79. package/dist/lint/rules/prom-ast.js +65 -0
  80. package/dist/lint/rules/promql-literal.js +65 -0
  81. package/dist/lint/rules/slo-literal.js +79 -0
  82. package/dist/lsp/completions.js +11 -0
  83. package/dist/lsp/hover.js +21 -0
  84. package/dist/manifest.json +1 -1
  85. package/dist/matchers.js +131 -0
  86. package/dist/model.js +108 -0
  87. package/dist/op/activities/index.js +20 -0
  88. package/dist/op/activities/promtool.js +87 -0
  89. package/dist/op/activities/rule-audit.js +128 -0
  90. package/dist/op/activities/rules-loaded.js +73 -0
  91. package/dist/op/activities/silence.js +123 -0
  92. package/dist/op/activity-contracts.js +51 -0
  93. package/dist/op/audit-op.js +28 -0
  94. package/dist/op/builders.js +57 -0
  95. package/dist/op/slo-rule-tests.js +148 -0
  96. package/dist/package-cli.js +14 -0
  97. package/dist/pin.js +13 -0
  98. package/dist/plugin.js +143 -0
  99. package/dist/prometheus-config.js +38 -0
  100. package/dist/promql-analysis.js +343 -0
  101. package/dist/promql.js +44 -0
  102. package/dist/rule-eval.js +466 -0
  103. package/dist/rules.js +97 -0
  104. package/dist/serializer.js +59 -0
  105. package/dist/skill-defs.js +41 -0
  106. package/dist/tools.js +85 -0
  107. package/dist/validate-cli.js +7 -0
  108. package/dist/validate-config.js +556 -0
  109. package/dist/validate-integrations.js +388 -0
  110. package/dist/validate-relabel.js +52 -0
  111. package/dist/validate-security.js +135 -0
  112. package/dist/validate.js +69 -0
  113. package/package.json +10 -10
  114. package/src/codegen/package.ts +2 -2
@@ -0,0 +1,119 @@
1
+ const RULES = `import { RuleGroup, type Rule } from "@intentius/chant-lexicon-prometheus";
2
+
3
+ const rules: Rule[] = [
4
+ {
5
+ record: "job:http_errors:ratio5m",
6
+ expr: 'sum by (job) (rate(http_requests_total{code=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m]))',
7
+ },
8
+ {
9
+ alert: "HttpErrorRatioHigh",
10
+ expr: "job:http_errors:ratio5m > 0.05",
11
+ for: "10m",
12
+ labels: { severity: "page" },
13
+ annotations: { summary: "{{ $labels.job }} is failing over 5% of requests" },
14
+ },
15
+ ];
16
+
17
+ const api = new RuleGroup({ name: "api", interval: "30s", rules });
18
+
19
+ export { api };
20
+ `;
21
+ const ALERTMANAGER = `import { Receiver, Route, type RouteProps, type WebhookConfig } from "@intentius/chant-lexicon-prometheus";
22
+
23
+ const hook: WebhookConfig[] = [{ url: "http://alert-sink:8080/" }];
24
+ const oncall = new Receiver({ name: "oncall", webhook_configs: hook });
25
+ const fallback = new Receiver({ name: "default" });
26
+
27
+ const children: RouteProps[] = [{ matchers: ['severity="page"'], receiver: oncall }];
28
+ const byAlert = ["alertname", "job"];
29
+ const root = new Route({ receiver: fallback, group_by: byAlert, routes: children });
30
+
31
+ export { oncall, fallback, root };
32
+ `;
33
+ const SLO_STYLE = `import { RuleGroup, type Rule } from "@intentius/chant-lexicon-prometheus";
34
+
35
+ // Error ratio over two windows, and an alert that needs both to agree.
36
+ const windows = ["5m", "1h"];
37
+
38
+ const recording: Rule[] = windows.map((w) => ({
39
+ record: \`job:http_errors:ratio\${w}\`,
40
+ expr: \`sum by (job) (rate(http_requests_total{code=~"5.."}[\${w}])) / sum by (job) (rate(http_requests_total[\${w}]))\`,
41
+ }));
42
+
43
+ const alerting: Rule[] = [
44
+ {
45
+ alert: "ErrorBudgetBurn",
46
+ expr: "job:http_errors:ratio1h > (14.4 * 0.001) and job:http_errors:ratio5m > (14.4 * 0.001)",
47
+ labels: { severity: "page" },
48
+ annotations: { summary: "{{ $labels.job }} is burning its error budget 14.4x too fast" },
49
+ },
50
+ ];
51
+
52
+ const errorRatios = new RuleGroup({ name: "error-ratios", rules: recording });
53
+ const burnAlerts = new RuleGroup({ name: "burn-alerts", rules: alerting });
54
+
55
+ export { errorRatios, burnAlerts };
56
+ `;
57
+ // ── slo ────────────────────────────────────────────────────────────────
58
+ const SLO_DECLARATION = `/**
59
+ * The SLO: 99.9% of checkout requests answer without a 5xx over 30 days.
60
+ * \`Slo\` builds one rule group: the error ratio over every window its alerts
61
+ * read, the error budget left, and burn-rate alerts that page on a fast burn
62
+ * (severity "page") and open a ticket on a slow one (severity "ticket").
63
+ * \`sloMetrics(checkout)\` returns the recorded series names, for a dashboard
64
+ * or another rule to read.
65
+ */
66
+ import { Slo } from "@intentius/chant-lexicon-prometheus";
67
+
68
+ const sli = {
69
+ errors: 'sum(rate(http_requests_total{job="checkout",code=~"5.."}[{{window}}]))',
70
+ total: 'sum(rate(http_requests_total{job="checkout"}[{{window}}]))',
71
+ };
72
+ const team = { team: "payments" };
73
+
74
+ const checkout = Slo({
75
+ name: "checkout",
76
+ objective: 0.999,
77
+ window: "30d",
78
+ description: "Checkout requests answer without a 5xx.",
79
+ sli,
80
+ labels: team,
81
+ });
82
+
83
+ export { checkout };
84
+ `;
85
+ const SLO_ALERTMANAGER = `/**
86
+ * Routing for the SLO's alerts, in the same build root so PROM202 checks that
87
+ * both severities they carry have a route.
88
+ */
89
+ import { InhibitRule, Receiver, Route, type RouteProps, type WebhookConfig } from "@intentius/chant-lexicon-prometheus";
90
+
91
+ const pager: WebhookConfig[] = [{ url: "http://pager-bridge:8080/alerts" }];
92
+ const oncall = new Receiver({ name: "oncall", webhook_configs: pager });
93
+
94
+ const ticketing: WebhookConfig[] = [{ url: "http://ticket-bridge:8080/alerts" }];
95
+ const tickets = new Receiver({ name: "tickets", webhook_configs: ticketing });
96
+
97
+ const fallback = new Receiver({ name: "default" });
98
+
99
+ const bySlo = ["alertname", "slo"];
100
+ const children: RouteProps[] = [
101
+ { matchers: ['severity="page"'], receiver: oncall },
102
+ { matchers: ['severity="ticket"'], receiver: tickets },
103
+ ];
104
+ const root = new Route({ receiver: fallback, group_by: bySlo, routes: children });
105
+
106
+ // A page for an SLO mutes its ticket: the fast burn already has someone on it.
107
+ const pageSource = ['severity="page"'];
108
+ const ticketTarget = ['severity="ticket"'];
109
+ const sameSlo = ["slo"];
110
+ const pageMutesTicket = new InhibitRule({ source_matchers: pageSource, target_matchers: ticketTarget, equal: sameSlo });
111
+
112
+ export { oncall, tickets, fallback, root, pageMutesTicket };
113
+ `;
114
+ export const DEFAULT_TEMPLATE = { src: { "rules.ts": RULES, "alertmanager.ts": ALERTMANAGER } };
115
+ export const RULES_TEMPLATE = { src: { "rules.ts": RULES } };
116
+ export const SLO_STYLE_TEMPLATE = { src: { "rules.ts": SLO_STYLE } };
117
+ export const SLO_TEMPLATE = { src: { "slo.ts": SLO_DECLARATION, "alertmanager.ts": SLO_ALERTMANAGER } };
118
+ /** The template names `chant init --lexicon prometheus --template <name>` takes, besides the default. */
119
+ export const TEMPLATE_NAMES = ["rules", "slo-style", "slo"];
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Alertmanager's receiver integrations and the HTTP client settings they
3
+ * share, typed against the Go structs of the pinned release (see ./pin.ts):
4
+ * `alertmanager/config` (`notifiers.go`), `alertmanager/notify/<name>`
5
+ * (`config.go`), `prometheus/common/config` (`http_config.go`,
6
+ * `headers.go`) and `prometheus/sigv4`.
7
+ *
8
+ * Field names are the YAML keys. Durations are strings (`30s`), secrets and
9
+ * URLs are strings, and every credential with a `*_file` sibling is best
10
+ * set through that sibling: Alertmanager does not expand environment
11
+ * variables in its config, and PROM001 flags a literal.
12
+ */
13
+ /** Every receiver integration: its key under a receiver, and the name of the type of one entry. */
14
+ export const RECEIVER_INTEGRATION_TYPES = Object.freeze({
15
+ discord_configs: "DiscordConfig",
16
+ email_configs: "EmailConfig",
17
+ incidentio_configs: "IncidentioConfig",
18
+ pagerduty_configs: "PagerDutyConfig",
19
+ slack_configs: "SlackConfig",
20
+ webhook_configs: "WebhookConfig",
21
+ opsgenie_configs: "OpsGenieConfig",
22
+ wechat_configs: "WechatConfig",
23
+ pushover_configs: "PushoverConfig",
24
+ victorops_configs: "VictorOpsConfig",
25
+ sns_configs: "SNSConfig",
26
+ telegram_configs: "TelegramConfig",
27
+ webex_configs: "WebexConfig",
28
+ msteams_configs: "MSTeamsConfig",
29
+ msteamsv2_configs: "MSTeamsV2Config",
30
+ jira_configs: "JiraConfig",
31
+ rocketchat_configs: "RocketchatConfig",
32
+ mattermost_configs: "MattermostConfig",
33
+ });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "eaf8af81d5e6bd5b3f3c1d1b4126b1ea9353afdcd0484884b694c03bcf93d89d",
4
+ "manifest.json": "d1b1198c7e2f8c8ca3bbe32b633db8bd0b2bb4639a15cab401cee30e69840144",
5
5
  "meta.json": "a06b8059e2de2ca6c8131bfbbfe7e143e818f2d2d497d3f45a9bc5c63af6aeeb",
6
6
  "types/index.d.ts": "150d72287453a24a40cdeffb8cf3e9dc6e77ab58b342b48d01902dc58fb5f85d",
7
7
  "rules/literal-credential.ts": "df0dc02b93f2da87d9b03ceca906206b40b3b7600b24cb733f6388d81f92aecb",
@@ -46,5 +46,5 @@
46
46
  "skills/chant-prometheus-alertmanager.md": "4c4a22bea6147653d09d7d3f55f81c4927963d4e2f50b2e430fbb9023e99570b",
47
47
  "skills/chant-prometheus-kubernetes.md": "cca016857891f4cabf1feb6f9db83c220a2e6ece200cd89e635851079f8070a8"
48
48
  },
49
- "composite": "f01924c8cc592c72321c90c55a4724bddeb025de1f819f64be70e3d9b5a17c1b"
49
+ "composite": "6c19a6c597d0d2ca495664e8f9e04858b445f7f186546df2aeec314a6ca734ec"
50
50
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The prometheus lexicon's chant audit catalog, contributed via
3
+ * `prometheusPlugin.auditCatalog()` (#687, #1346).
4
+ *
5
+ * Every post-synth check reads the emitted rule file or `alertmanager.yml`
6
+ * (or any output document shaped like one), so they are all `yamlBased` and
7
+ * fire on an audit of standalone files too. The three source-level lint rules
8
+ * read TypeScript, so they are constructed with `yamlBased: false`; they are
9
+ * listed for a reader who meets them in a lint report.
10
+ */
11
+ import { applyLineage, auditRule } from "@intentius/chant/audit/catalog";
12
+ import { prometheusAuditLineage } from "./audit-lineage.js";
13
+ function sourceRule(id, category, title, remediation) {
14
+ return { id, tier: "merge-worthy", fixKind: "guidance", category, title, remediation, yamlBased: false };
15
+ }
16
+ function outputRule(id, tier, category, title, remediation) {
17
+ return auditRule(id, tier, "guidance", title, remediation, { category });
18
+ }
19
+ export const prometheusAuditCatalog = {
20
+ PROM001: sourceRule("PROM001", "security", "Alertmanager credential declared as a literal", "Mount the secret as a file and set the field's *_file sibling (api_url_file, routing_key_file, auth_password_file, credentials_file) instead."),
21
+ PROM002: sourceRule("PROM002", "correctness", "Literal PromQL expression in a RuleGroup does not parse", "Fix the expression at the offset the message names."),
22
+ PROM003: sourceRule("PROM003", "correctness", "Slo objective, window or SLI expression is invalid", "Set objective strictly between 0 and 1, window to a Prometheus duration such as 28d, and write each SLI expression as PromQL with {{window}} where the range goes."),
23
+ PROM101: outputRule("PROM101", "merge-worthy", "correctness", "Rule group name repeated in a rule file", "Give every group in the rule file its own name."),
24
+ PROM102: outputRule("PROM102", "report-only", "correctness", "Two rules share a name and label set", "Rename one rule, or give the two different labels (for alerts, usually a different severity)."),
25
+ PROM103: outputRule("PROM103", "merge-worthy", "correctness", "Rule or group duration is not a Prometheus duration", "Write durations as <number><unit> terms in descending order, e.g. 30s, 5m, 1h30m."),
26
+ PROM104: outputRule("PROM104", "merge-worthy", "correctness", "Rule expression is not valid PromQL", "Fix the expression at the offset the message names."),
27
+ PROM105: outputRule("PROM105", "merge-worthy", "correctness", "Rule or group is malformed", "Set exactly one of record or alert with a non-empty name; keep for, keep_firing_for and annotations to alerting rules."),
28
+ PROM106: outputRule("PROM106", "report-only", "best-practice", "Alerting rule has no severity label", "Add a severity label (on the rule or the group) that an Alertmanager route matches."),
29
+ PROM107: outputRule("PROM107", "report-only", "best-practice", "Alerting rule has no summary or description", "Add a summary annotation saying what is wrong, and a description or runbook_url saying what to do."),
30
+ PROM201: outputRule("PROM201", "merge-worthy", "correctness", "Route sends to an undeclared receiver", "Declare the receiver, or reference the declared Receiver entity instead of a name string."),
31
+ PROM202: outputRule("PROM202", "merge-worthy", "correctness", "Alert severity not routed by any route", "Add a route matching the severity (e.g. severity=\"page\") below the root, or change the alert's severity to one that is routed."),
32
+ PROM203: outputRule("PROM203", "merge-worthy", "correctness", "Receiver or time interval name repeated", "Give every receiver and every time interval its own name."),
33
+ PROM204: outputRule("PROM204", "merge-worthy", "correctness", "Route names an undeclared time interval", "Declare the TimeInterval, or reference the declared entity instead of a name string."),
34
+ PROM205: outputRule("PROM205", "merge-worthy", "correctness", "Root route missing, without a receiver, or with matchers", "Declare exactly one root Route with a receiver and no matchers, and nest every other route under it."),
35
+ PROM206: outputRule("PROM206", "merge-worthy", "correctness", "Matcher does not parse", 'Write matchers as label, operator and quoted value, e.g. severity="page" or team=~"db|infra".'),
36
+ PROM207: outputRule("PROM207", "report-only", "best-practice", "Receiver declared but never routed to", "Route to the receiver, or remove it."),
37
+ PROM208: outputRule("PROM208", "merge-worthy", "correctness", "Alertmanager duration is not a duration", "Write durations as <number><unit> terms in descending order, e.g. 30s, 5m, 4h."),
38
+ PROM209: outputRule("PROM209", "merge-worthy", "correctness", "Receiver integration missing its destination or credential", "Set the integration's destination and credential (url, webhook_url, api_key, routing_key, chat_id, room_id, to/smarthost/from, ...), through its *_file or global equivalent where there is one."),
39
+ PROM210: outputRule("PROM210", "merge-worthy", "correctness", "Integration or global setting Alertmanager rejects", "Set one of each value and its *_file, and use a value Alertmanager allows (e.g. message_type text or markdown, parse_mode Markdown, MarkdownV2 or HTML)."),
40
+ PROM211: outputRule("PROM211", "report-only", "best-practice", "Alerting rule has no for, or for: 0s", "Set for to how long the condition must hold before the alert fires, e.g. 5m."),
41
+ PROM212: outputRule("PROM212", "report-only", "best-practice", "Alerting rule has no runbook_url annotation (opt-in)", "Add a runbook_url annotation linking to what the responder should do."),
42
+ PROM213: outputRule("PROM213", "merge-worthy", "correctness", "Alert expression has no comparison", "Add the condition the alert fires on, e.g. > 0.05, or use absent() for a missing series."),
43
+ PROM214: outputRule("PROM214", "merge-worthy", "correctness", "Alert template reads a label the expression aggregates away", "Keep the label in the aggregation's by (...), or stop reading it in the template."),
44
+ PROM215: outputRule("PROM215", "merge-worthy", "correctness", "rate, irate or increase over a name that is not a counter's", "Read a counter (a name ending in _total, _count, _sum or _bucket), or use delta() or deriv() for a gauge."),
45
+ PROM216: outputRule("PROM216", "merge-worthy", "correctness", "histogram_quantile over a series without _bucket, or without le", "Pass the histogram's _bucket series, and keep le in the aggregation: sum by (le, ...) (rate(x_bucket[5m]))."),
46
+ PROM217: outputRule("PROM217", "report-only", "best-practice", "Recording rule name is not level:metric:operations", "Name the recorded series level:metric:operations, e.g. job:http_requests:rate5m."),
47
+ PROM218: outputRule("PROM218", "report-only", "best-practice", "Regex matcher needs no regex, or is anchored", 'Use = or != for a plain value, and drop ^ and $ from a regex, which Prometheus anchors already (job=~"api|web").'),
48
+ PROM219: outputRule("PROM219", "merge-worthy", "correctness", "Alerting rule sets the alertname label", "Remove the alertname label; rename the rule instead."),
49
+ PROM220: outputRule("PROM220", "merge-worthy", "security", "Receiver turns off TLS certificate verification", "Remove insecure_skip_verify, and set ca_file to the CA that signed the endpoint's certificate."),
50
+ PROM221: outputRule("PROM221", "merge-worthy", "security", "SMTP credentials sent with require_tls false", "Set require_tls (or global.smtp_require_tls) to true, or use force_implicit_tls with an SMTPS port."),
51
+ PROM222: outputRule("PROM222", "merge-worthy", "security", "Credentials sent to an http:// receiver URL", "Point the integration at an https:// URL."),
52
+ PROM223: outputRule("PROM223", "merge-worthy", "correctness", "repeat_interval is shorter than group_interval", "Set repeat_interval to a multiple of group_interval, or lower group_interval."),
53
+ PROM224: outputRule("PROM224", "merge-worthy", "correctness", "Inhibit rule matches one alert as source and target, with no equal", "List the labels source and target must share under equal (e.g. alertname, cluster), or make the matchers exclusive."),
54
+ PROM225: outputRule("PROM225", "merge-worthy", "correctness", "labeldrop or labelkeep relabel step carries a field it does not take", "Remove source_labels, separator, target_label, modulus and replacement from the step; keep only regex (and action)."),
55
+ PROM301: outputRule("PROM301", "merge-worthy", "correctness", "Rule reads a connector metric no collector in the build emits", "Read the name spanMetricsNames(), serviceGraphNames() or genAiMetrics() gives for the declared connector, and group by its declared dimensions, or add the dimension to the connector."),
56
+ };
57
+ /** Post-synth checks the `recommended` lint preset leaves out (#3363); see `prometheusPlugin.lintPresets()`. */
58
+ export const OPT_IN_CHECKS = new Set(["PROM212"]);
59
+ // Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
60
+ applyLineage(prometheusAuditCatalog, prometheusAuditLineage);
@@ -0,0 +1,42 @@
1
+ const RULEFMT = "https://github.com/prometheus/prometheus/blob/main/model/rulefmt/rulefmt.go";
2
+ const AM_CONFIG = "https://github.com/prometheus/alertmanager/blob/main/config/config.go";
3
+ const PINT = "https://github.com/cloudflare/pint/blob/main/docs/checks";
4
+ const amtool = (relation) => ({
5
+ tool: "amtool",
6
+ url: AM_CONFIG,
7
+ relation,
8
+ });
9
+ export const prometheusAuditLineage = {
10
+ PROM002: [
11
+ { tool: "promtool", url: RULEFMT, relation: "equivalent" },
12
+ { tool: "pint", rule: "promql/syntax", url: `${PINT}/promql/syntax.md`, relation: "equivalent" },
13
+ ],
14
+ PROM101: [{ tool: "promtool", url: RULEFMT, relation: "equivalent" }],
15
+ PROM102: [{ tool: "pint", rule: "rule/duplicate", url: `${PINT}/rule/duplicate.md`, relation: "overlaps" }],
16
+ PROM103: [
17
+ { tool: "promtool", url: RULEFMT, relation: "equivalent" },
18
+ { tool: "pint", rule: "alerts/for", url: `${PINT}/alerts/for.md`, relation: "overlaps" },
19
+ ],
20
+ PROM104: [
21
+ { tool: "promtool", url: RULEFMT, relation: "equivalent" },
22
+ { tool: "pint", rule: "promql/syntax", url: `${PINT}/promql/syntax.md`, relation: "equivalent" },
23
+ ],
24
+ PROM105: [{ tool: "promtool", url: RULEFMT, relation: "equivalent" }],
25
+ PROM106: [{ tool: "pint", rule: "rule/label", url: `${PINT}/rule/label.md`, relation: "overlaps" }],
26
+ PROM107: [{ tool: "pint", rule: "alerts/annotation", url: `${PINT}/alerts/annotation.md`, relation: "overlaps" }],
27
+ PROM201: [amtool("equivalent")],
28
+ PROM203: [amtool("equivalent")],
29
+ PROM204: [amtool("equivalent")],
30
+ PROM205: [amtool("equivalent")],
31
+ PROM206: [amtool("equivalent")],
32
+ PROM208: [amtool("equivalent")],
33
+ PROM209: [amtool("overlaps")],
34
+ PROM210: [amtool("overlaps")],
35
+ PROM211: [{ tool: "pint", rule: "rule/for", url: `${PINT}/rule/for.md`, relation: "overlaps" }],
36
+ PROM212: [{ tool: "pint", rule: "alerts/annotation", url: `${PINT}/alerts/annotation.md`, relation: "overlaps" }],
37
+ PROM213: [{ tool: "pint", rule: "alerts/comparison", url: `${PINT}/alerts/comparison.md`, relation: "equivalent" }],
38
+ PROM214: [{ tool: "pint", rule: "alerts/template", url: `${PINT}/alerts/template.md`, relation: "overlaps" }],
39
+ PROM215: [{ tool: "pint", rule: "promql/rate", url: `${PINT}/promql/rate.md`, relation: "overlaps" }],
40
+ PROM217: [{ tool: "pint", rule: "rule/name", url: `${PINT}/rule/name.md`, relation: "overlaps" }],
41
+ PROM218: [{ tool: "pint", rule: "promql/regexp", url: `${PINT}/promql/regexp.md`, relation: "equivalent" }],
42
+ };
@@ -0,0 +1,68 @@
1
+ import { prom101 } from "./prom101.js";
2
+ import { prom102 } from "./prom102.js";
3
+ import { prom103 } from "./prom103.js";
4
+ import { prom104 } from "./prom104.js";
5
+ import { prom105 } from "./prom105.js";
6
+ import { prom106 } from "./prom106.js";
7
+ import { prom107 } from "./prom107.js";
8
+ import { prom201 } from "./prom201.js";
9
+ import { prom202 } from "./prom202.js";
10
+ import { prom203 } from "./prom203.js";
11
+ import { prom204 } from "./prom204.js";
12
+ import { prom205 } from "./prom205.js";
13
+ import { prom206 } from "./prom206.js";
14
+ import { prom207 } from "./prom207.js";
15
+ import { prom208 } from "./prom208.js";
16
+ import { prom209 } from "./prom209.js";
17
+ import { prom210 } from "./prom210.js";
18
+ import { prom211 } from "./prom211.js";
19
+ import { prom212 } from "./prom212.js";
20
+ import { prom213 } from "./prom213.js";
21
+ import { prom214 } from "./prom214.js";
22
+ import { prom215 } from "./prom215.js";
23
+ import { prom216 } from "./prom216.js";
24
+ import { prom217 } from "./prom217.js";
25
+ import { prom218 } from "./prom218.js";
26
+ import { prom219 } from "./prom219.js";
27
+ import { prom220 } from "./prom220.js";
28
+ import { prom221 } from "./prom221.js";
29
+ import { prom222 } from "./prom222.js";
30
+ import { prom223 } from "./prom223.js";
31
+ import { prom224 } from "./prom224.js";
32
+ import { prom225 } from "./prom225.js";
33
+ import { prom301 } from "./prom301.js";
34
+ export const postSynthChecks = [
35
+ prom101,
36
+ prom102,
37
+ prom103,
38
+ prom104,
39
+ prom105,
40
+ prom106,
41
+ prom107,
42
+ prom201,
43
+ prom202,
44
+ prom203,
45
+ prom204,
46
+ prom205,
47
+ prom206,
48
+ prom207,
49
+ prom208,
50
+ prom209,
51
+ prom210,
52
+ prom211,
53
+ prom212,
54
+ prom213,
55
+ prom214,
56
+ prom215,
57
+ prom216,
58
+ prom217,
59
+ prom218,
60
+ prom219,
61
+ prom220,
62
+ prom221,
63
+ prom222,
64
+ prom223,
65
+ prom224,
66
+ prom225,
67
+ prom301,
68
+ ];
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Shared plumbing for the prometheus post-synth checks: find the rule files
3
+ * and `alertmanager.yml` documents in a build's output, and run the
4
+ * plain-function checks over them.
5
+ *
6
+ * Documents are recognized by shape, not by which lexicon emitted them, so a
7
+ * rule file that reaches the output some other way (a hand-written YAML
8
+ * sidecar) is checked too. `PrometheusRule` manifests are not read here:
9
+ * `chant build` hands each lexicon's checks only that lexicon's output, so the
10
+ * k8s lexicon's WK8606 runs `prometheusRuleDiagnostics` (below) over them.
11
+ */
12
+ import { loadAll } from "js-yaml";
13
+ import { buildCollectorConfigs, collectorMetricIssues, collectorMetrics } from "../../collector-metrics.js";
14
+ import { looksLikePrometheusConfig } from "../../config-model.js";
15
+ import { validateRelabelFields } from "../../validate-relabel.js";
16
+ import { looksLikeAlertmanagerConfig, looksLikeRuleFile } from "../../model.js";
17
+ import { validateAlertmanagerConfig, validateRuleFile, validateRunbookUrls, validateSeverityRouting, } from "../../validate-config.js";
18
+ /** Every rule file and Alertmanager config in the output. Parsed with js-yaml rather than `ctx.docs`, which keeps block scalars and flow lists as strings. */
19
+ export function prometheusDocs(ctx) {
20
+ const found = { ruleFiles: [], alertmanager: [], prometheus: [] };
21
+ for (const [lexicon, output] of ctx.outputs) {
22
+ const texts = typeof output === "string"
23
+ ? [[lexicon, output]]
24
+ : [[lexicon, output.primary], ...Object.entries(output.files ?? {})];
25
+ for (const [source, text] of texts) {
26
+ if (!text)
27
+ continue;
28
+ let docs;
29
+ try {
30
+ docs = loadAll(text);
31
+ }
32
+ catch {
33
+ continue; // not YAML, or not ours
34
+ }
35
+ for (const doc of docs) {
36
+ if (looksLikeRuleFile(doc))
37
+ found.ruleFiles.push({ source, config: doc });
38
+ else if (looksLikeAlertmanagerConfig(doc))
39
+ found.alertmanager.push({ source, config: doc });
40
+ else if (looksLikePrometheusConfig(doc))
41
+ found.prometheus.push({ source, config: doc });
42
+ }
43
+ }
44
+ }
45
+ return found;
46
+ }
47
+ function toDiagnostic(issue, source) {
48
+ return {
49
+ checkId: issue.code,
50
+ severity: issue.severity,
51
+ message: source && source !== "prometheus" ? `${source}: ${issue.message}` : issue.message,
52
+ ...(issue.subject ? { entity: issue.subject } : {}),
53
+ lexicon: "prometheus",
54
+ };
55
+ }
56
+ /** Diagnostics for one rule-file code (PROM101-PROM107, PROM211, PROM213-PROM219) across every rule file in the output. */
57
+ export function ruleFileDiagnostics(ctx, code) {
58
+ return prometheusDocs(ctx).ruleFiles.flatMap(({ source, config }) => validateRuleFile(config)
59
+ .filter((i) => i.code === code)
60
+ .map((i) => toDiagnostic(i, source)));
61
+ }
62
+ /** Diagnostics for one Alertmanager code (PROM201, PROM203-PROM210, PROM220-PROM224) across every `alertmanager.yml` in the output. */
63
+ export function alertmanagerDiagnostics(ctx, code) {
64
+ return prometheusDocs(ctx).alertmanager.flatMap(({ source, config }) => validateAlertmanagerConfig(config)
65
+ .filter((i) => i.code === code)
66
+ .map((i) => toDiagnostic(i, source)));
67
+ }
68
+ /** PROM225 across every `prometheus.yml` in the output. */
69
+ export function relabelDiagnostics(ctx) {
70
+ return prometheusDocs(ctx).prometheus.flatMap(({ source, config }) => validateRelabelFields(config).map((i) => toDiagnostic(i, source)));
71
+ }
72
+ /** PROM212 across every rule file in the output. Opt-in: the lexicon's `recommended` lint preset leaves it out (see ../../plugin.ts). */
73
+ export function runbookDiagnostics(ctx) {
74
+ return prometheusDocs(ctx).ruleFiles.flatMap(({ source, config }) => validateRunbookUrls(config).map((i) => toDiagnostic(i, source)));
75
+ }
76
+ /**
77
+ * PROM301: every rule expression in the output against the metrics the
78
+ * build's collector configs emit. Silent when the build has none.
79
+ */
80
+ export function collectorMetricDiagnostics(ctx) {
81
+ const { ruleFiles } = prometheusDocs(ctx);
82
+ if (ruleFiles.length === 0)
83
+ return [];
84
+ const emitted = collectorMetrics(buildCollectorConfigs(ctx));
85
+ if (!emitted)
86
+ return [];
87
+ const out = [];
88
+ for (const { source, config } of ruleFiles) {
89
+ for (const group of Array.isArray(config?.groups) ? config.groups : []) {
90
+ for (const rule of Array.isArray(group?.rules) ? group.rules : []) {
91
+ const r = rule;
92
+ if (typeof r.expr !== "string")
93
+ continue;
94
+ const subject = `${group.name}/${typeof r.record === "string" ? r.record : String(r.alert ?? "")}`;
95
+ for (const issue of collectorMetricIssues(r.expr, emitted)) {
96
+ const message = `rule ${subject} ${issue.message}`;
97
+ out.push({
98
+ checkId: "PROM301",
99
+ severity: "warning",
100
+ message: source && source !== "prometheus" ? `${source}: ${message}` : message,
101
+ entity: subject,
102
+ lexicon: "prometheus",
103
+ });
104
+ }
105
+ }
106
+ }
107
+ }
108
+ return out;
109
+ }
110
+ /** PROM202: joins every rule file in the output against every Alertmanager config in it. Silent when either is absent. */
111
+ export function routingDiagnostics(ctx) {
112
+ const { ruleFiles, alertmanager } = prometheusDocs(ctx);
113
+ if (ruleFiles.length === 0)
114
+ return [];
115
+ const files = ruleFiles.map((r) => r.config);
116
+ return alertmanager.flatMap(({ source, config }) => validateSeverityRouting(files, config).map((i) => toDiagnostic(i, source)));
117
+ }
118
+ /**
119
+ * The rule-file checks (PROM101-PROM107, PROM211, PROM213-PROM219) over the
120
+ * groups of every `PrometheusRule` in the build, whichever lexicon emitted
121
+ * it. `spec.groups` is a rule file's `groups`, so `validateRuleFile` reads it
122
+ * as is. Findings keep their PROM ids and name the PrometheusRule's namespace
123
+ * and name. PROM212 (runbook URLs) stays opt-in and is not run here.
124
+ */
125
+ export function prometheusRuleDiagnostics(ctx) {
126
+ const out = [];
127
+ for (const output of ctx.outputs.values()) {
128
+ const text = typeof output === "string" ? output : output.primary;
129
+ if (!text)
130
+ continue;
131
+ let docs;
132
+ try {
133
+ docs = loadAll(text);
134
+ }
135
+ catch {
136
+ continue;
137
+ }
138
+ for (const raw of docs) {
139
+ const doc = raw;
140
+ if (!doc || typeof doc !== "object" || doc.kind !== "PrometheusRule")
141
+ continue;
142
+ if (typeof doc.apiVersion !== "string" || !doc.apiVersion.startsWith("monitoring.coreos.com/"))
143
+ continue;
144
+ const groups = doc.spec?.groups;
145
+ if (!Array.isArray(groups))
146
+ continue;
147
+ const name = String(doc.metadata?.name ?? "");
148
+ const source = `PrometheusRule ${doc.metadata?.namespace ? `${doc.metadata.namespace}/` : ""}${name}`;
149
+ for (const issue of validateRuleFile({ groups })) {
150
+ if (issue.code === "PROM212")
151
+ continue;
152
+ out.push({ ...toDiagnostic(issue, source), ...(issue.subject ? {} : { entity: name }) });
153
+ }
154
+ }
155
+ }
156
+ return out;
157
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * PROM101: Two rule groups in one rule file share a name
3
+ *
4
+ * Prometheus refuses to load a rule file whose group names repeat.
5
+ */
6
+ import { ruleFileDiagnostics } from "./prom-helpers.js";
7
+ export const prom101 = {
8
+ id: "PROM101",
9
+ description: "Two rule groups in one rule file share a name",
10
+ check(ctx) {
11
+ return ruleFileDiagnostics(ctx, "PROM101");
12
+ },
13
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * PROM102: Two rules share a name and label set
3
+ *
4
+ * Two recording rules (or two alerts) with the same name and labels produce the same series and overwrite each other. This follows promtool's duplicate-rules lint: alerts that share a name and differ by a label, such as severity, are fine.
5
+ */
6
+ import { ruleFileDiagnostics } from "./prom-helpers.js";
7
+ export const prom102 = {
8
+ id: "PROM102",
9
+ description: "Two rules share a name and label set",
10
+ check(ctx) {
11
+ return ruleFileDiagnostics(ctx, "PROM102");
12
+ },
13
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * PROM103: A rule or group duration is not a Prometheus duration
3
+ *
4
+ * for, keep_firing_for, interval and query_offset take durations such as 30s, 5m or 1h30m. Prometheus refuses the rule file otherwise.
5
+ */
6
+ import { ruleFileDiagnostics } from "./prom-helpers.js";
7
+ export const prom103 = {
8
+ id: "PROM103",
9
+ description: "A rule or group duration is not a Prometheus duration",
10
+ check(ctx) {
11
+ return ruleFileDiagnostics(ctx, "PROM103");
12
+ },
13
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * PROM104: A rule expression is not valid PromQL
3
+ *
4
+ * Every expr is parsed with the PromQL grammar the Prometheus project publishes. A syntax error fails the rule file at load time.
5
+ */
6
+ import { ruleFileDiagnostics } from "./prom-helpers.js";
7
+ export const prom104 = {
8
+ id: "PROM104",
9
+ description: "A rule expression is not valid PromQL",
10
+ check(ctx) {
11
+ return ruleFileDiagnostics(ctx, "PROM104");
12
+ },
13
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * PROM105: A rule or group is malformed
3
+ *
4
+ * A rule sets exactly one of record and alert, has a non-empty name, and a recording rule takes no for, keep_firing_for or annotations. A group has a name.
5
+ */
6
+ import { ruleFileDiagnostics } from "./prom-helpers.js";
7
+ export const prom105 = {
8
+ id: "PROM105",
9
+ description: "A rule or group is malformed",
10
+ check(ctx) {
11
+ return ruleFileDiagnostics(ctx, "PROM105");
12
+ },
13
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * PROM106: An alerting rule has no severity label
3
+ *
4
+ * Alertmanager routes on labels, and severity is the one every route in this lexicon's examples and checks keys on. Without it the alert can only be routed by name.
5
+ */
6
+ import { ruleFileDiagnostics } from "./prom-helpers.js";
7
+ export const prom106 = {
8
+ id: "PROM106",
9
+ description: "An alerting rule has no severity label",
10
+ check(ctx) {
11
+ return ruleFileDiagnostics(ctx, "PROM106");
12
+ },
13
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * PROM107: An alerting rule has no summary or description annotation
3
+ *
4
+ * A notification carries only the alert's labels unless an annotation says what is wrong.
5
+ */
6
+ import { ruleFileDiagnostics } from "./prom-helpers.js";
7
+ export const prom107 = {
8
+ id: "PROM107",
9
+ description: "An alerting rule has no summary or description annotation",
10
+ check(ctx) {
11
+ return ruleFileDiagnostics(ctx, "PROM107");
12
+ },
13
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * PROM201: A route sends to a receiver that is not declared
3
+ *
4
+ * Alertmanager refuses a config whose routes name an undefined receiver.
5
+ */
6
+ import { alertmanagerDiagnostics } from "./prom-helpers.js";
7
+ export const prom201 = {
8
+ id: "PROM201",
9
+ description: "A route sends to a receiver that is not declared",
10
+ check(ctx) {
11
+ return alertmanagerDiagnostics(ctx, "PROM201");
12
+ },
13
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * PROM202: An alert severity is not routed by any route
3
+ *
4
+ * Joins the rule files and the Alertmanager config in one build root. An alert whose severity no route below the root matches falls through to the root's default receiver. Silent when the rules and the Alertmanager config are built in different build roots (chant #1939).
5
+ */
6
+ import { routingDiagnostics } from "./prom-helpers.js";
7
+ export const prom202 = {
8
+ id: "PROM202",
9
+ description: "An alert severity is not routed by any route",
10
+ check(ctx) {
11
+ return routingDiagnostics(ctx);
12
+ },
13
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * PROM203: Two receivers or two time intervals share a name
3
+ *
4
+ * Alertmanager refuses a config with repeated receiver or time interval names.
5
+ */
6
+ import { alertmanagerDiagnostics } from "./prom-helpers.js";
7
+ export const prom203 = {
8
+ id: "PROM203",
9
+ description: "Two receivers or two time intervals share a name",
10
+ check(ctx) {
11
+ return alertmanagerDiagnostics(ctx, "PROM203");
12
+ },
13
+ };