@intentius/chant-lexicon-grafana 0.124.30 → 0.124.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/dist/actions/grafana.js +36 -0
  2. package/dist/actions/index.js +1 -0
  3. package/dist/alerting-build.js +262 -0
  4. package/dist/alerting.js +125 -0
  5. package/dist/annotations.js +47 -0
  6. package/dist/api/apply.js +320 -0
  7. package/dist/api/bind.js +44 -0
  8. package/dist/api/client.js +113 -0
  9. package/dist/api/converge.js +61 -0
  10. package/dist/api/dashboards.js +210 -0
  11. package/dist/api/datasources.js +24 -0
  12. package/dist/api/fake-grafana-writes.js +185 -0
  13. package/dist/api/fake-grafana.js +161 -0
  14. package/dist/api/folders.js +290 -0
  15. package/dist/api/library-panels.js +82 -0
  16. package/dist/build.js +767 -0
  17. package/dist/catalog.js +198 -0
  18. package/dist/codegen/docs-cli.js +3 -0
  19. package/dist/codegen/docs.js +130 -0
  20. package/dist/codegen/generate-cli.js +7 -0
  21. package/dist/codegen/generate.js +97 -0
  22. package/dist/codegen/package.js +28 -0
  23. package/dist/codegen/schema-types.js +159 -0
  24. package/dist/composites/agent-dashboard.js +460 -0
  25. package/dist/composites/catalog.js +306 -0
  26. package/dist/composites/index.js +8 -0
  27. package/dist/composites/red-dashboard.js +135 -0
  28. package/dist/composites/shared.js +74 -0
  29. package/dist/composites/slo-alert-rules.js +96 -0
  30. package/dist/composites/slo-dashboard.js +153 -0
  31. package/dist/config.js +110 -0
  32. package/dist/contact-point-check.js +61 -0
  33. package/dist/contact-point-secrets.js +32 -0
  34. package/dist/contact-point-settings.gen.js +421 -0
  35. package/dist/dashboard.js +60 -0
  36. package/dist/datasource-refs.js +170 -0
  37. package/dist/datasource-settings.js +57 -0
  38. package/dist/datasource.js +68 -0
  39. package/dist/deep-observe-hooks.js +266 -0
  40. package/dist/deep-observe.js +171 -0
  41. package/dist/describe-resources.js +249 -0
  42. package/dist/detect.js +119 -0
  43. package/dist/export-resources.js +73 -0
  44. package/dist/folder.js +86 -0
  45. package/dist/import/alerting.js +514 -0
  46. package/dist/import/edits.js +90 -0
  47. package/dist/import/embedded.js +60 -0
  48. package/dist/import/generator.js +656 -0
  49. package/dist/import/live-export.js +246 -0
  50. package/dist/import/mappings.js +317 -0
  51. package/dist/import/model.js +23 -0
  52. package/dist/import/normalize-alerting.js +106 -0
  53. package/dist/import/normalize.js +426 -0
  54. package/dist/import/operator.js +252 -0
  55. package/dist/import/parser.js +1480 -0
  56. package/dist/import/report.js +58 -0
  57. package/dist/import/testdata/fixtures.js +122 -0
  58. package/dist/import/v2.js +814 -0
  59. package/dist/index.js +59 -0
  60. package/dist/init-templates.js +305 -0
  61. package/dist/integrity.json +2 -2
  62. package/dist/k8s.js +548 -0
  63. package/dist/library-panel.js +83 -0
  64. package/dist/lint/audit-catalog.js +40 -0
  65. package/dist/lint/audit-lineage.js +9 -0
  66. package/dist/lint/post-synth/graf101.js +13 -0
  67. package/dist/lint/post-synth/graf102.js +13 -0
  68. package/dist/lint/post-synth/graf103.js +13 -0
  69. package/dist/lint/post-synth/graf104.js +13 -0
  70. package/dist/lint/post-synth/graf105.js +13 -0
  71. package/dist/lint/post-synth/graf106.js +13 -0
  72. package/dist/lint/post-synth/graf107.js +13 -0
  73. package/dist/lint/post-synth/graf108.js +13 -0
  74. package/dist/lint/post-synth/graf109.js +13 -0
  75. package/dist/lint/post-synth/graf110.js +13 -0
  76. package/dist/lint/post-synth/graf111.js +13 -0
  77. package/dist/lint/post-synth/graf112.js +13 -0
  78. package/dist/lint/post-synth/graf113.js +13 -0
  79. package/dist/lint/post-synth/graf114.js +13 -0
  80. package/dist/lint/post-synth/graf115.js +13 -0
  81. package/dist/lint/post-synth/graf116.js +13 -0
  82. package/dist/lint/post-synth/graf117.js +13 -0
  83. package/dist/lint/post-synth/graf118.js +37 -0
  84. package/dist/lint/post-synth/grafana-helpers.js +89 -0
  85. package/dist/lint/post-synth/index.js +38 -0
  86. package/dist/lint/rules/grafana-ast.js +70 -0
  87. package/dist/lint/rules/index.js +6 -0
  88. package/dist/lint/rules/literal-secret.js +109 -0
  89. package/dist/lint/rules/uid-syntax.js +53 -0
  90. package/dist/lsp/completions.js +11 -0
  91. package/dist/lsp/hover.js +29 -0
  92. package/dist/manifest.json +1 -1
  93. package/dist/op/activities/grafana-apply.js +147 -0
  94. package/dist/op/activities/index.js +7 -0
  95. package/dist/op/builders.js +19 -0
  96. package/dist/ownership.js +102 -0
  97. package/dist/package-cli.js +14 -0
  98. package/dist/panel-options.js +12 -0
  99. package/dist/panels.js +296 -0
  100. package/dist/pin.js +113 -0
  101. package/dist/plugin.js +172 -0
  102. package/dist/promql-check.js +176 -0
  103. package/dist/query-models.js +13 -0
  104. package/dist/query-syntax.js +129 -0
  105. package/dist/query.js +227 -0
  106. package/dist/schema/annotationslist.gen.js +5 -0
  107. package/dist/schema/azuremonitor.gen.js +5 -0
  108. package/dist/schema/barchart.gen.js +5 -0
  109. package/dist/schema/bargauge.gen.js +5 -0
  110. package/dist/schema/bigquery.gen.js +5 -0
  111. package/dist/schema/candlestick.gen.js +5 -0
  112. package/dist/schema/canvas.gen.js +5 -0
  113. package/dist/schema/cloudwatch.gen.js +5 -0
  114. package/dist/schema/dashboard.gen.js +6 -0
  115. package/dist/schema/dashboardlist.gen.js +5 -0
  116. package/dist/schema/datagrid.gen.js +5 -0
  117. package/dist/schema/elasticsearch.gen.js +5 -0
  118. package/dist/schema/expr.gen.js +5 -0
  119. package/dist/schema/gauge.gen.js +5 -0
  120. package/dist/schema/geomap.gen.js +5 -0
  121. package/dist/schema/googlecloudmonitoring.gen.js +5 -0
  122. package/dist/schema/grafanapyroscope.gen.js +5 -0
  123. package/dist/schema/heatmap.gen.js +5 -0
  124. package/dist/schema/histogram.gen.js +5 -0
  125. package/dist/schema/index.js +40 -0
  126. package/dist/schema/logs.gen.js +5 -0
  127. package/dist/schema/loki.gen.js +5 -0
  128. package/dist/schema/news.gen.js +5 -0
  129. package/dist/schema/nodegraph.gen.js +5 -0
  130. package/dist/schema/piechart.gen.js +5 -0
  131. package/dist/schema/prometheus.gen.js +5 -0
  132. package/dist/schema/stat.gen.js +5 -0
  133. package/dist/schema/statetimeline.gen.js +5 -0
  134. package/dist/schema/statushistory.gen.js +5 -0
  135. package/dist/schema/table.gen.js +5 -0
  136. package/dist/schema/tempo.gen.js +5 -0
  137. package/dist/schema/text.gen.js +5 -0
  138. package/dist/schema/timeseries.gen.js +5 -0
  139. package/dist/schema/trend.gen.js +5 -0
  140. package/dist/schema/xychart.gen.js +5 -0
  141. package/dist/schema-validate.js +265 -0
  142. package/dist/serializer.js +40 -0
  143. package/dist/skill-defs.js +60 -0
  144. package/dist/spec/fetch-cli.js +26 -0
  145. package/dist/spec/fetch-notifiers-cli.js +53 -0
  146. package/dist/spec/fetch-units-cli.js +40 -0
  147. package/dist/spec/notifiers.js +126 -0
  148. package/dist/spec/overlay.js +92 -0
  149. package/dist/spec/schemas.gen.js +42 -0
  150. package/dist/spec/schemas.js +62 -0
  151. package/dist/spec/units.gen.js +280 -0
  152. package/dist/spec/units.js +72 -0
  153. package/dist/transformations.js +127 -0
  154. package/dist/util.js +34 -0
  155. package/dist/validate-alerting.js +444 -0
  156. package/dist/validate-cli.js +7 -0
  157. package/dist/validate-output.js +635 -0
  158. package/dist/validate.js +122 -0
  159. package/dist/validation.js +13 -0
  160. package/dist/variables.js +70 -0
  161. package/package.json +10 -10
  162. package/src/codegen/package.ts +2 -2
  163. package/src/spec/fetch-notifiers-cli.ts +2 -2
  164. package/src/spec/fetch-units-cli.ts +2 -2
  165. package/src/spec/overlay.ts +2 -2
  166. package/src/spec/schemas.ts +2 -2
