@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.
- package/README.md +5 -2
- package/dist/build.d.ts +17 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/composites/index.d.ts +1 -1
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/slo.d.ts +23 -0
- package/dist/composites/slo.d.ts.map +1 -1
- package/dist/config-model.d.ts +272 -0
- package/dist/config-model.d.ts.map +1 -0
- package/dist/detect.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +7 -0
- package/dist/import/embedded.d.ts.map +1 -1
- package/dist/import/generator.d.ts +17 -1
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts +12 -2
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/import/slo.d.ts.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +5 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +2 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/prom-helpers.d.ts +7 -0
- package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/prom225.d.ts +10 -0
- package/dist/lint/post-synth/prom225.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +10 -0
- package/dist/model.d.ts +1 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/okf/index.md +3 -0
- package/dist/okf/rules/PROM225.md +11 -0
- package/dist/okf/types/PrometheusConfig.md +9 -0
- package/dist/okf/types/ScrapeConfig.md +9 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/prometheus-config.d.ts +39 -0
- package/dist/prometheus-config.d.ts.map +1 -0
- package/dist/rules/prom-helpers.ts +10 -1
- package/dist/rules/prom225.ts +19 -0
- package/dist/serializer.d.ts +14 -10
- package/dist/serializer.d.ts.map +1 -1
- package/dist/tools.d.ts +9 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/validate-config.d.ts +1 -1
- package/dist/validate-config.d.ts.map +1 -1
- package/dist/validate-relabel.d.ts +14 -0
- package/dist/validate-relabel.d.ts.map +1 -0
- package/dist/validate.d.ts +2 -1
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/build.ts +73 -0
- package/src/catalog.ts +14 -1
- package/src/composites/composites.test.ts +72 -2
- package/src/composites/index.ts +1 -0
- package/src/composites/slo.ts +43 -4
- package/src/config-model.ts +327 -0
- package/src/detect.ts +5 -4
- package/src/generated/lexicon-prometheus.json +10 -0
- package/src/import/embedded.test.ts +46 -2
- package/src/import/embedded.ts +32 -1
- package/src/import/generator.ts +138 -0
- package/src/import/parser.test.ts +2 -2
- package/src/import/parser.ts +72 -5
- package/src/import/prometheus-roundtrip.test.ts +148 -0
- package/src/import/slo.ts +15 -0
- package/src/import/testdata/prometheus-full.yml +149 -0
- package/src/import/testdata/schemastore-prometheus.json +1635 -0
- package/src/index.ts +21 -2
- package/src/lint/audit-catalog.ts +7 -0
- package/src/lint/audit-lineage.test.ts +1 -1
- package/src/lint/audit-lineage.ts +2 -0
- package/src/lint/post-synth/checks.test.ts +34 -0
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/prom-helpers.ts +10 -1
- package/src/lint/post-synth/prom225.ts +19 -0
- package/src/model.ts +1 -0
- package/src/plugin.test.ts +1 -0
- package/src/plugin.ts +3 -2
- package/src/prometheus-config.test.ts +230 -0
- package/src/prometheus-config.ts +71 -0
- package/src/schemastore.test.ts +142 -0
- package/src/serializer.ts +31 -21
- package/src/tools.ts +31 -3
- package/src/validate-config.ts +2 -1
- package/src/validate-relabel.ts +53 -0
- package/src/validate.test.ts +17 -0
- package/src/validate.ts +27 -3
|
@@ -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
|
+
});
|
package/src/serializer.ts
CHANGED
|
@@ -1,30 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prometheus serializer.
|
|
3
3
|
*
|
|
4
|
-
* Emits the
|
|
4
|
+
* Emits the files a Prometheus setup reads:
|
|
5
5
|
*
|
|
6
6
|
* - the rule file (`groups:`), from every `RuleGroup` in the build, the file
|
|
7
7
|
* `rule_files:` points at and `promtool check rules` reads;
|
|
8
|
+
* - `prometheus.yml`, from the `PrometheusConfig` and `ScrapeConfig`
|
|
9
|
+
* entities, the file `promtool check config` reads;
|
|
8
10
|
* - `alertmanager.yml`, from the `Route`, `Receiver`, `InhibitRule`,
|
|
9
11
|
* `TimeInterval` and `AlertmanagerSettings` entities, the file
|
|
10
12
|
* `amtool check-config` reads.
|
|
11
13
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* child routes keep the order they are written in, which is the
|
|
17
|
-
* matters to Prometheus and Alertmanager; groups,
|
|
18
|
-
* intervals, whose order means nothing, are sorted by name.
|
|
14
|
+
* The first of those a build produces, in that order, is the primary output
|
|
15
|
+
* and the others are written beside it: the rule file, then `prometheus.yml`
|
|
16
|
+
* (`PROMETHEUS_FILE`), then `alertmanager.yml`. A project that declares only
|
|
17
|
+
* rule groups, or only Alertmanager entities, sees no change. Rules inside a
|
|
18
|
+
* group and child routes keep the order they are written in, which is the
|
|
19
|
+
* order that matters to Prometheus and Alertmanager; groups, scrape jobs,
|
|
20
|
+
* receivers and time intervals, whose order means nothing, are sorted by name.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
* stamp.
|
|
22
|
+
* None of these files has a metadata channel, so there is no ownership marker
|
|
23
|
+
* to stamp.
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
25
27
|
import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
|
|
26
28
|
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
27
|
-
import { buildAlertmanagerConfig, buildRuleFile, emitYaml } from "./build";
|
|
29
|
+
import { buildAlertmanagerConfig, buildPrometheusConfig, buildRuleFile, emitYaml } from "./build";
|
|
30
|
+
|
|
31
|
+
/** The filename `prometheus.yml` is written under when it sits beside a rule file or `alertmanager.yml`. */
|
|
32
|
+
export const PROMETHEUS_FILE = "prometheus.yml";
|
|
28
33
|
|
|
29
34
|
/** The filename `alertmanager.yml` is written under when it sits beside a rule file. */
|
|
30
35
|
export const ALERTMANAGER_FILE = "alertmanager.yml";
|
|
@@ -35,20 +40,25 @@ export const prometheusSerializer: Serializer = {
|
|
|
35
40
|
|
|
36
41
|
serialize(entities: Map<string, Declarable>, _outputs?: LexiconOutput[]): string | SerializerResult {
|
|
37
42
|
const rules = buildRuleFile(entities);
|
|
43
|
+
const prom = buildPrometheusConfig(entities);
|
|
38
44
|
const am = buildAlertmanagerConfig(entities);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
const warnings = [...prom.warnings, ...am.warnings];
|
|
46
|
+
|
|
47
|
+
// The files this build writes, in the order of the primary: the rule file,
|
|
48
|
+
// then prometheus.yml, then alertmanager.yml.
|
|
49
|
+
const out: Array<[string, string]> = [];
|
|
50
|
+
if (rules.groups.length > 0) out.push(["rules", emitYaml(rules.config)]);
|
|
51
|
+
if (prom.count > 0) out.push([PROMETHEUS_FILE, emitYaml(prom.config)]);
|
|
52
|
+
if (am.count > 0) out.push([ALERTMANAGER_FILE, emitYaml(am.config)]);
|
|
42
53
|
|
|
43
|
-
if (
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
const text = emitYaml(am.config);
|
|
47
|
-
return warnings.length === 0 ? text : { primary: text, warnings };
|
|
54
|
+
if (out.length === 0) return "";
|
|
55
|
+
if (out.length === 1) {
|
|
56
|
+
return warnings.length === 0 ? out[0][1] : { primary: out[0][1], warnings };
|
|
48
57
|
}
|
|
58
|
+
const [[, primary], ...rest] = out;
|
|
49
59
|
return {
|
|
50
|
-
primary
|
|
51
|
-
files:
|
|
60
|
+
primary,
|
|
61
|
+
files: Object.fromEntries(rest),
|
|
52
62
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
53
63
|
};
|
|
54
64
|
},
|
package/src/tools.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `promtool check rules` and `amtool check-config`, when they are installed.
|
|
2
|
+
* `promtool check rules`, `promtool check config` and `amtool check-config`, when they are installed.
|
|
3
3
|
*
|
|
4
4
|
* The lexicon's own checks cover structure, references and PromQL syntax.
|
|
5
5
|
* The upstream tools add what only Prometheus and Alertmanager know (PromQL
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { spawnSync } from "child_process";
|
|
13
|
-
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
13
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
14
14
|
import { tmpdir } from "os";
|
|
15
|
-
import { join } from "path";
|
|
15
|
+
import { dirname, join } from "path";
|
|
16
16
|
|
|
17
17
|
export interface ToolResult {
|
|
18
18
|
/** False when the binary isn't installed; `ok` and `output` are then meaningless. */
|
|
@@ -46,6 +46,34 @@ export function promtoolCheckRules(yaml: string, bin = process.env.PROMTOOL ?? "
|
|
|
46
46
|
return runOnText(bin, (f) => ["check", "rules", f], "rules.yml", yaml);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Run `promtool check config` over `prometheus.yml` text. The config is written
|
|
51
|
+
* as `prometheus.yml` in a directory of its own, and `files` (path relative to
|
|
52
|
+
* that directory to contents) are written beside it: `check config` also reads
|
|
53
|
+
* the rule files, certificates and credentials files the config names, so a
|
|
54
|
+
* config that names any needs them here. `bin` defaults to `promtool` on PATH.
|
|
55
|
+
*/
|
|
56
|
+
export function promtoolCheckConfig(
|
|
57
|
+
yaml: string,
|
|
58
|
+
files: Record<string, string> = {},
|
|
59
|
+
bin = process.env.PROMTOOL ?? "promtool",
|
|
60
|
+
): ToolResult {
|
|
61
|
+
if (!hasTool(bin)) return { ran: false, ok: false, output: "" };
|
|
62
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-prometheus-"));
|
|
63
|
+
try {
|
|
64
|
+
writeFileSync(join(dir, "prometheus.yml"), yaml);
|
|
65
|
+
for (const [path, content] of Object.entries(files)) {
|
|
66
|
+
const target = join(dir, path);
|
|
67
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
68
|
+
writeFileSync(target, content);
|
|
69
|
+
}
|
|
70
|
+
const r = spawnSync(bin, ["check", "config", "prometheus.yml"], { cwd: dir, encoding: "utf-8" });
|
|
71
|
+
return { ran: true, ok: r.status === 0, output: `${r.stdout ?? ""}${r.stderr ?? ""}` };
|
|
72
|
+
} finally {
|
|
73
|
+
rmSync(dir, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
/**
|
|
50
78
|
* Run `promtool test rules` over rule file YAML and a unit-test file for it.
|
|
51
79
|
* The test file's `rule_files:` should name `rules.yml`, which is where the
|
package/src/validate-config.ts
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROM225: a `labeldrop` or `labelkeep` relabel step carries a field the
|
|
3
|
+
* action does not take.
|
|
4
|
+
*
|
|
5
|
+
* Both actions match `regex` against label names, so `source_labels`,
|
|
6
|
+
* `separator`, `target_label`, `modulus` and `replacement` have nothing to
|
|
7
|
+
* act on. Prometheus rejects the file when it loads it (the importer's
|
|
8
|
+
* fixture was fixed for the same reason, #3537).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PrometheusConfigFile } from "./config-model";
|
|
12
|
+
import type { PrometheusIssue } from "./validate-config";
|
|
13
|
+
|
|
14
|
+
const RELABEL_KEYS = new Set(["relabel_configs", "metric_relabel_configs", "alert_relabel_configs", "write_relabel_configs"]);
|
|
15
|
+
const FOREIGN_FIELDS = ["source_labels", "separator", "target_label", "modulus", "replacement"] as const;
|
|
16
|
+
|
|
17
|
+
/** Every relabel step in a `prometheus.yml`, with where it sits, e.g. `scrape_configs[0].relabel_configs[1]`. */
|
|
18
|
+
function relabelSteps(node: unknown, path: string, out: Array<{ path: string; step: Record<string, unknown> }>): void {
|
|
19
|
+
if (Array.isArray(node)) {
|
|
20
|
+
node.forEach((v, i) => relabelSteps(v, `${path}[${i}]`, out));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (typeof node !== "object" || node === null) return;
|
|
24
|
+
for (const [key, value] of Object.entries(node)) {
|
|
25
|
+
const here = path ? `${path}.${key}` : key;
|
|
26
|
+
if (RELABEL_KEYS.has(key) && Array.isArray(value)) {
|
|
27
|
+
value.forEach((step, i) => {
|
|
28
|
+
if (typeof step === "object" && step !== null && !Array.isArray(step)) out.push({ path: `${here}[${i}]`, step: step as Record<string, unknown> });
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
relabelSteps(value, here, out);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** PROM225 over one `prometheus.yml`. */
|
|
37
|
+
export function validateRelabelFields(config: PrometheusConfigFile): PrometheusIssue[] {
|
|
38
|
+
const steps: Array<{ path: string; step: Record<string, unknown> }> = [];
|
|
39
|
+
relabelSteps(config, "", steps);
|
|
40
|
+
const issues: PrometheusIssue[] = [];
|
|
41
|
+
for (const { path, step } of steps) {
|
|
42
|
+
if (step.action !== "labeldrop" && step.action !== "labelkeep") continue;
|
|
43
|
+
const extra = FOREIGN_FIELDS.filter((f) => step[f] !== undefined);
|
|
44
|
+
if (extra.length === 0) continue;
|
|
45
|
+
issues.push({
|
|
46
|
+
code: "PROM225",
|
|
47
|
+
severity: "error",
|
|
48
|
+
subject: path,
|
|
49
|
+
message: `${path}: a ${String(step.action)} step matches regex against label names and takes no ${extra.join(", ")}; Prometheus rejects the file, so remove ${extra.length === 1 ? "it" : "them"}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return issues;
|
|
53
|
+
}
|
|
@@ -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,20 +1,23 @@
|
|
|
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";
|
|
15
18
|
|
|
16
19
|
/** Every entity class the package must keep exporting. */
|
|
17
|
-
export const REQUIRED_NAMES = ["RuleGroup", "Route", "Receiver", "InhibitRule", "TimeInterval", "AlertmanagerSettings"];
|
|
20
|
+
export const REQUIRED_NAMES = ["RuleGroup", "ScrapeConfig", "PrometheusConfig", "Route", "Receiver", "InhibitRule", "TimeInterval", "AlertmanagerSettings"];
|
|
18
21
|
|
|
19
22
|
export async function validate(): Promise<ValidateResult> {
|
|
20
23
|
const checks: ValidateCheck[] = [];
|
|
@@ -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
|
}
|