@intentius/chant-lexicon-prometheus 0.96.0 → 0.98.0
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 -0
- package/dist/alertmanager.d.ts +4 -2
- package/dist/alertmanager.d.ts.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/import/generator.d.ts +35 -0
- package/dist/import/generator.d.ts.map +1 -0
- package/dist/import/parser.d.ts +52 -0
- package/dist/import/parser.d.ts.map +1 -0
- package/dist/import/slo.d.ts +15 -0
- package/dist/import/slo.d.ts.map +1 -0
- package/dist/integrity.json +7 -7
- package/dist/lint/rules/literal-credential.d.ts.map +1 -1
- package/dist/lint/rules/prom-ast.d.ts +8 -0
- package/dist/lint/rules/prom-ast.d.ts.map +1 -1
- package/dist/lint/rules/promql-literal.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/model.d.ts +21 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/literal-credential.ts +17 -11
- package/dist/rules/prom-ast.ts +36 -0
- package/dist/rules/promql-literal.ts +15 -1
- package/dist/skills/chant-prometheus-alertmanager.md +4 -0
- package/dist/skills/chant-prometheus.md +11 -0
- package/package.json +2 -2
- package/src/alertmanager.ts +4 -1
- package/src/build.ts +2 -0
- package/src/codegen/docs.ts +5 -1
- package/src/import/cli.test.ts +98 -0
- package/src/import/generated-types.e2e.test.ts +75 -0
- package/src/import/generator.test.ts +121 -0
- package/src/import/generator.ts +570 -0
- package/src/import/parser.test.ts +120 -0
- package/src/import/parser.ts +348 -0
- package/src/import/roundtrip.test.ts +321 -0
- package/src/import/slo.ts +175 -0
- package/src/import/testdata/alertmanager-full.yml +89 -0
- package/src/import/testdata/fixtures.ts +114 -0
- package/src/import/testdata/rules-full.yml +42 -0
- package/src/import/testdata/upstream/alertmanager-route-labels.yml +65 -0
- package/src/import/testdata/upstream/alertmanager-simple.yml +130 -0
- package/src/import/testdata/upstream/prometheus-alerting-rules.yml +15 -0
- package/src/import/testdata/upstream/prometheus-alerting-templates.yml +23 -0
- package/src/import/testdata/upstream/prometheus-recording-rules.yml +7 -0
- package/src/lint/rules/literal-credential.ts +17 -11
- package/src/lint/rules/prom-ast.ts +36 -0
- package/src/lint/rules/promql-literal.ts +15 -1
- package/src/lint/rules/rules.test.ts +28 -0
- package/src/model.ts +22 -0
- package/src/plugin.ts +10 -0
- package/src/skills/chant-prometheus-alertmanager.md +4 -0
- package/src/skills/chant-prometheus.md +11 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
3
|
-
import { calleeName, CREDENTIAL_CLASS, literalText, position, propertyName } from "./prom-ast";
|
|
3
|
+
import { calleeName, constInitializers, CREDENTIAL_CLASS, literalText, position, propertyName, resolveConst } from "./prom-ast";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Alertmanager fields that hold a secret, each with a `*_file` sibling that
|
|
@@ -41,13 +41,18 @@ export const literalCredentialRule: LintRule = {
|
|
|
41
41
|
const diagnostics: LintDiagnostic[] = [];
|
|
42
42
|
const source = context.sourceFile;
|
|
43
43
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
const consts = constInitializers(source);
|
|
45
|
+
const scanned = new Set<ts.Node>();
|
|
46
|
+
|
|
47
|
+
// A value is followed to the const it names, so a list lifted out of the
|
|
48
|
+
// constructor (`email_configs: heartbeatEmail`) is scanned where it is used.
|
|
49
|
+
const scanValue = (key: string | undefined, value: ts.Expression) => {
|
|
50
|
+
const init = resolveConst(value, consts);
|
|
51
|
+
if (ts.isObjectLiteralExpression(init) || ts.isArrayLiteralExpression(init)) {
|
|
52
|
+
if (scanned.has(init)) return;
|
|
53
|
+
scanned.add(init);
|
|
54
|
+
if (ts.isObjectLiteralExpression(init)) scan(init);
|
|
55
|
+
else for (const el of init.elements) scanValue(undefined, el);
|
|
51
56
|
return;
|
|
52
57
|
}
|
|
53
58
|
if (!key || !SECRET_FIELDS.has(key)) return;
|
|
@@ -64,8 +69,9 @@ export const literalCredentialRule: LintRule = {
|
|
|
64
69
|
|
|
65
70
|
const scan = (obj: ts.ObjectLiteralExpression) => {
|
|
66
71
|
for (const prop of obj.properties) {
|
|
67
|
-
if (
|
|
68
|
-
|
|
72
|
+
if (ts.isPropertyAssignment(prop)) scanValue(propertyName(prop), prop.initializer);
|
|
73
|
+
else if (ts.isShorthandPropertyAssignment(prop)) scanValue(prop.name.text, prop.name);
|
|
74
|
+
else if (ts.isSpreadAssignment(prop)) scanValue(undefined, prop.expression);
|
|
69
75
|
}
|
|
70
76
|
};
|
|
71
77
|
|
|
@@ -73,7 +79,7 @@ export const literalCredentialRule: LintRule = {
|
|
|
73
79
|
if (ts.isNewExpression(node) || ts.isCallExpression(node)) {
|
|
74
80
|
const name = calleeName(node);
|
|
75
81
|
const arg = node.arguments?.[0];
|
|
76
|
-
if (name && CREDENTIAL_CLASS.test(name) && arg
|
|
82
|
+
if (name && CREDENTIAL_CLASS.test(name) && arg) scanValue(undefined, arg);
|
|
77
83
|
}
|
|
78
84
|
ts.forEachChild(node, visit);
|
|
79
85
|
};
|
|
@@ -28,3 +28,39 @@ export function position(source: ts.SourceFile, node: ts.Node): { line: number;
|
|
|
28
28
|
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
29
29
|
return { line: line + 1, column: character + 1 };
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every `const name = <initializer>` in the file, so a rule can follow a
|
|
34
|
+
* value lifted out of a constructor into a named const (the way COR001 asks
|
|
35
|
+
* for it, and the way `chant import` writes it) back to its literal.
|
|
36
|
+
*/
|
|
37
|
+
export function constInitializers(source: ts.SourceFile): Map<string, ts.Expression> {
|
|
38
|
+
const consts = new Map<string, ts.Expression>();
|
|
39
|
+
const collect = (node: ts.Node) => {
|
|
40
|
+
if (ts.isVariableDeclarationList(node) && node.flags & ts.NodeFlags.Const) {
|
|
41
|
+
for (const d of node.declarations) {
|
|
42
|
+
if (ts.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
ts.forEachChild(node, collect);
|
|
46
|
+
};
|
|
47
|
+
collect(source);
|
|
48
|
+
return consts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `expr` itself, or the initializer of the const it names (followed through `as` and `satisfies`). */
|
|
52
|
+
export function resolveConst(expr: ts.Expression, consts: Map<string, ts.Expression>): ts.Expression {
|
|
53
|
+
let cur = expr;
|
|
54
|
+
for (let hops = 0; hops < 8; hops++) {
|
|
55
|
+
if (ts.isAsExpression(cur) || ts.isSatisfiesExpression(cur) || ts.isParenthesizedExpression(cur)) {
|
|
56
|
+
cur = cur.expression;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ts.isIdentifier(cur) && consts.has(cur.text)) {
|
|
60
|
+
cur = consts.get(cur.text)!;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
return cur;
|
|
66
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
3
|
-
import { calleeName, literalText, position, propertyName } from "./prom-ast";
|
|
3
|
+
import { calleeName, constInitializers, literalText, position, propertyName, resolveConst } from "./prom-ast";
|
|
4
4
|
import { checkPromql } from "../../promql";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -21,7 +21,21 @@ export const promqlLiteralRule: LintRule = {
|
|
|
21
21
|
const diagnostics: LintDiagnostic[] = [];
|
|
22
22
|
const source = context.sourceFile;
|
|
23
23
|
|
|
24
|
+
const consts = constInitializers(source);
|
|
25
|
+
const scanned = new Set<ts.Node>();
|
|
26
|
+
|
|
27
|
+
// Rules lifted into a const (`rules: apiRules`) are followed to it and
|
|
28
|
+
// checked where the group uses them.
|
|
29
|
+
const follow = (value: ts.Expression) => {
|
|
30
|
+
const init = resolveConst(value, consts);
|
|
31
|
+
if (init !== value && !scanned.has(init)) {
|
|
32
|
+
scanned.add(init);
|
|
33
|
+
scan(init);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
24
36
|
const scan = (node: ts.Node) => {
|
|
37
|
+
if (ts.isShorthandPropertyAssignment(node)) follow(node.name);
|
|
38
|
+
if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.initializer)) follow(node.initializer);
|
|
25
39
|
if (ts.isPropertyAssignment(node) && propertyName(node) === "expr") {
|
|
26
40
|
const text = literalText(node.initializer);
|
|
27
41
|
if (text !== undefined) {
|
|
@@ -44,6 +44,22 @@ describe("PROM001 literal credential", () => {
|
|
|
44
44
|
);
|
|
45
45
|
expect(diags).toEqual([]);
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
test("follows values lifted into named consts, shorthand props and spreads", () => {
|
|
49
|
+
const diags = literalCredentialRule.check(
|
|
50
|
+
ctx(`
|
|
51
|
+
const heartbeatEmail: EmailConfig[] = [{ to: "a@example.com", auth_password: "hunter2" }];
|
|
52
|
+
const heartbeat = new Receiver({ name: "heartbeat", email_configs: heartbeatEmail });
|
|
53
|
+
const oncallUntyped = { opsgenie_configs: [{ api_key: "x" }], pagerduty_configs: [{ service_key: "k" }] };
|
|
54
|
+
const oncall = new Receiver({ name: "oncall", ...oncallUntyped });
|
|
55
|
+
const global: AlertmanagerGlobalConfig = { smtp_auth_password: "p" };
|
|
56
|
+
const settings = new AlertmanagerSettings({ global });
|
|
57
|
+
const safe: PagerDutyConfig[] = [{ routing_key_file: "/etc/pd" }];
|
|
58
|
+
const pager = new Receiver({ name: "pager", pagerduty_configs: safe });
|
|
59
|
+
`),
|
|
60
|
+
);
|
|
61
|
+
expect(diags.map((d) => d.message.split("`")[1])).toEqual(["auth_password", "service_key", "smtp_auth_password"]);
|
|
62
|
+
});
|
|
47
63
|
});
|
|
48
64
|
|
|
49
65
|
describe("PROM002 PromQL literal", () => {
|
|
@@ -61,6 +77,18 @@ describe("PROM002 PromQL literal", () => {
|
|
|
61
77
|
expect(diags[0].line).toBe(3);
|
|
62
78
|
});
|
|
63
79
|
|
|
80
|
+
test("follows rules lifted into a named const", () => {
|
|
81
|
+
const diags = promqlLiteralRule.check(
|
|
82
|
+
ctx(`
|
|
83
|
+
const apiRules: Rule[] = [{ record: "a:b", expr: "sum(" }, { alert: "A", expr: "up == 0" }];
|
|
84
|
+
const api = new RuleGroup({ name: "api", rules: apiRules });
|
|
85
|
+
const rules: Rule[] = [{ record: "c:d", expr: "rate(x[5m]" }];
|
|
86
|
+
const other = new RuleGroup({ name: "other", rules });
|
|
87
|
+
`),
|
|
88
|
+
);
|
|
89
|
+
expect(diags.map((d) => d.line)).toEqual([2, 4]);
|
|
90
|
+
});
|
|
91
|
+
|
|
64
92
|
test("ignores built expressions and exprs outside a RuleGroup", () => {
|
|
65
93
|
const diags = promqlLiteralRule.check(
|
|
66
94
|
ctx(`
|
package/src/model.ts
CHANGED
|
@@ -180,6 +180,8 @@ export interface PagerDutyConfig extends NotifierBase {
|
|
|
180
180
|
/** One entry under `receivers:`. */
|
|
181
181
|
export interface ReceiverConfig {
|
|
182
182
|
name: string;
|
|
183
|
+
/** Labels on the receiver, exposed to notification templates. */
|
|
184
|
+
labels?: LabelSet;
|
|
183
185
|
webhook_configs?: WebhookConfig[];
|
|
184
186
|
email_configs?: EmailConfig[];
|
|
185
187
|
slack_configs?: SlackConfig[];
|
|
@@ -198,11 +200,18 @@ export interface RouteConfig {
|
|
|
198
200
|
repeat_interval?: string;
|
|
199
201
|
mute_time_intervals?: string[];
|
|
200
202
|
active_time_intervals?: string[];
|
|
203
|
+
/**
|
|
204
|
+
* Labels on the route, inherited by child routes and exposed to
|
|
205
|
+
* notification templates as `routeLabels`. Values may be Go templates.
|
|
206
|
+
*/
|
|
207
|
+
labels?: LabelSet;
|
|
201
208
|
routes?: RouteConfig[];
|
|
202
209
|
}
|
|
203
210
|
|
|
204
211
|
/** One entry under `inhibit_rules:`. */
|
|
205
212
|
export interface InhibitRuleConfig {
|
|
213
|
+
/** A name for the rule, shown in Alertmanager's logs and metrics. */
|
|
214
|
+
name?: string;
|
|
206
215
|
source_matchers?: string[];
|
|
207
216
|
target_matchers?: string[];
|
|
208
217
|
/** Labels that must be equal on source and target for the inhibition to apply. */
|
|
@@ -254,6 +263,18 @@ export interface AlertmanagerGlobalConfig {
|
|
|
254
263
|
http_config?: HttpClientConfig;
|
|
255
264
|
}
|
|
256
265
|
|
|
266
|
+
/** `tracing:` settings: where Alertmanager sends its own traces. */
|
|
267
|
+
export interface AlertmanagerTracingConfig {
|
|
268
|
+
client_type?: "grpc" | "http";
|
|
269
|
+
endpoint?: string;
|
|
270
|
+
sampling_fraction?: number;
|
|
271
|
+
insecure?: boolean;
|
|
272
|
+
headers?: Record<string, string>;
|
|
273
|
+
compression?: string;
|
|
274
|
+
timeout?: string;
|
|
275
|
+
tls_config?: HttpClientConfig["tls_config"];
|
|
276
|
+
}
|
|
277
|
+
|
|
257
278
|
/** A whole `alertmanager.yml`, the file `amtool check-config` reads. */
|
|
258
279
|
export interface AlertmanagerConfig {
|
|
259
280
|
global?: AlertmanagerGlobalConfig;
|
|
@@ -262,6 +283,7 @@ export interface AlertmanagerConfig {
|
|
|
262
283
|
inhibit_rules?: InhibitRuleConfig[];
|
|
263
284
|
receivers?: ReceiverConfig[];
|
|
264
285
|
time_intervals?: TimeIntervalConfig[];
|
|
286
|
+
tracing?: AlertmanagerTracingConfig;
|
|
265
287
|
}
|
|
266
288
|
|
|
267
289
|
/** The four receiver integrations this lexicon types. */
|
package/src/plugin.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { prometheusAuditCatalog } from "./lint/audit-catalog";
|
|
|
9
9
|
import { completions } from "./lsp/completions";
|
|
10
10
|
import { hover } from "./lsp/hover";
|
|
11
11
|
import { detectTemplate } from "./detect";
|
|
12
|
+
import { PrometheusParser } from "./import/parser";
|
|
13
|
+
import { PrometheusGenerator } from "./import/generator";
|
|
12
14
|
import { initTemplates } from "./init-templates";
|
|
13
15
|
import { prometheusSkills } from "./skill-defs";
|
|
14
16
|
import { CATALOG } from "./catalog";
|
|
@@ -110,6 +112,14 @@ export const prometheusPlugin: LexiconPlugin = {
|
|
|
110
112
|
return detectTemplate(data);
|
|
111
113
|
},
|
|
112
114
|
|
|
115
|
+
templateParser() {
|
|
116
|
+
return new PrometheusParser();
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
templateGenerator() {
|
|
120
|
+
return new PrometheusGenerator();
|
|
121
|
+
},
|
|
122
|
+
|
|
113
123
|
initTemplates(template?: string) {
|
|
114
124
|
return initTemplates(template);
|
|
115
125
|
},
|
|
@@ -47,6 +47,10 @@ export const pageMutesTicket = new InhibitRule({
|
|
|
47
47
|
});
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Starting from an existing alertmanager.yml
|
|
51
|
+
|
|
52
|
+
Import it rather than retyping it: `chant import alertmanager.yml --output src` (with `--lexicon prometheus` outside a chant project). The importer writes `receivers.ts`, `time-intervals.ts`, `routes.ts` (the root `Route`, children as `RouteProps` that reference receivers and time intervals by variable), `inhibit-rules.ts` and `settings.ts`. `*_file` paths and Go templates are kept as written; a literal credential is imported as found and PROM001 reports it, so offer to move it to the `*_file` field. Integrations the lexicon doesn't type (`opsgenie_configs`, `msteams_configs`, ...) are spread in from an untyped const. `match`/`match_re` and the top-level `mute_time_intervals` come back in their current spelling (`matchers`, `time_intervals`), and `chant import` prints a warning for each.
|
|
53
|
+
|
|
50
54
|
## What the checks hold you to
|
|
51
55
|
|
|
52
56
|
- One root route (the `Route` no other route nests), with a receiver and no matchers (PROM205).
|
|
@@ -15,6 +15,17 @@ The prometheus lexicon (`@intentius/chant-lexicon-prometheus`) types Prometheus
|
|
|
15
15
|
export default { lexicons: ["prometheus"] };
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Starting from an existing rule file
|
|
19
|
+
|
|
20
|
+
When the user already has a rule file, import it rather than retyping it:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
chant import rules.yml --output src # add --lexicon prometheus outside a chant project
|
|
24
|
+
chant build src --lexicon prometheus -o rules.yml # the same groups back
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The importer writes `rules.ts` (per group, a `Rule[]` const of plain objects and a `RuleGroup`) and `slos.ts` for any group an `Slo()` built, as the `Slo` call. Multi-line expressions become template literals and Go templates in annotations stay as written. Read the warnings `chant import` prints: they name any field Prometheus doesn't define, which is not carried.
|
|
28
|
+
|
|
18
29
|
## Declaring a group
|
|
19
30
|
|
|
20
31
|
A group takes the rule file's own keys (`name`, `interval`, `query_offset`, `limit`, `labels`, `rules`). Rules are plain objects: `record` + `expr` for a recording rule, `alert` + `expr` (+ `for`, `keep_firing_for`, `labels`, `annotations`) for an alerting rule.
|