@intentius/chant-lexicon-prometheus 0.106.4 → 0.108.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 (111) hide show
  1. package/README.md +6 -1
  2. package/dist/api/alertmanager.d.ts +56 -0
  3. package/dist/api/alertmanager.d.ts.map +1 -0
  4. package/dist/api/apply.d.ts +126 -0
  5. package/dist/api/apply.d.ts.map +1 -0
  6. package/dist/api/bind.d.ts +49 -0
  7. package/dist/api/bind.d.ts.map +1 -0
  8. package/dist/api/client.d.ts +76 -0
  9. package/dist/api/client.d.ts.map +1 -0
  10. package/dist/api/evaluated.d.ts +36 -0
  11. package/dist/api/evaluated.d.ts.map +1 -0
  12. package/dist/api/fake-servers.d.ts +74 -0
  13. package/dist/api/fake-servers.d.ts.map +1 -0
  14. package/dist/api/index.d.ts +18 -0
  15. package/dist/api/index.d.ts.map +1 -0
  16. package/dist/api/ruler.d.ts +87 -0
  17. package/dist/api/ruler.d.ts.map +1 -0
  18. package/dist/composites/alert-routing.d.ts +104 -0
  19. package/dist/composites/alert-routing.d.ts.map +1 -0
  20. package/dist/composites/catalog.d.ts.map +1 -1
  21. package/dist/composites/index.d.ts +6 -0
  22. package/dist/composites/index.d.ts.map +1 -1
  23. package/dist/composites/red-alerts.d.ts +96 -0
  24. package/dist/composites/red-alerts.d.ts.map +1 -0
  25. package/dist/composites/watchdog.d.ts +69 -0
  26. package/dist/composites/watchdog.d.ts.map +1 -0
  27. package/dist/config.d.ts +273 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/describe-resources.d.ts +48 -0
  30. package/dist/describe-resources.d.ts.map +1 -0
  31. package/dist/export-resources.d.ts +41 -0
  32. package/dist/export-resources.d.ts.map +1 -0
  33. package/dist/import/alertmanager-live.d.ts +35 -0
  34. package/dist/import/alertmanager-live.d.ts.map +1 -0
  35. package/dist/import/parser.d.ts +12 -0
  36. package/dist/import/parser.d.ts.map +1 -1
  37. package/dist/index.d.ts +6 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/integrity.json +3 -3
  40. package/dist/lint/post-synth/prom-helpers.d.ts +11 -3
  41. package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -1
  42. package/dist/manifest.json +1 -1
  43. package/dist/op/activities/index.d.ts +25 -0
  44. package/dist/op/activities/index.d.ts.map +1 -0
  45. package/dist/op/activities/promtool.d.ts +78 -0
  46. package/dist/op/activities/promtool.d.ts.map +1 -0
  47. package/dist/op/activities/rule-audit.d.ts +65 -0
  48. package/dist/op/activities/rule-audit.d.ts.map +1 -0
  49. package/dist/op/activities/rules-loaded.d.ts +60 -0
  50. package/dist/op/activities/rules-loaded.d.ts.map +1 -0
  51. package/dist/op/activities/silence.d.ts +80 -0
  52. package/dist/op/activities/silence.d.ts.map +1 -0
  53. package/dist/op/activity-contracts.d.ts +113 -0
  54. package/dist/op/activity-contracts.d.ts.map +1 -0
  55. package/dist/op/audit-op.d.ts +38 -0
  56. package/dist/op/audit-op.d.ts.map +1 -0
  57. package/dist/op/builders.d.ts +57 -0
  58. package/dist/op/builders.d.ts.map +1 -0
  59. package/dist/op/slo-rule-tests.d.ts +44 -0
  60. package/dist/op/slo-rule-tests.d.ts.map +1 -0
  61. package/dist/plugin.d.ts.map +1 -1
  62. package/dist/rules/prom-helpers.ts +38 -3
  63. package/package.json +19 -3
  64. package/src/api/alertmanager.ts +122 -0
  65. package/src/api/apply.test.ts +218 -0
  66. package/src/api/apply.ts +375 -0
  67. package/src/api/bind.ts +87 -0
  68. package/src/api/client.ts +138 -0
  69. package/src/api/evaluated.test.ts +78 -0
  70. package/src/api/evaluated.ts +106 -0
  71. package/src/api/fake-servers.ts +209 -0
  72. package/src/api/index.ts +29 -0
  73. package/src/api/ruler.ts +168 -0
  74. package/src/api/testdata/am-status-original.yml +85 -0
  75. package/src/composites/alert-routing.test.ts +163 -0
  76. package/src/composites/alert-routing.ts +277 -0
  77. package/src/composites/catalog.test.ts +10 -2
  78. package/src/composites/catalog.ts +214 -0
  79. package/src/composites/index.ts +14 -0
  80. package/src/composites/red-alerts.test.ts +182 -0
  81. package/src/composites/red-alerts.ts +214 -0
  82. package/src/composites/watchdog.test.ts +85 -0
  83. package/src/composites/watchdog.ts +142 -0
  84. package/src/config.test.ts +93 -0
  85. package/src/config.ts +288 -0
  86. package/src/describe-resources.test.ts +325 -0
  87. package/src/describe-resources.ts +328 -0
  88. package/src/export-resources.test.ts +253 -0
  89. package/src/export-resources.ts +193 -0
  90. package/src/import/alertmanager-live.test.ts +66 -0
  91. package/src/import/alertmanager-live.ts +291 -0
  92. package/src/import/parser.ts +18 -1
  93. package/src/import/roundtrip.test.ts +1 -0
  94. package/src/index.ts +37 -0
  95. package/src/lint/post-synth/prom-helpers.ts +38 -3
  96. package/src/op/activities/index.ts +31 -0
  97. package/src/op/activities/promtool.test.ts +107 -0
  98. package/src/op/activities/promtool.ts +149 -0
  99. package/src/op/activities/rule-audit.test.ts +89 -0
  100. package/src/op/activities/rule-audit.ts +168 -0
  101. package/src/op/activities/rules-loaded.test.ts +61 -0
  102. package/src/op/activities/rules-loaded.ts +98 -0
  103. package/src/op/activities/silence.test.ts +107 -0
  104. package/src/op/activities/silence.ts +175 -0
  105. package/src/op/activity-contracts.test.ts +141 -0
  106. package/src/op/activity-contracts.ts +96 -0
  107. package/src/op/audit-op.ts +54 -0
  108. package/src/op/builders.ts +82 -0
  109. package/src/op/slo-rule-tests.test.ts +84 -0
  110. package/src/op/slo-rule-tests.ts +173 -0
  111. package/src/plugin.ts +16 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The promtool tests generated from an `Slo`: their shape always, and
