@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,21 @@
1
+ /**
2
+ * PROM211: An alerting rule has no for, or for: 0s
3
+ *
4
+ * The alert fires on the first evaluation that matches, so one bad scrape
5
+ * pages. Left alone when the expression reads no series (vector(1), a dead
6
+ * man's switch) or already spans a window: every selector read through a
7
+ * *_over_time function, or two conditions joined by and (the multi-window
8
+ * burn-rate form, where the short window does what for would).
9
+ */
10
+
11
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
12
+ import { ruleFileDiagnostics } from "./prom-helpers";
13
+
14
+ export const prom211: PostSynthCheck = {
15
+ id: "PROM211",
16
+ description: "An alerting rule has no for, or for: 0s",
17
+
18
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
19
+ return ruleFileDiagnostics(ctx, "PROM211");
20
+ },
21
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * PROM212: An alerting rule has no runbook_url annotation (opt-in)
3
+ *
4
+ * Off unless enabled: the lint preset all, or a lint.rules entry for PROM212,
5
+ * turns it on. See validateRunbookUrls.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { runbookDiagnostics } from "./prom-helpers";
10
+
11
+ export const prom212: PostSynthCheck = {
12
+ id: "PROM212",
13
+ description: "An alerting rule has no runbook_url annotation (opt-in)",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ return runbookDiagnostics(ctx);
17
+ },
18
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * PROM213: An alert expression has no comparison
3
+ *
4
+ * Every series the expression returns is an alert, so an expression with no
5
+ * comparison fires for all of them. absent(), absent_over_time() and unless
6
+ * count as a condition; a comparison with bool does not. An expression that
7
+ * reads no series (vector(1), a dead man's switch) is left alone.
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { ruleFileDiagnostics } from "./prom-helpers";
12
+
13
+ export const prom213: PostSynthCheck = {
14
+ id: "PROM213",
15
+ description: "An alert expression has no comparison",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ return ruleFileDiagnostics(ctx, "PROM213");
19
+ },
20
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * PROM214: An alert template reads a label the expression aggregates away
3
+ *
4
+ * A $labels.x whose x the expression's by (...) leaves out, or its without
5
+ * (...) names, renders empty. Labels the rule or its group set are left alone.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { ruleFileDiagnostics } from "./prom-helpers";
10
+
11
+ export const prom214: PostSynthCheck = {
12
+ id: "PROM214",
13
+ description: "An alert template reads a label the expression aggregates away",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ return ruleFileDiagnostics(ctx, "PROM214");
17
+ },
18
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * PROM215: rate, irate or increase over a name that is not a counter's
3
+ *
4
+ * These functions treat every drop as a counter reset. A name not ending in
5
+ * _total, _count, _sum or _bucket is read as a gauge.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { ruleFileDiagnostics } from "./prom-helpers";
10
+
11
+ export const prom215: PostSynthCheck = {
12
+ id: "PROM215",
13
+ description: "rate, irate or increase over a name that is not a counter's",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ return ruleFileDiagnostics(ctx, "PROM215");
17
+ },
18
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * PROM216: histogram_quantile over a series without _bucket, or without le
3
+ *
4
+ * A classic histogram's quantile needs its _bucket series with the le label
5
+ * kept. A native histogram has no _bucket series, so this check does not apply
6
+ * to one.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { ruleFileDiagnostics } from "./prom-helpers";
11
+
12
+ export const prom216: PostSynthCheck = {
13
+ id: "PROM216",
14
+ description: "histogram_quantile over a series without _bucket, or without le",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ return ruleFileDiagnostics(ctx, "PROM216");
18
+ },
19
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * PROM217: A recording rule name is not in level:metric:operations form
3
+ *
4
+ * Prometheus's naming convention for recorded series, e.g.
5
+ * job:http_requests:rate5m.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { ruleFileDiagnostics } from "./prom-helpers";
10
+
11
+ export const prom217: PostSynthCheck = {
12
+ id: "PROM217",
13
+ description: "A recording rule name is not in level:metric:operations form",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ return ruleFileDiagnostics(ctx, "PROM217");
17
+ },
18
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * PROM218: A regex matcher needs no regex, or is anchored
3
+ *
4
+ * A =~ or !~ with no regex metacharacters is an equality matcher written the
5
+ * slow way. Prometheus anchors every regex, so a leading ^ or trailing $ does
6
+ * nothing.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { ruleFileDiagnostics } from "./prom-helpers";
11
+
12
+ export const prom218: PostSynthCheck = {
13
+ id: "PROM218",
14
+ description: "A regex matcher needs no regex, or is anchored",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ return ruleFileDiagnostics(ctx, "PROM218");
18
+ },
19
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * PROM219: An alerting rule sets the alertname label
3
+ *
4
+ * Prometheus sets alertname to the rule's name after applying the rule's
5
+ * labels, so the value is overwritten.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { ruleFileDiagnostics } from "./prom-helpers";
10
+
11
+ export const prom219: PostSynthCheck = {
12
+ id: "PROM219",
13
+ description: "An alerting rule sets the alertname label",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ return ruleFileDiagnostics(ctx, "PROM219");
17
+ },
18
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * PROM220: A receiver turns off TLS certificate verification
3
+ *
4
+ * tls_config.insecure_skip_verify: true on a receiver, or in global, accepts
5
+ * any certificate the endpoint presents.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { alertmanagerDiagnostics } from "./prom-helpers";
10
+
11
+ export const prom220: PostSynthCheck = {
12
+ id: "PROM220",
13
+ description: "A receiver turns off TLS certificate verification",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ return alertmanagerDiagnostics(ctx, "PROM220");
17
+ },
18
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * PROM221: SMTP credentials sent with require_tls false
3
+ *
4
+ * An email receiver with SMTP auth and require_tls false (its own, or
5
+ * global.smtp_require_tls) can send the credentials in clear text. Implicit
6
+ * TLS is left alone.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { alertmanagerDiagnostics } from "./prom-helpers";
11
+
12
+ export const prom221: PostSynthCheck = {
13
+ id: "PROM221",
14
+ description: "SMTP credentials sent with require_tls false",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ return alertmanagerDiagnostics(ctx, "PROM221");
18
+ },
19
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * PROM222: Credentials sent to an http:// receiver URL
3
+ *
4
+ * An integration whose URL is http:// and whose request carries a credential:
5
+ * user info in the URL, http_config auth, or a key or token field.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { alertmanagerDiagnostics } from "./prom-helpers";
10
+
11
+ export const prom222: PostSynthCheck = {
12
+ id: "PROM222",
13
+ description: "Credentials sent to an http:// receiver URL",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ return alertmanagerDiagnostics(ctx, "PROM222");
17
+ },
18
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * PROM223: repeat_interval is shorter than group_interval
3
+ *
4
+ * Alertmanager resends only when it flushes a group, every group_interval, so
5
+ * a shorter repeat_interval never takes effect. Intervals are inherited down
6
+ * the routing tree from Alertmanager's defaults.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { alertmanagerDiagnostics } from "./prom-helpers";
11
+
12
+ export const prom223: PostSynthCheck = {
13
+ id: "PROM223",
14
+ description: "repeat_interval is shorter than group_interval",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ return alertmanagerDiagnostics(ctx, "PROM223");
18
+ },
19
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * PROM224: An inhibit rule matches one alert as source and target, with no equal
3
+ *
4
+ * With no equal, any alert matching the source mutes every alert matching the
5
+ * target. When one alert can match both, alerts of that kind mute each other.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { alertmanagerDiagnostics } from "./prom-helpers";
10
+
11
+ export const prom224: PostSynthCheck = {
12
+ id: "PROM224",
13
+ description: "An inhibit rule matches one alert as source and target, with no equal",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ return alertmanagerDiagnostics(ctx, "PROM224");
17
+ },
18
+ };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * PROM301 (#3364): rule selectors under a connector namespace against what
3
+ * the build's collector configs emit.
4
+ */
5
+ import { describe, expect, test } from "vitest";
6
+ import type { Declarable } from "@intentius/chant/declarable";
7
+ import { makePostSynthCtx, makePostSynthCtxFromFiles } from "@intentius/chant-test-utils";
8
+ import { PrometheusExporter, ServiceGraphConnector, SpanMetricsConnector } from "@intentius/chant-lexicon-otel/components/index";
9
+ import { genAiMetrics, genAiPipeline, type GenAiMetricsOptions } from "@intentius/chant-lexicon-otel/genai";
10
+ import { collectorYaml } from "@intentius/chant-lexicon-otel/collector";
11
+ import { prom301 } from "./prom301";
12
+ import { emitYaml } from "../../build";
13
+ import { ruleGroupConfig } from "../../rules";
14
+ import { GenAiRules, type GenAiAlerting, type GenAiPrice } from "../../composites/genai";
15
+ import { Slo } from "../../composites/slo";
16
+ import type { RuleGroupConfig } from "../../model";
17
+
18
+ const entities = (...list: Declarable[]) => new Map(list.map((e, i) => [`e${i}`, e] as [string, Declarable]));
19
+
20
+ /** A rule file of one recording rule per expression. */
21
+ function rules(...exprs: string[]): string {
22
+ return JSON.stringify({ groups: [{ name: "g", rules: exprs.map((expr, i) => ({ record: `job:r${i}:rate5m`, expr })) }] });
23
+ }
24
+
25
+ /** PROM301's messages for a rule file, with the collector declared as otel entities in the same build. */
26
+ function found(text: string, ...collector: Declarable[]): string[] {
27
+ return prom301.check(makePostSynthCtx("prometheus", text, entities(...collector))).map((d) => d.message);
28
+ }
29
+
30
+ describe("PROM301", () => {
31
+ test("a rule reading traces_span_metrics_calls_total with no spanmetrics connector of that namespace is reported", () => {
32
+ const msgs = found(rules("sum by (service_name) (rate(traces_span_metrics_calls_total[5m]))"), new SpanMetricsConnector({ namespace: "shop" }));
33
+ expect(msgs).toEqual(["rule g/job:r0:rate5m reads traces_span_metrics_calls_total, which no collector config in the build emits under traces_span_metrics"]);
34
+ });
35
+
36
+ test("the names a declared connector emits pass, a misspelt one under its namespace does not", () => {
37
+ const shop = new SpanMetricsConnector({ namespace: "shop", histogram: { unit: "s" } });
38
+ expect(found(rules("sum by (service_name) (rate(shop_calls_total[5m]))", "histogram_quantile(0.9, sum by (le) (rate(shop_duration_seconds_bucket[5m])))"), shop)).toEqual([]);
39
+ expect(found(rules("sum(rate(shop_call_total[5m]))"), shop)).toEqual([
40
+ "rule g/job:r0:rate5m reads shop_call_total, which no collector config in the build emits under shop",
41
+ ]);
42
+ // A duration histogram in ms, read as seconds.
43
+ expect(found(rules("sum(rate(traces_span_metrics_duration_seconds_count[5m]))"), new SpanMetricsConnector({}))).toHaveLength(1);
44
+ });
45
+
46
+ test("a by (...) label that is not a declared dimension", () => {
47
+ const spans = new SpanMetricsConnector({ dimensions: [{ name: "http.route" }] });
48
+ expect(found(rules("sum by (service_name, http_route, job) (rate(traces_span_metrics_calls_total[5m]))"), spans)).toEqual([]);
49
+ expect(found(rules("sum by (http_method) (rate(traces_span_metrics_calls_total[5m]))"), spans)).toEqual([
50
+ "rule g/job:r0:rate5m groups traces_span_metrics_calls_total by http_method, which is not a dimension the collector config declares for it",
51
+ ]);
52
+ // le belongs to the _bucket series only.
53
+ expect(found(rules("sum by (le) (rate(traces_span_metrics_duration_milliseconds_count[5m]))"), spans)).toHaveLength(1);
54
+ // label_replace writes a label of its own, so the aggregation over it is not checked.
55
+ expect(found(rules('sum by (route) (label_replace(rate(traces_span_metrics_calls_total[5m]), "route", "$1", "http_route", "(.*)"))'), spans)).toEqual([]);
56
+ });
57
+
58
+ test("an exporter that copies every resource attribute leaves the labels open", () => {
59
+ const spans = new SpanMetricsConnector({});
60
+ const exporter = new PrometheusExporter({ endpoint: "0.0.0.0:8889", resource_to_telemetry_conversion: { enabled: true } });
61
+ expect(found(rules("sum by (k8s_namespace_name) (rate(traces_span_metrics_calls_total[5m]))"), spans, exporter)).toEqual([]);
62
+ });
63
+
64
+ test("the exporter's namespace moves every name", () => {
65
+ const spans = new SpanMetricsConnector({});
66
+ const exporter = new PrometheusExporter({ endpoint: "0.0.0.0:8889", namespace: "otel" });
67
+ expect(found(rules("sum(rate(otel_traces_span_metrics_calls_total[5m]))"), spans, exporter)).toEqual([]);
68
+ expect(found(rules("sum(rate(otel_traces_span_metrics_call_total[5m]))"), spans, exporter)).toHaveLength(1);
69
+ });
70
+
71
+ test("servicegraph names and labels", () => {
72
+ const graph = new ServiceGraphConnector({ dimensions: ["http.method"] });
73
+ expect(found(rules("sum by (client, server, client_http_method) (rate(traces_service_graph_request_total[5m]))"), graph)).toEqual([]);
74
+ expect(found(rules("sum by (client) (rate(traces_service_graph_request_duration_seconds_count[5m]))"), graph)).toHaveLength(1);
75
+ expect(found(rules("sum by (peer) (rate(traces_service_graph_request_failed_total[5m]))"), graph)).toHaveLength(1);
76
+ });
77
+
78
+ test("silent for names outside every namespace, and with no collector config in the build", () => {
79
+ expect(found(rules("sum by (job) (rate(http_requests_total[5m]))"), new SpanMetricsConnector({}))).toEqual([]);
80
+ expect(found(rules("sum by (nope) (rate(traces_span_metrics_calls_totl[5m]))"))).toEqual([]);
81
+ });
82
+
83
+ test("reads a collector config from the output too, as in a ConfigMap or a multi-lexicon context", () => {
84
+ const yaml = collectorYaml(genAiPipeline({ namespace: "shop" }));
85
+ const ctx = makePostSynthCtxFromFiles("prometheus", { "collector.yaml": yaml }, rules("sum(rate(traces_span_metrics_calls_total[5m]))"));
86
+ expect(prom301.check(ctx)).toHaveLength(1);
87
+ });
88
+ });
89
+
90
+ describe("PROM301 reads renamed connector types as the built-ins they name", () => {
91
+ const config = (connectors: string) => `receivers:
92
+ otlp: {}
93
+ exporters:
94
+ prometheus:
95
+ endpoint: 0.0.0.0:8889
96
+ connectors:
97
+ ${connectors}
98
+ service:
99
+ pipelines:
100
+ traces:
101
+ receivers: [otlp]
102
+ exporters: [debug]
103
+ `;
104
+ const ctx = (yaml: string, text: string) => makePostSynthCtxFromFiles("prometheus", { "collector.yaml": yaml }, text);
105
+
106
+ test("span_metrics, service_graph and signal_to_metrics", () => {
107
+ const yaml = config(` span_metrics/shop:
108
+ namespace: shop
109
+ service_graph: {}
110
+ signal_to_metrics/genai_client:
111
+ spans:
112
+ - name: gen_ai.client.operation.duration
113
+ unit: s
114
+ attributes: [{ key: gen_ai.operation.name }]
115
+ histogram: { buckets: [1] }
116
+ `);
117
+ const read = rules(
118
+ "sum by (service_name) (rate(shop_calls_total[5m]))",
119
+ "sum by (client) (rate(traces_service_graph_request_total[5m]))",
120
+ "sum by (gen_ai_operation_name, le) (rate(gen_ai_client_operation_duration_seconds_bucket[5m]))",
121
+ );
122
+ expect(prom301.check(ctx(yaml, read))).toEqual([]);
123
+ expect(prom301.check(ctx(yaml, rules("sum(rate(shop_call_total[5m]))", "sum(rate(gen_ai_client_operation_seconds_count[5m]))"))).map((d) => d.entity)).toEqual([
124
+ "g/job:r0:rate5m",
125
+ "g/job:r1:rate5m",
126
+ ]);
127
+ });
128
+ });
129
+
130
+ describe("PROM301 on the lexicon's own rules", () => {
131
+ const groupYaml = (group: RuleGroupConfig) => emitYaml({ groups: [group] });
132
+ const prices: GenAiPrice[] = [{ provider: "anthropic", model: "m1", inputPerMTok: 3, outputPerMTok: 15, currency: "USD", source: "https://example.com", asOf: "2026-09-29" }];
133
+ const alerts: GenAiAlerting = { errorRatio: true, latency: true, toolErrorRatio: true, budgets: [{ amount: 5, currency: "USD", per: "hour" }] };
134
+
135
+ test.each([
136
+ ["spans", {}],
137
+ ["derived client metrics", { clientMetrics: "derive" }],
138
+ ["provider dimensions and a namespace", { providerDimensions: true, namespace: "ai" }],
139
+ ] as Array<[string, GenAiMetricsOptions]>)("GenAiRules over genAiPipeline with %s, with and without its alerts", (_label, options) => {
140
+ const collector = genAiPipeline({ ...options, metricExporters: [new PrometheusExporter({ endpoint: "0.0.0.0:8889" })] });
141
+ const genAi = genAiMetrics(options);
142
+ for (const props of [{ genAi, prices }, { genAi, prices, alerts }]) {
143
+ expect(found(groupYaml(ruleGroupConfig(GenAiRules(props).rules)), ...collector)).toEqual([]);
144
+ }
145
+ });
146
+
147
+ test("Slo over the default spanmetrics connector", () => {
148
+ const calls = "traces_span_metrics_calls_total";
149
+ const slo = Slo({
150
+ name: "checkout",
151
+ objective: 0.999,
152
+ window: "30d",
153
+ sli: { errors: `sum(rate(${calls}{span_name="checkout",status_code="STATUS_CODE_ERROR"}[{{window}}]))`, total: `sum(rate(${calls}{span_name="checkout"}[{{window}}]))` },
154
+ });
155
+ expect(found(groupYaml(ruleGroupConfig(slo.rules)), new SpanMetricsConnector({}))).toEqual([]);
156
+ });
157
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * PROM301: A rule reads a connector metric no collector in the build emits
3
+ *
4
+ * For each selector in a rule whose name falls under a namespace the build's
5
+ * spanmetrics, servicegraph or GenAI connectors own (spanmetrics' and
6
+ * servicegraph's defaults included), reports a name no collector config in
7
+ * the build emits, and a by (...) label that is not a dimension the config
8
+ * declares for the metrics aggregated. Silent for names outside those
9
+ * namespaces, and when the build has no collector config. See
10
+ * ../../collector-metrics.ts.
11
+ */
12
+
13
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
14
+ import { collectorMetricDiagnostics } from "./prom-helpers";
15
+
16
+ export const prom301: PostSynthCheck = {
17
+ id: "PROM301",
18
+ description: "A rule reads a connector metric no collector in the build emits",
19
+
20
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
21
+ return collectorMetricDiagnostics(ctx);
22
+ },
23
+ };
@@ -1,5 +1,6 @@
1
1
  import * as ts from "typescript";
2
2
  import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
3
+ import { SLACK_APP_URL } from "../../validate-integrations";
3
4
  import { calleeName, constInitializers, CREDENTIAL_CLASS, literalText, position, propertyName, resolveConst } from "./prom-ast";
4
5
 
5
6
  /**
@@ -89,6 +90,9 @@ export const literalCredentialRule: LintRule = {
89
90
  if (only && integration !== undefined && integration !== only) return;
90
91
  const text = literalText(init);
91
92
  if (text === undefined || text === "") return;
93
+ // A Slack bot posts to the public chat.postMessage endpoint with its token
94
+ // in http_config, and update_message needs exactly that api_url (PROM210).
95
+ if (key === "api_url" && text === SLACK_APP_URL) return;
92
96
  diagnostics.push({
93
97
  ruleId: "PROM001",
94
98
  severity: "error",
@@ -45,6 +45,22 @@ describe("PROM001 literal credential", () => {
45
45
  expect(diags).toEqual([]);
46
46
  });
47
47
 
48
+ test("accepts Slack's bot endpoint as api_url, which update_message needs", () => {
49
+ const diags = literalCredentialRule.check(
50
+ ctx(`
51
+ new Receiver({
52
+ name: "chat",
53
+ slack_configs: [{
54
+ api_url: "https://slack.com/api/chat.postMessage",
55
+ update_message: true,
56
+ http_config: { authorization: { credentials_file: "/etc/slack-bot-token" } },
57
+ }],
58
+ });
59
+ `),
60
+ );
61
+ expect(diags).toEqual([]);
62
+ });
63
+
48
64
  test("follows values lifted into named consts, shorthand props and spreads", () => {
49
65
  const diags = literalCredentialRule.check(
50
66
  ctx(`
@@ -37,6 +37,22 @@ describe("prometheus plugin", () => {
37
37
  "PROM207",
38
38
  "PROM208",
39
39
  "PROM209",
40
+ "PROM210",
41
+ "PROM211",
42
+ "PROM212",
43
+ "PROM213",
44
+ "PROM214",
45
+ "PROM215",
46
+ "PROM216",
47
+ "PROM217",
48
+ "PROM218",
49
+ "PROM219",
50
+ "PROM220",
51
+ "PROM221",
52
+ "PROM222",
53
+ "PROM223",
54
+ "PROM224",
55
+ "PROM301",
40
56
  ]);
41
57
  });
42
58
 
@@ -45,6 +61,13 @@ describe("prometheus plugin", () => {
45
61
  expect(Object.keys(prometheusAuditCatalog).sort()).toEqual([...ids].sort());
46
62
  });
47
63
 
64
+ it("leaves PROM212 out of the recommended preset, and has it in all (#3363)", () => {
65
+ const presets = prometheusPlugin.lintPresets!();
66
+ expect(presets.all).toContain("PROM212");
67
+ expect(presets.recommended).not.toContain("PROM212");
68
+ expect(presets.all.filter((id) => !presets.recommended.includes(id))).toEqual(["PROM212"]);
69
+ });
70
+
48
71
  it("detects a rule file and an alertmanager.yml, and nothing else", () => {
49
72
  expect(prometheusPlugin.detectTemplate!({ groups: [{ name: "a", rules: [] }] })).toBe(true);
50
73
  expect(prometheusPlugin.detectTemplate!({ route: { receiver: "x" }, receivers: [{ name: "x" }] })).toBe(true);
package/src/plugin.ts CHANGED
@@ -5,13 +5,13 @@ import { createDiffTool } from "@intentius/chant/lexicon-plugin-helpers";
5
5
  import { prometheusSerializer } from "./serializer";
6
6
  import { rules } from "./lint/rules";
7
7
  import { postSynthChecks } from "./lint/post-synth";
8
- import { prometheusAuditCatalog } from "./lint/audit-catalog";
8
+ import { OPT_IN_CHECKS, prometheusAuditCatalog } from "./lint/audit-catalog";
9
9
  import { completions } from "./lsp/completions";
10
10
  import { hover } from "./lsp/hover";
11
11
  import { detectTemplate } from "./detect";
12
12
  import { PrometheusParser } from "./import/parser";
13
13
  import { PrometheusGenerator } from "./import/generator";
14
- import { ruleGroupsImporter } from "./import/embedded";
14
+ import { alertmanagerImporter, ruleGroupsImporter } from "./import/embedded";
15
15
  import { DEFAULT_TEMPLATE, RULES_TEMPLATE, SLO_STYLE_TEMPLATE, SLO_TEMPLATE } from "./init-templates";
16
16
  import { prometheusSkills } from "./skill-defs";
17
17
  import { CATALOG } from "./catalog";
@@ -89,6 +89,17 @@ export const prometheusPlugin: LexiconPlugin = {
89
89
  return prometheusAuditCatalog;
90
90
  },
91
91
 
92
+ /**
93
+ * `recommended` (what `chant build` reports by default) is every check but
94
+ * the opt-in ones; `all` adds them. A project turns one on with
95
+ * `lint.presets: { prometheus: "all" }`, or by naming it in `lint.rules`
96
+ * (`{ PROM212: "warning" }`), which keeps a check whatever the preset.
97
+ */
98
+ lintPresets() {
99
+ const all = Object.keys(prometheusAuditCatalog);
100
+ return { recommended: all.filter((id) => !OPT_IN_CHECKS.has(id)), all };
101
+ },
102
+
92
103
  skills: prometheusSkills,
93
104
 
94
105
  composites() {
@@ -122,7 +133,7 @@ export const prometheusPlugin: LexiconPlugin = {
122
133
  },
123
134
 
124
135
  embeddedImporters() {
125
- return [ruleGroupsImporter];
136
+ return [ruleGroupsImporter, alertmanagerImporter];
126
137
  },
127
138
 
128
139
  // `chant init --lexicon prometheus [--template rules|slo-style|slo]`; see ./init-templates.ts.