@intentius/chant-lexicon-prometheus 0.105.1 → 0.106.3

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 (91) hide show
  1. package/README.md +5 -2
  2. package/dist/build.d.ts +17 -0
  3. package/dist/build.d.ts.map +1 -1
  4. package/dist/catalog.d.ts +1 -1
  5. package/dist/catalog.d.ts.map +1 -1
  6. package/dist/composites/index.d.ts +1 -1
  7. package/dist/composites/index.d.ts.map +1 -1
  8. package/dist/composites/slo.d.ts +23 -0
  9. package/dist/composites/slo.d.ts.map +1 -1
  10. package/dist/config-model.d.ts +272 -0
  11. package/dist/config-model.d.ts.map +1 -0
  12. package/dist/detect.d.ts.map +1 -1
  13. package/dist/import/embedded.d.ts +7 -0
  14. package/dist/import/embedded.d.ts.map +1 -1
  15. package/dist/import/generator.d.ts +17 -1
  16. package/dist/import/generator.d.ts.map +1 -1
  17. package/dist/import/parser.d.ts +12 -2
  18. package/dist/import/parser.d.ts.map +1 -1
  19. package/dist/import/slo.d.ts.map +1 -1
  20. package/dist/index.d.ts +5 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/integrity.json +5 -4
  23. package/dist/lint/audit-catalog.d.ts.map +1 -1
  24. package/dist/lint/audit-lineage.d.ts +2 -0
  25. package/dist/lint/audit-lineage.d.ts.map +1 -1
  26. package/dist/lint/post-synth/index.d.ts.map +1 -1
  27. package/dist/lint/post-synth/prom-helpers.d.ts +7 -0
  28. package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -1
  29. package/dist/lint/post-synth/prom225.d.ts +10 -0
  30. package/dist/lint/post-synth/prom225.d.ts.map +1 -0
  31. package/dist/manifest.json +1 -1
  32. package/dist/meta.json +10 -0
  33. package/dist/model.d.ts +1 -0
  34. package/dist/model.d.ts.map +1 -1
  35. package/dist/okf/index.md +3 -0
  36. package/dist/okf/rules/PROM225.md +11 -0
  37. package/dist/okf/types/PrometheusConfig.md +9 -0
  38. package/dist/okf/types/ScrapeConfig.md +9 -0
  39. package/dist/plugin.d.ts.map +1 -1
  40. package/dist/prometheus-config.d.ts +39 -0
  41. package/dist/prometheus-config.d.ts.map +1 -0
  42. package/dist/rules/prom-helpers.ts +10 -1
  43. package/dist/rules/prom225.ts +19 -0
  44. package/dist/serializer.d.ts +14 -10
  45. package/dist/serializer.d.ts.map +1 -1
  46. package/dist/tools.d.ts +9 -1
  47. package/dist/tools.d.ts.map +1 -1
  48. package/dist/validate-config.d.ts +1 -1
  49. package/dist/validate-config.d.ts.map +1 -1
  50. package/dist/validate-relabel.d.ts +14 -0
  51. package/dist/validate-relabel.d.ts.map +1 -0
  52. package/dist/validate.d.ts +2 -1
  53. package/dist/validate.d.ts.map +1 -1
  54. package/package.json +2 -2
  55. package/src/build.ts +73 -0
  56. package/src/catalog.ts +14 -1
  57. package/src/composites/composites.test.ts +72 -2
  58. package/src/composites/index.ts +1 -0
  59. package/src/composites/slo.ts +43 -4
  60. package/src/config-model.ts +327 -0
  61. package/src/detect.ts +5 -4
  62. package/src/generated/lexicon-prometheus.json +10 -0
  63. package/src/import/embedded.test.ts +46 -2
  64. package/src/import/embedded.ts +32 -1
  65. package/src/import/generator.ts +138 -0
  66. package/src/import/parser.test.ts +2 -2
  67. package/src/import/parser.ts +72 -5
  68. package/src/import/prometheus-roundtrip.test.ts +148 -0
  69. package/src/import/slo.ts +15 -0
  70. package/src/import/testdata/prometheus-full.yml +149 -0
  71. package/src/import/testdata/schemastore-prometheus.json +1635 -0
  72. package/src/index.ts +21 -2
  73. package/src/lint/audit-catalog.ts +7 -0
  74. package/src/lint/audit-lineage.test.ts +1 -1
  75. package/src/lint/audit-lineage.ts +2 -0
  76. package/src/lint/post-synth/checks.test.ts +34 -0
  77. package/src/lint/post-synth/index.ts +2 -0
  78. package/src/lint/post-synth/prom-helpers.ts +10 -1
  79. package/src/lint/post-synth/prom225.ts +19 -0
  80. package/src/model.ts +1 -0
  81. package/src/plugin.test.ts +1 -0
  82. package/src/plugin.ts +3 -2
  83. package/src/prometheus-config.test.ts +230 -0
  84. package/src/prometheus-config.ts +71 -0
  85. package/src/schemastore.test.ts +142 -0
  86. package/src/serializer.ts +31 -21
  87. package/src/tools.ts +31 -3
  88. package/src/validate-config.ts +2 -1
  89. package/src/validate-relabel.ts +53 -0
  90. package/src/validate.test.ts +17 -0
  91. package/src/validate.ts +27 -3
