@intentius/chant-lexicon-prometheus 0.97.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.
Files changed (53) hide show
  1. package/README.md +5 -0
  2. package/dist/alertmanager.d.ts +4 -2
  3. package/dist/alertmanager.d.ts.map +1 -1
  4. package/dist/build.d.ts.map +1 -1
  5. package/dist/codegen/docs.d.ts.map +1 -1
  6. package/dist/import/generator.d.ts +35 -0
  7. package/dist/import/generator.d.ts.map +1 -0
  8. package/dist/import/parser.d.ts +52 -0
  9. package/dist/import/parser.d.ts.map +1 -0
  10. package/dist/import/slo.d.ts +15 -0
  11. package/dist/import/slo.d.ts.map +1 -0
  12. package/dist/integrity.json +7 -7
  13. package/dist/lint/rules/literal-credential.d.ts.map +1 -1
  14. package/dist/lint/rules/prom-ast.d.ts +8 -0
  15. package/dist/lint/rules/prom-ast.d.ts.map +1 -1
  16. package/dist/lint/rules/promql-literal.d.ts.map +1 -1
  17. package/dist/manifest.json +1 -1
  18. package/dist/model.d.ts +21 -0
  19. package/dist/model.d.ts.map +1 -1
  20. package/dist/plugin.d.ts.map +1 -1
  21. package/dist/rules/literal-credential.ts +17 -11
  22. package/dist/rules/prom-ast.ts +36 -0
  23. package/dist/rules/promql-literal.ts +15 -1
  24. package/dist/skills/chant-prometheus-alertmanager.md +4 -0
  25. package/dist/skills/chant-prometheus.md +11 -0
  26. package/package.json +2 -2
  27. package/src/alertmanager.ts +4 -1
  28. package/src/build.ts +2 -0
  29. package/src/codegen/docs.ts +5 -1
  30. package/src/import/cli.test.ts +98 -0
  31. package/src/import/generated-types.e2e.test.ts +75 -0
  32. package/src/import/generator.test.ts +121 -0
  33. package/src/import/generator.ts +570 -0
  34. package/src/import/parser.test.ts +120 -0
  35. package/src/import/parser.ts +348 -0
  36. package/src/import/roundtrip.test.ts +321 -0
  37. package/src/import/slo.ts +175 -0
  38. package/src/import/testdata/alertmanager-full.yml +89 -0
  39. package/src/import/testdata/fixtures.ts +114 -0
  40. package/src/import/testdata/rules-full.yml +42 -0
  41. package/src/import/testdata/upstream/alertmanager-route-labels.yml +65 -0
  42. package/src/import/testdata/upstream/alertmanager-simple.yml +130 -0
  43. package/src/import/testdata/upstream/prometheus-alerting-rules.yml +15 -0
  44. package/src/import/testdata/upstream/prometheus-alerting-templates.yml +23 -0
  45. package/src/import/testdata/upstream/prometheus-recording-rules.yml +7 -0
  46. package/src/lint/rules/literal-credential.ts +17 -11
  47. package/src/lint/rules/prom-ast.ts +36 -0
  48. package/src/lint/rules/promql-literal.ts +15 -1
  49. package/src/lint/rules/rules.test.ts +28 -0
  50. package/src/model.ts +22 -0
  51. package/src/plugin.ts +10 -0
  52. package/src/skills/chant-prometheus-alertmanager.md +4 -0
  53. 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 scanValue = (key: string | undefined, init: ts.Expression) => {
45
- if (ts.isObjectLiteralExpression(init)) {
46
- scan(init);
47
- return;
48
- }
49
- if (ts.isArrayLiteralExpression(init)) {
50
- for (const el of init.elements) if (ts.isObjectLiteralExpression(el)) scan(el);
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 (!ts.isPropertyAssignment(prop)) continue;
68
- scanValue(propertyName(prop), prop.initializer);
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 && ts.isObjectLiteralExpression(arg)) scan(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.