@intentius/chant-lexicon-prometheus 0.105.1 → 0.106.1

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 (80) 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/slo.d.ts +10 -0
  7. package/dist/composites/slo.d.ts.map +1 -1
  8. package/dist/config-model.d.ts +272 -0
  9. package/dist/config-model.d.ts.map +1 -0
  10. package/dist/detect.d.ts.map +1 -1
  11. package/dist/import/generator.d.ts +3 -1
  12. package/dist/import/generator.d.ts.map +1 -1
  13. package/dist/import/parser.d.ts +12 -2
  14. package/dist/import/parser.d.ts.map +1 -1
  15. package/dist/import/slo.d.ts.map +1 -1
  16. package/dist/index.d.ts +4 -3
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/integrity.json +5 -4
  19. package/dist/lint/audit-catalog.d.ts.map +1 -1
  20. package/dist/lint/audit-lineage.d.ts +2 -0
  21. package/dist/lint/audit-lineage.d.ts.map +1 -1
  22. package/dist/lint/post-synth/index.d.ts.map +1 -1
  23. package/dist/lint/post-synth/prom-helpers.d.ts +7 -0
  24. package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -1
  25. package/dist/lint/post-synth/prom225.d.ts +10 -0
  26. package/dist/lint/post-synth/prom225.d.ts.map +1 -0
  27. package/dist/manifest.json +1 -1
  28. package/dist/meta.json +10 -0
  29. package/dist/model.d.ts +1 -0
  30. package/dist/model.d.ts.map +1 -1
  31. package/dist/okf/index.md +3 -0
  32. package/dist/okf/rules/PROM225.md +11 -0
  33. package/dist/okf/types/PrometheusConfig.md +9 -0
  34. package/dist/okf/types/ScrapeConfig.md +9 -0
  35. package/dist/plugin.d.ts.map +1 -1
  36. package/dist/prometheus-config.d.ts +39 -0
  37. package/dist/prometheus-config.d.ts.map +1 -0
  38. package/dist/rules/prom-helpers.ts +10 -1
  39. package/dist/rules/prom225.ts +19 -0
  40. package/dist/serializer.d.ts +14 -10
  41. package/dist/serializer.d.ts.map +1 -1
  42. package/dist/tools.d.ts +9 -1
  43. package/dist/tools.d.ts.map +1 -1
  44. package/dist/validate-config.d.ts +1 -1
  45. package/dist/validate-config.d.ts.map +1 -1
  46. package/dist/validate-relabel.d.ts +14 -0
  47. package/dist/validate-relabel.d.ts.map +1 -0
  48. package/dist/validate.d.ts.map +1 -1
  49. package/package.json +2 -2
  50. package/src/build.ts +73 -0
  51. package/src/catalog.ts +14 -1
  52. package/src/composites/composites.test.ts +43 -1
  53. package/src/composites/slo.ts +22 -4
  54. package/src/config-model.ts +327 -0
  55. package/src/detect.ts +5 -4
  56. package/src/generated/lexicon-prometheus.json +10 -0
  57. package/src/import/generator.ts +120 -0
  58. package/src/import/parser.test.ts +2 -2
  59. package/src/import/parser.ts +72 -5
  60. package/src/import/prometheus-roundtrip.test.ts +148 -0
  61. package/src/import/slo.ts +15 -0
  62. package/src/import/testdata/prometheus-full.yml +149 -0
  63. package/src/index.ts +20 -2
  64. package/src/lint/audit-catalog.ts +7 -0
  65. package/src/lint/audit-lineage.test.ts +1 -1
  66. package/src/lint/audit-lineage.ts +2 -0
  67. package/src/lint/post-synth/checks.test.ts +34 -0
  68. package/src/lint/post-synth/index.ts +2 -0
  69. package/src/lint/post-synth/prom-helpers.ts +10 -1
  70. package/src/lint/post-synth/prom225.ts +19 -0
  71. package/src/model.ts +1 -0
  72. package/src/plugin.test.ts +1 -0
  73. package/src/plugin.ts +1 -0
  74. package/src/prometheus-config.test.ts +230 -0
  75. package/src/prometheus-config.ts +71 -0
  76. package/src/serializer.ts +31 -21
  77. package/src/tools.ts +31 -3
  78. package/src/validate-config.ts +2 -1
  79. package/src/validate-relabel.ts +53 -0
  80. package/src/validate.ts +1 -1