3
+ * `promtool test rules` over them when promtool is on PATH (or named by
4
+ * $PROMTOOL).
5
+ */
6
+ import { beforeAll, describe, expect, test } from "vitest";
7
+ import { load } from "js-yaml";
8
+ import { Slo, sloMetrics } from "../composites/slo";
9
+ import { ruleFileYaml } from "../build";
10
+ import { hasTool, promtoolTestRules } from "../tools";
11
+ import { seriesLabels, seriesValues, sloRuleTests } from "./slo-rule-tests";
12
+
13
+ const PROMTOOL = process.env.PROMTOOL ?? "promtool";
14
+
15
+ const slo = Slo({
16
+ name: "checkout",
17
+ objective: 0.995,
18
+ window: "30d",
19
+ sli: {
20
+ good: 'sum(rate(requests_total{code!~"5.."}[{{window}}]))',
21
+ total: "sum(rate(requests_total[{{window}}]))",
22
+ },
23
+ });
24
+ const input = { slo, good: 'requests_total{code="200"}', bad: 'requests_total{code="500"}' };
25
+
26
+ interface TestFile {
27
+ rule_files: string[];
28
+ evaluation_interval: string;
29
+ tests: Array<{
30
+ interval: string;
31
+ input_series: Array<{ series: string; values: string }>;
32
+ alert_rule_test: Array<{ eval_time: string; alertname: string; exp_alerts: Array<{ exp_labels: Record<string, string> }> }>;
33
+ }>;
34
+ }
35
+
36
+ describe("sloRuleTests", () => {
37
+ test("series parsing and promtool's a+bxn notation", () => {
38
+ expect(seriesLabels('requests_total{code="200", job="api"}')).toEqual({ __name__: "requests_total", code: "200", job: "api" });
39
+ expect(seriesLabels("up")).toEqual({ __name__: "up" });
40
+ expect(() => seriesLabels('x{code=~"5.."}')).toThrow(/equality only/);
41
+ expect(seriesValues([0, 10, 20, 30, 35, 40])).toBe("0+10x3 35+5x1");
42
+ expect(seriesValues([5])).toBe("5");
43
+ });
44
+
45
+ test("one file per step size, each naming rules.yml, two scenarios per burn-rate pair", () => {
46
+ const files = sloRuleTests([input]).map((y) => load(y) as TestFile);
47
+ const m = sloMetrics(slo);
48
+ expect(files.map((f) => f.evaluation_interval)).toEqual(["1m", "5m"]);
49
+ for (const f of files) expect(f.rule_files).toEqual(["rules.yml"]);
50
+ expect(files.reduce((n, f) => n + f.tests.length, 0)).toBe(2 * m.burnRates.length);
51
+ expect(files[0].tests[0].input_series.map((s) => s.series)).toEqual([input.good, input.bad]);
52
+ });
53
+
54
+ test("the expectations come from the evaluator: each pair fires above its rate and not below it", () => {
55
+ const files = sloRuleTests([input]).map((y) => load(y) as TestFile);
56
+ const m = sloMetrics(slo);
57
+ const tests = files.flatMap((f) => f.tests);
58
+ m.burnRates.forEach((b, i) => {
59
+ const [above, below] = [tests[2 * i], tests[2 * i + 1]];
60
+ const fires = (t: (typeof tests)[number], k: number) =>
61
+ t.alert_rule_test[k].exp_alerts.some((a) => Object.entries(b.labels).every(([key, v]) => a.exp_labels[key] === v));
62
+ expect(fires(above, 0), `${b.long}/${b.short} before onset`).toBe(false);
63
+ expect(fires(above, 1), `${b.long}/${b.short} after onset`).toBe(true);
64
+ expect(fires(above, 2), `${b.long}/${b.short} end of burn`).toBe(true);
65
+ expect(fires(below, 0), `${b.long}/${b.short} at 0.9x`).toBe(false);
66
+ expect(above.alert_rule_test[0].alertname).toBe(m.alertName);
67
+ });
68
+ });
69
+
70
+ describe.skipIf(!hasTool(PROMTOOL))("with promtool", () => {
71
+ // promtool takes most of 20s over the ticket pairs' days of samples on a CI
72
+ // runner, past the unit budget, which does not count a beforeAll.
73
+ let results: Array<{ ok: boolean; output: string }> = [];
74
+ beforeAll(() => {
75
+ const rules = ruleFileYaml([slo.rules]);
76
+ results = sloRuleTests([input]).map((yaml) => promtoolTestRules(rules, yaml, PROMTOOL));
77
+ }, 300_000);
78
+
79
+ test("promtool test rules agrees with every generated file", () => {
80
+ expect(results).toHaveLength(2);
81
+ for (const r of results) expect(r.ok, r.output).toBe(true);
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,173 @@
1
+ /**
2
+ * `promtool test rules` input generated from `Slo` declarations (#3369),
3
+ * lifted from `composites/slo-burn.test.ts`.
4
+ *
5
+ * For each burn-rate pair an `Slo` builds, two synthetic scenarios over a
6
+ * good and a bad request counter: healthy for the pair's long window, then
7
+ * burning the error budget at 1.25 times the pair's factor (it must fire)
8
+ * and at 0.9 times it (it must not). The small evaluator in `rule-eval.ts`
9
+ * runs the SLO's rules over each scenario and says which alerts fire at a
10
+ * few evaluation times around the expected onset; those become the
11
+ * `alert_rule_test` expectations, so `promtool test rules` holds Prometheus
12
+ * to the same answer.
13
+ *
14
+ * The SLI expressions are the author's, so the generator cannot invent the
15
+ * series they read: `good` and `bad` name one series each (`metric{labels}`),
16
+ * counters such that the SLI counts `good` as good events and `bad` as bad
17
+ * ones. The evaluator throws on PromQL it does not implement, which then
18
+ * fails here, at authoring time, rather than in promtool.
19
+ *
20
+ * Pairs whose long window is a day or more step in five minutes, the rest in
21
+ * one, and promtool's `evaluation_interval` is per file, so the result is
22
+ * one test file per step size.
23
+ */
24
+
25
+ import { dump } from "js-yaml";
26
+ import { sloMetrics, type SloBurnRate, type SloInstance } from "../composites/slo";
27
+ import { ruleGroupConfig } from "../rules";
28
+ import { RuleEvaluator, type FiringAlert } from "../rule-eval";
29
+ import { durationMs } from "../duration";
30
+ import type { AlertingRuleConfig } from "../model";
31
+
32
+ const MIN = 60_000;
33
+
34
+ export interface SloTestInput {
35
+ /** The `Slo(...)` result. */
36
+ slo: SloInstance;
37
+ /** The good-events counter, e.g. `requests_total{code="200"}`. */
38
+ good: string;
39
+ /** The bad-events counter, e.g. `requests_total{code="500"}`. */
40
+ bad: string;
41
+ /** Events per minute. Default 1000. */
42
+ rate?: number;
43
+ }
44
+
45
+ interface Scenario {
46
+ step: number;
47
+ healthy: number;
48
+ burning: number;
49
+ burn: number;
50
+ }
51
+
52
+ /** `metric{a="1",b="2"}` as a label set with `__name__`. */
53
+ export function seriesLabels(series: string): Record<string, string> {
54
+ const m = /^\s*([A-Za-z_:][A-Za-z0-9_:]*)\s*(?:\{(.*)\})?\s*$/.exec(series);
55
+ if (!m) throw new Error(`"${series}" is not a series: write metric{label="value",...}`);
56
+ const labels: Record<string, string> = { __name__: m[1] };
57
+ const body = m[2]?.trim();
58
+ if (body) {
59
+ const re = /\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"((?:[^"\\]|\\.)*)"\s*(?:,|$)/y;
60
+ let pos = 0;
61
+ while (pos < body.length) {
62
+ re.lastIndex = pos;
63
+ const p = re.exec(body);
64
+ if (!p) throw new Error(`"${series}": label matchers must be name="value" (equality only)`);
65
+ labels[p[1]] = p[2].replace(/\\(.)/g, "$1");
66
+ pos = re.lastIndex;
67
+ }
68
+ }
69
+ return labels;
70
+ }
71
+
72
+ /** Counter samples as promtool's `a+bxn` segments. */
73
+ export function seriesValues(values: number[]): string {
74
+ const parts: string[] = [];
75
+ let i = 0;
76
+ while (i < values.length) {
77
+ const start = values[i];
78
+ if (i + 1 >= values.length) {
79
+ parts.push(`${start}`);
80
+ break;
81
+ }
82
+ const inc = values[i + 1] - values[i];
83
+ let j = i + 1;
84
+ while (j + 1 < values.length && Math.abs(values[j + 1] - values[j] - inc) < 1e-9) j++;
85
+ parts.push(`${start}+${inc}x${j - i}`);
86
+ i = j + 1;
87
+ }
88
+ return parts.join(" ");
89
+ }
90
+
91
+ function isPair(a: FiringAlert, b: SloBurnRate): boolean {
92
+ return a.labels.alertname === b.alert && Object.entries(b.labels).every(([k, v]) => a.labels[k] === v);
93
+ }
94
+
95
+ const minutes = (d: string): number => durationMs(d)! / MIN;
96
+
97
+ /** One scenario: the counters' samples, and the evaluator's firing alerts by minute. */
98
+ function simulate(input: SloTestInput, s: Scenario) {
99
+ const m = sloMetrics(input.slo);
100
+ const rate = input.rate ?? 1000;
101
+ const goodLabels = seriesLabels(input.good);
102
+ const badLabels = seriesLabels(input.bad);
103
+ const ev = new RuleEvaluator([ruleGroupConfig(input.slo.rules)]);
104
+ const total = s.healthy + s.burning;
105
+ const good: number[] = [];
106
+ const bad: number[] = [];
107
+ let g = 0;
108
+ let e = 0;
109
+ const timeline = new Map<number, FiringAlert[]>();
110
+ for (let minute = 0; minute <= total; minute += s.step) {
111
+ if (minute > 0) {
112
+ const burning = minute > s.healthy && minute <= s.healthy + s.burning;
113
+ const b = burning ? rate * s.step * s.burn * m.errorBudget : 0;
114
+ e += b;
115
+ g += rate * s.step - b;
116
+ }
117
+ good.push(g);
118
+ bad.push(e);
119
+ ev.add(goodLabels, minute * MIN, g);
120
+ ev.add(badLabels, minute * MIN, e);
121
+ timeline.set(minute, ev.step(minute * MIN));
122
+ }
123
+ return { good, bad, timeline };
124
+ }
125
+
126
+ function testGroup(input: SloTestInput, s: Scenario, checkAt: number[]) {
127
+ const m = sloMetrics(input.slo);
128
+ const alertRules = ruleGroupConfig(input.slo.rules).rules.filter((r): r is AlertingRuleConfig => "alert" in r);
129
+ const { good, bad, timeline } = simulate(input, s);
130
+ return {
131
+ interval: `${s.step}m`,
132
+ input_series: [
133
+ { series: input.good, values: seriesValues(good) },
134
+ { series: input.bad, values: seriesValues(bad) },
135
+ ],
136
+ alert_rule_test: checkAt.map((minute) => ({
137
+ eval_time: `${minute}m`,
138
+ alertname: m.alertName,
139
+ exp_alerts: (timeline.get(minute) ?? [])
140
+ .filter((a) => a.labels.alertname === m.alertName)
141
+ .map((a) => {
142
+ const rule = alertRules.find((r) => Object.entries(r.labels ?? {}).every(([k, v]) => a.labels[k] === v));
143
+ const { alertname: _, ...labels } = a.labels;
144
+ return { exp_labels: labels, ...(rule?.annotations ? { exp_annotations: rule.annotations } : {}) };
145
+ }),
146
+ })),
147
+ };
148
+ }
149
+
150
+ /**
151
+ * The `promtool test rules` files for these SLOs, one per step size, each
152
+ * naming `rules.yml` (where `promtoolTestRules` writes the rule file).
153
+ */
154
+ export function sloRuleTests(inputs: SloTestInput[]): string[] {
155
+ const byStep = new Map<number, unknown[]>();
156
+ for (const input of inputs) {
157
+ for (const b of sloMetrics(input.slo).burnRates) {
158
+ const long = minutes(b.long);
159
+ const step = long >= 24 * 60 ? 5 : 1;
160
+ const snap = (x: number) => Math.round(x / step) * step;
161
+ const above: Scenario = { step, healthy: long, burning: long, burn: b.factor * 1.25 };
162
+ const fireAt = above.healthy + ((long - step) * b.factor) / above.burn;
163
+ const below: Scenario = { step, healthy: long, burning: long, burn: b.factor * 0.9 };
164
+ const groups = byStep.get(step) ?? [];
165
+ groups.push(testGroup(input, above, [snap(fireAt - 4 * step), snap(fireAt + 4 * step), above.healthy + long]));
166
+ groups.push(testGroup(input, below, [below.healthy + long]));
167
+ byStep.set(step, groups);
168
+ }
169
+ }
170
+ return [...byStep.entries()]
171
+ .sort(([a], [b]) => a - b)
172
+ .map(([step, tests]) => dump({ rule_files: ["rules.yml"], evaluation_interval: `${step}m`, tests }, { lineWidth: -1 }));
173
+ }
package/src/plugin.ts CHANGED
@@ -18,6 +18,7 @@ import { CATALOG } from "./catalog";
18
18
  import { PROMETHEUS_PIN } from "./pin";
19
19
  import { PROMQL_GRAMMAR } from "./promql";
20
20
  import { compositeCatalog } from "./composites/catalog";
21
+ import { prometheusConfigSchema } from "./config";
21
22
 
22
23
  const catalogResource: McpResourceContribution = {
23
24
  uri: "prometheus:resource-catalog",
@@ -39,6 +40,7 @@ const catalogResource: McpResourceContribution = {
39
40
  export const prometheusPlugin: LexiconPlugin = {
40
41
  name: "prometheus",
41
42
  serializer: prometheusSerializer,
43
+ configSchema: prometheusConfigSchema,
42
44
 
43
45
  // ── Required lifecycle methods ────────────────────────────────
44
46
 
@@ -153,6 +155,20 @@ export const prometheusPlugin: LexiconPlugin = {
153
155
  return hover(ctx);
154
156
  },
155
157
 
158
+ // ── Live observation and export (#3371) ──────────────────────
159
+ // Behind dynamic imports, so `chant build` never loads a transport. The
160
+ // ruler and Alertmanager are named by `prometheus.profiles.<env>` (./config.ts).
161
+
162
+ async describeResources(options) {
163
+ const { describeResources } = await import("./describe-resources");
164
+ return describeResources(options);
165
+ },
166
+
167
+ async exportResources(options) {
168
+ const { exportResources } = await import("./export-resources");
169
+ return exportResources(options);
170
+ },
171
+
156
172
  async docs(options?: { verbose?: boolean }) {
157
173
  const { generateDocs } = await import("./codegen/docs");
158
174
  return generateDocs(options);