package/src/catalog.ts CHANGED
@@ -6,13 +6,14 @@
6
6
 
7
7
  import type { LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
8
8
  import { RULE_GROUP_TYPE } from "./rules";
9
+ import { PROMETHEUS_CONFIG_TYPE, SCRAPE_CONFIG_TYPE } from "./prometheus-config";
9
10
  import { INHIBIT_RULE_TYPE, RECEIVER_TYPE, ROUTE_TYPE, SETTINGS_TYPE, TIME_INTERVAL_TYPE } from "./alertmanager";
10
11
 
11
12
  export interface CatalogEntry {
12
13
  className: string;
13
14
  entityType: string;
14
15
  /** The file the entity serializes into. */
15
- file: "rule file" | "alertmanager.yml";
16
+ file: "rule file" | "prometheus.yml" | "alertmanager.yml";
16
17
  description: string;
17
18
  }
18
19
 
@@ -23,6 +24,18 @@ export const CATALOG: CatalogEntry[] = [
23
24
  file: "rule file",
24
25
  description: "A group of recording and alerting rules, evaluated together on one interval",
25
26
  },
27
+ {
28
+ className: "ScrapeConfig",
29
+ entityType: SCRAPE_CONFIG_TYPE,
30
+ file: "prometheus.yml",
31
+ description: "One scrape job: static or discovered targets, scrape settings, and relabelling",
32
+ },
33
+ {
34
+ className: "PrometheusConfig",
35
+ entityType: PROMETHEUS_CONFIG_TYPE,
36
+ file: "prometheus.yml",
37
+ description: "The global, alerting, rule_files, remote_write, remote_read and otlp sections of prometheus.yml",
38
+ },
26
39
  {
27
40
  className: "Route",
28
41
  entityType: ROUTE_TYPE,
@@ -4,11 +4,12 @@
4
4
  */
5
5
  import { describe, expect, test } from "vitest";
6
6
  import { load } from "js-yaml";
7
- import { Slo, sloMetrics, sloPropsProblem, type SloProps } from "./slo";
7
+ import { Slo, eventCount, sliExprProblem, sloMetrics, sloPropsProblem, type SloProps } from "./slo";
8
8
  import { ruleGroupConfig } from "../rules";
9
9
  import { ruleFileYaml } from "../build";
10
10
  import { validateRuleFile, validateSeverityRouting } from "../validate-config";
11
11
  import { hasTool, promtoolCheckRules } from "../tools";
12
+ import { RuleEvaluator } from "../rule-eval";
12
13
  import type { AlertingRuleConfig, RecordingRuleConfig, RuleFileConfig } from "../model";
13
14
 
14
15
  const PROMTOOL = process.env.PROMTOOL ?? "promtool";
@@ -179,11 +180,52 @@ describe("the rules an Slo builds", () => {
179
180
  expect(alertsOf(direct)).toEqual([]);
180
181
  const rec = recordings(direct);
181
182
  expect(rec.map((r) => r.record)).toEqual(["slo:sli_error:ratio_rate28d", "slo:objective:ratio", "slo:error_budget:remaining"]);
182
- expect(rec[0].expr).toBe("(sum(rate(e_total[28d])))\n/\n(sum(rate(t_total[28d])))");
183
+ expect(rec[0].expr).toBe(
184
+ "((sum(rate(e_total[28d]))) or 0 * ((sum(rate(t_total[28d]))) > 0))\n/\n((sum(rate(t_total[28d]))) > 0)",
185
+ );
183
186
  expect(sloMetrics(direct).group).toBe("slo-direct");
184
187
  expect(sloMetrics(direct).burnRates).toEqual([]);
185
188
  });
186
189
 
190
+ test("a window with no events records nothing, so a sparse SLI keeps a finite budget", () => {
191
+ const sparseProps: SloProps = {
192
+ name: "sparse",
193
+ objective: 0.9,
194
+ window: "1h",
195
+ sli: { errors: "sum(increase(e_total[{{window}}]))", total: "sum(increase(t_total[{{window}}]))" },
196
+ alerting: { page: { burnRates: [{ long: "30m", short: "5m", factor: 2 }] }, ticket: false },
197
+ };
198
+ const sparse = Slo(sparseProps);
199
+ const ratio = recordings(sparse)[0].expr;
200
+ expect(ratio).toBe("((sum(increase(e_total[5m]))) or 0 * ((sum(increase(t_total[5m]))) > 0))\n/\n((sum(increase(t_total[5m]))) > 0)");
201
+ const good = Slo({ ...sparseProps, name: "sparse-good", sli: { good: "sum(increase(g_total[{{window}}]))", total: "sum(increase(t_total[{{window}}]))" } });
202
+ expect(recordings(good)[0].expr).toBe(
203
+ "1 - (\n ((sum(increase(g_total[5m]))) or 0 * ((sum(increase(t_total[5m]))) > 0))\n /\n ((sum(increase(t_total[5m]))) > 0)\n)",
204
+ );
205
+
206
+ // Counters that sit still except for two events at minute 20, one of them bad.
207
+ // Sampled every minute: the evaluator's range is (t - range, t], so a 5m range
208
+ // over 5m samples holds one point and increase() has nothing to subtract.
209
+ const ev = new RuleEvaluator([ruleGroupConfig(sparse.rules)]);
210
+ const MIN = 60_000;
211
+ let t = 0;
212
+ let e = 0;
213
+ for (let minute = 0; minute <= 50; minute += 1) {
214
+ if (minute === 20) {
215
+ t = 2;
216
+ e = 1;
217
+ }
218
+ ev.add({ __name__: "e_total" }, minute * MIN, e);
219
+ ev.add({ __name__: "t_total" }, minute * MIN, t);
220
+ ev.step(minute * MIN);
221
+ }
222
+ const at = (name: string) => ev.query(`${name}{slo="sparse"}`, 50 * MIN);
223
+ expect(at("slo:sli_error:ratio_rate5m")).toEqual([]); // minutes 30 to 50 saw no events
224
+ const budget = at("slo:error_budget:remaining");
225
+ expect(budget).toHaveLength(1);
226
+ expect(Number.isFinite(budget[0].value)).toBe(true);
227
+ });
228
+
187
229
  test.skipIf(!hasPromtool)("the same Slo renders inside the rule file promtool accepts", () => {
188
230
  const r = promtoolCheckRules(ruleFileYaml([slo.rules]), PROMTOOL);
189
231
  expect(r.ran).toBe(true);
@@ -251,3 +293,31 @@ describe("sloMetrics", () => {
251
293
  expect(sloMetrics(slo).errorRatio["1h"]).toBe("slo:sli_error:ratio_rate1h");
252
294
  });
253
295
  });
296
+
297
+ describe("eventCount (#3548)", () => {
298
+ const sel = 'jobs_total{result="ok"}';
299
+
300
+ test("counts a series' first sample: the increase where the series existed, its own value where it did not", () => {
301
+ expect(eventCount(sel)).toBe(
302
+ 'sum(clamp_min(jobs_total{result="ok"} - jobs_total{result="ok"} offset {{window}}, 0) or (jobs_total{result="ok"} unless jobs_total{result="ok"} offset {{window}}))',
303
+ );
304
+ });
305
+
306
+ test("takes a literal window for use outside an Slo", () => {
307
+ expect(eventCount("jobs_total", "1h")).toBe(
308
+ "sum(clamp_min(jobs_total - jobs_total offset 1h, 0) or (jobs_total unless jobs_total offset 1h))",
309
+ );
310
+ });
311
+
312
+ test("is an SLI expression an Slo accepts", () => {
313
+ expect(sliExprProblem(eventCount(sel))).toBeUndefined();
314
+ expect(sloPropsProblem({ ...orderAck, sli: { errors: eventCount('jobs_total{result="bad"}'), total: eventCount("jobs_total") } })).toBeUndefined();
315
+ });
316
+
317
+ test("rejects a selector with a range, an offset or a function, and an empty one", () => {
318
+ expect(() => eventCount("")).toThrow(/selector/);
319
+ expect(() => eventCount("jobs_total[5m]")).toThrow(/plain instant vector/);
320
+ expect(() => eventCount("jobs_total offset 5m")).toThrow(/plain instant vector/);
321
+ expect(() => eventCount("sum(jobs_total)")).toThrow(/plain instant vector/);
322
+ });
323
+ });
@@ -7,6 +7,7 @@ export {
7
7
  sloMetrics,
8
8
  sloPropsProblem,
9
9
  sliExprProblem,
10
+ eventCount,
10
11
  DEFAULT_BURN_RATES,
11
12
  SLO_WINDOW_PLACEHOLDER,
12
13
  } from "./slo";
@@ -14,6 +14,16 @@
14
14
  * pair's factor: the long window keeps a short spike from paging, the
15
15
  * short window stops the alert soon after the burn does.
16
16
  *
17
+ * An SLI that sees no events in a window (a sparse one: a job that runs a few
18
+ * times a day) records no ratio for that window, where good/total would be
19
+ * 0/0 and record NaN. The SLO window's ratio is an average of the shortest
20
+ * recorded ratio, and one NaN in a range keeps `avg_over_time` NaN for the
21
+ * whole window, so the idle windows are left out and the average covers only
22
+ * the windows that had events. The SLI expressions are the caller's: `rate()`
23
+ * over a counter that first appears inside the range sees one sample and
24
+ * counts nothing, so a sparse SLI should count events as an increase that
25
+ * includes a series' first sample (`eventCount`, see the SLOs page).
26
+ *
17
27
  * Both kinds share a group because Prometheus evaluates a group's rules in
18
28
  * order: the alerts read the ratios recorded in the same evaluation. In
19
29
  * separate groups they would read the previous evaluation's, and with an
@@ -337,6 +347,27 @@ export function sliExprProblem(expr: unknown): string | undefined {
337
347
  return undefined;
338
348
  }
339
349
 
350
+ /**
351
+ * The number of events a counter saw over a window, counting a series' first
352
+ * sample. `rate()` and `increase()` need two samples in the range, so a counter
353
+ * that first appears inside the window reads as no events for the first one.
354
+ * This is the increase over the window where the series existed before it
355
+ * (never below 0, so a counter reset counts nothing), and the series' own
356
+ * value where it did not. Use it for `good`, `errors` and `total` of a sparse
357
+ * SLI; `window` defaults to the `{{window}}` placeholder an `Slo` fills in.
358
+ *
359
+ * `selector` is an instant vector selector, e.g. `jobs_total{result="ok"}`. It
360
+ * appears three times, so it must have no range and no `offset` of its own.
361
+ */
362
+ export function eventCount(selector: string, window: string = SLO_WINDOW_PLACEHOLDER): string {
363
+ const sel = typeof selector === "string" ? selector.trim() : "";
364
+ if (sel === "") throw new Error("eventCount: selector must be an instant vector selector, e.g. jobs_total{result=\"ok\"}");
365
+ if (/\boffset\b/.test(sel) || /\[[^\]]*\]\s*$/.test(sel) || /^(sum|rate|increase)\b/.test(sel)) {
366
+ throw new Error(`eventCount: "${sel}" must be a plain instant vector selector, with no range, offset or function`);
367
+ }
368
+ return `sum(clamp_min(${sel} - ${sel} offset ${window}, 0) or (${sel} unless ${sel} offset ${window}))`;
369
+ }
370
+
340
371
  function withWindow(expr: string, window: string): string {
341
372
  return expr.split(SLO_WINDOW_PLACEHOLDER).join(window);
342
373
  }
@@ -379,10 +410,18 @@ function metricsOf(r: Resolved): SloMetrics {
379
410
  function recordingRules(r: Resolved, m: SloMetrics): RecordingRule[] {
380
411
  const { props } = r;
381
412
  const sli = props.sli as { good?: string; errors?: string; total: string };
382
- const ratio = (w: string) =>
383
- sli.errors !== undefined
384
- ? `(${withWindow(sli.errors, w)})\n/\n(${withWindow(sli.total, w)})`
385
- : `1 - (\n (${withWindow(sli.good!, w)})\n /\n (${withWindow(sli.total, w)})\n)`;
413
+ // A window with no events records nothing: `total > 0` drops the idle
414
+ // window's 0/0 NaN, and the SLO window's average then covers only the
415
+ // windows that had events. The numerator falls back to `0 * total`, so a
416
+ // window whose events were all good (or all bad) still records 0 (or 1)
417
+ // where its own series is absent.
418
+ const ratio = (w: string) => {
419
+ const total = `(${withWindow(sli.total, w)}) > 0`;
420
+ const some = (expr: string) => `((${expr}) or 0 * (${total}))`;
421
+ return sli.errors !== undefined
422
+ ? `${some(withWindow(sli.errors, w))}\n/\n(${total})`
423
+ : `1 - (\n ${some(withWindow(sli.good!, w))}\n /\n (${total})\n)`;
424
+ };
386
425
  const labels = { slo: props.name };
387
426
  const rules: RecordingRule[] = [];
388
427
  const alertWindows = m.windows.slice(0, -1);
@@ -0,0 +1,327 @@
1
+ /**
2
+ * The plain-data model of `prometheus.yml`: global settings, scrape configs,
3
+ * relabelling, service discovery, Alertmanager targets and remote storage.
4
+ *
5
+ * Written by hand against Prometheus v3.15.0 (`config/config.go` and
6
+ * `discovery/*`), field names in the file's own snake_case. SchemaStore's
7
+ * `prometheus.json` is not used to generate these: it is strict about unknown
8
+ * keys and behind v3.15.0 (no `runtime`, `otlp` or `tracing`, 11 of about 20
9
+ * `global` keys, 24 of 32 discovery kinds), so types generated from it would
10
+ * reject configs Prometheus accepts.
11
+ *
12
+ * Six discovery kinds are typed: kubernetes, file, http, dns, ec2 and consul.
13
+ * Any other `<kind>_sd_configs` key is carried as untyped data.
14
+ */
15
+
16
+ import type { HttpClientConfig } from "./integrations";
17
+ import type { LabelSet } from "./model";
18
+
19
+ /** One `static_configs` entry: fixed targets and the labels they share. */
20
+ export interface StaticConfig {
21
+ /** `host:port` addresses to scrape (or, under `alerting`, to send alerts to). */
22
+ targets: string[];
23
+ labels?: LabelSet;
24
+ }
25
+
26
+ /** The actions a relabel step can take. */
27
+ export type RelabelAction =
28
+ | "replace"
29
+ | "lowercase"
30
+ | "uppercase"
31
+ | "keep"
32
+ | "drop"
33
+ | "keepequal"
34
+ | "dropequal"
35
+ | "hashmod"
36
+ | "labelmap"
37
+ | "labeldrop"
38
+ | "labelkeep";
39
+
40
+ /** One `relabel_configs` / `metric_relabel_configs` / `write_relabel_configs` step. */
41
+ export interface RelabelConfig {
42
+ /** Labels whose values are joined with `separator` and matched against `regex`. */
43
+ source_labels?: string[];
44
+ /** Defaults to `;`. */
45
+ separator?: string;
46
+ target_label?: string;
47
+ /** An RE2 regex, anchored. Defaults to `(.*)`. */
48
+ regex?: string;
49
+ modulus?: number;
50
+ /** Defaults to `$1`. */
51
+ replacement?: string;
52
+ /** Defaults to `replace`. */
53
+ action?: RelabelAction;
54
+ }
55
+
56
+ // ── Service discovery ───────────────────────────────────────────────
57
+
58
+ /** `kubernetes_sd_configs` entry. */
59
+ export interface KubernetesSDConfig extends HttpClientConfig {
60
+ role: "pod" | "service" | "endpoints" | "endpointslice" | "node" | "ingress";
61
+ kubeconfig_file?: string;
62
+ api_server?: string;
63
+ namespaces?: { own_namespace?: boolean; names?: string[] };
64
+ selectors?: Array<{ role: string; label?: string; field?: string }>;
65
+ attach_metadata?: { node?: boolean; namespace?: boolean };
66
+ }
67
+
68
+ /** `file_sd_configs` entry: target files Prometheus re-reads. */
69
+ export interface FileSDConfig {
70
+ /** Paths or globs of YAML and JSON target files. */
71
+ files: string[];
72
+ refresh_interval?: string;
73
+ }
74
+
75
+ /** `http_sd_configs` entry: targets fetched from a URL. */
76
+ export interface HttpSDConfig extends HttpClientConfig {
77
+ url: string;
78
+ refresh_interval?: string;
79
+ }
80
+
81
+ /** `dns_sd_configs` entry. */
82
+ export interface DnsSDConfig {
83
+ names: string[];
84
+ /** `SRV` (default), `A`, `AAAA`, `MX` or `NS`. */
85
+ type?: "SRV" | "A" | "AAAA" | "MX" | "NS";
86
+ /** Required for every type but `SRV`. */
87
+ port?: number;
88
+ refresh_interval?: string;
89
+ }
90
+
91
+ /** `ec2_sd_configs` entry. */
92
+ export interface Ec2SDConfig extends HttpClientConfig {
93
+ region?: string;
94
+ endpoint?: string;
95
+ /** A credential: prefer the environment or an instance role. */
96
+ access_key?: string;
97
+ secret_key?: string;
98
+ profile?: string;
99
+ role_arn?: string;
100
+ refresh_interval?: string;
101
+ port?: number;
102
+ filters?: Array<{ name: string; values: string[] }>;
103
+ }
104
+
105
+ /** `consul_sd_configs` entry. */
106
+ export interface ConsulSDConfig extends HttpClientConfig {
107
+ server?: string;
108
+ /** A credential. */
109
+ token?: string;
110
+ datacenter?: string;
111
+ namespace?: string;
112
+ partition?: string;
113
+ scheme?: "http" | "https";
114
+ username?: string;
115
+ password?: string;
116
+ allow_stale?: boolean;
117
+ services?: string[];
118
+ tags?: string[];
119
+ tag_separator?: string;
120
+ node_meta?: Record<string, string>;
121
+ refresh_interval?: string;
122
+ filter?: string;
123
+ }
124
+
125
+ /** The service discovery lists of a scrape config or an Alertmanager target group. */
126
+ export interface ServiceDiscoveryConfigs {
127
+ kubernetes_sd_configs?: KubernetesSDConfig[];
128
+ file_sd_configs?: FileSDConfig[];
129
+ http_sd_configs?: HttpSDConfig[];
130
+ dns_sd_configs?: DnsSDConfig[];
131
+ ec2_sd_configs?: Ec2SDConfig[];
132
+ consul_sd_configs?: ConsulSDConfig[];
133
+ /** Any other discovery kind (`azure_sd_configs`, `gce_sd_configs`, ...), carried as written. */
134
+ [other: `${string}_sd_configs`]: unknown[] | undefined;
135
+ }
136
+
137
+ /** The discovery kinds typed above. */
138
+ export const TYPED_SD_KINDS = Object.freeze([
139
+ "kubernetes_sd_configs",
140
+ "file_sd_configs",
141
+ "http_sd_configs",
142
+ "dns_sd_configs",
143
+ "ec2_sd_configs",
144
+ "consul_sd_configs",
145
+ ]);
146
+
147
+ // ── Scrape configs ──────────────────────────────────────────────────
148
+
149
+ /** Limits and histogram settings that exist both in `global` and per scrape config. */
150
+ export interface ScrapeDefaults {
151
+ scrape_interval?: string;
152
+ scrape_timeout?: string;
153
+ /** e.g. `PrometheusProto`, `OpenMetricsText1.0.0`, `PrometheusText0.0.4`. */
154
+ scrape_protocols?: string[];
155
+ body_size_limit?: string;
156
+ sample_limit?: number;
157
+ label_limit?: number;
158
+ label_name_length_limit?: number;
159
+ label_value_length_limit?: number;
160
+ target_limit?: number;
161
+ keep_dropped_targets?: number;
162
+ always_scrape_classic_histograms?: boolean;
163
+ convert_classic_histograms_to_nhcb?: boolean;
164
+ metric_name_validation_scheme?: "utf8" | "legacy";
165
+ metric_name_escaping_scheme?: "allow-utf-8" | "underscores" | "dots" | "values";
166
+ native_histogram_bucket_limit?: number;
167
+ native_histogram_min_bucket_factor?: number;
168
+ }
169
+
170
+ /** One scrape job. */
171
+ export interface ScrapeJobConfig extends ScrapeDefaults, HttpClientConfig, ServiceDiscoveryConfigs {
172
+ /** Unique across the file; the `job` label on every target. */
173
+ job_name: string;
174
+ honor_labels?: boolean;
175
+ honor_timestamps?: boolean;
176
+ track_timestamps_staleness?: boolean;
177
+ fallback_scrape_protocol?: string;
178
+ /** Defaults to `/metrics`. */
179
+ metrics_path?: string;
180
+ /** `http` (default) or `https`. */
181
+ scheme?: "http" | "https";
182
+ /** URL query parameters sent with every scrape. */
183
+ params?: Record<string, string[]>;
184
+ enable_compression?: boolean;
185
+ static_configs?: StaticConfig[];
186
+ relabel_configs?: RelabelConfig[];
187
+ metric_relabel_configs?: RelabelConfig[];
188
+ }
189
+
190
+ // ── Top-level sections ──────────────────────────────────────────────
191
+
192
+ /** `global:`. */
193
+ export interface PrometheusGlobalConfig extends ScrapeDefaults {
194
+ evaluation_interval?: string;
195
+ rule_query_offset?: string;
196
+ /** Labels added to every series sent to remote storage and every alert. */
197
+ external_labels?: LabelSet;
198
+ query_log_file?: string;
199
+ scrape_failure_log_file?: string;
200
+ }
201
+
202
+ /** One `alerting.alertmanagers` entry. */
203
+ export interface AlertmanagerTarget extends HttpClientConfig, ServiceDiscoveryConfigs {
204
+ timeout?: string;
205
+ api_version?: "v2";
206
+ path_prefix?: string;
207
+ scheme?: "http" | "https";
208
+ relabel_configs?: RelabelConfig[];
209
+ alert_relabel_configs?: RelabelConfig[];
210
+ static_configs?: StaticConfig[];
211
+ }
212
+
213
+ /** `alerting:`. */
214
+ export interface PrometheusAlertingConfig {
215
+ alert_relabel_configs?: RelabelConfig[];
216
+ alertmanagers?: AlertmanagerTarget[];
217
+ }
218
+
219
+ /** `remote_write[].queue_config`. */
220
+ export interface QueueConfig {
221
+ capacity?: number;
222
+ max_shards?: number;
223
+ min_shards?: number;
224
+ max_samples_per_send?: number;
225
+ batch_send_deadline?: string;
226
+ min_backoff?: string;
227
+ max_backoff?: string;
228
+ retry_on_http_429?: boolean;
229
+ sample_age_limit?: string;
230
+ }
231
+
232
+ /** One `remote_write` entry. */
233
+ export interface RemoteWriteConfig extends HttpClientConfig {
234
+ url: string;
235
+ name?: string;
236
+ remote_timeout?: string;
237
+ headers?: Record<string, string>;
238
+ write_relabel_configs?: RelabelConfig[];
239
+ send_exemplars?: boolean;
240
+ send_native_histograms?: boolean;
241
+ queue_config?: QueueConfig;
242
+ metadata_config?: { send?: boolean; send_interval?: string; max_samples_per_send?: number };
243
+ sigv4?: {
244
+ region?: string;
245
+ access_key?: string;
246
+ secret_key?: string;
247
+ profile?: string;
248
+ role_arn?: string;
249
+ };
250
+ azuread?: Record<string, unknown>;
251
+ google_iam?: Record<string, unknown>;
252
+ }
253
+
254
+ /** One `remote_read` entry. */
255
+ export interface RemoteReadConfig extends HttpClientConfig {
256
+ url: string;
257
+ name?: string;
258
+ remote_timeout?: string;
259
+ headers?: Record<string, string>;
260
+ read_recent?: boolean;
261
+ required_matchers?: LabelSet;
262
+ filter_external_labels?: boolean;
263
+ }
264
+
265
+ /** `otlp:`: how OTLP metrics pushed to Prometheus become series. */
266
+ export interface OtlpConfig {
267
+ promote_all_resource_attributes?: boolean;
268
+ promote_resource_attributes?: string[];
269
+ ignore_resource_attributes?: string[];
270
+ translation_strategy?: "UnderscoreEscapingWithSuffixes" | "UnderscoreEscapingWithoutSuffixes" | "NoUTF8EscapingWithSuffixes" | "NoTranslation";
271
+ keep_identifying_resource_attributes?: boolean;
272
+ convert_histograms_to_nhcb?: boolean;
273
+ }
274
+
275
+ /** The sections of a `prometheus.yml` besides its scrape jobs. */
276
+ export interface PrometheusConfigSections {
277
+ global?: PrometheusGlobalConfig;
278
+ alerting?: PrometheusAlertingConfig;
279
+ /** Paths or globs of rule files, relative to the config file. */
280
+ rule_files?: string[];
281
+ /** Paths or globs of files holding more `scrape_configs`. */
282
+ scrape_config_files?: string[];
283
+ remote_write?: RemoteWriteConfig[];
284
+ remote_read?: RemoteReadConfig[];
285
+ otlp?: OtlpConfig;
286
+ /** TSDB and exemplar settings, carried as written. */
287
+ storage?: Record<string, unknown>;
288
+ /** Where Prometheus sends its own traces, carried as written. */
289
+ tracing?: Record<string, unknown>;
290
+ /** Go runtime settings (`gogc`), carried as written. */
291
+ runtime?: Record<string, unknown>;
292
+ }
293
+
294
+ /** A whole `prometheus.yml`. */
295
+ export interface PrometheusConfigFile extends PrometheusConfigSections {
296
+ scrape_configs?: ScrapeJobConfig[];
297
+ }
298
+
299
+ /** Top-level keys of `prometheus.yml`, in the order Prometheus documents them. */
300
+ export const PROMETHEUS_CONFIG_SECTIONS = Object.freeze([
301
+ "global",
302
+ "alerting",
303
+ "rule_files",
304
+ "scrape_config_files",
305
+ "scrape_configs",
306
+ "remote_write",
307
+ "remote_read",
308
+ "otlp",
309
+ "storage",
310
+ "tracing",
311
+ "runtime",
312
+ ]);
313
+
314
+ const GLOBAL_ONLY_KEYS = ["scrape_interval", "scrape_timeout", "evaluation_interval", "external_labels", "rule_query_offset", "query_log_file"];
315
+
316
+ /** True when a parsed document has the shape of a `prometheus.yml`. */
317
+ export function looksLikePrometheusConfig(value: unknown): value is PrometheusConfigFile {
318
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
319
+ const v = value as Record<string, unknown>;
320
+ if ("apiVersion" in v || "kind" in v || "groups" in v || "route" in v || "receivers" in v) return false;
321
+ if (PROMETHEUS_CONFIG_SECTIONS.some((k) => k !== "global" && k !== "runtime" && k !== "storage" && k !== "tracing" && k in v)) {
322
+ return true;
323
+ }
324
+ // `global:` alone is Alertmanager's too; only Prometheus's own keys settle it.
325
+ const g = v.global;
326
+ return typeof g === "object" && g !== null && GLOBAL_ONLY_KEYS.some((k) => k in g);
327
+ }
package/src/detect.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Template detection for the prometheus lexicon: a parsed document is ours
3
- * when it is shaped like a rule file (`groups:` of named rule lists) or an
4
- * `alertmanager.yml` (`route:` or `receivers:` at the top level). Kept free
3
+ * when it is shaped like a rule file (`groups:` of named rule lists), an
4
+ * `alertmanager.yml` (`route:` or `receivers:` at the top level) or a
5
+ * `prometheus.yml` (`scrape_configs:`, `remote_write:` and the like). Kept free
5
6
  * of the plugin and the TypeScript compiler so it bundles for edge runtimes,
6
7
  * like the other lexicons' `detect` modules.
7
8
  */
8
- import { looksLikeAlertmanagerConfig, looksLikeRuleFile } from "./model";
9
+ import { looksLikeAlertmanagerConfig, looksLikePrometheusConfig, looksLikeRuleFile } from "./model";
9
10
 
10
11
  export function detectTemplate(data: unknown): boolean {
11
- return looksLikeRuleFile(data) || looksLikeAlertmanagerConfig(data);
12
+ return looksLikeRuleFile(data) || looksLikeAlertmanagerConfig(data) || looksLikePrometheusConfig(data);
12
13
  }
@@ -9,6 +9,11 @@
9
9
  "kind": "resource",
10
10
  "lexicon": "prometheus"
11
11
  },
12
+ "PrometheusConfig": {
13
+ "resourceType": "Prometheus::Config::PrometheusConfig",
14
+ "kind": "resource",
15
+ "lexicon": "prometheus"
16
+ },
12
17
  "Receiver": {
13
18
  "resourceType": "Prometheus::Alertmanager::Receiver",
14
19
  "kind": "resource",
@@ -24,6 +29,11 @@
24
29
  "kind": "resource",
25
30
  "lexicon": "prometheus"
26
31
  },
32
+ "ScrapeConfig": {
33
+ "resourceType": "Prometheus::Config::ScrapeConfig",
34
+ "kind": "resource",
35
+ "lexicon": "prometheus"
36
+ },
27
37
  "TimeInterval": {
28
38
  "resourceType": "Prometheus::Alertmanager::TimeInterval",
29
39
  "kind": "resource",
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "vitest";
2
2
  import { embeddedDocument, type EmbeddedContent } from "@intentius/chant/import/embedded";
3
3
  import { prometheusPlugin } from "../plugin";
4
- import { alertmanagerImporter, ruleGroupsImporter } from "./embedded";
4
+ import { alertmanagerImporter, prometheusConfigImporter, ruleGroupsImporter } from "./embedded";
5
5
 
6
6
  const GROUPS = [
7
7
  { name: "api", rules: [{ alert: "ApiDown", expr: "up{job=\"api\"} == 0", for: "5m" }] },
@@ -26,7 +26,7 @@ const site = (over: Partial<EmbeddedContent>): EmbeddedContent => ({
26
26
 
27
27
  describe("rule groups embedded in another lexicon's resource (#2962)", () => {
28
28
  test("the plugin registers the importers", () => {
29
- expect(prometheusPlugin.embeddedImporters?.()).toEqual([ruleGroupsImporter, alertmanagerImporter]);
29
+ expect(prometheusPlugin.embeddedImporters?.()).toEqual([ruleGroupsImporter, alertmanagerImporter, prometheusConfigImporter]);
30
30
  });
31
31
 
32
32
  test("matches a PrometheusRule's groups and a rule file held as text; not an alertmanager.yml", () => {
@@ -103,3 +103,47 @@ describe("an alertmanager.yml embedded in another lexicon's resource (#3031)", (
103
103
  expect(out.warnings).toEqual([]);
104
104
  });
105
105
  });
106
+
107
+ const PROMETHEUS = `global:
108
+ scrape_interval: 15s
109
+ rule_files:
110
+ - /etc/prometheus/rules.yml
111
+ scrape_configs:
112
+ - job_name: node
113
+ static_configs:
114
+ - targets: ["node:9100"]
115
+ - job_name: api
116
+ metrics_path: /metrics
117
+ static_configs:
118
+ - targets: ["api:8080"]
119
+ `;
120
+
121
+ describe("a prometheus.yml embedded in another lexicon's resource (#3365)", () => {
122
+ const configMap = (text: string) =>
123
+ site({ hostType: "K8s::Core::ConfigMap", location: 'ConfigMap prometheus data["prometheus.yml"]', text, document: embeddedDocument(text) });
124
+
125
+ test("matches a prometheus.yml held as text; not a rule file, an alertmanager.yml, nor a selected member", () => {
126
+ expect(prometheusConfigImporter.matches(configMap(PROMETHEUS))).toBe(true);
127
+ expect(prometheusConfigImporter.matches(configMap(RULE_FILE))).toBe(false);
128
+ expect(prometheusConfigImporter.matches(configMap(ALERTMANAGER))).toBe(false);
129
+ expect(prometheusConfigImporter.matches(configMap("storage:\n path: /data\n"))).toBe(false);
130
+ expect(prometheusConfigImporter.matches(site({ document: { scrape_configs: [] }, select: "groups" }))).toBe(false);
131
+ expect(ruleGroupsImporter.matches(configMap(PROMETHEUS))).toBe(false);
132
+ expect(alertmanagerImporter.matches(configMap(PROMETHEUS))).toBe(false);
133
+ });
134
+
135
+ test("becomes prometheusConfigYaml over every declaration the standalone import writes", () => {
136
+ const out = prometheusConfigImporter.import(configMap(PROMETHEUS));
137
+ expect(out.files.map((f) => f.path)).toEqual(["scrape-configs.ts", "prometheus.ts"]);
138
+ expect(out.value).toEqual({
139
+ bindings: [
140
+ { from: "scrape-configs.ts", name: "node" },
141
+ { from: "scrape-configs.ts", name: "api" },
142
+ { from: "prometheus.ts", name: "prometheus" },
143
+ ],
144
+ shape: "list",
145
+ through: { from: "@intentius/chant-lexicon-prometheus", name: "prometheusConfigYaml" },
146
+ });
147
+ expect(out.warnings).toEqual([]);
148
+ });
149
+ });