@intentius/chant-lexicon-prometheus 0.96.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 (237) hide show
  1. package/README.md +58 -0
  2. package/dist/alertmanager.d.ts +81 -0
  3. package/dist/alertmanager.d.ts.map +1 -0
  4. package/dist/build.d.ts +33 -0
  5. package/dist/build.d.ts.map +1 -0
  6. package/dist/catalog.d.ts +17 -0
  7. package/dist/catalog.d.ts.map +1 -0
  8. package/dist/codegen/docs-cli.d.ts +3 -0
  9. package/dist/codegen/docs-cli.d.ts.map +1 -0
  10. package/dist/codegen/docs.d.ts +9 -0
  11. package/dist/codegen/docs.d.ts.map +1 -0
  12. package/dist/codegen/generate-cli.d.ts +3 -0
  13. package/dist/codegen/generate-cli.d.ts.map +1 -0
  14. package/dist/codegen/generate.d.ts +19 -0
  15. package/dist/codegen/generate.d.ts.map +1 -0
  16. package/dist/codegen/package.d.ts +7 -0
  17. package/dist/codegen/package.d.ts.map +1 -0
  18. package/dist/composites/catalog.d.ts +3 -0
  19. package/dist/composites/catalog.d.ts.map +1 -0
  20. package/dist/composites/index.d.ts +6 -0
  21. package/dist/composites/index.d.ts.map +1 -0
  22. package/dist/composites/slo.d.ts +205 -0
  23. package/dist/composites/slo.d.ts.map +1 -0
  24. package/dist/detect.d.ts +2 -0
  25. package/dist/detect.d.ts.map +1 -0
  26. package/dist/duration.d.ts +16 -0
  27. package/dist/duration.d.ts.map +1 -0
  28. package/dist/index.d.ts +14 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/init-templates.d.ts +13 -0
  31. package/dist/init-templates.d.ts.map +1 -0
  32. package/dist/integrity.json +33 -0
  33. package/dist/lint/audit-catalog.d.ts +13 -0
  34. package/dist/lint/audit-catalog.d.ts.map +1 -0
  35. package/dist/lint/post-synth/index.d.ts +3 -0
  36. package/dist/lint/post-synth/index.d.ts.map +1 -0
  37. package/dist/lint/post-synth/prom-helpers.d.ts +33 -0
  38. package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -0
  39. package/dist/lint/post-synth/prom101.d.ts +8 -0
  40. package/dist/lint/post-synth/prom101.d.ts.map +1 -0
  41. package/dist/lint/post-synth/prom102.d.ts +8 -0
  42. package/dist/lint/post-synth/prom102.d.ts.map +1 -0
  43. package/dist/lint/post-synth/prom103.d.ts +8 -0
  44. package/dist/lint/post-synth/prom103.d.ts.map +1 -0
  45. package/dist/lint/post-synth/prom104.d.ts +8 -0
  46. package/dist/lint/post-synth/prom104.d.ts.map +1 -0
  47. package/dist/lint/post-synth/prom105.d.ts +8 -0
  48. package/dist/lint/post-synth/prom105.d.ts.map +1 -0
  49. package/dist/lint/post-synth/prom106.d.ts +8 -0
  50. package/dist/lint/post-synth/prom106.d.ts.map +1 -0
  51. package/dist/lint/post-synth/prom107.d.ts +8 -0
  52. package/dist/lint/post-synth/prom107.d.ts.map +1 -0
  53. package/dist/lint/post-synth/prom201.d.ts +8 -0
  54. package/dist/lint/post-synth/prom201.d.ts.map +1 -0
  55. package/dist/lint/post-synth/prom202.d.ts +8 -0
  56. package/dist/lint/post-synth/prom202.d.ts.map +1 -0
  57. package/dist/lint/post-synth/prom203.d.ts +8 -0
  58. package/dist/lint/post-synth/prom203.d.ts.map +1 -0
  59. package/dist/lint/post-synth/prom204.d.ts +8 -0
  60. package/dist/lint/post-synth/prom204.d.ts.map +1 -0
  61. package/dist/lint/post-synth/prom205.d.ts +8 -0
  62. package/dist/lint/post-synth/prom205.d.ts.map +1 -0
  63. package/dist/lint/post-synth/prom206.d.ts +8 -0
  64. package/dist/lint/post-synth/prom206.d.ts.map +1 -0
  65. package/dist/lint/post-synth/prom207.d.ts +8 -0
  66. package/dist/lint/post-synth/prom207.d.ts.map +1 -0
  67. package/dist/lint/post-synth/prom208.d.ts +8 -0
  68. package/dist/lint/post-synth/prom208.d.ts.map +1 -0
  69. package/dist/lint/post-synth/prom209.d.ts +8 -0
  70. package/dist/lint/post-synth/prom209.d.ts.map +1 -0
  71. package/dist/lint/rules/index.d.ts +7 -0
  72. package/dist/lint/rules/index.d.ts.map +1 -0
  73. package/dist/lint/rules/literal-credential.d.ts +18 -0
  74. package/dist/lint/rules/literal-credential.d.ts.map +1 -0
  75. package/dist/lint/rules/prom-ast.d.ts +14 -0
  76. package/dist/lint/rules/prom-ast.d.ts.map +1 -0
  77. package/dist/lint/rules/promql-literal.d.ts +11 -0
  78. package/dist/lint/rules/promql-literal.d.ts.map +1 -0
  79. package/dist/lint/rules/slo-literal.d.ts +13 -0
  80. package/dist/lint/rules/slo-literal.d.ts.map +1 -0
  81. package/dist/lsp/completions.d.ts +4 -0
  82. package/dist/lsp/completions.d.ts.map +1 -0
  83. package/dist/lsp/hover.d.ts +4 -0
  84. package/dist/lsp/hover.d.ts.map +1 -0
  85. package/dist/manifest.json +8 -0
  86. package/dist/matchers.d.ts +29 -0
  87. package/dist/matchers.d.ts.map +1 -0
  88. package/dist/meta.json +32 -0
  89. package/dist/model.d.ts +262 -0
  90. package/dist/model.d.ts.map +1 -0
  91. package/dist/okf/index.md +34 -0
  92. package/dist/okf/rules/PROM001.md +16 -0
  93. package/dist/okf/rules/PROM002.md +15 -0
  94. package/dist/okf/rules/PROM003.md +11 -0
  95. package/dist/okf/rules/PROM101.md +11 -0
  96. package/dist/okf/rules/PROM102.md +11 -0
  97. package/dist/okf/rules/PROM103.md +11 -0
  98. package/dist/okf/rules/PROM104.md +11 -0
  99. package/dist/okf/rules/PROM105.md +11 -0
  100. package/dist/okf/rules/PROM106.md +11 -0
  101. package/dist/okf/rules/PROM107.md +11 -0
  102. package/dist/okf/rules/PROM201.md +11 -0
  103. package/dist/okf/rules/PROM202.md +11 -0
  104. package/dist/okf/rules/PROM203.md +11 -0
  105. package/dist/okf/rules/PROM204.md +11 -0
  106. package/dist/okf/rules/PROM205.md +11 -0
  107. package/dist/okf/rules/PROM206.md +11 -0
  108. package/dist/okf/rules/PROM207.md +11 -0
  109. package/dist/okf/rules/PROM208.md +11 -0
  110. package/dist/okf/rules/PROM209.md +11 -0
  111. package/dist/okf/types/AlertmanagerSettings.md +13 -0
  112. package/dist/okf/types/InhibitRule.md +9 -0
  113. package/dist/okf/types/Receiver.md +13 -0
  114. package/dist/okf/types/Route.md +9 -0
  115. package/dist/okf/types/RuleGroup.md +13 -0
  116. package/dist/okf/types/TimeInterval.md +9 -0
  117. package/dist/package-cli.d.ts +3 -0
  118. package/dist/package-cli.d.ts.map +1 -0
  119. package/dist/pin.d.ts +20 -0
  120. package/dist/pin.d.ts.map +1 -0
  121. package/dist/plugin.d.ts +10 -0
  122. package/dist/plugin.d.ts.map +1 -0
  123. package/dist/promql.d.ts +25 -0
  124. package/dist/promql.d.ts.map +1 -0
  125. package/dist/rule-eval.d.ts +67 -0
  126. package/dist/rule-eval.d.ts.map +1 -0
  127. package/dist/rules/literal-credential.ts +84 -0
  128. package/dist/rules/prom-ast.ts +30 -0
  129. package/dist/rules/prom-helpers.ts +89 -0
  130. package/dist/rules/prom101.ts +17 -0
  131. package/dist/rules/prom102.ts +17 -0
  132. package/dist/rules/prom103.ts +17 -0
  133. package/dist/rules/prom104.ts +17 -0
  134. package/dist/rules/prom105.ts +17 -0
  135. package/dist/rules/prom106.ts +17 -0
  136. package/dist/rules/prom107.ts +17 -0
  137. package/dist/rules/prom201.ts +17 -0
  138. package/dist/rules/prom202.ts +17 -0
  139. package/dist/rules/prom203.ts +17 -0
  140. package/dist/rules/prom204.ts +17 -0
  141. package/dist/rules/prom205.ts +17 -0
  142. package/dist/rules/prom206.ts +17 -0
  143. package/dist/rules/prom207.ts +17 -0
  144. package/dist/rules/prom208.ts +17 -0
  145. package/dist/rules/prom209.ts +17 -0
  146. package/dist/rules/promql-literal.ts +55 -0
  147. package/dist/rules/slo-literal.ts +81 -0
  148. package/dist/rules.d.ts +59 -0
  149. package/dist/rules.d.ts.map +1 -0
  150. package/dist/serializer.d.ts +27 -0
  151. package/dist/serializer.d.ts.map +1 -0
  152. package/dist/skill-defs.d.ts +3 -0
  153. package/dist/skill-defs.d.ts.map +1 -0
  154. package/dist/skills/chant-prometheus-alertmanager.md +58 -0
  155. package/dist/skills/chant-prometheus-kubernetes.md +48 -0
  156. package/dist/skills/chant-prometheus.md +91 -0
  157. package/dist/tools.d.ts +30 -0
  158. package/dist/tools.d.ts.map +1 -0
  159. package/dist/types/index.d.ts +3 -0
  160. package/dist/validate-cli.d.ts +3 -0
  161. package/dist/validate-cli.d.ts.map +1 -0
  162. package/dist/validate-config.d.ts +35 -0
  163. package/dist/validate-config.d.ts.map +1 -0
  164. package/dist/validate.d.ts +11 -0
  165. package/dist/validate.d.ts.map +1 -0
  166. package/package.json +77 -0
  167. package/src/alertmanager.ts +143 -0
  168. package/src/build.ts +199 -0
  169. package/src/catalog.ts +65 -0
  170. package/src/codegen/docs-cli.ts +4 -0
  171. package/src/codegen/docs.ts +104 -0
  172. package/src/codegen/generate-cli.ts +8 -0
  173. package/src/codegen/generate.ts +49 -0
  174. package/src/codegen/package.ts +41 -0
  175. package/src/composites/catalog.test.ts +37 -0
  176. package/src/composites/catalog.ts +65 -0
  177. package/src/composites/composites.test.ts +256 -0
  178. package/src/composites/index.ts +23 -0
  179. package/src/composites/slo-burn.test.ts +289 -0
  180. package/src/composites/slo.ts +471 -0
  181. package/src/detect.ts +12 -0
  182. package/src/duration.ts +51 -0
  183. package/src/generated/lexicon-prometheus.json +32 -0
  184. package/src/index.ts +97 -0
  185. package/src/init-templates.test.ts +29 -0
  186. package/src/init-templates.ts +76 -0
  187. package/src/lint/audit-catalog.ts +123 -0
  188. package/src/lint/post-synth/index.ts +37 -0
  189. package/src/lint/post-synth/post-synth.test.ts +256 -0
  190. package/src/lint/post-synth/prom-helpers.ts +89 -0
  191. package/src/lint/post-synth/prom101.ts +17 -0
  192. package/src/lint/post-synth/prom102.ts +17 -0
  193. package/src/lint/post-synth/prom103.ts +17 -0
  194. package/src/lint/post-synth/prom104.ts +17 -0
  195. package/src/lint/post-synth/prom105.ts +17 -0
  196. package/src/lint/post-synth/prom106.ts +17 -0
  197. package/src/lint/post-synth/prom107.ts +17 -0
  198. package/src/lint/post-synth/prom201.ts +17 -0
  199. package/src/lint/post-synth/prom202.ts +17 -0
  200. package/src/lint/post-synth/prom203.ts +17 -0
  201. package/src/lint/post-synth/prom204.ts +17 -0
  202. package/src/lint/post-synth/prom205.ts +17 -0
  203. package/src/lint/post-synth/prom206.ts +17 -0
  204. package/src/lint/post-synth/prom207.ts +17 -0
  205. package/src/lint/post-synth/prom208.ts +17 -0
  206. package/src/lint/post-synth/prom209.ts +17 -0
  207. package/src/lint/rules/index.ts +11 -0
  208. package/src/lint/rules/literal-credential.ts +84 -0
  209. package/src/lint/rules/prom-ast.ts +30 -0
  210. package/src/lint/rules/promql-literal.ts +55 -0
  211. package/src/lint/rules/rules.test.ts +114 -0
  212. package/src/lint/rules/slo-literal.ts +81 -0
  213. package/src/lsp/completions.test.ts +23 -0
  214. package/src/lsp/completions.ts +15 -0
  215. package/src/lsp/hover.test.ts +27 -0
  216. package/src/lsp/hover.ts +26 -0
  217. package/src/matchers.ts +132 -0
  218. package/src/model.test.ts +104 -0
  219. package/src/model.ts +290 -0
  220. package/src/package-cli.ts +17 -0
  221. package/src/pin.ts +13 -0
  222. package/src/plugin.test.ts +73 -0
  223. package/src/plugin.ts +129 -0
  224. package/src/promql.ts +46 -0
  225. package/src/rule-eval.ts +397 -0
  226. package/src/rules.ts +127 -0
  227. package/src/serializer.test.ts +179 -0
  228. package/src/serializer.ts +55 -0
  229. package/src/skill-defs.ts +43 -0
  230. package/src/skills/chant-prometheus-alertmanager.md +58 -0
  231. package/src/skills/chant-prometheus-kubernetes.md +48 -0
  232. package/src/skills/chant-prometheus.md +91 -0
  233. package/src/tools.test.ts +119 -0
  234. package/src/tools.ts +70 -0
  235. package/src/validate-cli.ts +7 -0
  236. package/src/validate-config.ts +400 -0
  237. package/src/validate.ts +66 -0
