@intentius/chant-lexicon-prometheus 0.106.1 → 0.106.4

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.
package/src/index.ts CHANGED
@@ -102,6 +102,7 @@ export {
102
102
  sloMetrics,
103
103
  sloPropsProblem,
104
104
  sliExprProblem,
105
+ eventCount,
105
106
  DEFAULT_BURN_RATES,
106
107
  SLO_WINDOW_PLACEHOLDER,
107
108
  type SloProps,
package/src/plugin.ts CHANGED
@@ -11,7 +11,7 @@ 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 { alertmanagerImporter, ruleGroupsImporter } from "./import/embedded";
14
+ import { alertmanagerImporter, prometheusConfigImporter, 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";
@@ -134,7 +134,7 @@ export const prometheusPlugin: LexiconPlugin = {
134
134
  },
135
135
 
136
136
  embeddedImporters() {
137
- return [ruleGroupsImporter, alertmanagerImporter];
137
+ return [ruleGroupsImporter, alertmanagerImporter, prometheusConfigImporter];
138
138
  },
139
139
 
140
140
  // `chant init --lexicon prometheus [--template rules|slo-style|slo]`; see ./init-templates.ts.
@@ -0,0 +1,142 @@
1
+ /**
2
+ * SchemaStore's `prometheus.json` as a cross-check on the `prometheus.yml`
3
+ * model, in tests only (#3365). The schema is vendored as a fixture
4
+ * (import/testdata/schemastore-prometheus.json) so the tests need no network.
5
+ *
6
+ * It is not what the types are generated from: the schema is strict
7
+ * (`additionalProperties: false`) and behind Prometheus v3.15.0, so a config
8
+ * Prometheus accepts can fail it. The tests walk a document down the schema
9
+ * and report every key a strict object does not list. A key in KNOWN_GAPS is
10
+ * one the schema lacks and the lexicon types; anything else is a mismatch.
11
+ */
12
+
13
+ import { describe, expect, test } from "vitest";
14
+ import { load } from "js-yaml";
15
+ import { readFileSync } from "fs";
16
+ import { join } from "path";
17
+ import type { Declarable } from "@intentius/chant/declarable";
18
+ import { prometheusConfigYaml } from "./build";
19
+ import { PROMETHEUS_CONFIG_SECTIONS, TYPED_SD_KINDS } from "./config-model";
20
+ import { PrometheusConfig, ScrapeConfig } from "./prometheus-config";
21
+
22
+ const testdata = join(import.meta.dirname, "import", "testdata");
23
+
24
+ type Schema = { [key: string]: unknown };
25
+ const SCHEMA = JSON.parse(readFileSync(join(testdata, "schemastore-prometheus.json"), "utf-8")) as Schema;
26
+
27
+ /**
28
+ * What SchemaStore's schema lacks that Prometheus v3.15.0 and the lexicon have,
29
+ * by the path to the key (`[]` is a list item).
30
+ */
31
+ const KNOWN_GAPS = [
32
+ "otlp",
33
+ "runtime",
34
+ "scrape_configs.[].kubernetes_sd_configs.[].attach_metadata",
35
+ ];
36
+
37
+ function deref(s: Schema): Schema {
38
+ let cur = s;
39
+ while (typeof cur.$ref === "string") {
40
+ let node: unknown = SCHEMA;
41
+ for (const part of cur.$ref.replace(/^#\//, "").split("/")) node = (node as Schema)[part];
42
+ cur = node as Schema;
43
+ }
44
+ return cur;
45
+ }
46
+
47
+ /** The schema and every `oneOf` / `anyOf` / `allOf` branch of it, references resolved. */
48
+ function branches(s: Schema): Schema[] {
49
+ const self = deref(s);
50
+ const out = [self];
51
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
52
+ for (const b of (self[key] as Schema[] | undefined) ?? []) out.push(...branches(b));
53
+ }
54
+ return out;
55
+ }
56
+
57
+ /** Paths of the keys in `value` that a strict object in `schema` does not list. */
58
+ function unlistedKeys(value: unknown, schema: Schema, path: string[] = [], out: string[] = []): string[] {
59
+ const bs = branches(schema);
60
+ if (Array.isArray(value)) {
61
+ for (const item of value) {
62
+ for (const b of bs) if (b.items) unlistedKeys(item, b.items as Schema, [...path, "[]"], out);
63
+ }
64
+ } else if (typeof value === "object" && value !== null) {
65
+ const props: Record<string, Schema> = {};
66
+ let strict = false;
67
+ const patterns: Array<[RegExp, Schema]> = [];
68
+ for (const b of bs) {
69
+ Object.assign(props, (b.properties as Record<string, Schema> | undefined) ?? {});
70
+ if (b.additionalProperties === false) strict = true;
71
+ for (const [p, sub] of Object.entries((b.patternProperties as Record<string, Schema> | undefined) ?? {})) {
72
+ patterns.push([new RegExp(p), sub]);
73
+ }
74
+ }
75
+ for (const [key, v] of Object.entries(value)) {
76
+ if (key in props) {
77
+ unlistedKeys(v, props[key], [...path, key], out);
78
+ continue;
79
+ }
80
+ const matched = patterns.filter(([re]) => re.test(key));
81
+ for (const [, sub] of matched) unlistedKeys(v, sub, [...path, key], out);
82
+ if (strict && matched.length === 0) out.push([...path, key].join("."));
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+
88
+ const gapsOf = (yaml: string) => [...new Set(unlistedKeys(load(yaml), SCHEMA))].sort();
89
+ const fixture = (name: string) => readFileSync(join(testdata, name), "utf-8");
90
+
91
+ describe("SchemaStore prometheus.json as a cross-check (#3365)", () => {
92
+ test("the vendored schema is SchemaStore's prometheus.json", () => {
93
+ expect(SCHEMA.$id).toBe("https://json.schemastore.org/prometheus.json");
94
+ expect(SCHEMA.additionalProperties).toBe(false);
95
+ });
96
+
97
+ test("the full fixture differs from the schema only by the known gaps", () => {
98
+ expect(gapsOf(fixture("prometheus-full.yml"))).toEqual([...KNOWN_GAPS].sort());
99
+ });
100
+
101
+ test("a config built from the typed sections, without the known gaps, has no key the schema lacks", () => {
102
+ const yaml = prometheusConfigYaml([
103
+ new PrometheusConfig({
104
+ global: { scrape_interval: "15s", scrape_timeout: "10s", evaluation_interval: "30s", external_labels: { cluster: "dev" } },
105
+ alerting: { alertmanagers: [{ scheme: "http", api_version: "v2", static_configs: [{ targets: ["alertmanager:9093"] }] }] },
106
+ rule_files: ["rules.yml"],
107
+ remote_write: [
108
+ {
109
+ url: "http://mimir:9009/api/v1/push",
110
+ queue_config: { capacity: 10000 },
111
+ write_relabel_configs: [{ source_labels: ["__name__"], regex: "debug_.*", action: "drop" }],
112
+ },
113
+ ],
114
+ }),
115
+ new ScrapeConfig({
116
+ job_name: "app",
117
+ scrape_interval: "5s",
118
+ params: { format: ["prometheus"] },
119
+ static_configs: [{ targets: ["app:8080"], labels: { tier: "web" } }],
120
+ metric_relabel_configs: [{ source_labels: ["__name__"], regex: "go_.*", action: "drop" }],
121
+ }),
122
+ new ScrapeConfig({ job_name: "dns", dns_sd_configs: [{ names: ["api.service.internal"], type: "A", port: 9100 }] }),
123
+ ] as Declarable[]);
124
+ expect(gapsOf(yaml)).toEqual([]);
125
+ });
126
+
127
+ test("the keys the lexicon adds to the top level are the ones the schema lacks", () => {
128
+ const schemaKeys = Object.keys(SCHEMA.properties as Record<string, unknown>);
129
+ // The lexicon types every section the schema has.
130
+ expect(schemaKeys.filter((k) => !PROMETHEUS_CONFIG_SECTIONS.includes(k))).toEqual([]);
131
+ // And three the schema predates.
132
+ expect(PROMETHEUS_CONFIG_SECTIONS.filter((k) => !schemaKeys.includes(k))).toEqual(["otlp", "tracing", "runtime"]);
133
+ });
134
+
135
+ test("every typed discovery kind is one the schema lists; the rest are carried untyped", () => {
136
+ const definitions = Object.keys(SCHEMA.definitions as Record<string, unknown>);
137
+ const schemaKinds = definitions.filter((k) => k.endsWith("_sd_configs"));
138
+ expect(schemaKinds).toHaveLength(24);
139
+ expect(TYPED_SD_KINDS.filter((k) => !schemaKinds.includes(k))).toEqual([]);
140
+ expect(schemaKinds.filter((k) => !TYPED_SD_KINDS.includes(k))).toHaveLength(18);
141
+ });
142
+ });
@@ -0,0 +1,17 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { validate } from "./validate";
3
+ import { hasTool } from "./tools";
4
+
5
+ const PROMTOOL = hasTool(process.env.PROMTOOL ?? "promtool");
6
+
7
+ describe("the lexicon's validate()", () => {
8
+ test("passes, and says whether promtool check config ran", async () => {
9
+ const result = await validate();
10
+ expect(result.checks.filter((c) => !c.ok)).toEqual([]);
11
+ expect(result.success).toBe(true);
12
+ const check = result.checks.find((c) => c.name.startsWith("promtool-check-config"));
13
+ expect(check).toBeDefined();
14
+ if (PROMTOOL) expect(check!.name).toBe("promtool-check-config");
15
+ else expect(check!.name).toContain("skipped");
16
+ });
17
+ });
package/src/validate.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * Validate the prometheus lexicon's own artifacts: every class is in the
3
3
  * registry, every entity constructs and serializes, and a sample rule file
4
- * and Alertmanager config pass the lexicon's own checks.
4
+ * and Alertmanager config pass the lexicon's own checks, and `promtool check
5
+ * config` accepts a sample `prometheus.yml` when `promtool` is installed.
5
6
  */
6
7
 
7
8
  import type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
8
9
  import { CATALOG, lexiconRegistry } from "./catalog";
9
10
  import { RuleGroup } from "./rules";
10
11
  import { AlertmanagerSettings, InhibitRule, Receiver, Route, TimeInterval } from "./alertmanager";
11
- import { buildAlertmanagerConfig, buildRuleFile } from "./build";
12
+ import { buildAlertmanagerConfig, buildRuleFile, prometheusConfigYaml, ruleFileYaml } from "./build";
13
+ import { PrometheusConfig, ScrapeConfig } from "./prometheus-config";
14
+ import { hasTool, promtoolCheckConfig } from "./tools";
12
15
  import { validateAlertmanagerConfig, validateRuleFile } from "./validate-config";
13
16
 
14
17
  export type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
@@ -62,5 +65,26 @@ export async function validate(): Promise<ValidateResult> {
62
65
  : { name: "alertmanager-roundtrip", ok: false, error: amIssues.map((i) => i.message).join("; ") },
63
66
  );
64
67
 
68
+ // `promtool check config` over a sample prometheus.yml, with the rule file it names beside it.
69
+ const promtool = process.env.PROMTOOL ?? "promtool";
70
+ if (!hasTool(promtool)) {
71
+ checks.push({ name: `promtool-check-config (skipped: ${promtool} is not installed)`, ok: true });
72
+ } else {
73
+ const config = prometheusConfigYaml([
74
+ new PrometheusConfig({
75
+ global: { scrape_interval: "15s", evaluation_interval: "30s" },
76
+ rule_files: ["rules.yml"],
77
+ alerting: { alertmanagers: [{ static_configs: [{ targets: ["alertmanager:9093"] }] }] },
78
+ }),
79
+ new ScrapeConfig({ job_name: "prometheus", static_configs: [{ targets: ["localhost:9090"] }] }),
80
+ ]);
81
+ const r = promtoolCheckConfig(config, { "rules.yml": ruleFileYaml([group]) }, promtool);
82
+ checks.push(
83
+ r.ok
84
+ ? { name: "promtool-check-config", ok: true }
85
+ : { name: "promtool-check-config", ok: false, error: r.output.trim() || "promtool check config failed" },
86
+ );
87
+ }
88
+
65
89
  return { success: checks.every((c) => c.ok), checks };
66
90
  }