@intentius/chant-lexicon-prometheus 0.124.31 → 0.124.32
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/dist/alertmanager.js +61 -0
- package/dist/api/alertmanager.js +98 -0
- package/dist/api/apply.js +306 -0
- package/dist/api/bind.js +66 -0
- package/dist/api/client.js +110 -0
- package/dist/api/evaluated.js +86 -0
- package/dist/api/fake-servers.js +165 -0
- package/dist/api/index.js +17 -0
- package/dist/api/ruler.js +122 -0
- package/dist/build.js +240 -0
- package/dist/catalog.js +66 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +105 -0
- package/dist/codegen/generate-cli.js +7 -0
- package/dist/codegen/generate.js +42 -0
- package/dist/codegen/package.js +28 -0
- package/dist/collector-metrics.js +225 -0
- package/dist/composites/alert-routing.js +210 -0
- package/dist/composites/catalog.js +344 -0
- package/dist/composites/genai.js +454 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/red-alerts.js +135 -0
- package/dist/composites/slo.js +353 -0
- package/dist/composites/watchdog.js +110 -0
- package/dist/config-model.js +52 -0
- package/dist/config.js +225 -0
- package/dist/describe-resources.js +299 -0
- package/dist/detect.js +12 -0
- package/dist/duration.js +54 -0
- package/dist/export-resources.js +163 -0
- package/dist/import/alertmanager-live.js +309 -0
- package/dist/import/embedded.js +100 -0
- package/dist/import/generator.js +683 -0
- package/dist/import/parser.js +428 -0
- package/dist/import/slo.js +207 -0
- package/dist/index.js +33 -0
- package/dist/init-templates.js +119 -0
- package/dist/integrations.js +33 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +60 -0
- package/dist/lint/audit-lineage.js +42 -0
- package/dist/lint/post-synth/index.js +68 -0
- package/dist/lint/post-synth/prom-helpers.js +157 -0
- package/dist/lint/post-synth/prom101.js +13 -0
- package/dist/lint/post-synth/prom102.js +13 -0
- package/dist/lint/post-synth/prom103.js +13 -0
- package/dist/lint/post-synth/prom104.js +13 -0
- package/dist/lint/post-synth/prom105.js +13 -0
- package/dist/lint/post-synth/prom106.js +13 -0
- package/dist/lint/post-synth/prom107.js +13 -0
- package/dist/lint/post-synth/prom201.js +13 -0
- package/dist/lint/post-synth/prom202.js +13 -0
- package/dist/lint/post-synth/prom203.js +13 -0
- package/dist/lint/post-synth/prom204.js +13 -0
- package/dist/lint/post-synth/prom205.js +13 -0
- package/dist/lint/post-synth/prom206.js +13 -0
- package/dist/lint/post-synth/prom207.js +13 -0
- package/dist/lint/post-synth/prom208.js +13 -0
- package/dist/lint/post-synth/prom209.js +13 -0
- package/dist/lint/post-synth/prom210.js +13 -0
- package/dist/lint/post-synth/prom211.js +17 -0
- package/dist/lint/post-synth/prom212.js +14 -0
- package/dist/lint/post-synth/prom213.js +16 -0
- package/dist/lint/post-synth/prom214.js +14 -0
- package/dist/lint/post-synth/prom215.js +14 -0
- package/dist/lint/post-synth/prom216.js +15 -0
- package/dist/lint/post-synth/prom217.js +14 -0
- package/dist/lint/post-synth/prom218.js +15 -0
- package/dist/lint/post-synth/prom219.js +14 -0
- package/dist/lint/post-synth/prom220.js +14 -0
- package/dist/lint/post-synth/prom221.js +15 -0
- package/dist/lint/post-synth/prom222.js +14 -0
- package/dist/lint/post-synth/prom223.js +15 -0
- package/dist/lint/post-synth/prom224.js +14 -0
- package/dist/lint/post-synth/prom225.js +15 -0
- package/dist/lint/post-synth/prom301.js +19 -0
- package/dist/lint/rules/index.js +8 -0
- package/dist/lint/rules/literal-credential.js +127 -0
- package/dist/lint/rules/prom-ast.js +65 -0
- package/dist/lint/rules/promql-literal.js +65 -0
- package/dist/lint/rules/slo-literal.js +79 -0
- package/dist/lsp/completions.js +11 -0
- package/dist/lsp/hover.js +21 -0
- package/dist/manifest.json +1 -1
- package/dist/matchers.js +131 -0
- package/dist/model.js +108 -0
- package/dist/op/activities/index.js +20 -0
- package/dist/op/activities/promtool.js +87 -0
- package/dist/op/activities/rule-audit.js +128 -0
- package/dist/op/activities/rules-loaded.js +73 -0
- package/dist/op/activities/silence.js +123 -0
- package/dist/op/activity-contracts.js +51 -0
- package/dist/op/audit-op.js +28 -0
- package/dist/op/builders.js +57 -0
- package/dist/op/slo-rule-tests.js +148 -0
- package/dist/package-cli.js +14 -0
- package/dist/pin.js +13 -0
- package/dist/plugin.js +143 -0
- package/dist/prometheus-config.js +38 -0
- package/dist/promql-analysis.js +343 -0
- package/dist/promql.js +44 -0
- package/dist/rule-eval.js +466 -0
- package/dist/rules.js +97 -0
- package/dist/serializer.js +59 -0
- package/dist/skill-defs.js +41 -0
- package/dist/tools.js +85 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate-config.js +556 -0
- package/dist/validate-integrations.js +388 -0
- package/dist/validate-relabel.js +52 -0
- package/dist/validate-security.js +135 -0
- package/dist/validate.js +69 -0
- package/package.json +10 -10
- package/src/codegen/package.ts +2 -2
package/dist/config.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `prometheus` namespace in `chant.config.ts` (#3371): which ruler and
|
|
3
|
+
* which Alertmanager each chant environment observes and imports from, and
|
|
4
|
+
* (#3372) applies to.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import type { ChantConfig } from "@intentius/chant/config";
|
|
8
|
+
* import "@intentius/chant-lexicon-prometheus"; // brings the `prometheus` key into ChantConfig
|
|
9
|
+
*
|
|
10
|
+
* export default {
|
|
11
|
+
* lexicons: ["prometheus"],
|
|
12
|
+
* prometheus: {
|
|
13
|
+
* profiles: {
|
|
14
|
+
* prod: {
|
|
15
|
+
* ruler: { kind: "mimir", url: "https://mimir.example.com", tenant: "shop", namespace: "shop-rules", token: { env: "MIMIR_TOKEN" } },
|
|
16
|
+
* alertmanager: { kind: "mimir", url: "https://mimir.example.com", tenant: "shop", token: { env: "MIMIR_TOKEN" } },
|
|
17
|
+
* },
|
|
18
|
+
* },
|
|
19
|
+
* },
|
|
20
|
+
* } satisfies ChantConfig;
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* A rule file has no namespace and no ownership marker, so the ruler
|
|
24
|
+
* namespaces a profile names are the boundary: `namespace` is where the
|
|
25
|
+
* project's groups live, `groupNamespaces` moves single groups to another,
|
|
26
|
+
* and nothing outside those namespaces is read for import or written by an
|
|
27
|
+
* apply. On a plain Prometheus the "namespace" is the rule file path
|
|
28
|
+
* `/api/v1/rules` reports as `file`, and may be left out.
|
|
29
|
+
*
|
|
30
|
+
* Credentials are named by their environment variable, never written in the
|
|
31
|
+
* config. An environment with no profile falls back to environment
|
|
32
|
+
* variables (see {@link resolveRulerTarget} and
|
|
33
|
+
* {@link resolveAlertmanagerTarget}).
|
|
34
|
+
*/
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
const envRef = z.strictObject({ env: z.string() });
|
|
37
|
+
const basicAuth = z.strictObject({ user: envRef, password: envRef });
|
|
38
|
+
/** The rulers chant reads rule groups from. `prometheus` is a plain Prometheus, read over `/api/v1/rules`. */
|
|
39
|
+
export const RULER_KINDS = ["mimir", "cortex", "loki", "prometheus"];
|
|
40
|
+
/** Where an Alertmanager config is read: a plain Alertmanager's `/api/v2/status`, or Mimir's and Cortex's `/api/v1/alerts`. */
|
|
41
|
+
export const ALERTMANAGER_KINDS = ["alertmanager", "mimir", "cortex"];
|
|
42
|
+
export const rulerProfileSchema = z.strictObject({
|
|
43
|
+
kind: z.enum(RULER_KINDS),
|
|
44
|
+
/** Base URL: the Mimir, Cortex or Loki endpoint the ruler API is served on, or the Prometheus server. */
|
|
45
|
+
url: z.string(),
|
|
46
|
+
/** `X-Scope-OrgID`. */
|
|
47
|
+
tenant: z.string().optional(),
|
|
48
|
+
/** The ruler namespace the project's rule groups live in (on a plain Prometheus, the rule file path). */
|
|
49
|
+
namespace: z.string().optional(),
|
|
50
|
+
/** Groups that live in another namespace than `namespace`, by group name. */
|
|
51
|
+
groupNamespaces: z.record(z.string(), z.string()).optional(),
|
|
52
|
+
/** Mimir's or Cortex's `-http.prometheus-http-prefix`. Defaults to `/prometheus` (to nothing for a plain Prometheus). */
|
|
53
|
+
prometheusPrefix: z.string().optional(),
|
|
54
|
+
token: envRef.optional(),
|
|
55
|
+
basicAuth: basicAuth.optional(),
|
|
56
|
+
});
|
|
57
|
+
export const alertmanagerProfileSchema = z.strictObject({
|
|
58
|
+
kind: z.enum(ALERTMANAGER_KINDS).optional(),
|
|
59
|
+
url: z.string(),
|
|
60
|
+
/** `X-Scope-OrgID`, for Mimir and Cortex. */
|
|
61
|
+
tenant: z.string().optional(),
|
|
62
|
+
/** Mimir's or Cortex's `-http.alertmanager-http-prefix`. Defaults to `/alertmanager`. */
|
|
63
|
+
alertmanagerPrefix: z.string().optional(),
|
|
64
|
+
token: envRef.optional(),
|
|
65
|
+
basicAuth: basicAuth.optional(),
|
|
66
|
+
});
|
|
67
|
+
export const prometheusProfileSchema = z.strictObject({
|
|
68
|
+
ruler: rulerProfileSchema.optional(),
|
|
69
|
+
alertmanager: alertmanagerProfileSchema.optional(),
|
|
70
|
+
});
|
|
71
|
+
export const prometheusConfigSchema = z.strictObject({
|
|
72
|
+
/** One ruler and one Alertmanager per chant environment. */
|
|
73
|
+
profiles: z.record(z.string(), prometheusProfileSchema).optional(),
|
|
74
|
+
});
|
|
75
|
+
export function isUnresolvedTarget(v) {
|
|
76
|
+
return "reason" in v;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The namespaces a ruler target declares, deduplicated, in the order they
|
|
80
|
+
* are named. This is the ownership boundary: import reads only these, and
|
|
81
|
+
* an apply (#3372) writes and deletes only inside these.
|
|
82
|
+
*/
|
|
83
|
+
export function declaredNamespaces(target) {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const ns of [target.namespace, ...Object.values(target.groupNamespaces)]) {
|
|
86
|
+
if (ns !== undefined && ns !== "" && !out.includes(ns))
|
|
87
|
+
out.push(ns);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** The namespace a declared group lives in: its `groupNamespaces` entry, else `namespace`. */
|
|
92
|
+
export function namespaceOfGroup(target, group) {
|
|
93
|
+
return target.groupNamespaces[group] ?? target.namespace;
|
|
94
|
+
}
|
|
95
|
+
function profileAuth(source, profile, env) {
|
|
96
|
+
if (profile.token) {
|
|
97
|
+
const token = env[profile.token.env];
|
|
98
|
+
if (!token)
|
|
99
|
+
return { reason: "no-credentials", detail: `${source}.token names ${profile.token.env}, which is not set` };
|
|
100
|
+
return { token };
|
|
101
|
+
}
|
|
102
|
+
if (profile.basicAuth) {
|
|
103
|
+
const user = env[profile.basicAuth.user.env];
|
|
104
|
+
const password = env[profile.basicAuth.password.env];
|
|
105
|
+
if (!user || !password) {
|
|
106
|
+
const missing = [!user ? profile.basicAuth.user.env : undefined, !password ? profile.basicAuth.password.env : undefined].filter(Boolean).join(" and ");
|
|
107
|
+
return { reason: "no-credentials", detail: `${source}.basicAuth names ${missing}, which ${missing.includes(" and ") ? "are" : "is"} not set` };
|
|
108
|
+
}
|
|
109
|
+
return { user, password };
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
function envAuth(env, prefix) {
|
|
114
|
+
const token = env[`${prefix}_TOKEN`];
|
|
115
|
+
if (token)
|
|
116
|
+
return { token };
|
|
117
|
+
const user = env[`${prefix}_USER`];
|
|
118
|
+
const password = env[`${prefix}_PASSWORD`];
|
|
119
|
+
return user && password ? { user, password } : undefined;
|
|
120
|
+
}
|
|
121
|
+
const trim = (url) => url.replace(/\/+$/, "");
|
|
122
|
+
const prefixOf = (p, fallback) => (p === undefined ? fallback : p === "" ? "" : `/${p.replace(/^\/+|\/+$/g, "")}`);
|
|
123
|
+
/**
|
|
124
|
+
* The ruler an environment reads from. Pure: `config` and `env` are passed in.
|
|
125
|
+
*
|
|
126
|
+
* 1. `prometheus.profiles.<environment>.ruler`. A profile that names a
|
|
127
|
+
* credential variable which is not set is `no-credentials`.
|
|
128
|
+
* 2. `PROMETHEUS_RULER_URL`, with `PROMETHEUS_RULER_KIND` (default `mimir`),
|
|
129
|
+
* `PROMETHEUS_RULER_TENANT`, `PROMETHEUS_RULER_NAMESPACE` and
|
|
130
|
+
* `PROMETHEUS_RULER_TOKEN` (or `_USER` and `_PASSWORD`).
|
|
131
|
+
* 3. `PROMETHEUS_URL`: a plain Prometheus, read over `/api/v1/rules`.
|
|
132
|
+
* 4. Otherwise `no-binding`.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveRulerTarget(input) {
|
|
135
|
+
const env = input.env ?? process.env;
|
|
136
|
+
const profile = input.environment !== undefined ? input.config?.prometheus?.profiles?.[input.environment]?.ruler : undefined;
|
|
137
|
+
if (profile) {
|
|
138
|
+
const source = `prometheus.profiles.${input.environment}.ruler`;
|
|
139
|
+
const auth = profileAuth(source, profile, env);
|
|
140
|
+
if (auth && isUnresolvedTarget(auth))
|
|
141
|
+
return auth;
|
|
142
|
+
if (profile.kind !== "prometheus" && !profile.namespace) {
|
|
143
|
+
return { reason: "no-binding", detail: `${source}.namespace is not set, and a ${profile.kind} ruler's groups live in a namespace` };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
kind: profile.kind,
|
|
147
|
+
url: trim(profile.url),
|
|
148
|
+
...(profile.tenant ? { tenant: profile.tenant } : {}),
|
|
149
|
+
...(auth ? { auth } : {}),
|
|
150
|
+
...(profile.namespace ? { namespace: profile.namespace } : {}),
|
|
151
|
+
groupNamespaces: profile.groupNamespaces ?? {},
|
|
152
|
+
prometheusPrefix: prefixOf(profile.prometheusPrefix, profile.kind === "prometheus" ? "" : "/prometheus"),
|
|
153
|
+
source,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const where = input.environment !== undefined ? `prometheus.profiles.${input.environment}.ruler is not declared and ` : "";
|
|
157
|
+
const rulerUrl = env.PROMETHEUS_RULER_URL;
|
|
158
|
+
if (rulerUrl) {
|
|
159
|
+
const kind = (env.PROMETHEUS_RULER_KIND ?? "mimir");
|
|
160
|
+
if (!RULER_KINDS.includes(kind)) {
|
|
161
|
+
return { reason: "no-binding", detail: `PROMETHEUS_RULER_KIND is "${kind}", not one of ${RULER_KINDS.join(", ")}` };
|
|
162
|
+
}
|
|
163
|
+
const auth = envAuth(env, "PROMETHEUS_RULER");
|
|
164
|
+
return {
|
|
165
|
+
kind,
|
|
166
|
+
url: trim(rulerUrl),
|
|
167
|
+
...(env.PROMETHEUS_RULER_TENANT ? { tenant: env.PROMETHEUS_RULER_TENANT } : {}),
|
|
168
|
+
...(auth ? { auth } : {}),
|
|
169
|
+
...(env.PROMETHEUS_RULER_NAMESPACE ? { namespace: env.PROMETHEUS_RULER_NAMESPACE } : {}),
|
|
170
|
+
groupNamespaces: {},
|
|
171
|
+
prometheusPrefix: "/prometheus",
|
|
172
|
+
source: "env PROMETHEUS_RULER_URL",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const promUrl = env.PROMETHEUS_URL;
|
|
176
|
+
if (promUrl) {
|
|
177
|
+
const auth = envAuth(env, "PROMETHEUS");
|
|
178
|
+
return { kind: "prometheus", url: trim(promUrl), ...(auth ? { auth } : {}), groupNamespaces: {}, prometheusPrefix: "", source: "env PROMETHEUS_URL" };
|
|
179
|
+
}
|
|
180
|
+
return { reason: "no-binding", detail: `${where}neither PROMETHEUS_RULER_URL nor PROMETHEUS_URL is set, so there is no ruler to read` };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The Alertmanager an environment reads from. Pure.
|
|
184
|
+
*
|
|
185
|
+
* 1. `prometheus.profiles.<environment>.alertmanager`.
|
|
186
|
+
* 2. `ALERTMANAGER_URL`, with `ALERTMANAGER_KIND` (default `alertmanager`),
|
|
187
|
+
* `ALERTMANAGER_TENANT` and `ALERTMANAGER_TOKEN` (or `_USER` and `_PASSWORD`).
|
|
188
|
+
* 3. Otherwise `no-binding`.
|
|
189
|
+
*/
|
|
190
|
+
export function resolveAlertmanagerTarget(input) {
|
|
191
|
+
const env = input.env ?? process.env;
|
|
192
|
+
const profile = input.environment !== undefined ? input.config?.prometheus?.profiles?.[input.environment]?.alertmanager : undefined;
|
|
193
|
+
if (profile) {
|
|
194
|
+
const source = `prometheus.profiles.${input.environment}.alertmanager`;
|
|
195
|
+
const auth = profileAuth(source, profile, env);
|
|
196
|
+
if (auth && isUnresolvedTarget(auth))
|
|
197
|
+
return auth;
|
|
198
|
+
return {
|
|
199
|
+
kind: profile.kind ?? "alertmanager",
|
|
200
|
+
url: trim(profile.url),
|
|
201
|
+
...(profile.tenant ? { tenant: profile.tenant } : {}),
|
|
202
|
+
...(auth ? { auth } : {}),
|
|
203
|
+
alertmanagerPrefix: prefixOf(profile.alertmanagerPrefix, "/alertmanager"),
|
|
204
|
+
source,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const url = env.ALERTMANAGER_URL;
|
|
208
|
+
if (!url) {
|
|
209
|
+
const where = input.environment !== undefined ? `prometheus.profiles.${input.environment}.alertmanager is not declared and ` : "";
|
|
210
|
+
return { reason: "no-binding", detail: `${where}ALERTMANAGER_URL is not set, so there is no Alertmanager to read` };
|
|
211
|
+
}
|
|
212
|
+
const kind = (env.ALERTMANAGER_KIND ?? "alertmanager");
|
|
213
|
+
if (!ALERTMANAGER_KINDS.includes(kind)) {
|
|
214
|
+
return { reason: "no-binding", detail: `ALERTMANAGER_KIND is "${kind}", not one of ${ALERTMANAGER_KINDS.join(", ")}` };
|
|
215
|
+
}
|
|
216
|
+
const auth = envAuth(env, "ALERTMANAGER");
|
|
217
|
+
return {
|
|
218
|
+
kind,
|
|
219
|
+
url: trim(url),
|
|
220
|
+
...(env.ALERTMANAGER_TENANT ? { tenant: env.ALERTMANAGER_TENANT } : {}),
|
|
221
|
+
...(auth ? { auth } : {}),
|
|
222
|
+
alertmanagerPrefix: "/alertmanager",
|
|
223
|
+
source: "env ALERTMANAGER_URL",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `describeResources()` for prometheus (#3371): is each declared rule group
|
|
3
|
+
* loaded on the environment's ruler and is it healthy, and is each declared
|
|
4
|
+
* Alertmanager entity in the config the environment's Alertmanager runs.
|
|
5
|
+
*
|
|
6
|
+
* Runs on core's observer harness. `bind()` resolves the ruler and the
|
|
7
|
+
* Alertmanager (./api/bind.ts), each on its own, so a project with only rule
|
|
8
|
+
* groups is not held up by having no Alertmanager. Verdicts:
|
|
9
|
+
*
|
|
10
|
+
* | Entity | Read | Absent when |
|
|
11
|
+
* |---|---|---|
|
|
12
|
+
* | `RuleGroup` on Mimir, Cortex, Loki | the config API's `/{namespace}/{group}`, in the namespace the profile gives the group | 404 |
|
|
13
|
+
* | `RuleGroup` on a plain Prometheus | `/api/v1/rules`, by name (and rule file, when the profile names one) | not listed |
|
|
14
|
+
* | `Receiver`, `TimeInterval` | the loaded Alertmanager config, by name | no entry of that name |
|
|
15
|
+
* | `Route` | the loaded routing tree, by receiver and matchers | no route with both |
|
|
16
|
+
* | `InhibitRule` | the loaded inhibit rules, by matchers and `equal` | no equal rule |
|
|
17
|
+
* | `AlertmanagerSettings` | the loaded config | no config (Mimir: the tenant has none) |
|
|
18
|
+
* | `ScrapeConfig`, `PrometheusConfig` | none: `prometheus.yml` is a file on disk | `unsupported-kind` |
|
|
19
|
+
*
|
|
20
|
+
* A group's health is from `/api/v1/rules` (./api/evaluated.ts): `health`
|
|
21
|
+
* (`ok`, `err`, `unknown`), the first rule error, and how many alerts are
|
|
22
|
+
* firing or pending. A failed health read leaves the group present with
|
|
23
|
+
* `health: "unknown"` and the reason; presence is the config API's answer.
|
|
24
|
+
* The Alertmanager's cluster status and version ride on its entities.
|
|
25
|
+
*
|
|
26
|
+
* Ownership: a rule group and an Alertmanager config carry no marker
|
|
27
|
+
* (./serializer.ts), so every verdict is `unknown`, and an `owned: true`
|
|
28
|
+
* read withholds them as `filtered`. What bounds chant on a ruler is the
|
|
29
|
+
* namespace: groups are only read in the namespaces the profile declares
|
|
30
|
+
* (./config.ts).
|
|
31
|
+
*/
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
33
|
+
import { observeEntities, } from "@intentius/chant/observation";
|
|
34
|
+
import { bindEndpoints, classifyPromFailure } from "./api/bind.js";
|
|
35
|
+
import { PromApiError } from "./api/client.js";
|
|
36
|
+
import { groupHealth } from "./api/evaluated.js";
|
|
37
|
+
import { isUnresolvedTarget, namespaceOfGroup } from "./config.js";
|
|
38
|
+
import { INHIBIT_RULE_TYPE, RECEIVER_TYPE, ROUTE_TYPE, SETTINGS_TYPE, TIME_INTERVAL_TYPE } from "./alertmanager.js";
|
|
39
|
+
import { RULE_GROUP_TYPE } from "./rules.js";
|
|
40
|
+
import { PROMETHEUS_CONFIG_TYPE, SCRAPE_CONFIG_TYPE } from "./prometheus-config.js";
|
|
41
|
+
import { loadPrometheusYaml, parseAlertmanagerDocument } from "./import/parser.js";
|
|
42
|
+
import { stripAlertmanagerDefaults } from "./import/alertmanager-live.js";
|
|
43
|
+
function isObject(v) {
|
|
44
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
45
|
+
}
|
|
46
|
+
/** What `unsupported-kind` says about `prometheus.yml` entities. */
|
|
47
|
+
export const PROMETHEUS_CONFIG_NOT_OBSERVED = "prometheus.yml is a file Prometheus reads from disk, not something its API serves for comparison; observe it through the lexicon that deploys it";
|
|
48
|
+
/** A failed read as the harness's per-entity verdict; anything else is rethrown for the harness to record. */
|
|
49
|
+
function unobservedFrom(err) {
|
|
50
|
+
if (err instanceof PromApiError)
|
|
51
|
+
return { unobserved: classifyPromFailure(err) };
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
function unbound(u) {
|
|
55
|
+
return { unobserved: { reason: u.reason, detail: u.detail } };
|
|
56
|
+
}
|
|
57
|
+
const FILTERED_DETAIL = "carries no ownership marker, so an owned-only read cannot show it is chant's";
|
|
58
|
+
// ── rule groups ─────────────────────────────────────────────────────
|
|
59
|
+
async function healthAttributes(ruler, file, name) {
|
|
60
|
+
try {
|
|
61
|
+
const groups = await ruler.evaluated();
|
|
62
|
+
const g = groups.find((x) => x.name === name && (file === undefined || x.file === file));
|
|
63
|
+
if (!g)
|
|
64
|
+
return { health: "unknown", healthDetail: `the ruler does not report "${name}" in ${ruler.evaluatedPath} yet` };
|
|
65
|
+
const h = groupHealth(g);
|
|
66
|
+
return {
|
|
67
|
+
health: h.health,
|
|
68
|
+
...(h.lastError ? { lastError: h.lastError } : {}),
|
|
69
|
+
failing: h.failing,
|
|
70
|
+
firing: h.firing,
|
|
71
|
+
pending: h.pending,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return { health: "unknown", healthDetail: classifyPromFailure(err).detail };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function observeRuleGroup(ruler, entity, owned) {
|
|
79
|
+
const name = typeof entity.props.name === "string" ? entity.props.name : entity.name;
|
|
80
|
+
if (ruler.target.kind === "prometheus") {
|
|
81
|
+
const file = namespaceOfGroup(ruler.target, name);
|
|
82
|
+
let groups;
|
|
83
|
+
try {
|
|
84
|
+
groups = await ruler.evaluated();
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return unobservedFrom(err);
|
|
88
|
+
}
|
|
89
|
+
const queried = ruler.evaluatedPath;
|
|
90
|
+
const g = groups.find((x) => x.name === name && (file === undefined || x.file === file));
|
|
91
|
+
if (!g)
|
|
92
|
+
return { absent: true, queried };
|
|
93
|
+
if (owned)
|
|
94
|
+
return { unobserved: { reason: "filtered", detail: `rule group "${name}" is loaded but ${FILTERED_DETAIL}` }, queried };
|
|
95
|
+
const h = groupHealth(g);
|
|
96
|
+
return {
|
|
97
|
+
present: {
|
|
98
|
+
type: RULE_GROUP_TYPE,
|
|
99
|
+
physicalId: `${g.file}/${name}`,
|
|
100
|
+
status: "PRESENT",
|
|
101
|
+
ownership: "unknown",
|
|
102
|
+
attributes: {
|
|
103
|
+
file: g.file,
|
|
104
|
+
rules: g.rules.length,
|
|
105
|
+
health: h.health,
|
|
106
|
+
...(h.lastError ? { lastError: h.lastError } : {}),
|
|
107
|
+
failing: h.failing,
|
|
108
|
+
firing: h.firing,
|
|
109
|
+
pending: h.pending,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
queried,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const namespace = namespaceOfGroup(ruler.target, name);
|
|
116
|
+
if (namespace === undefined) {
|
|
117
|
+
return { unobserved: { reason: "no-binding", detail: `${ruler.target.source} names no namespace for rule group "${name}"` } };
|
|
118
|
+
}
|
|
119
|
+
const queried = ruler.groupPath(namespace, name);
|
|
120
|
+
let raw;
|
|
121
|
+
try {
|
|
122
|
+
raw = await ruler.readGroup(namespace, name);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return unobservedFrom(err);
|
|
126
|
+
}
|
|
127
|
+
if (!raw)
|
|
128
|
+
return { absent: true, queried };
|
|
129
|
+
if (owned)
|
|
130
|
+
return { unobserved: { reason: "filtered", detail: `rule group "${name}" is in namespace "${namespace}" but ${FILTERED_DETAIL}` }, queried };
|
|
131
|
+
const meta = {
|
|
132
|
+
type: RULE_GROUP_TYPE,
|
|
133
|
+
physicalId: `${namespace}/${name}`,
|
|
134
|
+
status: "PRESENT",
|
|
135
|
+
ownership: "unknown",
|
|
136
|
+
attributes: {
|
|
137
|
+
namespace,
|
|
138
|
+
rules: Array.isArray(raw.rules) ? raw.rules.length : 0,
|
|
139
|
+
...(typeof raw.interval === "string" ? { interval: raw.interval } : {}),
|
|
140
|
+
...(await healthAttributes(ruler, namespace, name)),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
return { present: meta, queried };
|
|
144
|
+
}
|
|
145
|
+
async function loadAlertmanager(am) {
|
|
146
|
+
return am.client.once("observe:config", async () => {
|
|
147
|
+
const live = await am.readConfig();
|
|
148
|
+
if (!live)
|
|
149
|
+
return undefined;
|
|
150
|
+
const loaded = loadPrometheusYaml(live.text);
|
|
151
|
+
if (!isObject(loaded))
|
|
152
|
+
throw new PromApiError(502, "GET", live.address, "the Alertmanager config is not a YAML mapping");
|
|
153
|
+
const doc = live.remarshalled ? stripAlertmanagerDefaults(loaded) : loaded;
|
|
154
|
+
const health = (await am.health()) ?? {};
|
|
155
|
+
return {
|
|
156
|
+
config: parseAlertmanagerDocument(doc).config,
|
|
157
|
+
address: live.address,
|
|
158
|
+
digest: createHash("sha256").update(live.text).digest("hex").slice(0, 16),
|
|
159
|
+
health: { ...health },
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** `name="value"`, whatever spacing and quoting it was written with. */
|
|
164
|
+
function normalMatcher(m) {
|
|
165
|
+
const match = /^\s*([^=!~\s]+)\s*(=~|!~|!=|=)\s*(.*?)\s*$/.exec(m);
|
|
166
|
+
if (!match)
|
|
167
|
+
return m.trim();
|
|
168
|
+
let value = match[3];
|
|
169
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
170
|
+
try {
|
|
171
|
+
value = JSON.parse(value);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
value = value.slice(1, -1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return `${match[1]}${match[2]}${JSON.stringify(value)}`;
|
|
178
|
+
}
|
|
179
|
+
function matcherKey(list) {
|
|
180
|
+
return (Array.isArray(list) ? list.map((m) => normalMatcher(String(m))) : []).sort().join(",");
|
|
181
|
+
}
|
|
182
|
+
function nameOf(ref) {
|
|
183
|
+
if (typeof ref === "string")
|
|
184
|
+
return ref;
|
|
185
|
+
if (isObject(ref) && isObject(ref.props) && typeof ref.props.name === "string")
|
|
186
|
+
return ref.props.name;
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
function routeKey(receiver, matchers) {
|
|
190
|
+
return `${nameOf(receiver) ?? ""}{${matcherKey(matchers)}}`;
|
|
191
|
+
}
|
|
192
|
+
function routesOf(root) {
|
|
193
|
+
if (!root)
|
|
194
|
+
return [];
|
|
195
|
+
const out = [root];
|
|
196
|
+
for (const child of root.routes ?? [])
|
|
197
|
+
out.push(...routesOf(child));
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
function inhibitKey(r) {
|
|
201
|
+
const rule = isObject(r) ? r : {};
|
|
202
|
+
const equal = Array.isArray(rule.equal) ? [...rule.equal].map(String).sort().join(",") : "";
|
|
203
|
+
return `${matcherKey(rule.source_matchers)}=>${matcherKey(rule.target_matchers)}/${equal}`;
|
|
204
|
+
}
|
|
205
|
+
async function observeAlertmanagerEntity(am, entity, owned) {
|
|
206
|
+
let loaded;
|
|
207
|
+
try {
|
|
208
|
+
loaded = await loadAlertmanager(am);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
return unobservedFrom(err);
|
|
212
|
+
}
|
|
213
|
+
const queried = am.target.kind === "alertmanager" ? am.statusPath : "/api/v1/alerts";
|
|
214
|
+
if (!loaded)
|
|
215
|
+
return { absent: true, queried };
|
|
216
|
+
const { config, health } = loaded;
|
|
217
|
+
const props = entity.props;
|
|
218
|
+
let physicalId;
|
|
219
|
+
let attributes = {};
|
|
220
|
+
switch (entity.type) {
|
|
221
|
+
case RECEIVER_TYPE: {
|
|
222
|
+
const r = (config.receivers ?? []).find((x) => x.name === props.name);
|
|
223
|
+
if (r) {
|
|
224
|
+
physicalId = `receiver/${r.name}`;
|
|
225
|
+
attributes = { integrations: Object.keys(r).filter((k) => k.endsWith("_configs")).sort() };
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case TIME_INTERVAL_TYPE: {
|
|
230
|
+
const t = (config.time_intervals ?? []).find((x) => x.name === props.name);
|
|
231
|
+
if (t)
|
|
232
|
+
physicalId = `time_interval/${t.name}`;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case ROUTE_TYPE: {
|
|
236
|
+
const key = routeKey(props.receiver, props.matchers);
|
|
237
|
+
const r = routesOf(config.route).find((x) => routeKey(x.receiver, x.matchers) === key);
|
|
238
|
+
if (r) {
|
|
239
|
+
physicalId = `route/${key}`;
|
|
240
|
+
attributes = { receiver: r.receiver ?? "", routes: (r.routes ?? []).length };
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
case INHIBIT_RULE_TYPE: {
|
|
245
|
+
const key = inhibitKey(props);
|
|
246
|
+
const i = (config.inhibit_rules ?? []).findIndex((x) => inhibitKey(x) === key);
|
|
247
|
+
if (i >= 0)
|
|
248
|
+
physicalId = `inhibit_rule/${i}`;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case SETTINGS_TYPE:
|
|
252
|
+
physicalId = "alertmanager";
|
|
253
|
+
attributes = { configDigest: loaded.digest, ...(config.templates ? { templates: config.templates.length } : {}) };
|
|
254
|
+
break;
|
|
255
|
+
default:
|
|
256
|
+
return { unobserved: { reason: "unsupported-kind", detail: `no prometheus reader for ${entity.type}` } };
|
|
257
|
+
}
|
|
258
|
+
if (physicalId === undefined)
|
|
259
|
+
return { absent: true, queried };
|
|
260
|
+
if (owned)
|
|
261
|
+
return { unobserved: { reason: "filtered", detail: `${entity.name} is in the loaded Alertmanager config but ${FILTERED_DETAIL}` }, queried };
|
|
262
|
+
return {
|
|
263
|
+
present: { type: entity.type, physicalId, status: "PRESENT", ownership: "unknown", attributes: { ...attributes, ...health } },
|
|
264
|
+
queried,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// ── harness ─────────────────────────────────────────────────────────
|
|
268
|
+
const ALERTMANAGER_TYPES = new Set([RECEIVER_TYPE, ROUTE_TYPE, INHIBIT_RULE_TYPE, TIME_INTERVAL_TYPE, SETTINGS_TYPE]);
|
|
269
|
+
function adapter(options) {
|
|
270
|
+
return {
|
|
271
|
+
bind: () => bindEndpoints({ ...options }),
|
|
272
|
+
classifyBindFailure: (err) => classifyPromFailure(err),
|
|
273
|
+
async read(endpoints, entity) {
|
|
274
|
+
if (entity.type === RULE_GROUP_TYPE) {
|
|
275
|
+
return isUnresolvedTarget(endpoints.ruler) ? unbound(endpoints.ruler) : observeRuleGroup(endpoints.ruler, entity, options.owned);
|
|
276
|
+
}
|
|
277
|
+
if (ALERTMANAGER_TYPES.has(entity.type)) {
|
|
278
|
+
return isUnresolvedTarget(endpoints.alertmanager)
|
|
279
|
+
? unbound(endpoints.alertmanager)
|
|
280
|
+
: observeAlertmanagerEntity(endpoints.alertmanager, entity, options.owned);
|
|
281
|
+
}
|
|
282
|
+
if (entity.type === SCRAPE_CONFIG_TYPE || entity.type === PROMETHEUS_CONFIG_TYPE) {
|
|
283
|
+
return { unobserved: { reason: "unsupported-kind", detail: PROMETHEUS_CONFIG_NOT_OBSERVED } };
|
|
284
|
+
}
|
|
285
|
+
return { unobserved: { reason: "unsupported-kind", detail: `no prometheus reader for ${entity.type}` } };
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/** Observe every declared prometheus entity against the environment's ruler and Alertmanager. */
|
|
290
|
+
export async function describeResources(options) {
|
|
291
|
+
const declared = [];
|
|
292
|
+
for (const name of options.entityNames) {
|
|
293
|
+
const entity = options.entities.get(name);
|
|
294
|
+
if (!entity)
|
|
295
|
+
continue;
|
|
296
|
+
declared.push({ name, type: entity.entityType, props: entity.props });
|
|
297
|
+
}
|
|
298
|
+
return observeEntities(declared, adapter(options));
|
|
299
|
+
}
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
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), 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
|
|
6
|
+
* of the plugin and the TypeScript compiler so it bundles for edge runtimes,
|
|
7
|
+
* like the other lexicons' `detect` modules.
|
|
8
|
+
*/
|
|
9
|
+
import { looksLikeAlertmanagerConfig, looksLikePrometheusConfig, looksLikeRuleFile } from "./model.js";
|
|
10
|
+
export function detectTemplate(data) {
|
|
11
|
+
return looksLikeRuleFile(data) || looksLikeAlertmanagerConfig(data) || looksLikePrometheusConfig(data);
|
|
12
|
+
}
|
package/dist/duration.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prometheus durations, as `model.ParseDuration` reads them.
|
|
3
|
+
*
|
|
4
|
+
* A duration is one or more `<number><unit>` terms, units in descending
|
|
5
|
+
* order and each at most once: `y`, `w`, `d`, `h`, `m`, `s`, `ms`. `1h30m`
|
|
6
|
+
* is valid, `30m1h` and `1.5h` are not. `0` on its own is valid. Prometheus
|
|
7
|
+
* (rule `for`, `interval`, range selectors) and Alertmanager (`group_wait`,
|
|
8
|
+
* `resolve_timeout`) share this grammar.
|
|
9
|
+
*/
|
|
10
|
+
const DURATION = /^(?:(\d+)y)?(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?(?:(\d+)ms)?$/;
|
|
11
|
+
const UNIT_MS = [365 * 86400000, 7 * 86400000, 86400000, 3600000, 60000, 1000, 1];
|
|
12
|
+
/** True when `value` is a Prometheus duration string. */
|
|
13
|
+
export function isValidDuration(value) {
|
|
14
|
+
if (typeof value !== "string" || value === "")
|
|
15
|
+
return false;
|
|
16
|
+
if (value === "0")
|
|
17
|
+
return true;
|
|
18
|
+
return DURATION.test(value);
|
|
19
|
+
}
|
|
20
|
+
/** A duration in milliseconds, or `undefined` when it isn't a valid duration. */
|
|
21
|
+
export function durationMs(value) {
|
|
22
|
+
if (value === "0")
|
|
23
|
+
return 0;
|
|
24
|
+
if (value === "")
|
|
25
|
+
return undefined;
|
|
26
|
+
const m = DURATION.exec(value);
|
|
27
|
+
if (!m)
|
|
28
|
+
return undefined;
|
|
29
|
+
let total = 0;
|
|
30
|
+
for (let i = 0; i < UNIT_MS.length; i++) {
|
|
31
|
+
const n = m[i + 1];
|
|
32
|
+
if (n !== undefined)
|
|
33
|
+
total += Number(n) * UNIT_MS[i];
|
|
34
|
+
}
|
|
35
|
+
return total;
|
|
36
|
+
}
|
|
37
|
+
/** Write a millisecond count as the shortest Prometheus duration, e.g. `5400000` -> `1h30m`. */
|
|
38
|
+
export function formatDuration(ms) {
|
|
39
|
+
if (!Number.isInteger(ms) || ms < 0)
|
|
40
|
+
throw new Error(`prometheus: ${ms} is not a whole, non-negative number of milliseconds`);
|
|
41
|
+
if (ms === 0)
|
|
42
|
+
return "0s";
|
|
43
|
+
const units = ["y", "w", "d", "h", "m", "s", "ms"];
|
|
44
|
+
let rest = ms;
|
|
45
|
+
let out = "";
|
|
46
|
+
for (let i = 0; i < UNIT_MS.length; i++) {
|
|
47
|
+
const n = Math.floor(rest / UNIT_MS[i]);
|
|
48
|
+
if (n > 0) {
|
|
49
|
+
out += `${n}${units[i]}`;
|
|
50
|
+
rest -= n * UNIT_MS[i];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|