@@ -0,0 +1,471 @@
1
+ /**
2
+ * `Slo`: a service level objective that builds to Prometheus rules.
3
+ *
4
+ * One declaration expands to one `RuleGroup`, `slo-<name>`, holding:
5
+ *
6
+ * - recording rules: the SLI's error ratio over every window the alerts read
7
+ * (`slo:sli_error:ratio_rate<window>`), the error ratio over the whole SLO
8
+ * window, the objective (`slo:objective:ratio`) and the share of the error
9
+ * budget left (`slo:error_budget:remaining`). Every series carries an
10
+ * `slo` label with the SLO's name.
11
+ * - multiwindow, multi-burn-rate alerts from the Google SRE Workbook
12
+ * ("Alerting on SLOs", alert 6). Each alert pairs a long window with a
13
+ * short one and fires while both burn the error budget faster than the
14
+ * pair's factor: the long window keeps a short spike from paging, the
15
+ * short window stops the alert soon after the burn does.
16
+ *
17
+ * Both kinds share a group because Prometheus evaluates a group's rules in
18
+ * order: the alerts read the ratios recorded in the same evaluation. In
19
+ * separate groups they would read the previous evaluation's, and with an
20
+ * evaluation interval of 5m (the lookback) not at all.
21
+ *
22
+ * The Workbook's factors (14.4, 6, 3 and 1) assume a 30-day SLO window: each
23
+ * is the share of the budget a pair may burn over its long window, times the
24
+ * SLO window, over the long window (2% of the budget in 1h is
25
+ * 0.02 * 720h / 1h = 14.4). For any other window the lexicon keeps the
26
+ * windows and budget shares and recomputes the factor the same way, so a
27
+ * 28-day SLO pages at 0.02 * 672h / 1h = 13.44.
28
+ *
29
+ * `sloMetrics()` returns the recorded series names, the objective, the
30
+ * window and the burn-rate thresholds, so a dashboard or another rule reads
31
+ * them from the declaration instead of repeating them.
32
+ */
33
+
34
+ import { Composite, type CompositeInstance } from "@intentius/chant/composite";
35
+ import { RuleGroup, type AlertingRule, type RecordingRule, type Rule, type RuleGroupEntity } from "../rules";
36
+ import type { LabelSet } from "../model";
37
+ import { durationMs, formatDuration, isValidDuration } from "../duration";
38
+ import { checkPromql } from "../promql";
39
+
40
+ /** The placeholder an SLI expression writes where its range goes, e.g. `[{{window}}]`. */
41
+ export const SLO_WINDOW_PLACEHOLDER = "{{window}}";
42
+
43
+ /**
44
+ * The SLI as two PromQL expressions over the same events, each with
45
+ * `{{window}}` where the range goes. Either good and total events, or
46
+ * errors (bad events) and total events.
47
+ */
48
+ export type SloSli =
49
+ | {
50
+ /** Rate of good events, e.g. `sum(rate(http_requests_total{code!~"5.."}[{{window}}]))`. */
51
+ good: string;
52
+ /** Rate of all events, e.g. `sum(rate(http_requests_total[{{window}}]))`. */
53
+ total: string;
54
+ }
55
+ | {
56
+ /** Rate of bad events, e.g. `sum(rate(http_requests_total{code=~"5.."}[{{window}}]))`. */
57
+ errors: string;
58
+ /** Rate of all events. */
59
+ total: string;
60
+ };
61
+
62
+ /** One long/short window pair and the burn rate it alerts at. */
63
+ export interface BurnRateWindow {
64
+ /** The long window, e.g. `1h`. At most the SLO window. */
65
+ long: string;
66
+ /** The short window, e.g. `5m`. Shorter than `long`; the Workbook uses a twelfth of it. */
67
+ short: string;
68
+ /**
69
+ * Share of the whole error budget this pair may burn over its long window
70
+ * before it alerts, e.g. `0.02`. The factor is derived from it and the SLO
71
+ * window. Set this or `factor`.
72
+ */
73
+ budgetConsumed?: number;
74
+ /** The burn rate itself, used as written for any SLO window. Set this or `budgetConsumed`. */
75
+ factor?: number;
76
+ }
77
+
78
+ /** One alert tier: paging or tickets. */
79
+ export interface SloAlertTier {
80
+ /** Window pairs, or `"default"` for the Workbook's pairs for this tier. */
81
+ burnRates?: "default" | BurnRateWindow[];
82
+ /** The `severity` label, which Alertmanager routes on (default: the tier's name, `page` or `ticket`). */
83
+ severity?: string;
84
+ /** How long a pair must hold before its alert fires (default: none; the short window already filters blips). */
85
+ for?: string;
86
+ /** More labels on this tier's alerts. */
87
+ labels?: LabelSet;
88
+ /** More annotations on this tier's alerts, e.g. `runbook_url`. */
89
+ annotations?: LabelSet;
90
+ }
91
+
92
+ export interface SloAlerting {
93
+ /** Fast burns that need someone now (default: on, 1h/5m at 2% and 6h/30m at 5% of the budget). `false` turns it off. */
94
+ page?: SloAlertTier | false;
95
+ /** Slow burns for working hours (default: on, 1d/2h at 10% and 3d/6h at 10% of the budget). `false` turns it off. */
96
+ ticket?: SloAlertTier | false;
97
+ /** The alert name every pair fires under (default: `ErrorBudgetBurn`). Pairs differ by labels. */
98
+ alertName?: string;
99
+ }
100
+
101
+ export interface SloProps {
102
+ /**
103
+ * The SLO's name, the `slo` label on every series and alert it builds and
104
+ * part of its group names. Letters, digits, `.`, `_` and `-`.
105
+ */
106
+ name: string;
107
+ /** The target share of good events, strictly between 0 and 1, e.g. `0.995`. */
108
+ objective: number;
109
+ /** The rolling window the objective holds over, a Prometheus duration, e.g. `28d` or `30d`. */
110
+ window: string;
111
+ /** The SLI: good (or error) events over total events, with `{{window}}` for the range. */
112
+ sli: SloSli;
113
+ /** A sentence for the alerts' description, e.g. what the SLO measures. */
114
+ description?: string;
115
+ /** Burn-rate alerts (default: both tiers with the Workbook's windows). */
116
+ alerting?: SloAlerting;
117
+ /** Labels added to every rule, e.g. `team` or `service`. */
118
+ labels?: LabelSet;
119
+ /** Evaluation interval of the group (default: Prometheus's `evaluation_interval`). */
120
+ interval?: string;
121
+ }
122
+
123
+ export type SloMembers = {
124
+ /** The recording rules, then the burn-rate alerts. */
125
+ rules: RuleGroupEntity;
126
+ };
127
+
128
+ /** What `Slo(...)` returns: its rule group, as `rules`. */
129
+ export type SloInstance = CompositeInstance<SloMembers> & SloMembers;
130
+
131
+ /** One burn-rate pair as built, for dashboards and tests. */
132
+ export interface SloBurnRate {
133
+ /** `page` or `ticket`. */
134
+ tier: "page" | "ticket";
135
+ /** The `severity` label the alert carries. */
136
+ severity: string;
137
+ long: string;
138
+ short: string;
139
+ /** The burn rate the pair fires above. */
140
+ factor: number;
141
+ /** The error ratio the pair fires above: `factor * (1 - objective)`. */
142
+ threshold: number;
143
+ /** The recorded error-ratio series each window reads. */
144
+ longRecord: string;
145
+ shortRecord: string;
146
+ /** The alert name. */
147
+ alert: string;
148
+ /** The labels that tell this pair's alert apart, `alertname` excluded. */
149
+ labels: LabelSet;
150
+ /** How long the budget lasts at exactly this burn rate. */
151
+ exhaustsIn: string;
152
+ }
153
+
154
+ /** The series and numbers an `Slo` builds, read by dashboards instead of repeating names. */
155
+ export interface SloMetrics {
156
+ /** The SLO's name, the value of the `slo` label. */
157
+ name: string;
158
+ objective: number;
159
+ /** `1 - objective`: the share of events allowed to be bad. */
160
+ errorBudget: number;
161
+ window: string;
162
+ /** The label every recorded series and alert carries. */
163
+ labels: { slo: string };
164
+ /** `{slo="<name>"}`, ready to append to a recorded series name. */
165
+ selector: string;
166
+ /** Every window with an error-ratio series, shortest first; the SLO window is last. */
167
+ windows: string[];
168
+ /** Error-ratio series by window, e.g. `errorRatio["5m"]` is `slo:sli_error:ratio_rate5m`. */
169
+ errorRatio: Record<string, string>;
170
+ /** The error-ratio series over the whole SLO window. */
171
+ windowErrorRatio: string;
172
+ /** `slo:error_budget:remaining`: 1 is untouched, 0 is spent, below 0 is overspent. */
173
+ errorBudgetRemaining: string;
174
+ /** `slo:objective:ratio`: the objective as a series. */
175
+ objectiveRatio: string;
176
+ /** The burn-rate pairs, page tier first. Empty when alerting is off. */
177
+ burnRates: SloBurnRate[];
178
+ /** The alert name every pair fires under. */
179
+ alertName: string;
180
+ /** The rule group's name. */
181
+ group: string;
182
+ }
183
+
184
+ /** The Workbook's pairs ("Alerting on SLOs", table 5-8), as shares of the budget. */
185
+ export const DEFAULT_BURN_RATES: Readonly<Record<"page" | "ticket", readonly Readonly<BurnRateWindow>[]>> = Object.freeze({
186
+ page: Object.freeze([
187
+ Object.freeze({ long: "1h", short: "5m", budgetConsumed: 0.02 }),
188
+ Object.freeze({ long: "6h", short: "30m", budgetConsumed: 0.05 }),
189
+ ]),
190
+ ticket: Object.freeze([
191
+ Object.freeze({ long: "1d", short: "2h", budgetConsumed: 0.1 }),
192
+ Object.freeze({ long: "3d", short: "6h", budgetConsumed: 0.1 }),
193
+ ]),
194
+ });
195
+
196
+ const DEFAULT_ALERT_NAME = "ErrorBudgetBurn";
197
+ const NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
198
+ const ALERT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
199
+ const SLO_METRICS = Symbol.for("chant.prometheus.slo");
200
+
201
+ /** Record names. */
202
+ const errorRatioRecord = (window: string) => `slo:sli_error:ratio_rate${window}`;
203
+ const ERROR_BUDGET_REMAINING = "slo:error_budget:remaining";
204
+ const OBJECTIVE_RATIO = "slo:objective:ratio";
205
+
206
+ /** A number as PromQL writes it, without float noise: `1 - 0.995` is `0.005`. */
207
+ function num(n: number): string {
208
+ return String(Number(n.toPrecision(10)));
209
+ }
210
+
211
+ function fail(name: string, message: string): never {
212
+ throw new Error(`Slo "${name}": ${message}`);
213
+ }
214
+
215
+ interface ResolvedPair {
216
+ tier: "page" | "ticket";
217
+ tierProps: SloAlertTier;
218
+ long: string;
219
+ short: string;
220
+ factor: number;
221
+ }
222
+
223
+ interface Resolved {
224
+ props: SloProps;
225
+ budget: number;
226
+ windowMs: number;
227
+ pairs: ResolvedPair[];
228
+ alertName: string;
229
+ /** Every window with an error-ratio series, shortest first, SLO window last. */
230
+ windows: string[];
231
+ }
232
+
233
+ /** Check the props and work out every pair's factor; throws on anything the rules could not be built from. */
234
+ function resolve(props: SloProps): Resolved {
235
+ const name = typeof props?.name === "string" ? props.name : "";
236
+ if (!NAME.test(name)) fail(name, "name must be letters, digits, '.', '_' or '-', starting with a letter or digit");
237
+ const problem = sloPropsProblem(props);
238
+ if (problem) fail(name, problem);
239
+
240
+ const windowMs = durationMs(props.window)!;
241
+ // Rounded, so 1 - 0.995 is 0.005 and not 0.0050000000000000044.
242
+ const budget = Number((1 - props.objective).toPrecision(12));
243
+ const alerting = props.alerting ?? {};
244
+ const alertName = alerting.alertName ?? DEFAULT_ALERT_NAME;
245
+ if (!ALERT_NAME.test(alertName)) fail(name, `alertName "${alertName}" is not a valid alert name`);
246
+
247
+ const pairs: ResolvedPair[] = [];
248
+ for (const tier of ["page", "ticket"] as const) {
249
+ const tierProps = alerting[tier];
250
+ if (tierProps === false) continue;
251
+ const t = tierProps ?? {};
252
+ if (t.for !== undefined && !isValidDuration(t.for)) fail(name, `alerting.${tier}.for "${t.for}" is not a Prometheus duration`);
253
+ const windows = t.burnRates === undefined || t.burnRates === "default" ? DEFAULT_BURN_RATES[tier] : t.burnRates;
254
+ if (!Array.isArray(windows) || windows.length === 0) fail(name, `alerting.${tier}.burnRates must be "default" or a non-empty list`);
255
+ windows.forEach((w, i) => {
256
+ const at = `alerting.${tier}.burnRates[${i}]`;
257
+ const longMs = isValidDuration(w.long) ? durationMs(w.long)! : 0;
258
+ const shortMs = isValidDuration(w.short) ? durationMs(w.short)! : 0;
259
+ if (longMs <= 0) fail(name, `${at}.long "${w.long}" is not a positive Prometheus duration`);
260
+ if (shortMs <= 0) fail(name, `${at}.short "${w.short}" is not a positive Prometheus duration`);
261
+ if (shortMs >= longMs) fail(name, `${at}: the short window ${w.short} must be shorter than the long window ${w.long}`);
262
+ if (longMs > windowMs) fail(name, `${at}: the long window ${w.long} is longer than the SLO window ${props.window}`);
263
+ const hasShare = w.budgetConsumed !== undefined;
264
+ const hasFactor = w.factor !== undefined;
265
+ if (hasShare === hasFactor) fail(name, `${at} must set exactly one of budgetConsumed and factor`);
266
+ let factor: number;
267
+ if (hasShare) {
268
+ const s = w.budgetConsumed!;
269
+ if (!(typeof s === "number" && s > 0 && s <= 1)) fail(name, `${at}.budgetConsumed must be above 0 and at most 1, got ${s}`);
270
+ factor = Number(((s * windowMs) / longMs).toPrecision(6));
271
+ } else {
272
+ factor = w.factor!;
273
+ if (!(typeof factor === "number" && Number.isFinite(factor) && factor > 0)) fail(name, `${at}.factor must be a positive number, got ${factor}`);
274
+ }
275
+ // An error ratio can't exceed 1, so a threshold at or above 1 never fires.
276
+ if (factor * budget >= 1) {
277
+ fail(name, `${at}: a burn rate of ${factor} on a budget of ${num(budget)} needs an error ratio of ${num(factor * budget)}, which can never happen`);
278
+ }
279
+ pairs.push({ tier, tierProps: t, long: w.long, short: w.short, factor });
280
+ });
281
+ }
282
+
283
+ const seen = new Set<string>();
284
+ for (const p of pairs) {
285
+ const key = `${p.tier} ${p.long} ${p.short}`;
286
+ if (seen.has(key)) fail(name, `alerting.${p.tier} lists the pair ${p.long}/${p.short} twice`);
287
+ seen.add(key);
288
+ }
289
+
290
+ const byMs = new Map<number, string>();
291
+ for (const p of pairs) {
292
+ for (const w of [p.long, p.short]) {
293
+ const ms = durationMs(w)!;
294
+ if (!byMs.has(ms)) byMs.set(ms, w);
295
+ }
296
+ }
297
+ byMs.delete(windowMs);
298
+ const windows = [...byMs.entries()].sort((a, b) => a[0] - b[0]).map(([, w]) => w);
299
+ windows.push(props.window);
300
+
301
+ return { props, budget, windowMs, pairs, alertName, windows };
302
+ }
303
+
304
+ /**
305
+ * What is wrong with an SLO's objective, window or SLI, or `undefined`.
306
+ * `Slo()` throws with this message; the PROM003 lint rule reports it at the
307
+ * literal in the source.
308
+ */
309
+ export function sloPropsProblem(props: Partial<SloProps>): string | undefined {
310
+ const o = props.objective;
311
+ if (typeof o !== "number" || !Number.isFinite(o) || o <= 0 || o >= 1) {
312
+ return `objective must be strictly between 0 and 1 (e.g. 0.995 for 99.5%), got ${String(o)}`;
313
+ }
314
+ if (!isValidDuration(props.window) || durationMs(props.window) === 0) {
315
+ return `window must be a positive Prometheus duration (e.g. 28d or 30d), got ${JSON.stringify(props.window)}`;
316
+ }
317
+ const sli = props.sli as Record<string, unknown> | undefined;
318
+ if (typeof sli !== "object" || sli === null) return "sli must set good and total, or errors and total";
319
+ const hasGood = "good" in sli;
320
+ const hasErrors = "errors" in sli;
321
+ if (hasGood === hasErrors || !("total" in sli)) return "sli must set good and total, or errors and total";
322
+ for (const key of [hasGood ? "good" : "errors", "total"]) {
323
+ const problem = sliExprProblem(sli[key]);
324
+ if (problem) return `sli.${key} ${problem}`;
325
+ }
326
+ return undefined;
327
+ }
328
+
329
+ /** What is wrong with one SLI expression, or `undefined`. */
330
+ export function sliExprProblem(expr: unknown): string | undefined {
331
+ if (typeof expr !== "string" || expr.trim() === "") return "must be a PromQL expression";
332
+ if (!expr.includes(SLO_WINDOW_PLACEHOLDER)) {
333
+ return `must contain ${SLO_WINDOW_PLACEHOLDER} where the range goes, e.g. rate(x[${SLO_WINDOW_PLACEHOLDER}]), so it can be recorded per window`;
334
+ }
335
+ const checked = checkPromql(withWindow(expr, "5m"));
336
+ if (!checked.ok) return `is not valid PromQL once ${SLO_WINDOW_PLACEHOLDER} is filled in: ${checked.message}`;
337
+ return undefined;
338
+ }
339
+
340
+ function withWindow(expr: string, window: string): string {
341
+ return expr.split(SLO_WINDOW_PLACEHOLDER).join(window);
342
+ }
343
+
344
+ function metricsOf(r: Resolved): SloMetrics {
345
+ const { props, budget, pairs, alertName, windows } = r;
346
+ const errorRatio: Record<string, string> = {};
347
+ for (const w of windows) errorRatio[w] = errorRatioRecord(w);
348
+ const burnRates: SloBurnRate[] = pairs.map((p) => ({
349
+ tier: p.tier,
350
+ severity: p.tierProps.severity ?? p.tier,
351
+ long: p.long,
352
+ short: p.short,
353
+ factor: p.factor,
354
+ threshold: Number((p.factor * budget).toPrecision(10)),
355
+ longRecord: errorRatioRecord(p.long),
356
+ shortRecord: errorRatioRecord(p.short),
357
+ alert: alertName,
358
+ labels: { slo: props.name, severity: p.tierProps.severity ?? p.tier, long_window: p.long, short_window: p.short },
359
+ exhaustsIn: formatDuration(Math.round(r.windowMs / p.factor / 60_000) * 60_000),
360
+ }));
361
+ return {
362
+ name: props.name,
363
+ objective: props.objective,
364
+ errorBudget: budget,
365
+ window: props.window,
366
+ labels: { slo: props.name },
367
+ selector: `{slo="${props.name}"}`,
368
+ windows: [...windows],
369
+ errorRatio,
370
+ windowErrorRatio: errorRatioRecord(props.window),
371
+ errorBudgetRemaining: ERROR_BUDGET_REMAINING,
372
+ objectiveRatio: OBJECTIVE_RATIO,
373
+ burnRates,
374
+ alertName,
375
+ group: `slo-${props.name}`,
376
+ };
377
+ }
378
+
379
+ function recordingRules(r: Resolved, m: SloMetrics): RecordingRule[] {
380
+ const { props } = r;
381
+ const sli = props.sli as { good?: string; errors?: string; total: string };
382
+ const ratio = (w: string) =>
383
+ sli.errors !== undefined
384
+ ? `(${withWindow(sli.errors, w)})\n/\n(${withWindow(sli.total, w)})`
385
+ : `1 - (\n (${withWindow(sli.good!, w)})\n /\n (${withWindow(sli.total, w)})\n)`;
386
+ const labels = { slo: props.name };
387
+ const rules: RecordingRule[] = [];
388
+ const alertWindows = m.windows.slice(0, -1);
389
+ for (const w of alertWindows) rules.push({ record: m.errorRatio[w], expr: ratio(w), labels });
390
+ // The whole window as an average of the shortest recorded ratio, as Sloth
391
+ // does: a range over weeks of raw counters, every evaluation, is the most
392
+ // expensive query a rule file can hold. Without alert windows there is no
393
+ // shorter ratio to average, so the window is read from the SLI directly.
394
+ rules.push({
395
+ record: m.windowErrorRatio,
396
+ expr: alertWindows.length > 0 ? `avg_over_time(${m.errorRatio[alertWindows[0]]}${m.selector}[${props.window}])` : ratio(props.window),
397
+ labels,
398
+ });
399
+ rules.push({ record: OBJECTIVE_RATIO, expr: `vector(${num(props.objective)})`, labels });
400
+ rules.push({ record: ERROR_BUDGET_REMAINING, expr: `1 - (${m.windowErrorRatio}${m.selector} / ${num(r.budget)})`, labels });
401
+ return rules;
402
+ }
403
+
404
+ function alertRules(r: Resolved, m: SloMetrics): AlertingRule[] {
405
+ const { props } = r;
406
+ return r.pairs.map((p, i) => {
407
+ const b = m.burnRates[i];
408
+ const cond = (record: string) => `${record}${m.selector} > (${num(p.factor)} * ${num(r.budget)})`;
409
+ const rule: AlertingRule = {
410
+ alert: b.alert,
411
+ expr: `(\n ${cond(b.longRecord)}\n)\nand\n(\n ${cond(b.shortRecord)}\n)`,
412
+ labels: { ...(p.tierProps.labels ?? {}), ...b.labels },
413
+ annotations: {
414
+ summary: `SLO ${props.name} is burning its error budget more than ${num(p.factor)}x too fast (${p.long} and ${p.short} windows)`,
415
+ description:
416
+ `${props.description ? `${props.description} ` : ""}The error ratio over both the last ${p.long} and the last ${p.short} is above ` +
417
+ `${num(b.threshold)} (${num(p.factor)} times the ${num(r.budget)} budget of a ${num(props.objective)} objective). ` +
418
+ `At that rate the ${props.window} error budget is gone in ${b.exhaustsIn}.`,
419
+ ...(p.tierProps.annotations ?? {}),
420
+ },
421
+ };
422
+ if (p.tierProps.for !== undefined) rule.for = p.tierProps.for;
423
+ return rule;
424
+ });
425
+ }
426
+
427
+ /**
428
+ * An SLO, built to recording rules and multiwindow multi-burn-rate alerts.
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * export const orderAck = Slo({
433
+ * name: "order-acknowledged",
434
+ * objective: 0.995,
435
+ * window: "28d",
436
+ * sli: {
437
+ * good: 'sum(rate(traces_span_metrics_calls_total{span_name="order.ack",status_code!="STATUS_CODE_ERROR"}[{{window}}]))',
438
+ * total: 'sum(rate(traces_span_metrics_calls_total{span_name="order.ack"}[{{window}}]))',
439
+ * },
440
+ * });
441
+ * // orderAck.rules is a RuleGroup; sloMetrics(orderAck) names its series.
442
+ * ```
443
+ */
444
+ export const Slo = Composite<SloProps, SloMembers>((props) => {
445
+ const r = resolve(props);
446
+ const m = metricsOf(r);
447
+ const rules: Rule[] = [...recordingRules(r, m), ...alertRules(r, m)];
448
+ const group = new RuleGroup({
449
+ name: m.group,
450
+ ...(props.interval !== undefined ? { interval: props.interval } : {}),
451
+ ...(props.labels !== undefined ? { labels: props.labels } : {}),
452
+ rules,
453
+ });
454
+ Object.defineProperty(group, SLO_METRICS, { value: m, enumerable: false });
455
+ return { rules: group };
456
+ }, "Slo");
457
+
458
+ /**
459
+ * The recorded series names, objective, window and burn-rate thresholds of
460
+ * an SLO. Pass the `Slo(...)` result, its rule group, or the props
461
+ * it was built from; a dashboard reads names from here, so renaming or
462
+ * re-windowing the SLO moves its panels with it.
463
+ */
464
+ export function sloMetrics(slo: SloInstance | RuleGroupEntity | SloProps): SloMetrics {
465
+ const stashed = (x: unknown): SloMetrics | undefined =>
466
+ typeof x === "object" && x !== null ? ((x as Record<symbol, unknown>)[SLO_METRICS] as SloMetrics | undefined) : undefined;
467
+ const direct = stashed(slo) ?? stashed((slo as Partial<SloMembers>).rules);
468
+ if (direct) return structuredClone(direct);
469
+ if (typeof slo === "object" && slo !== null && "objective" in slo && "sli" in slo) return metricsOf(resolve(slo as SloProps));
470
+ throw new Error("sloMetrics: pass an Slo(...) result, its rule group, or SLO props");
471
+ }
package/src/detect.ts 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) or an
4
+ * `alertmanager.yml` (`route:` or `receivers:` at the top level). Kept free
5
+ * of the plugin and the TypeScript compiler so it bundles for edge runtimes,
6
+ * like the other lexicons' `detect` modules.
7
+ */
8
+ import { looksLikeAlertmanagerConfig, looksLikeRuleFile } from "./model";
9
+
10
+ export function detectTemplate(data: unknown): boolean {
11
+ return looksLikeRuleFile(data) || looksLikeAlertmanagerConfig(data);
12
+ }
@@ -0,0 +1,51 @@
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
+
11
+ const DURATION = /^(?:(\d+)y)?(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?(?:(\d+)ms)?$/;
12
+
13
+ const UNIT_MS = [365 * 86_400_000, 7 * 86_400_000, 86_400_000, 3_600_000, 60_000, 1_000, 1];
14
+
15
+ /** True when `value` is a Prometheus duration string. */
16
+ export function isValidDuration(value: unknown): value is string {
17
+ if (typeof value !== "string" || value === "") return false;
18
+ if (value === "0") return true;
19
+ return DURATION.test(value);
20
+ }
21
+
22
+ /** A duration in milliseconds, or `undefined` when it isn't a valid duration. */
23
+ export function durationMs(value: string): number | undefined {
24
+ if (value === "0") return 0;
25
+ if (value === "") return undefined;
26
+ const m = DURATION.exec(value);
27
+ if (!m) return undefined;
28
+ let total = 0;
29
+ for (let i = 0; i < UNIT_MS.length; i++) {
30
+ const n = m[i + 1];
31
+ if (n !== undefined) total += Number(n) * UNIT_MS[i];
32
+ }
33
+ return total;
34
+ }
35
+
36
+ /** Write a millisecond count as the shortest Prometheus duration, e.g. `5400000` -> `1h30m`. */
37
+ export function formatDuration(ms: number): string {
38
+ if (!Number.isInteger(ms) || ms < 0) throw new Error(`prometheus: ${ms} is not a whole, non-negative number of milliseconds`);
39
+ if (ms === 0) return "0s";
40
+ const units = ["y", "w", "d", "h", "m", "s", "ms"];
41
+ let rest = ms;
42
+ let out = "";
43
+ for (let i = 0; i < UNIT_MS.length; i++) {
44
+ const n = Math.floor(rest / UNIT_MS[i]);
45
+ if (n > 0) {
46
+ out += `${n}${units[i]}`;
47
+ rest -= n * UNIT_MS[i];
48
+ }
49
+ }
50
+ return out;
51
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "AlertmanagerSettings": {
3
+ "resourceType": "Prometheus::Alertmanager::Settings",
4
+ "kind": "resource",
5
+ "lexicon": "prometheus"
6
+ },
7
+ "InhibitRule": {
8
+ "resourceType": "Prometheus::Alertmanager::InhibitRule",
9
+ "kind": "resource",
10
+ "lexicon": "prometheus"
11
+ },
12
+ "Receiver": {
13
+ "resourceType": "Prometheus::Alertmanager::Receiver",
14
+ "kind": "resource",
15
+ "lexicon": "prometheus"
16
+ },
17
+ "Route": {
18
+ "resourceType": "Prometheus::Alertmanager::Route",
19
+ "kind": "resource",
20
+ "lexicon": "prometheus"
21
+ },
22
+ "RuleGroup": {
23
+ "resourceType": "Prometheus::Rules::RuleGroup",
24
+ "kind": "resource",
25
+ "lexicon": "prometheus"
26
+ },
27
+ "TimeInterval": {
28
+ "resourceType": "Prometheus::Alertmanager::TimeInterval",
29
+ "kind": "resource",
30
+ "lexicon": "prometheus"
31
+ }
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,97 @@
1
+ // Prometheus lexicon.
2
+
3
+ // Plugin and serializer
4
+ export { prometheusPlugin } from "./plugin";
5
+ export { prometheusSerializer, ALERTMANAGER_FILE } from "./serializer";
6
+
7
+ // Rule groups
8
+ export {
9
+ RuleGroup,
10
+ RULE_GROUP_TYPE,
11
+ isRuleGroup,
12
+ ruleGroupConfig,
13
+ ruleConfig,
14
+ type RuleGroupProps,
15
+ type RuleGroupEntity,
16
+ type Rule,
17
+ type RecordingRule,
18
+ type AlertingRule,
19
+ } from "./rules";
20
+
21
+ // Alertmanager
22
+ export {
23
+ Route,
24
+ Receiver,
25
+ InhibitRule,
26
+ TimeInterval,
27
+ AlertmanagerSettings,
28
+ ROUTE_TYPE,
29
+ RECEIVER_TYPE,
30
+ INHIBIT_RULE_TYPE,
31
+ TIME_INTERVAL_TYPE,
32
+ SETTINGS_TYPE,
33
+ isRoute,
34
+ isReceiver,
35
+ isInhibitRule,
36
+ isTimeInterval,
37
+ isAlertmanagerSettings,
38
+ isAlertmanagerEntity,
39
+ type RouteProps,
40
+ type RouteEntity,
41
+ type ReceiverProps,
42
+ type ReceiverEntity,
43
+ type ReceiverRef,
44
+ type InhibitRuleProps,
45
+ type InhibitRuleEntity,
46
+ type TimeIntervalProps,
47
+ type TimeIntervalEntity,
48
+ type TimeIntervalRef,
49
+ type AlertmanagerSettingsProps,
50
+ type AlertmanagerSettingsEntity,
51
+ } from "./alertmanager";
52
+
53
+ // The plain-data model, building, YAML and checks
54
+ export * from "./model";
55
+ export {
56
+ buildRuleFile,
57
+ buildAlertmanagerConfig,
58
+ ruleFileYaml,
59
+ alertmanagerYaml,
60
+ emitYaml,
61
+ type BuiltRuleFile,
62
+ type BuiltAlertmanager,
63
+ } from "./build";
64
+ export { isValidDuration, durationMs, formatDuration } from "./duration";
65
+ export { parseMatchers, matcherMatches, matcher, type Matcher, type MatchOp, type ParsedMatchers } from "./matchers";
66
+ export { checkPromql, PROMQL_GRAMMAR, type PromqlCheck } from "./promql";
67
+ export {
68
+ validateRuleFile,
69
+ validateAlertmanagerConfig,
70
+ validateSeverityRouting,
71
+ alertSeverities,
72
+ type PrometheusIssue,
73
+ type PrometheusIssueCode,
74
+ } from "./validate-config";
75
+ export { PROMETHEUS_PIN } from "./pin";
76
+
77
+ // promtool and amtool, when installed
78
+ export { promtoolCheckRules, promtoolTestRules, amtoolCheckConfig, hasTool, type ToolResult } from "./tools";
79
+
80
+ // Composites
81
+ export {
82
+ Slo,
83
+ sloMetrics,
84
+ sloPropsProblem,
85
+ sliExprProblem,
86
+ DEFAULT_BURN_RATES,
87
+ SLO_WINDOW_PLACEHOLDER,
88
+ type SloProps,
89
+ type SloSli,
90
+ type SloAlerting,
91
+ type SloAlertTier,
92
+ type BurnRateWindow,
93
+ type SloMembers,
94
+ type SloInstance,
95
+ type SloMetrics,
96
+ type SloBurnRate,
97
+ } from "./composites";