@@ -0,0 +1,306 @@
1
+ export const compositeCatalog = [
2
+ {
3
+ "name": "AgentDashboard",
4
+ "lexicon": "grafana",
5
+ "description": "A dashboard for agents instrumented with GenAI spans.",
6
+ "bundles": [
7
+ "Dashboard"
8
+ ],
9
+ "params": [
10
+ {
11
+ "name": "genAi",
12
+ "type": "GenAiMetrics | { metrics: GenAiMetrics }",
13
+ "required": false,
14
+ "description": "The preset's metrics: `genAiMetrics(options)` with the options the collector was built with, or `genAiComponents(options)`."
15
+ },
16
+ {
17
+ "name": "rules",
18
+ "type": "GenAiRulesInstance | RuleGroupEntity | GenAiRuleMetrics",
19
+ "required": false,
20
+ "description": "Read the series a prometheus `GenAiRules` records instead of the preset's metrics."
21
+ },
22
+ {
23
+ "name": "quantile",
24
+ "type": "number",
25
+ "required": false,
26
+ "description": "The latency quantile per model and per tool (default 0.95)."
27
+ },
28
+ {
29
+ "name": "datasource",
30
+ "type": "DatasourceInput<\"prometheus\">",
31
+ "required": true,
32
+ "description": "The Prometheus the metrics are queried from: a declared `Datasource`, or `{ type: \"prometheus\", uid }` for one declared elsewhere."
33
+ },
34
+ {
35
+ "name": "title",
36
+ "type": "string",
37
+ "required": false
38
+ },
39
+ {
40
+ "name": "uid",
41
+ "type": "string",
42
+ "required": false,
43
+ "description": "The dashboard uid."
44
+ },
45
+ {
46
+ "name": "description",
47
+ "type": "string",
48
+ "required": false
49
+ },
50
+ {
51
+ "name": "tags",
52
+ "type": "string[]",
53
+ "required": false
54
+ },
55
+ {
56
+ "name": "folder",
57
+ "type": "string | FolderEntity",
58
+ "required": false,
59
+ "description": "The Grafana folder to provision it into: a path, or a `Folder` to pin its uid."
60
+ },
61
+ {
62
+ "name": "time",
63
+ "type": "{ from: string; to: string }",
64
+ "required": false,
65
+ "description": "Initial time range."
66
+ },
67
+ {
68
+ "name": "refresh",
69
+ "type": "string",
70
+ "required": false,
71
+ "description": "Auto-refresh interval, e.g. `30s`."
72
+ },
73
+ {
74
+ "name": "links",
75
+ "type": "DashboardLinkInput[]",
76
+ "required": false
77
+ }
78
+ ]
79
+ },
80
+ {
81
+ "name": "RedDashboard",
82
+ "lexicon": "grafana",
83
+ "description": "A RED dashboard (rate, errors, duration) per service, built from a `spanmetrics` connector declaration.",
84
+ "bundles": [
85
+ "Dashboard"
86
+ ],
87
+ "params": [
88
+ {
89
+ "name": "spanMetrics",
90
+ "type": "OTelComponent<\"connector\", \"spanmetrics\", SpanMetricsConnectorConfig> | SpanMetricsNames",
91
+ "required": true,
92
+ "description": "The `spanmetrics` connector the metrics come from, or the names `spanMetricsNames()` returned for it."
93
+ },
94
+ {
95
+ "name": "exporter",
96
+ "type": "OTelComponent<\"exporter\", \"prometheus\", PrometheusExporterConfig> | PrometheusNaming",
97
+ "required": false,
98
+ "description": "The `prometheus` exporter that serves them, when its `namespace` or `add_metric_suffixes` changes the names."
99
+ },
100
+ {
101
+ "name": "quantiles",
102
+ "type": "number[]",
103
+ "required": false,
104
+ "description": "Duration quantiles, one panel each (default p50, p95 and p99)."
105
+ },
106
+ {
107
+ "name": "spanKinds",
108
+ "type": "SpanKind[]",
109
+ "required": false,
110
+ "description": "The span kinds the queries count (default server and consumer spans)."
111
+ },
112
+ {
113
+ "name": "datasource",
114
+ "type": "DatasourceInput<\"prometheus\">",
115
+ "required": true,
116
+ "description": "The Prometheus the metrics are queried from: a declared `Datasource`, or `{ type: \"prometheus\", uid }` for one declared elsewhere."
117
+ },
118
+ {
119
+ "name": "title",
120
+ "type": "string",
121
+ "required": false
122
+ },
123
+ {
124
+ "name": "uid",
125
+ "type": "string",
126
+ "required": false,
127
+ "description": "The dashboard uid."
128
+ },
129
+ {
130
+ "name": "description",
131
+ "type": "string",
132
+ "required": false
133
+ },
134
+ {
135
+ "name": "tags",
136
+ "type": "string[]",
137
+ "required": false
138
+ },
139
+ {
140
+ "name": "folder",
141
+ "type": "string | FolderEntity",
142
+ "required": false,
143
+ "description": "The Grafana folder to provision it into: a path, or a `Folder` to pin its uid."
144
+ },
145
+ {
146
+ "name": "time",
147
+ "type": "{ from: string; to: string }",
148
+ "required": false,
149
+ "description": "Initial time range."
150
+ },
151
+ {
152
+ "name": "refresh",
153
+ "type": "string",
154
+ "required": false,
155
+ "description": "Auto-refresh interval, e.g. `30s`."
156
+ },
157
+ {
158
+ "name": "links",
159
+ "type": "DashboardLinkInput[]",
160
+ "required": false
161
+ }
162
+ ]
163
+ },
164
+ {
165
+ "name": "SloAlertRules",
166
+ "lexicon": "grafana",
167
+ "description": "Grafana-managed burn-rate alerts for an `Slo`: one rule per window pair, with the labels and annotations the `Slo`'s Prometheus alerts carry.",
168
+ "bundles": [
169
+ "AlertRuleGroup"
170
+ ],
171
+ "params": [
172
+ {
173
+ "name": "slo",
174
+ "type": "SloInstance | RuleGroupEntity | SloMetrics",
175
+ "required": true,
176
+ "description": "The `Slo(...)` the rules alert on, its rule group, or what `sloMetrics()` returned for it."
177
+ },
178
+ {
179
+ "name": "datasource",
180
+ "type": "DatasourceInput<\"prometheus\">",
181
+ "required": true,
182
+ "description": "The Prometheus holding the `Slo`'s recorded series: a declared `Datasource`, an `ExternalDatasource`, or `{ type: \"prometheus\", uid }`."
183
+ },
184
+ {
185
+ "name": "folder",
186
+ "type": "string",
187
+ "required": true,
188
+ "description": "The Grafana folder the rules are stored in, by title."
189
+ },
190
+ {
191
+ "name": "group",
192
+ "type": "string",
193
+ "required": false,
194
+ "description": "The rule group's name."
195
+ },
196
+ {
197
+ "name": "interval",
198
+ "type": "string",
199
+ "required": false,
200
+ "description": "How often the rules are evaluated, a multiple of 10s."
201
+ },
202
+ {
203
+ "name": "contactPoint",
204
+ "type": "ContactPointEntity | string",
205
+ "required": false,
206
+ "description": "Send the alerts straight to this contact point."
207
+ },
208
+ {
209
+ "name": "for",
210
+ "type": "string",
211
+ "required": false,
212
+ "description": "How long a pair must hold before its alert fires."
213
+ },
214
+ {
215
+ "name": "labels",
216
+ "type": "Record<string, string>",
217
+ "required": false,
218
+ "description": "More labels on every rule, e.g. `team`."
219
+ },
220
+ {
221
+ "name": "annotations",
222
+ "type": "Record<string, string>",
223
+ "required": false,
224
+ "description": "More annotations on every rule, e.g. `runbook_url`."
225
+ },
226
+ {
227
+ "name": "noDataState",
228
+ "type": "NoDataState",
229
+ "required": false,
230
+ "description": "What no data does."
231
+ },
232
+ {
233
+ "name": "execErrState",
234
+ "type": "ExecErrState",
235
+ "required": false,
236
+ "description": "What a query error does."
237
+ }
238
+ ]
239
+ },
240
+ {
241
+ "name": "SloDashboard",
242
+ "lexicon": "grafana",
243
+ "description": "A dashboard for one SLO: the SLI over its window against the objective, the error budget left, and each burn-rate alert's two windows with the rate it fires...",
244
+ "bundles": [
245
+ "Dashboard"
246
+ ],
247
+ "params": [
248
+ {
249
+ "name": "slo",
250
+ "type": "SloInstance | RuleGroupEntity | SloMetrics",
251
+ "required": true,
252
+ "description": "The `Slo(...)` the dashboard reads, its rule group, or what `sloMetrics()` returned for it."
253
+ },
254
+ {
255
+ "name": "datasource",
256
+ "type": "DatasourceInput<\"prometheus\">",
257
+ "required": true,
258
+ "description": "The Prometheus the metrics are queried from: a declared `Datasource`, or `{ type: \"prometheus\", uid }` for one declared elsewhere."
259
+ },
260
+ {
261
+ "name": "title",
262
+ "type": "string",
263
+ "required": false
264
+ },
265
+ {
266
+ "name": "uid",
267
+ "type": "string",
268
+ "required": false,
269
+ "description": "The dashboard uid."
270
+ },
271
+ {
272
+ "name": "description",
273
+ "type": "string",
274
+ "required": false
275
+ },
276
+ {
277
+ "name": "tags",
278
+ "type": "string[]",
279
+ "required": false
280
+ },
281
+ {
282
+ "name": "folder",
283
+ "type": "string | FolderEntity",
284
+ "required": false,
285
+ "description": "The Grafana folder to provision it into: a path, or a `Folder` to pin its uid."
286
+ },
287
+ {
288
+ "name": "time",
289
+ "type": "{ from: string; to: string }",
290
+ "required": false,
291
+ "description": "Initial time range."
292
+ },
293
+ {
294
+ "name": "refresh",
295
+ "type": "string",
296
+ "required": false,
297
+ "description": "Auto-refresh interval, e.g. `30s`."
298
+ },
299
+ {
300
+ "name": "links",
301
+ "type": "DashboardLinkInput[]",
302
+ "required": false
303
+ }
304
+ ]
305
+ }
306
+ ];
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Grafana lexicon composites: dashboards built from the declarations they
3
+ * read, so renaming a metric at its source moves it in the panels.
4
+ */
5
+ export { RedDashboard, redQueries, RED_DEFAULT_SPAN_KINDS } from "./red-dashboard.js";
6
+ export { SloDashboard, sloQueries } from "./slo-dashboard.js";
7
+ export { AgentDashboard, agentQueries, agentRuleQueries } from "./agent-dashboard.js";
8
+ export { SloAlertRules, sloAlertQueries } from "./slo-alert-rules.js";
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `RedDashboard`: rate, errors and duration per service, from a
3
+ * `spanmetrics` connector declaration.
4
+ *
5
+ * The metric names come from the connector's config through the otel
6
+ * lexicon's `spanMetricsNames()`: its namespace, its histogram unit, the
7
+ * default dimensions it keeps, and optionally the `prometheus` exporter's
8
+ * namespace. Renaming the connector's namespace moves every query here.
9
+ *
10
+ * The queries count server and consumer spans only by default, the spans
11
+ * that handle a request or a message, so a service's outgoing calls and
12
+ * its internal spans don't inflate its rate or dilute its error ratio.
13
+ */
14
+ import { Composite } from "@intentius/chant/composite";
15
+ import { RED_DEFAULT_SPAN_KINDS as SHARED_DEFAULT_SPAN_KINDS, spanMetricsNames, spanMetricsRedQueries, } from "@intentius/chant-lexicon-otel/metric-names";
16
+ import { Dashboard } from "../dashboard.js";
17
+ import { Row, TimeSeriesPanel } from "../panels.js";
18
+ import { PromQuery } from "../query.js";
19
+ import { QueryVariable } from "../variables.js";
20
+ import { slugUid } from "../util.js";
21
+ import { dashboardProps, durationUnit, legend, quantileName, requireDatasource, selector } from "./shared.js";
22
+ /** The kinds `RedDashboard` counts unless told otherwise: the spans that serve a request or consume a message. */
23
+ export const RED_DEFAULT_SPAN_KINDS = SHARED_DEFAULT_SPAN_KINDS;
24
+ const DEFAULT_QUANTILES = [0.5, 0.95, 0.99];
25
+ function cap(s) {
26
+ return s.charAt(0).toUpperCase() + s.slice(1);
27
+ }
28
+ function isNames(x) {
29
+ return typeof x === "object" && x !== null && "calls" in x && "labels" in x && "errorStatus" in x;
30
+ }
31
+ /**
32
+ * The PromQL the RED dashboard runs, from the connector's names. Exposed for
33
+ * tests and for panels of your own. `spanKinds` defaults to server and
34
+ * consumer spans; `[]` counts every kind. The expressions come from the otel
35
+ * lexicon's `spanMetricsRedQueries()`, which the prometheus lexicon's
36
+ * `RedAlerts` reads too.
37
+ */
38
+ export function redQueries(names, quantiles = DEFAULT_QUANTILES, serviceFilter = "$service", spanKinds) {
39
+ const q = spanMetricsRedQueries(names, { range: "$__rate_interval", quantiles, serviceMatch: serviceFilter, spanKinds, owner: "RedDashboard" });
40
+ return {
41
+ rate: q.rate,
42
+ errorRatio: q.errorRatio,
43
+ duration: q.duration,
44
+ services: `label_values(${selector(names.calls.prometheus, q.kindMatchers)}, ${q.service})`,
45
+ };
46
+ }
47
+ /** "server and consumer spans", for panel descriptions. */
48
+ function kindsText(names, kinds) {
49
+ const counted = kinds ?? (names.labels.spanKind ? RED_DEFAULT_SPAN_KINDS : []);
50
+ if (counted.length === 0)
51
+ return "spans";
52
+ const words = [...new Set(counted)].map((k) => k.replace(/^SPAN_KIND_/, "").toLowerCase());
53
+ return `${words.length > 1 ? `${words.slice(0, -1).join(", ")} and ${words[words.length - 1]}` : words[0]} spans`;
54
+ }
55
+ /**
56
+ * A RED dashboard (rate, errors, duration) per service, built from a
57
+ * `spanmetrics` connector declaration.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { SpanMetricsConnector } from "@intentius/chant-lexicon-otel";
62
+ * import { Datasource, RedDashboard } from "@intentius/chant-lexicon-grafana";
63
+ *
64
+ * export const spans = new SpanMetricsConnector({ namespace: "shop", histogram: { unit: "s" } });
65
+ * export const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090" });
66
+ * export const red = RedDashboard({ spanMetrics: spans, datasource: prometheus });
67
+ * ```
68
+ */
69
+ export const RedDashboard = Composite((props) => {
70
+ requireDatasource("RedDashboard", props.datasource);
71
+ if (!props.spanMetrics)
72
+ throw new Error("RedDashboard: spanMetrics is required (a spanmetrics connector)");
73
+ const names = isNames(props.spanMetrics) ? props.spanMetrics : spanMetricsNames(props.spanMetrics, props.exporter);
74
+ const quantiles = props.quantiles ?? DEFAULT_QUANTILES;
75
+ for (const q of quantiles) {
76
+ if (!(typeof q === "number" && q > 0 && q < 1))
77
+ throw new Error(`RedDashboard: a quantile must be between 0 and 1, got ${String(q)}`);
78
+ }
79
+ const q = redQueries(names, quantiles, "$service", props.spanKinds);
80
+ const counted = kindsText(names, props.spanKinds);
81
+ const svc = names.labels.service;
82
+ const ds = props.datasource;
83
+ const service = new QueryVariable({
84
+ name: "service",
85
+ label: "Service",
86
+ datasource: ds,
87
+ query: q.services,
88
+ multi: true,
89
+ includeAll: true,
90
+ allValue: ".*",
91
+ refresh: "onTimeRangeChange",
92
+ sort: 1,
93
+ });
94
+ const rate = new TimeSeriesPanel({
95
+ title: "Rate",
96
+ description: `${cap(counted)} per second by service, from ${names.calls.prometheus}.`,
97
+ datasource: ds,
98
+ gridPos: { w: 12, h: 8 },
99
+ targets: [new PromQuery({ expr: q.rate, legendFormat: legend(svc) })],
100
+ fieldConfig: { defaults: { unit: "reqps" } },
101
+ });
102
+ const errors = new TimeSeriesPanel({
103
+ title: "Errors",
104
+ description: `Share of ${counted} with ${names.labels.statusCode}="${names.errorStatus}", by service.`,
105
+ datasource: ds,
106
+ gridPos: { w: 12, h: 8 },
107
+ targets: [new PromQuery({ expr: q.errorRatio, legendFormat: legend(svc) })],
108
+ fieldConfig: { defaults: { unit: "percentunit", min: 0 } },
109
+ });
110
+ const rows = [new Row({ title: "Rate and errors", panels: [rate, errors] })];
111
+ if (names.duration && q.duration.length > 0) {
112
+ const width = Math.max(6, Math.floor(24 / q.duration.length));
113
+ const unit = durationUnit(names.duration.unit);
114
+ const panels = q.duration.map((d) => new TimeSeriesPanel({
115
+ title: `Duration ${quantileName(d.quantile)}`,
116
+ description: `${quantileName(d.quantile)} duration of ${counted} by service, from ${names.duration.prometheus}.`,
117
+ datasource: ds,
118
+ gridPos: { w: width, h: 8 },
119
+ targets: [new PromQuery({ expr: d.expr, legendFormat: legend(svc) })],
120
+ fieldConfig: { defaults: { unit } },
121
+ }));
122
+ rows.push(new Row({ title: "Duration", panels }));
123
+ }
124
+ const dashboard = new Dashboard({
125
+ ...dashboardProps(props, {
126
+ title: "Services: rate, errors, duration",
127
+ uid: slugUid(`red-${names.namespace || "spans"}`),
128
+ description: `Rate, errors and duration per service from the ${names.namespace || "unprefixed"} span metrics.`,
129
+ tags: ["red", "spanmetrics"],
130
+ }),
131
+ variables: [service],
132
+ panels: rows,
133
+ });
134
+ return { dashboard };
135
+ }, "RedDashboard");
@@ -0,0 +1,74 @@
1
+ /**
2
+ * What the dashboard composites share: the dashboard-level props they all
3
+ * take, a PromQL selector builder, and the field configs they reuse.
4
+ */
5
+ import { promErrorRatio, promNumber, promQuantile, promSelector, promSumRate, } from "@intentius/chant-lexicon-otel/metric-names";
6
+ // The PromQL builders live in the otel lexicon's metric-names, next to the
7
+ // names they read, so the prometheus lexicon's RedAlerts builds the same
8
+ // expressions without importing grafana. Here they default the range to
9
+ // Grafana's `$__rate_interval`.
10
+ /** `metric{a="x", b=~"y"}`; just `metric` with no matchers. */
11
+ export function selector(metric, matchers) {
12
+ return promSelector(metric, matchers);
13
+ }
14
+ /** `sum by (labels) (rate(sel[range]))`, or `sum (...)` with no labels. */
15
+ export function sumRate(sel, by, range = "$__rate_interval") {
16
+ return promSumRate(sel, by, range);
17
+ }
18
+ /**
19
+ * `errors / all` as rates summed by `by`, with the numerator padded to zero:
20
+ * `(errors or all * 0) / all`. A group with calls but no error series gets
21
+ * 0 instead of dropping out of the result, so a panel shows 0% rather than
22
+ * "No data" for a service that has had no errors.
23
+ */
24
+ export function errorRatio(errorSel, allSel, by) {
25
+ return promErrorRatio(errorSel, allSel, by, "$__rate_interval");
26
+ }
27
+ /** `histogram_quantile(q, sum by (le, labels) (rate(bucket[range])))`. */
28
+ export function quantile(q, bucketSel, by) {
29
+ return promQuantile(q, bucketSel, by, "$__rate_interval");
30
+ }
31
+ /** A number as PromQL and Grafana thresholds write it, without float noise. */
32
+ export function num(n) {
33
+ return promNumber(n);
34
+ }
35
+ /** `p95` for 0.95, `p99.9` for 0.999. */
36
+ export function quantileName(q) {
37
+ return `p${num(q * 100)}`;
38
+ }
39
+ /** Legend text naming the label, `{{service_name}}`. */
40
+ export function legend(...labels) {
41
+ return labels.map((l) => `{{${l}}}`).join(" ");
42
+ }
43
+ /** Grafana's unit id for a spanmetrics histogram unit (`ms` or `s`). */
44
+ export function durationUnit(unit) {
45
+ return unit === "s" ? "s" : "ms";
46
+ }
47
+ /** Absolute thresholds from a base colour and `[value, colour]` steps. */
48
+ export function thresholds(base, steps) {
49
+ return { mode: "absolute", steps: [{ value: null, color: base }, ...steps.map(([value, color]) => ({ value, color }))] };
50
+ }
51
+ /** The props a composite passes to `Dashboard`, from the options and its own defaults. */
52
+ export function dashboardProps(o, defaults) {
53
+ return {
54
+ title: o.title ?? defaults.title,
55
+ uid: o.uid ?? defaults.uid,
56
+ description: o.description ?? defaults.description,
57
+ tags: o.tags ?? defaults.tags,
58
+ time: o.time ?? { from: "now-6h", to: "now" },
59
+ ...(o.refresh !== undefined ? { refresh: o.refresh } : { refresh: "1m" }),
60
+ graphTooltip: "sharedCrosshair",
61
+ ...(o.folder !== undefined ? { folder: o.folder } : {}),
62
+ ...(o.links !== undefined ? { links: o.links } : {}),
63
+ };
64
+ }
65
+ /** Throw unless a Prometheus datasource was passed. */
66
+ export function requireDatasource(composite, ds) {
67
+ if (ds === undefined || ds === null)
68
+ throw new Error(`${composite}: datasource is required (a Prometheus Datasource or { type: "prometheus", uid })`);
69
+ const type = ds.datasourceType ??
70
+ ds.pluginType ??
71
+ ds.type;
72
+ if (type !== "prometheus")
73
+ throw new Error(`${composite}: datasource must be a Prometheus datasource, got ${String(type)}`);
74
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `SloAlertRules`: an `Slo`'s multiwindow, multi-burn-rate alerts as
3
+ * Grafana-managed alert rules, for teams that page from Grafana rather
4
+ * than from Prometheus and Alertmanager.
5
+ *
6
+ * The windows, factors, thresholds and labels come from the prometheus
7
+ * lexicon's `sloMetrics()`, the same numbers the `Slo`'s own Prometheus
8
+ * alerts use. Each burn-rate pair becomes one rule that reads the two
9
+ * error ratios the `Slo` records (`slo:sli_error:ratio_rate<window>`) as
10
+ * instant Prometheus queries, and a math expression that holds while both
11
+ * are above the pair's threshold:
12
+ *
13
+ * A = slo:sli_error:ratio_rate1h{slo="checkout"}
14
+ * B = slo:sli_error:ratio_rate5m{slo="checkout"}
15
+ * C = $A > 0.0144 && $B > 0.0144 (condition)
16
+ *
17
+ * So the `Slo`'s rule group must be loaded into the Prometheus the rules
18
+ * query: the recording rules are what make a 3-day window cheap to
19
+ * evaluate every minute.
20
+ */
21
+ import { Composite } from "@intentius/chant/composite";
22
+ import { sloMetrics } from "@intentius/chant-lexicon-prometheus/composites/slo";
23
+ import { AlertRule, AlertRuleGroup, MathExpression, } from "../alerting.js";
24
+ import { PromQuery } from "../query.js";
25
+ import { slugUid } from "../util.js";
26
+ import { num, requireDatasource } from "./shared.js";
27
+ function isMetrics(x) {
28
+ return typeof x === "object" && x !== null && "errorBudgetRemaining" in x && "burnRates" in x && "selector" in x;
29
+ }
30
+ /** A rule uid for one pair: the SLO's name cut to fit, then the windows, at most 40 characters. */
31
+ function pairUid(name, long, short) {
32
+ const suffix = `-${long}-${short}`.toLowerCase();
33
+ return `${slugUid(`slo ${name}`).slice(0, 40 - suffix.length).replace(/-$/, "")}${suffix}`;
34
+ }
35
+ /** The queries and condition of each burn-rate rule, from `sloMetrics()`. Exposed for tests and for rules of your own. */
36
+ export function sloAlertQueries(m) {
37
+ return m.burnRates.map((b) => ({
38
+ ...b,
39
+ uid: pairUid(m.name, b.long, b.short),
40
+ longExpr: `${b.longRecord}${m.selector}`,
41
+ shortExpr: `${b.shortRecord}${m.selector}`,
42
+ condition: `$A > ${num(b.threshold)} && $B > ${num(b.threshold)}`,
43
+ }));
44
+ }
45
+ /**
46
+ * Grafana-managed burn-rate alerts for an `Slo`: one rule per window pair,
47
+ * with the labels and annotations the `Slo`'s Prometheus alerts carry.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { Slo } from "@intentius/chant-lexicon-prometheus";
52
+ * import { SloAlertRules } from "@intentius/chant-lexicon-grafana";
53
+ *
54
+ * export const checkout = Slo({ name: "checkout", objective: 0.999, window: "30d", sli: { ... } });
55
+ * export const checkoutAlerts = SloAlertRules({ slo: checkout, datasource: prometheus, folder: "SLOs" });
56
+ * ```
57
+ */
58
+ export const SloAlertRules = Composite((props) => {
59
+ requireDatasource("SloAlertRules", props.datasource);
60
+ if (!props.slo)
61
+ throw new Error("SloAlertRules: slo is required (an Slo(...) declaration)");
62
+ if (typeof props.folder !== "string" || props.folder.trim() === "")
63
+ throw new Error("SloAlertRules: folder is required (the Grafana folder title)");
64
+ const m = isMetrics(props.slo) ? props.slo : sloMetrics(props.slo);
65
+ const pairs = sloAlertQueries(m);
66
+ if (pairs.length === 0) {
67
+ throw new Error(`SloAlertRules: the Slo "${m.name}" has alerting turned off, so it has no burn-rate windows to alert on`);
68
+ }
69
+ const ds = props.datasource;
70
+ const budget = num(m.errorBudget);
71
+ const rules = pairs.map((p) => new AlertRule({
72
+ title: `SLO ${m.name} error budget burn ${p.long}/${p.short} (${p.severity})`,
73
+ uid: p.uid,
74
+ data: [
75
+ new PromQuery({ datasource: ds, expr: p.longExpr, instant: true, range: false }),
76
+ new PromQuery({ datasource: ds, expr: p.shortExpr, instant: true, range: false }),
77
+ new MathExpression({ expression: p.condition }),
78
+ ],
79
+ condition: "C",
80
+ noDataState: props.noDataState ?? "OK",
81
+ execErrState: props.execErrState ?? "Error",
82
+ ...(props.for !== undefined ? { for: props.for } : {}),
83
+ labels: { ...(props.labels ?? {}), ...p.labels },
84
+ annotations: {
85
+ summary: `SLO ${m.name} is burning its error budget more than ${num(p.factor)}x too fast (${p.long} and ${p.short} windows)`,
86
+ description: `The error ratio over both the last ${p.long} and the last ${p.short} is above ${num(p.threshold)} ` +
87
+ `(${num(p.factor)} times the ${budget} budget of a ${num(m.objective)} objective). ` +
88
+ `At that rate the ${m.window} error budget is gone in ${p.exhaustsIn}.`,
89
+ ...(props.annotations ?? {}),
90
+ },
91
+ ...(props.contactPoint !== undefined ? { notification_settings: { receiver: props.contactPoint } } : {}),
92
+ }));
93
+ return {
94
+ rules: new AlertRuleGroup({ name: props.group ?? m.group, folder: props.folder, interval: props.interval ?? "1m", rules }),
95
+ };
96
+ }, "SloAlertRules");