@@ -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 (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
@@ -379,10 +389,18 @@ function metricsOf(r: Resolved): SloMetrics {
379
389
  function recordingRules(r: Resolved, m: SloMetrics): RecordingRule[] {
380
390
  const { props } = r;
381
391
  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)`;
392
+ // A window with no events records nothing: `total > 0` drops the idle
393
+ // window's 0/0 NaN, and the SLO window's average then covers only the
394
+ // windows that had events. The numerator falls back to `0 * total`, so a
395
+ // window whose events were all good (or all bad) still records 0 (or 1)
396
+ // where its own series is absent.
397
+ const ratio = (w: string) => {
398
+ const total = `(${withWindow(sli.total, w)}) > 0`;
399
+ const some = (expr: string) => `((${expr}) or 0 * (${total}))`;
400
+ return sli.errors !== undefined
401
+ ? `${some(withWindow(sli.errors, w))}\n/\n(${total})`
402
+ : `1 - (\n ${some(withWindow(sli.good!, w))}\n /\n (${total})\n)`;
403
+ };
386
404
  const labels = { slo: props.name };
387
405
  const rules: RecordingRule[] = [];
388
406
  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",
@@ -24,6 +24,8 @@ import type { GeneratedFile, TypeScriptGenerator } from "@intentius/chant/import
24
24
  import type { TemplateIR } from "@intentius/chant/import/parser";
25
25
  import type {
26
26
  AlertmanagerConfig,
27
+ PrometheusConfigFile,
28
+ ScrapeJobConfig,
27
29
  InhibitRuleConfig,
28
30
  ReceiverConfig,
29
31
  RouteConfig,
@@ -35,8 +37,10 @@ import { ALERTMANAGER_GLOBAL_FIELDS, RECEIVER_INTEGRATION_TYPES } from "../model
35
37
  import { PROMETHEUS_PIN } from "../pin";
36
38
  import {
37
39
  ALERTMANAGER_RESOURCE_TYPE,
40
+ PROMETHEUS_RESOURCE_TYPE,
38
41
  RULE_FILE_RESOURCE_TYPE,
39
42
  type AlertmanagerResourceProperties,
43
+ type PrometheusResourceProperties,
40
44
  type RuleFileResourceProperties,
41
45
  } from "./parser";
42
46
  import { recognizeSlo } from "./slo";
@@ -259,6 +263,22 @@ const LEXICON_NAMES = [
259
263
  "TimeInterval",
260
264
  "TimePeriodConfig",
261
265
  "InhibitRule",
266
+ "ScrapeConfig",
267
+ "ScrapeConfigProps",
268
+ "PrometheusConfig",
269
+ "StaticConfig",
270
+ "RelabelConfig",
271
+ "PrometheusGlobalConfig",
272
+ "PrometheusAlertingConfig",
273
+ "RemoteWriteConfig",
274
+ "RemoteReadConfig",
275
+ "OtlpConfig",
276
+ "KubernetesSDConfig",
277
+ "FileSDConfig",
278
+ "HttpSDConfig",
279
+ "DnsSDConfig",
280
+ "Ec2SDConfig",
281
+ "ConsulSDConfig",
262
282
  "AlertmanagerSettings",
263
283
  "AlertmanagerGlobalConfig",
264
284
  "AlertmanagerTracingConfig",
@@ -579,6 +599,104 @@ export function generateAlertmanager(config: AlertmanagerConfig): { files: Gener
579
599
  };
580
600
  }
581
601
 
602
+ // ── prometheus.yml ───────────────────────────────────────────────────
603
+
604
+ /** The type each hoisted scrape config field is declared as; others use `ScrapeConfigProps["field"]`. */
605
+ const SCRAPE_FIELD_TYPES: Record<string, string> = {
606
+ static_configs: "StaticConfig[]",
607
+ relabel_configs: "RelabelConfig[]",
608
+ metric_relabel_configs: "RelabelConfig[]",
609
+ params: "Record<string, string[]>",
610
+ kubernetes_sd_configs: "KubernetesSDConfig[]",
611
+ file_sd_configs: "FileSDConfig[]",
612
+ http_sd_configs: "HttpSDConfig[]",
613
+ dns_sd_configs: "DnsSDConfig[]",
614
+ ec2_sd_configs: "Ec2SDConfig[]",
615
+ consul_sd_configs: "ConsulSDConfig[]",
616
+ };
617
+
618
+ /** The type each top-level section is declared as. */
619
+ const SECTION_TYPES: Record<string, string> = {
620
+ global: "PrometheusGlobalConfig",
621
+ alerting: "PrometheusAlertingConfig",
622
+ remote_write: "RemoteWriteConfig[]",
623
+ remote_read: "RemoteReadConfig[]",
624
+ otlp: "OtlpConfig",
625
+ storage: "Record<string, unknown>",
626
+ tracing: "Record<string, unknown>",
627
+ runtime: "Record<string, unknown>",
628
+ };
629
+
630
+ /** Generate the TypeScript declaring one `prometheus.yml`. */
631
+ export function generatePrometheusConfigFiles(config: PrometheusConfigFile): GeneratedFile[] {
632
+ const names = new Names([...LEXICON_NAMES, ...Object.keys(SECTION_TYPES)]);
633
+ const modules: Module[] = [];
634
+
635
+ for (const [mod, items] of chunk(config.scrape_configs ?? [], "scrape-configs", "Scrape jobs: where Prometheus pulls metrics from")) {
636
+ modules.push(mod);
637
+ mod.use("ScrapeConfig");
638
+ for (const job of items as ScrapeJobConfig[]) {
639
+ const v = names.claim(job.job_name, "Scrape", "scrape");
640
+ const lines: string[] = [];
641
+ const entries: Array<[string, string]> = [["job_name", stringLiteral(job.job_name)]];
642
+ for (const [key, value] of Object.entries(job)) {
643
+ if (key === "job_name" || value === undefined) continue;
644
+ if (!needsHoist(value)) {
645
+ entries.push([key, tsLiteral(value, 2, 4 + key.length)]);
646
+ continue;
647
+ }
648
+ const known = SCRAPE_FIELD_TYPES[key];
649
+ const type = known ?? (key.endsWith("_sd_configs") ? undefined : `ScrapeConfigProps["${key}"]`);
650
+ const c = names.claim(`${v} ${key.replace(/_configs$/, "")}`, "Config", "scrape");
651
+ if (type) {
652
+ if (type.startsWith("ScrapeConfigProps")) mod.types.add("ScrapeConfigProps");
653
+ else if (!type.startsWith("Record<")) mod.types.add(type.replace(/\[\]$/, ""));
654
+ const head = `const ${c}: ${type} = `;
655
+ lines.push(`${head}${tsLiteral(value, 0, head.length)};`);
656
+ } else {
657
+ lines.push(`// ${key} is not a discovery kind the lexicon types, so it is carried as data, untyped.`);
658
+ const head = `const ${c} = `;
659
+ lines.push(`${head}${tsLiteral(value, 0, head.length)};`);
660
+ }
661
+ entries.push([key, c]);
662
+ }
663
+ const ctor = `const ${v} = new ScrapeConfig(`;
664
+ lines.push(`${ctor}${objectOf(entries, 0, ctor.length)});`);
665
+ mod.block(lines);
666
+ mod.exports.push(v);
667
+ }
668
+ }
669
+
670
+ const sectionKeys = Object.keys(config).filter((k) => k !== "scrape_configs");
671
+ if (sectionKeys.length > 0) {
672
+ const mod = new Module("prometheus.ts", "The prometheus.yml sections besides the scrape jobs");
673
+ modules.push(mod);
674
+ mod.use("PrometheusConfig");
675
+ const lines: string[] = [];
676
+ const entries: Array<[string, string]> = [];
677
+ for (const key of sectionKeys) {
678
+ const value = (config as Record<string, unknown>)[key];
679
+ const type = SECTION_TYPES[key];
680
+ if (!type || !needsHoist(value)) {
681
+ entries.push([key, tsLiteral(value, 2, 4 + key.length)]);
682
+ continue;
683
+ }
684
+ if (!type.startsWith("Record<")) mod.types.add(type.replace(/\[\]$/, ""));
685
+ const c = names.claim(key.replace(/_/g, " "), "Config", "prometheus");
686
+ const head = `const ${c}: ${type} = `;
687
+ lines.push(`${head}${tsLiteral(value, 0, head.length)};`);
688
+ entries.push([key, c]);
689
+ }
690
+ const v = names.claim("prometheus", "Settings", "prometheus");
691
+ const ctor = `const ${v} = new PrometheusConfig(`;
692
+ lines.push(`${ctor}${objectOf(entries, 0, ctor.length)});`);
693
+ mod.block(lines);
694
+ mod.exports.push(v);
695
+ }
696
+
697
+ return modules.map((m) => ({ path: m.path, content: m.render() }));
698
+ }
699
+
582
700
  /** The rule file and `alertmanager.yml` TypeScript generator `chant import` runs. */
583
701
  export class PrometheusGenerator implements TypeScriptGenerator {
584
702
  generate(ir: TemplateIR): GeneratedFile[] {
@@ -586,6 +704,8 @@ export class PrometheusGenerator implements TypeScriptGenerator {
586
704
  for (const r of ir.resources) {
587
705
  if (r.type === RULE_FILE_RESOURCE_TYPE) {
588
706
  files.push(...generateRuleFileFiles((r.properties as unknown as RuleFileResourceProperties).file));
707
+ } else if (r.type === PROMETHEUS_RESOURCE_TYPE) {
708
+ files.push(...generatePrometheusConfigFiles((r.properties as unknown as PrometheusResourceProperties).config));
589
709
  } else if (r.type === ALERTMANAGER_RESOURCE_TYPE) {
590
710
  files.push(...generateAlertmanagerFiles((r.properties as unknown as AlertmanagerResourceProperties).config));
591
711
  }
@@ -11,9 +11,9 @@ describe("parsePrometheusYaml", () => {
11
11
  });
12
12
 
13
13
  test("rejects a document that is neither, and one that is not a mapping", () => {
14
- expect(() => parsePrometheusYaml("receivers:\n otlp: {}\n")).toThrow(/neither a Prometheus rule file/);
14
+ expect(() => parsePrometheusYaml("receivers:\n otlp: {}\n")).toThrow(/is not a Prometheus rule file .*an alertmanager\.yml .*or a prometheus\.yml/);
15
15
  expect(() => parsePrometheusYaml("- a\n- b\n")).toThrow(/YAML mapping/);
16
- expect(() => parsePrometheusYaml("")).toThrow(/neither/);
16
+ expect(() => parsePrometheusYaml("")).toThrow(/is not a Prometheus rule file/);
17
17
  });
18
18
 
19
19
  test("a rule's values are strings, label values included, and a group's limit a number", () => {