@intentius/chant-lexicon-grafana 0.97.0 → 0.99.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 (451) hide show
  1. package/README.md +15 -5
  2. package/dist/actions/grafana.d.ts +24 -0
  3. package/dist/actions/grafana.d.ts.map +1 -0
  4. package/dist/actions/index.d.ts +2 -0
  5. package/dist/actions/index.d.ts.map +1 -0
  6. package/dist/alerting-build.d.ts +128 -0
  7. package/dist/alerting-build.d.ts.map +1 -0
  8. package/dist/alerting.d.ts +273 -0
  9. package/dist/alerting.d.ts.map +1 -0
  10. package/dist/api/apply.d.ts +156 -0
  11. package/dist/api/apply.d.ts.map +1 -0
  12. package/dist/api/bind.d.ts +32 -0
  13. package/dist/api/bind.d.ts.map +1 -0
  14. package/dist/api/client.d.ts +76 -0
  15. package/dist/api/client.d.ts.map +1 -0
  16. package/dist/api/converge.d.ts +33 -0
  17. package/dist/api/converge.d.ts.map +1 -0
  18. package/dist/api/dashboards.d.ts +102 -0
  19. package/dist/api/dashboards.d.ts.map +1 -0
  20. package/dist/api/datasources.d.ts +26 -0
  21. package/dist/api/datasources.d.ts.map +1 -0
  22. package/dist/api/fake-grafana-writes.d.ts +41 -0
  23. package/dist/api/fake-grafana-writes.d.ts.map +1 -0
  24. package/dist/api/fake-grafana.d.ts +61 -0
  25. package/dist/api/fake-grafana.d.ts.map +1 -0
  26. package/dist/api/folders.d.ts +110 -0
  27. package/dist/api/folders.d.ts.map +1 -0
  28. package/dist/api/library-panels.d.ts +52 -0
  29. package/dist/api/library-panels.d.ts.map +1 -0
  30. package/dist/build.d.ts +30 -2
  31. package/dist/build.d.ts.map +1 -1
  32. package/dist/catalog.d.ts +1 -1
  33. package/dist/catalog.d.ts.map +1 -1
  34. package/dist/codegen/docs.d.ts.map +1 -1
  35. package/dist/codegen/schema-types.d.ts +3 -3
  36. package/dist/codegen/schema-types.d.ts.map +1 -1
  37. package/dist/composites/agent-dashboard.d.ts.map +1 -1
  38. package/dist/composites/catalog.d.ts.map +1 -1
  39. package/dist/composites/index.d.ts +4 -2
  40. package/dist/composites/index.d.ts.map +1 -1
  41. package/dist/composites/red-dashboard.d.ts +21 -2
  42. package/dist/composites/red-dashboard.d.ts.map +1 -1
  43. package/dist/composites/shared.d.ts +7 -0
  44. package/dist/composites/shared.d.ts.map +1 -1
  45. package/dist/composites/slo-alert-rules.d.ts +92 -0
  46. package/dist/composites/slo-alert-rules.d.ts.map +1 -0
  47. package/dist/config.d.ts +111 -0
  48. package/dist/config.d.ts.map +1 -0
  49. package/dist/contact-point-secrets.d.ts +17 -0
  50. package/dist/contact-point-secrets.d.ts.map +1 -0
  51. package/dist/dashboard.d.ts +16 -2
  52. package/dist/dashboard.d.ts.map +1 -1
  53. package/dist/datasource-refs.d.ts +95 -0
  54. package/dist/datasource-refs.d.ts.map +1 -0
  55. package/dist/datasource.d.ts +29 -0
  56. package/dist/datasource.d.ts.map +1 -1
  57. package/dist/deep-observe-hooks.d.ts +64 -0
  58. package/dist/deep-observe-hooks.d.ts.map +1 -0
  59. package/dist/deep-observe.d.ts +42 -0
  60. package/dist/deep-observe.d.ts.map +1 -0
  61. package/dist/describe-resources.d.ts +49 -0
  62. package/dist/describe-resources.d.ts.map +1 -0
  63. package/dist/detect.d.ts +28 -1
  64. package/dist/detect.d.ts.map +1 -1
  65. package/dist/export-resources.d.ts +38 -0
  66. package/dist/export-resources.d.ts.map +1 -0
  67. package/dist/import/alerting.d.ts +40 -0
  68. package/dist/import/alerting.d.ts.map +1 -0
  69. package/dist/import/edits.d.ts +47 -0
  70. package/dist/import/edits.d.ts.map +1 -0
  71. package/dist/import/embedded.d.ts +14 -0
  72. package/dist/import/embedded.d.ts.map +1 -0
  73. package/dist/import/generator.d.ts +60 -0
  74. package/dist/import/generator.d.ts.map +1 -0
  75. package/dist/import/live-export.d.ts +108 -0
  76. package/dist/import/live-export.d.ts.map +1 -0
  77. package/dist/import/mappings.d.ts +83 -0
  78. package/dist/import/mappings.d.ts.map +1 -0
  79. package/dist/import/model.d.ts +98 -0
  80. package/dist/import/model.d.ts.map +1 -0
  81. package/dist/import/normalize-alerting.d.ts +26 -0
  82. package/dist/import/normalize-alerting.d.ts.map +1 -0
  83. package/dist/import/normalize.d.ts +74 -0
  84. package/dist/import/normalize.d.ts.map +1 -0
  85. package/dist/import/parser.d.ts +110 -0
  86. package/dist/import/parser.d.ts.map +1 -0
  87. package/dist/import/report.d.ts +26 -0
  88. package/dist/import/report.d.ts.map +1 -0
  89. package/dist/import/testdata/fixtures.d.ts +35 -0
  90. package/dist/import/testdata/fixtures.d.ts.map +1 -0
  91. package/dist/import/v2.d.ts +72 -0
  92. package/dist/import/v2.d.ts.map +1 -0
  93. package/dist/index.d.ts +26 -9
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/integrity.json +18 -10
  96. package/dist/k8s.d.ts +107 -0
  97. package/dist/k8s.d.ts.map +1 -0
  98. package/dist/lint/audit-catalog.d.ts +2 -1
  99. package/dist/lint/audit-catalog.d.ts.map +1 -1
  100. package/dist/lint/post-synth/graf101.d.ts +2 -2
  101. package/dist/lint/post-synth/graf102.d.ts +1 -1
  102. package/dist/lint/post-synth/graf107.d.ts +1 -1
  103. package/dist/lint/post-synth/graf108.d.ts +8 -0
  104. package/dist/lint/post-synth/graf108.d.ts.map +1 -0
  105. package/dist/lint/post-synth/graf109.d.ts +8 -0
  106. package/dist/lint/post-synth/graf109.d.ts.map +1 -0
  107. package/dist/lint/post-synth/graf111.d.ts +8 -0
  108. package/dist/lint/post-synth/graf111.d.ts.map +1 -0
  109. package/dist/lint/post-synth/graf112.d.ts +8 -0
  110. package/dist/lint/post-synth/graf112.d.ts.map +1 -0
  111. package/dist/lint/post-synth/graf113.d.ts +8 -0
  112. package/dist/lint/post-synth/graf113.d.ts.map +1 -0
  113. package/dist/lint/post-synth/graf114.d.ts +8 -0
  114. package/dist/lint/post-synth/graf114.d.ts.map +1 -0
  115. package/dist/lint/post-synth/graf115.d.ts +8 -0
  116. package/dist/lint/post-synth/graf115.d.ts.map +1 -0
  117. package/dist/lint/post-synth/grafana-helpers.d.ts +7 -5
  118. package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -1
  119. package/dist/lint/post-synth/index.d.ts.map +1 -1
  120. package/dist/lint/rules/literal-secret.d.ts +7 -3
  121. package/dist/lint/rules/literal-secret.d.ts.map +1 -1
  122. package/dist/lsp/hover.d.ts.map +1 -1
  123. package/dist/manifest.json +1 -1
  124. package/dist/members.d.ts +27 -0
  125. package/dist/members.d.ts.map +1 -0
  126. package/dist/meta.json +135 -0
  127. package/dist/okf/index.md +36 -2
  128. package/dist/okf/rules/GRAF002.md +3 -2
  129. package/dist/okf/rules/GRAF101.md +3 -2
  130. package/dist/okf/rules/GRAF102.md +1 -0
  131. package/dist/okf/rules/GRAF108.md +11 -0
  132. package/dist/okf/rules/GRAF109.md +15 -0
  133. package/dist/okf/rules/GRAF111.md +11 -0
  134. package/dist/okf/rules/GRAF112.md +16 -0
  135. package/dist/okf/rules/GRAF113.md +16 -0
  136. package/dist/okf/rules/GRAF114.md +11 -0
  137. package/dist/okf/rules/GRAF115.md +11 -0
  138. package/dist/okf/types/AlertListPanel.md +9 -0
  139. package/dist/okf/types/AlertQuery.md +9 -0
  140. package/dist/okf/types/AlertRule.md +9 -0
  141. package/dist/okf/types/AlertRuleGroup.md +9 -0
  142. package/dist/okf/types/BarChartPanel.md +9 -0
  143. package/dist/okf/types/BarGaugePanel.md +9 -0
  144. package/dist/okf/types/CanvasPanel.md +9 -0
  145. package/dist/okf/types/ClassicConditionsExpression.md +9 -0
  146. package/dist/okf/types/ContactPoint.md +14 -0
  147. package/dist/okf/types/Dashboard.md +1 -0
  148. package/dist/okf/types/Datasource.md +3 -2
  149. package/dist/okf/types/ExternalDatasource.md +15 -0
  150. package/dist/okf/types/FlameGraphPanel.md +9 -0
  151. package/dist/okf/types/GeomapPanel.md +9 -0
  152. package/dist/okf/types/HistogramPanel.md +9 -0
  153. package/dist/okf/types/MathExpression.md +9 -0
  154. package/dist/okf/types/MuteTiming.md +13 -0
  155. package/dist/okf/types/NodeGraphPanel.md +9 -0
  156. package/dist/okf/types/NotificationPolicy.md +9 -0
  157. package/dist/okf/types/NotificationTemplate.md +9 -0
  158. package/dist/okf/types/PieChartPanel.md +9 -0
  159. package/dist/okf/types/ReduceExpression.md +9 -0
  160. package/dist/okf/types/ResampleExpression.md +9 -0
  161. package/dist/okf/types/SqlExpression.md +9 -0
  162. package/dist/okf/types/StateTimelinePanel.md +9 -0
  163. package/dist/okf/types/StatusHistoryPanel.md +9 -0
  164. package/dist/okf/types/ThresholdExpression.md +9 -0
  165. package/dist/okf/types/TrendPanel.md +9 -0
  166. package/dist/okf/types/XYChartPanel.md +9 -0
  167. package/dist/op/activities/grafana-apply.d.ts +75 -0
  168. package/dist/op/activities/grafana-apply.d.ts.map +1 -0
  169. package/dist/op/activities/index.d.ts +9 -0
  170. package/dist/op/activities/index.d.ts.map +1 -0
  171. package/dist/op/builders.d.ts +24 -0
  172. package/dist/op/builders.d.ts.map +1 -0
  173. package/dist/ownership.d.ts +67 -0
  174. package/dist/ownership.d.ts.map +1 -0
  175. package/dist/panel-options.d.ts +65 -0
  176. package/dist/panel-options.d.ts.map +1 -0
  177. package/dist/panels.d.ts +46 -9
  178. package/dist/panels.d.ts.map +1 -1
  179. package/dist/pin.d.ts +29 -7
  180. package/dist/pin.d.ts.map +1 -1
  181. package/dist/plugin.d.ts.map +1 -1
  182. package/dist/promql-check.d.ts +51 -0
  183. package/dist/promql-check.d.ts.map +1 -0
  184. package/dist/query.d.ts +9 -8
  185. package/dist/query.d.ts.map +1 -1
  186. package/dist/rules/graf101.ts +3 -3
  187. package/dist/rules/graf102.ts +1 -1
  188. package/dist/rules/graf107.ts +1 -1
  189. package/dist/rules/graf108.ts +17 -0
  190. package/dist/rules/graf109.ts +17 -0
  191. package/dist/rules/graf111.ts +17 -0
  192. package/dist/rules/graf112.ts +17 -0
  193. package/dist/rules/graf113.ts +17 -0
  194. package/dist/rules/graf114.ts +17 -0
  195. package/dist/rules/graf115.ts +17 -0
  196. package/dist/rules/grafana-helpers.ts +15 -8
  197. package/dist/rules/literal-secret.ts +60 -6
  198. package/dist/schema/barchart.gen.d.ts +198 -0
  199. package/dist/schema/barchart.gen.d.ts.map +1 -0
  200. package/dist/schema/bargauge.gen.d.ts +97 -0
  201. package/dist/schema/bargauge.gen.d.ts.map +1 -0
  202. package/dist/schema/canvas.gen.d.ts +152 -0
  203. package/dist/schema/canvas.gen.d.ts.map +1 -0
  204. package/dist/schema/dashboard.gen.d.ts +196 -21
  205. package/dist/schema/dashboard.gen.d.ts.map +1 -1
  206. package/dist/schema/expr.gen.d.ts +606 -0
  207. package/dist/schema/expr.gen.d.ts.map +1 -0
  208. package/dist/schema/gauge.gen.d.ts.map +1 -1
  209. package/dist/schema/geomap.gen.d.ts +100 -0
  210. package/dist/schema/geomap.gen.d.ts.map +1 -0
  211. package/dist/schema/heatmap.gen.d.ts.map +1 -1
  212. package/dist/schema/histogram.gen.d.ts +132 -0
  213. package/dist/schema/histogram.gen.d.ts.map +1 -0
  214. package/dist/schema/index.d.ts +12 -0
  215. package/dist/schema/index.d.ts.map +1 -1
  216. package/dist/schema/logs.gen.d.ts +1 -0
  217. package/dist/schema/logs.gen.d.ts.map +1 -1
  218. package/dist/schema/loki.gen.d.ts.map +1 -1
  219. package/dist/schema/nodegraph.gen.d.ts +49 -0
  220. package/dist/schema/nodegraph.gen.d.ts.map +1 -0
  221. package/dist/schema/piechart.gen.d.ts +127 -0
  222. package/dist/schema/piechart.gen.d.ts.map +1 -0
  223. package/dist/schema/prometheus.gen.d.ts.map +1 -1
  224. package/dist/schema/stat.gen.d.ts.map +1 -1
  225. package/dist/schema/statetimeline.gen.d.ts +124 -0
  226. package/dist/schema/statetimeline.gen.d.ts.map +1 -0
  227. package/dist/schema/statushistory.gen.d.ts +114 -0
  228. package/dist/schema/statushistory.gen.d.ts.map +1 -0
  229. package/dist/schema/table.gen.d.ts +22 -3
  230. package/dist/schema/table.gen.d.ts.map +1 -1
  231. package/dist/schema/tempo.gen.d.ts.map +1 -1
  232. package/dist/schema/text.gen.d.ts.map +1 -1
  233. package/dist/schema/timeseries.gen.d.ts +5 -1
  234. package/dist/schema/timeseries.gen.d.ts.map +1 -1
  235. package/dist/schema/trend.gen.d.ts +180 -0
  236. package/dist/schema/trend.gen.d.ts.map +1 -0
  237. package/dist/schema/xychart.gen.d.ts +145 -0
  238. package/dist/schema/xychart.gen.d.ts.map +1 -0
  239. package/dist/schema-validate.d.ts +30 -4
  240. package/dist/schema-validate.d.ts.map +1 -1
  241. package/dist/serializer.d.ts +11 -3
  242. package/dist/serializer.d.ts.map +1 -1
  243. package/dist/skill-defs.d.ts.map +1 -1
  244. package/dist/skills/chant-grafana-alerting.md +49 -0
  245. package/dist/skills/chant-grafana-provisioning.md +3 -3
  246. package/dist/skills/chant-grafana.md +12 -6
  247. package/dist/spec/fetch-units-cli.d.ts +3 -0
  248. package/dist/spec/fetch-units-cli.d.ts.map +1 -0
  249. package/dist/spec/overlay.d.ts +43 -0
  250. package/dist/spec/overlay.d.ts.map +1 -0
  251. package/dist/spec/schemas.d.ts +13 -7
  252. package/dist/spec/schemas.d.ts.map +1 -1
  253. package/dist/spec/units.d.ts +35 -0
  254. package/dist/spec/units.d.ts.map +1 -0
  255. package/dist/spec/units.gen.d.ts +2 -0
  256. package/dist/spec/units.gen.d.ts.map +1 -0
  257. package/dist/transformations.d.ts +459 -0
  258. package/dist/transformations.d.ts.map +1 -0
  259. package/dist/util.d.ts +6 -0
  260. package/dist/util.d.ts.map +1 -1
  261. package/dist/validate-alerting.d.ts +65 -0
  262. package/dist/validate-alerting.d.ts.map +1 -0
  263. package/dist/validate-output.d.ts +49 -9
  264. package/dist/validate-output.d.ts.map +1 -1
  265. package/dist/validate.d.ts.map +1 -1
  266. package/dist/variables.d.ts +7 -2
  267. package/dist/variables.d.ts.map +1 -1
  268. package/package.json +12 -4
  269. package/src/actions/actions.test.ts +24 -0
  270. package/src/actions/grafana.ts +40 -0
  271. package/src/actions/index.ts +1 -0
  272. package/src/alerting-build.ts +376 -0
  273. package/src/alerting.e2e.test.ts +212 -0
  274. package/src/alerting.test.ts +272 -0
  275. package/src/alerting.ts +435 -0
  276. package/src/api/apply.test.ts +238 -0
  277. package/src/api/apply.ts +384 -0
  278. package/src/api/bind.ts +58 -0
  279. package/src/api/client.ts +148 -0
  280. package/src/api/converge.ts +63 -0
  281. package/src/api/dashboards.ts +247 -0
  282. package/src/api/datasources.ts +33 -0
  283. package/src/api/fake-grafana-writes.ts +201 -0
  284. package/src/api/fake-grafana.ts +186 -0
  285. package/src/api/folders.ts +265 -0
  286. package/src/api/library-panels.ts +101 -0
  287. package/src/build.test.ts +210 -5
  288. package/src/build.ts +180 -43
  289. package/src/catalog.ts +86 -2
  290. package/src/codegen/docs.ts +42 -9
  291. package/src/codegen/schema-types.ts +16 -6
  292. package/src/composites/agent-dashboard.ts +6 -3
  293. package/src/composites/catalog.test.ts +2 -2
  294. package/src/composites/catalog.ts +82 -0
  295. package/src/composites/composites.test.ts +73 -9
  296. package/src/composites/index.ts +4 -2
  297. package/src/composites/queries.e2e.test.ts +374 -0
  298. package/src/composites/red-dashboard.ts +83 -12
  299. package/src/composites/shared.ts +11 -0
  300. package/src/composites/slo-alert-rules.ts +149 -0
  301. package/src/config.test.ts +61 -0
  302. package/src/config.ts +138 -0
  303. package/src/contact-point-secrets.ts +63 -0
  304. package/src/dashboard.ts +16 -2
  305. package/src/datasource-refs.test.ts +56 -0
  306. package/src/datasource-refs.ts +189 -0
  307. package/src/datasource.ts +52 -0
  308. package/src/deep-observe-hooks.ts +252 -0
  309. package/src/deep-observe.test.ts +265 -0
  310. package/src/deep-observe.ts +164 -0
  311. package/src/describe-resources.test.ts +255 -0
  312. package/src/describe-resources.ts +209 -0
  313. package/src/detect.ts +79 -2
  314. package/src/export-resources.test.ts +129 -0
  315. package/src/export-resources.ts +84 -0
  316. package/src/generated/lexicon-grafana.json +135 -0
  317. package/src/import/alerting-roundtrip.test.ts +161 -0
  318. package/src/import/alerting.ts +500 -0
  319. package/src/import/edits.ts +95 -0
  320. package/src/import/embedded.test.ts +45 -0
  321. package/src/import/embedded.ts +47 -0
  322. package/src/import/generated-types.e2e.test.ts +118 -0
  323. package/src/import/generator.test.ts +210 -0
  324. package/src/import/generator.ts +462 -0
  325. package/src/import/live-export.ts +264 -0
  326. package/src/import/mappings.ts +264 -0
  327. package/src/import/model.ts +110 -0
  328. package/src/import/normalize-alerting.ts +101 -0
  329. package/src/import/normalize.test.ts +117 -0
  330. package/src/import/normalize.ts +364 -0
  331. package/src/import/parser.test.ts +440 -0
  332. package/src/import/parser.ts +1236 -0
  333. package/src/import/report.ts +62 -0
  334. package/src/import/roundtrip.test.ts +468 -0
  335. package/src/import/testdata/fixtures.ts +129 -0
  336. package/src/import/v2.test.ts +292 -0
  337. package/src/import/v2.ts +803 -0
  338. package/src/import.e2e.test.ts +31 -84
  339. package/src/import.test.ts +9 -3
  340. package/src/index.ts +190 -5
  341. package/src/k8s.test.ts +98 -0
  342. package/src/k8s.ts +197 -0
  343. package/src/lint/audit-catalog.ts +60 -3
  344. package/src/lint/core-lint.test.ts +43 -0
  345. package/src/lint/post-synth/graf101.ts +3 -3
  346. package/src/lint/post-synth/graf102.ts +1 -1
  347. package/src/lint/post-synth/graf107.ts +1 -1
  348. package/src/lint/post-synth/graf108.ts +17 -0
  349. package/src/lint/post-synth/graf109.ts +17 -0
  350. package/src/lint/post-synth/graf111.ts +17 -0
  351. package/src/lint/post-synth/graf112.ts +17 -0
  352. package/src/lint/post-synth/graf113.ts +17 -0
  353. package/src/lint/post-synth/graf114.ts +17 -0
  354. package/src/lint/post-synth/graf115.ts +17 -0
  355. package/src/lint/post-synth/grafana-helpers.ts +15 -8
  356. package/src/lint/post-synth/index.ts +14 -0
  357. package/src/lint/post-synth/post-synth.test.ts +235 -11
  358. package/src/lint/rules/literal-secret.ts +60 -6
  359. package/src/lint/rules/rules.test.ts +27 -0
  360. package/src/lsp/completions.test.ts +5 -0
  361. package/src/lsp/hover.test.ts +7 -0
  362. package/src/lsp/hover.ts +8 -1
  363. package/src/members.ts +68 -0
  364. package/src/observe.e2e.test.ts +255 -0
  365. package/src/op/activities/grafana-apply.e2e.test.ts +175 -0
  366. package/src/op/activities/grafana-apply.test.ts +229 -0
  367. package/src/op/activities/grafana-apply.ts +171 -0
  368. package/src/op/activities/index.ts +8 -0
  369. package/src/op/builders.test.ts +40 -0
  370. package/src/op/builders.ts +34 -0
  371. package/src/ownership.ts +103 -0
  372. package/src/panel-options.ts +66 -0
  373. package/src/panels.test.ts +51 -0
  374. package/src/panels.ts +148 -10
  375. package/src/pin.ts +57 -6
  376. package/src/plugin.test.ts +5 -2
  377. package/src/plugin.ts +66 -0
  378. package/src/promql-check.test.ts +108 -0
  379. package/src/promql-check.ts +189 -0
  380. package/src/query.ts +13 -8
  381. package/src/schema/barchart.gen.ts +221 -0
  382. package/src/schema/bargauge.gen.ts +111 -0
  383. package/src/schema/canvas.gen.ts +176 -0
  384. package/src/schema/dashboard.gen.ts +206 -22
  385. package/src/schema/expr.gen.ts +618 -0
  386. package/src/schema/gauge.gen.ts +2 -1
  387. package/src/schema/geomap.gen.ts +112 -0
  388. package/src/schema/heatmap.gen.ts +2 -1
  389. package/src/schema/histogram.gen.ts +151 -0
  390. package/src/schema/index.ts +12 -0
  391. package/src/schema/logs.gen.ts +3 -1
  392. package/src/schema/loki.gen.ts +2 -1
  393. package/src/schema/nodegraph.gen.ts +58 -0
  394. package/src/schema/piechart.gen.ts +146 -0
  395. package/src/schema/prometheus.gen.ts +2 -1
  396. package/src/schema/stat.gen.ts +2 -1
  397. package/src/schema/statetimeline.gen.ts +143 -0
  398. package/src/schema/statushistory.gen.ts +132 -0
  399. package/src/schema/table.gen.ts +27 -4
  400. package/src/schema/tempo.gen.ts +2 -1
  401. package/src/schema/text.gen.ts +2 -1
  402. package/src/schema/timeseries.gen.ts +7 -2
  403. package/src/schema/trend.gen.ts +208 -0
  404. package/src/schema/xychart.gen.ts +167 -0
  405. package/src/schema-overlay.test.ts +282 -0
  406. package/src/schema-validate.ts +141 -50
  407. package/src/schema.test.ts +4 -2
  408. package/src/serializer.test.ts +13 -1
  409. package/src/serializer.ts +11 -3
  410. package/src/skill-defs.ts +17 -0
  411. package/src/skills/chant-grafana-alerting.md +49 -0
  412. package/src/skills/chant-grafana-provisioning.md +3 -3
  413. package/src/skills/chant-grafana.md +12 -6
  414. package/src/spec/fetch-cli.ts +3 -3
  415. package/src/spec/fetch-units-cli.ts +37 -0
  416. package/src/spec/overlay/canvas.overlay.json +142 -0
  417. package/src/spec/overlay/dashboard.overlay.json +378 -0
  418. package/src/spec/overlay/expr.overlay.json +14 -0
  419. package/src/spec/overlay/geomap.overlay.json +35 -0
  420. package/src/spec/overlay/logs.overlay.json +14 -0
  421. package/src/spec/overlay/nodegraph.overlay.json +31 -0
  422. package/src/spec/overlay/piechart.overlay.json +17 -0
  423. package/src/spec/overlay/table.overlay.json +84 -0
  424. package/src/spec/overlay/timeseries.overlay.json +21 -0
  425. package/src/spec/overlay/trend.overlay.json +17 -0
  426. package/src/spec/overlay/xychart.overlay.json +16 -0
  427. package/src/spec/overlay.ts +105 -0
  428. package/src/spec/schemas/barchart.jsonschema.json +419 -0
  429. package/src/spec/schemas/bargauge.jsonschema.json +224 -0
  430. package/src/spec/schemas/canvas.jsonschema.json +356 -0
  431. package/src/spec/schemas/dashboardv2.jsonschema.json +3387 -0
  432. package/src/spec/schemas/expr.jsonschema.json +987 -0
  433. package/src/spec/schemas/geomap.jsonschema.json +220 -0
  434. package/src/spec/schemas/histogram.jsonschema.json +308 -0
  435. package/src/spec/schemas/nodegraph.jsonschema.json +77 -0
  436. package/src/spec/schemas/piechart.jsonschema.json +275 -0
  437. package/src/spec/schemas/statetimeline.jsonschema.json +324 -0
  438. package/src/spec/schemas/statushistory.jsonschema.json +292 -0
  439. package/src/spec/schemas/trend.jsonschema.json +449 -0
  440. package/src/spec/schemas/xychart.jsonschema.json +428 -0
  441. package/src/spec/schemas.ts +22 -10
  442. package/src/spec/units.gen.ts +281 -0
  443. package/src/spec/units.ts +77 -0
  444. package/src/transformations.test.ts +65 -0
  445. package/src/transformations.ts +574 -0
  446. package/src/util.ts +8 -0
  447. package/src/validate-alerting.test.ts +255 -0
  448. package/src/validate-alerting.ts +428 -0
  449. package/src/validate-output.ts +332 -101
  450. package/src/validate.ts +14 -0
  451. package/src/variables.ts +7 -2
@@ -0,0 +1,212 @@
1
+ /**
2
+ * "Alerting builds to provisioning files Grafana loads as they are", against
3
+ * Grafana itself.
4
+ *
5
+ * Two builds are mounted into a pinned `grafana/grafana` container
6
+ * (`CHANT_GRAFANA_ALERTING_IMAGE`, default 13.2.2) and read back through
7
+ * the provisioning API (`/api/v1/provisioning/alert-rules`, `contact-points`,
8
+ * `policies`, `mute-timings`, `templates`):
9
+ *
10
+ * 1. the alerting example: an `Slo`'s burn-rate rules (`SloAlertRules`),
11
+ * hand-written rules with typed expressions, contact points, a policy
12
+ * tree, a mute timing and a template;
13
+ * 2. Grafana's own exports in `test/fixtures/alerting/grafana-13.2.2/`,
14
+ * imported with `chant import` and built back, provisioned beside the
15
+ * example's datasources (the exports query uids `prom` and `loki`).
16
+ *
17
+ * Every rule, contact point, timing and template must come back with the
18
+ * uid, refIds, datasources, labels and routes chant wrote.
19
+ *
20
+ * Skipped, with the reason in the test name, when Docker is not running.
21
+ * Run it through the Docker slot lock:
22
+ * `.docker-slot.sh <label> -- npx vitest run lexicons/grafana/src/alerting.e2e.test.ts`.
23
+ */
24
+ import { execSync } from "node:child_process";
25
+ import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
26
+ import { tmpdir } from "node:os";
27
+ import { dirname, join } from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+ import { afterAll, describe, expect, it } from "vitest";
30
+ import { load } from "js-yaml";
31
+ import { build } from "@intentius/chant/build";
32
+ import type { SerializerResult } from "@intentius/chant/serializer";
33
+ import { prometheusSerializer } from "@intentius/chant-lexicon-prometheus/serializer";
34
+ import { grafanaSerializer } from "./serializer";
35
+ import { ALERTING_FILE, type AlertingFile } from "./alerting-build";
36
+ import { DATASOURCES_FILE } from "./build";
37
+ import { GrafanaParser } from "./import/parser";
38
+ import { GrafanaGenerator } from "./import/generator";
39
+ import { fixturesDir, projectDir, read, removeDir, writeFiles } from "./import/testdata/fixtures";
40
+
41
+ const IMAGE = process.env.CHANT_GRAFANA_ALERTING_IMAGE ?? "grafana/grafana:13.2.2";
42
+
43
+ function available(cmd: string): boolean {
44
+ try {
45
+ execSync(cmd, { stdio: "ignore" });
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ const hasDocker = available("docker info");
53
+ const skipReason = hasDocker ? "" : " (skipped: Docker is not running)";
54
+ const AUTH = `Basic ${Buffer.from("admin:admin").toString("base64")}`;
55
+ const example = join(dirname(dirname(fileURLToPath(import.meta.url))), "examples", "alerting", "src");
56
+
57
+ type Json = Record<string, unknown>;
58
+
59
+ async function waitFor<T>(what: string, fn: () => Promise<T | undefined>, timeoutMs = 120_000): Promise<T> {
60
+ const deadline = Date.now() + timeoutMs;
61
+ let last: unknown;
62
+ while (Date.now() < deadline) {
63
+ try {
64
+ const v = await fn();
65
+ if (v !== undefined) return v;
66
+ } catch (err) {
67
+ last = err;
68
+ }
69
+ await new Promise((r) => setTimeout(r, 1000));
70
+ }
71
+ throw new Error(`timed out waiting for ${what}${last ? `: ${String(last)}` : ""}`);
72
+ }
73
+
74
+ const containers: string[] = [];
75
+ const dirs: string[] = [];
76
+
77
+ afterAll(() => {
78
+ for (const c of containers) {
79
+ try {
80
+ execSync(`docker rm -f ${c}`, { stdio: "ignore" });
81
+ } catch {
82
+ // already gone
83
+ }
84
+ }
85
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
86
+ });
87
+
88
+ /** Start Grafana with `files` mounted as its provisioning directory; resolves to an API client once every rule is loaded. */
89
+ async function grafanaWith(files: Record<string, string>, label: string, expectedRules: number) {
90
+ const dir = mkdtempSync(join(tmpdir(), "chant-grafana-alerting-"));
91
+ dirs.push(dir);
92
+ for (const [file, content] of Object.entries(files)) {
93
+ mkdirSync(dirname(join(dir, file)), { recursive: true });
94
+ writeFileSync(join(dir, file), content);
95
+ }
96
+ const name = `chant-grafana-alerting-${label}-${process.pid}`;
97
+ containers.push(name);
98
+ execSync(
99
+ [
100
+ "docker run -d",
101
+ `--name ${name}`,
102
+ "-p 127.0.0.1::3000",
103
+ "-e GF_SECURITY_ADMIN_PASSWORD=admin",
104
+ "-e GF_ANALYTICS_REPORTING_ENABLED=false",
105
+ "-e GF_ANALYTICS_CHECK_FOR_UPDATES=false",
106
+ // The secrets the example's contact points read with $__env{...}.
107
+ "-e SLACK_ONCALL_WEBHOOK=https://hooks.slack.com/services/T0/B0/X",
108
+ "-e TICKETS_TOKEN=token",
109
+ "-e ONCALL_SLACK_URL=https://hooks.slack.com/services/T0/B0/Y",
110
+ `-v ${join(dir, "provisioning")}:/etc/grafana/provisioning:ro`,
111
+ IMAGE,
112
+ ].join(" "),
113
+ { stdio: "ignore" },
114
+ );
115
+ const port = execSync(`docker port ${name} 3000/tcp`).toString().trim().split("\n")[0].split(":").pop();
116
+ const base = `http://127.0.0.1:${port}`;
117
+ const api = async <T = Json>(path: string): Promise<{ status: number; body: T }> => {
118
+ const res = await fetch(`${base}${path}`, { headers: { authorization: AUTH } });
119
+ return { status: res.status, body: (await res.json()) as T };
120
+ };
121
+ await waitFor("Grafana to answer", async () => ((await fetch(`${base}/api/health`)).ok ? true : undefined));
122
+ const rules = await waitFor(`${expectedRules} provisioned alert rules`, async () => {
123
+ const r = await api<Json[]>("/api/v1/provisioning/alert-rules");
124
+ return r.status === 200 && r.body.length >= expectedRules ? r.body : undefined;
125
+ }).catch((err) => {
126
+ throw new Error(`${String(err)}\n${execSync(`docker logs ${name} 2>&1 | grep -i -E "alert|provision" | grep -i -E "error|fail" | tail -20`).toString()}`);
127
+ });
128
+ return { api, rules, name };
129
+ }
130
+
131
+ /** What a rule must keep through provisioning. */
132
+ function ruleShape(r: Json) {
133
+ const data = (r.data as Json[]).map((q) => [q.refId, q.datasourceUid, (q.model as Json).type ?? (q.model as Json).expr ?? null]);
134
+ return { uid: r.uid, title: r.title, condition: r.condition || undefined, data, labels: r.labels ?? {}, record: r.record ?? undefined };
135
+ }
136
+
137
+ function builtRules(file: AlertingFile): Array<ReturnType<typeof ruleShape>> {
138
+ return (file.groups ?? []).flatMap((g) => g.rules.map((r) => ruleShape(r as unknown as Json)));
139
+ }
140
+
141
+ function byUid<T extends { uid?: unknown }>(list: T[]): T[] {
142
+ return [...list].sort((a, b) => String(a.uid).localeCompare(String(b.uid)));
143
+ }
144
+
145
+ describe.skipIf(!hasDocker)(`Grafana provisions chant's alerting files${skipReason}`, () => {
146
+ it(`the alerting example, into ${IMAGE}`, { timeout: 300_000 }, async () => {
147
+ const result = await build(example, [prometheusSerializer, grafanaSerializer]);
148
+ expect(result.errors).toEqual([]);
149
+ const files = (result.outputs.get("grafana") as SerializerResult).files ?? {};
150
+ const built = load(files[ALERTING_FILE]) as AlertingFile;
151
+ const expected = builtRules(built);
152
+ const { api, rules } = await grafanaWith(files, "example", expected.length);
153
+
154
+ expect(byUid(rules.map(ruleShape))).toEqual(byUid(expected));
155
+ for (const r of rules) expect(r.provenance).toBe("file");
156
+ // Folders are created from their titles.
157
+ const folders = (await api<Json[]>("/api/folders")).body.map((f) => f.title);
158
+ for (const g of built.groups!) expect(folders).toContain(g.folder);
159
+
160
+ const contactPoints = (await api<Json[]>("/api/v1/provisioning/contact-points")).body;
161
+ const builtReceivers = built.contactPoints!.flatMap((c) => c.receivers.map((r) => [c.name, r.uid, r.type]));
162
+ expect(contactPoints.map((c) => [c.name, c.uid, c.type]).sort()).toEqual(builtReceivers.sort());
163
+ // A $__env{...} secret was read from the environment: Grafana holds it, and redacts it on the way out.
164
+ expect((contactPoints.find((c) => c.type === "slack")!.settings as Json).url).toBe("[REDACTED]");
165
+
166
+ const policy = (await api<Json>("/api/v1/provisioning/policies")).body;
167
+ const builtPolicy = built.policies![0] as Json;
168
+ expect(policy.receiver).toBe(builtPolicy.receiver);
169
+ expect(policy.group_by).toEqual(builtPolicy.group_by);
170
+ expect((policy.routes as Json[]).map((r) => [r.receiver, r.object_matchers, r.mute_time_intervals ?? []])).toEqual(
171
+ (builtPolicy.routes as Json[]).map((r) => [r.receiver, r.object_matchers, r.mute_time_intervals ?? []]),
172
+ );
173
+
174
+ const timings = (await api<Json[]>("/api/v1/provisioning/mute-timings")).body;
175
+ expect(timings.map((t) => [t.name, t.time_intervals])).toEqual(built.muteTimes!.map((t) => [t.name, t.time_intervals]));
176
+ const templates = (await api<Json[]>("/api/v1/provisioning/templates")).body;
177
+ expect(templates.map((t) => [t.name, t.template])).toEqual(built.templates!.map((t) => [t.name, t.template]));
178
+ });
179
+
180
+ it(`Grafana's own exports, imported and built back, into ${IMAGE}`, { timeout: 300_000 }, async () => {
181
+ const dir = projectDir();
182
+ let files: Record<string, string>;
183
+ try {
184
+ // Each export imported into the same project, as `chant import` would one after another.
185
+ for (const f of readdirSync(join(fixturesDir, "alerting", "grafana-13.2.2")).sort()) {
186
+ const ir = new GrafanaParser().parse(read("alerting", "grafana-13.2.2", f));
187
+ const generated = new GrafanaGenerator().generate(ir).map((g) => ({ ...g, path: `${f.replace(/\.yaml$/, "")}/${g.path}` }));
188
+ writeFiles(join(dir, "src"), generated);
189
+ }
190
+ const result = await build(join(dir, "src"), [grafanaSerializer]);
191
+ expect(result.errors).toEqual([]);
192
+ files = { ...((result.outputs.get("grafana") as SerializerResult).files ?? {}) };
193
+ } finally {
194
+ removeDir(dir);
195
+ }
196
+ // The exports' datasources (uids prom and loki) are the example's.
197
+ const exampleBuild = await build(example, [prometheusSerializer, grafanaSerializer]);
198
+ files[DATASOURCES_FILE] = ((exampleBuild.outputs.get("grafana") as SerializerResult).files ?? {})[DATASOURCES_FILE];
199
+ const built = load(files[ALERTING_FILE]) as AlertingFile;
200
+ const expected = builtRules(built);
201
+ const { api, rules } = await grafanaWith(files, "exports", expected.length);
202
+
203
+ expect(byUid(rules.map(ruleShape))).toEqual(byUid(expected));
204
+ const source = load(read("alerting", "grafana-13.2.2", "alert-rules.yaml")) as AlertingFile;
205
+ expect(rules.map((r) => r.uid).sort()).toEqual(source.groups!.flatMap((g) => g.rules.map((r) => r.uid)).sort());
206
+ const contactPoints = (await api<Json[]>("/api/v1/provisioning/contact-points")).body;
207
+ expect(contactPoints.map((c) => c.uid).sort()).toEqual(["chant-fx-oncall-email", "chant-fx-oncall-slack", "chant-fx-tickets"]);
208
+ const policy = (await api<Json>("/api/v1/provisioning/policies")).body;
209
+ expect(policy.receiver).toBe("oncall");
210
+ expect((policy.routes as unknown[]).length).toBe(2);
211
+ });
212
+ });
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Alerting entities -> `provisioning/alerting/chant.yaml`: defaults, the
3
+ * query and expression models, references between entities, and what the
4
+ * builder refuses. Detection of alerting files against dashboards and
5
+ * Prometheus rule files, and the `SloAlertRules` composite.
6
+ */
7
+ import { describe, expect, test } from "vitest";
8
+ import { readdirSync, readFileSync } from "fs";
9
+ import { join } from "path";
10
+ import { load } from "js-yaml";
11
+ import { Slo, sloMetrics } from "@intentius/chant-lexicon-prometheus/composites/slo";
12
+ import { looksLikeRuleFile } from "@intentius/chant-lexicon-prometheus/model";
13
+ import { Datasource, ExternalDatasource } from "./datasource";
14
+ import { Dashboard } from "./dashboard";
15
+ import { LokiQuery, PromQuery } from "./query";
16
+ import { DatasourceVariable } from "./variables";
17
+ import {
18
+ AlertQuery,
19
+ AlertRule,
20
+ AlertRuleGroup,
21
+ ClassicConditionsExpression,
22
+ ContactPoint,
23
+ MathExpression,
24
+ MuteTiming,
25
+ NotificationPolicy,
26
+ NotificationTemplate,
27
+ ReduceExpression,
28
+ ResampleExpression,
29
+ SqlExpression,
30
+ ThresholdExpression,
31
+ } from "./alerting";
32
+ import { ALERTING_FILE, alertRuleJson, buildAlerting, contactPointJson, durationSeconds, type AlertingFile } from "./alerting-build";
33
+ import { buildGrafana } from "./build";
34
+ import { looksLikeAlertingProvisioning, looksLikeDashboard } from "./detect";
35
+ import { SloAlertRules, sloAlertQueries } from "./composites/slo-alert-rules";
36
+ import { ALERTING, fixturesDir, read } from "./import/testdata/fixtures";
37
+
38
+ type Json = Record<string, unknown>;
39
+
40
+ const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", uid: "prom" });
41
+ const loki = new Datasource({ name: "Loki", type: "loki" });
42
+
43
+ describe("alert rules", () => {
44
+ test("defaults: uid from the title, condition from the last query, 10 minutes of data, expressions with Grafana's datasource", () => {
45
+ const r = alertRuleJson(
46
+ new AlertRule({
47
+ title: "Checkout errors",
48
+ data: [
49
+ new PromQuery({ datasource: prometheus, expr: "sum(up)", instant: true }),
50
+ new ReduceExpression({ expression: "A", reducer: "last", settings: { mode: "dropNN" } }),
51
+ new ThresholdExpression({ expression: "B", conditions: [{ evaluator: { type: "gt", params: [1] }, unloadEvaluator: { type: "lt", params: [0.5] } }] }),
52
+ ],
53
+ }),
54
+ );
55
+ expect(r).toEqual({
56
+ uid: "checkout-errors",
57
+ title: "Checkout errors",
58
+ condition: "C",
59
+ data: [
60
+ { refId: "A", relativeTimeRange: { from: 600, to: 0 }, datasourceUid: "prom", model: { datasource: { type: "prometheus", uid: "prom" }, refId: "A", expr: "sum(up)", instant: true } },
61
+ {
62
+ refId: "B",
63
+ datasourceUid: "__expr__",
64
+ model: { datasource: { type: "__expr__", uid: "__expr__" }, refId: "B", type: "reduce", expression: "A", reducer: "last", settings: { mode: "dropNN" } },
65
+ },
66
+ {
67
+ refId: "C",
68
+ datasourceUid: "__expr__",
69
+ model: {
70
+ datasource: { type: "__expr__", uid: "__expr__" },
71
+ refId: "C",
72
+ type: "threshold",
73
+ expression: "B",
74
+ conditions: [{ evaluator: { type: "gt", params: [1] }, unloadEvaluator: { type: "lt", params: [0.5] } }],
75
+ },
76
+ },
77
+ ],
78
+ });
79
+ });
80
+
81
+ test("every expression type, time ranges as durations, AlertQuery with a raw model or a typed query, and the rule's own settings", () => {
82
+ const dash = new Dashboard({ title: "Checkout", uid: "checkout" });
83
+ const r = alertRuleJson(
84
+ new AlertRule({
85
+ title: "All of it",
86
+ uid: "all",
87
+ relativeTimeRange: { from: "1h", to: "5m" },
88
+ data: [
89
+ new AlertQuery({ datasource: "sql-uid", model: { rawSql: "SELECT 1", format: "table" }, refId: "Q" }),
90
+ new AlertQuery({ query: new LokiQuery({ datasource: loki, expr: "count_over_time({a=\"b\"}[5m])" }), queryType: "instant", relativeTimeRange: { from: 300 } }),
91
+ new ResampleExpression({ expression: "Q", window: "1m", downsampler: "mean", upsampler: "fillna" }),
92
+ new MathExpression({ expression: "$C * 2", refId: "M" }),
93
+ new ClassicConditionsExpression({
94
+ conditions: [{ evaluator: { type: "gt", params: [0] }, operator: { type: "and" }, query: { params: ["M"] }, reducer: { type: "last" } }],
95
+ }),
96
+ new SqlExpression({ expression: "SELECT * FROM Q", format: "table" }),
97
+ ],
98
+ condition: "E",
99
+ for: "5m",
100
+ keepFiringFor: "1m",
101
+ noDataState: "OK",
102
+ execErrState: "KeepLast",
103
+ missing_series_evals_to_resolve: 2,
104
+ isPaused: true,
105
+ dashboardUid: dash,
106
+ panelId: 3,
107
+ labels: { team: "a" },
108
+ }),
109
+ );
110
+ expect(r.data.map((q) => [q.refId, q.datasourceUid, q.relativeTimeRange, q.queryType, (q.model as Json).type])).toEqual([
111
+ ["Q", "sql-uid", { from: 3600, to: 300 }, undefined, undefined],
112
+ ["B", "loki", { from: 300, to: 0 }, "instant", undefined],
113
+ ["C", "__expr__", undefined, undefined, "resample"],
114
+ ["M", "__expr__", undefined, undefined, "math"],
115
+ ["E", "__expr__", undefined, undefined, "classic_conditions"],
116
+ ["F", "__expr__", undefined, undefined, "sql"],
117
+ ]);
118
+ expect(r.data[0].model).toEqual({ rawSql: "SELECT 1", format: "table" });
119
+ expect(r.data[1].model).toMatchObject({ datasource: { type: "loki", uid: "loki" }, refId: "B", expr: 'count_over_time({a="b"}[5m])' });
120
+ expect(r).toMatchObject({ condition: "E", for: "5m", keepFiringFor: "1m", noDataState: "OK", execErrState: "KeepLast", missing_series_evals_to_resolve: 2, isPaused: true, dashboardUid: "checkout", panelId: 3 });
121
+ });
122
+
123
+ test("a recording rule has no condition", () => {
124
+ const r = alertRuleJson(new AlertRule({ title: "rps", data: [new PromQuery({ datasource: prometheus, expr: "sum(rate(x[5m]))" })], record: { metric: "x:rate5m", from: "A", targetDatasourceUid: "prom" } }));
125
+ expect(r.condition).toBeUndefined();
126
+ expect(r.record).toEqual({ metric: "x:rate5m", from: "A", targetDatasourceUid: "prom" });
127
+ });
128
+
129
+ test("refuses a query with no datasource, a datasource variable, and a duration it cannot read", () => {
130
+ expect(() => alertRuleJson(new AlertRule({ title: "t", data: [new PromQuery({ expr: "up" })] }))).toThrow(/data\[0\] has no datasource/);
131
+ const variable = new DatasourceVariable({ name: "ds", pluginType: "prometheus" });
132
+ expect(() => alertRuleJson(new AlertRule({ title: "t", data: [new PromQuery({ datasource: variable, expr: "up" })] }))).toThrow(/without dashboard variables/);
133
+ expect(() => alertRuleJson(new AlertRule({ title: "t", relativeTimeRange: { from: "ten minutes" }, data: [new PromQuery({ datasource: prometheus, expr: "up" })] }))).toThrow(/not a duration/);
134
+ expect(durationSeconds("1h30m", "x")).toBe(5400);
135
+ expect(() => durationSeconds(1.5, "x")).toThrow(/whole number/);
136
+ });
137
+ });
138
+
139
+ describe("the alerting file", () => {
140
+ const oncall = new ContactPoint({
141
+ name: "oncall",
142
+ receivers: [
143
+ { type: "slack", settings: { url: "$__env{SLACK_URL}" } },
144
+ { type: "slack", settings: { url: "$__env{SLACK_URL_2}" } },
145
+ { uid: "mail", type: "email", settings: { addresses: "a@example.com" }, disableResolveMessage: true },
146
+ ],
147
+ });
148
+ const weekends = new MuteTiming({ name: "weekends", time_intervals: [{ weekdays: ["saturday", "sunday"] }] });
149
+ const template = new NotificationTemplate({ name: "t", template: '{{ define "t" }}x{{ end }}' });
150
+ const policy = new NotificationPolicy({
151
+ receiver: oncall,
152
+ group_by: ["alertname"],
153
+ routes: [{ receiver: "tickets", object_matchers: [["severity", "=", "ticket"]], mute_time_intervals: [weekends], routes: [{ matchers: ['team="a"'], active_time_intervals: ["nights"] }] }],
154
+ });
155
+ const rule = new AlertRule({ title: "r", data: [new PromQuery({ datasource: prometheus, expr: "up" })], notification_settings: { receiver: oncall, mute_time_intervals: [weekends] } });
156
+ const b = new AlertRuleGroup({ name: "b", folder: "Z", rules: [rule] });
157
+ const a = new AlertRuleGroup({ name: "a", folder: "Z", interval: "30s", orgId: 2, rules: [] });
158
+
159
+ test("contact point receivers get uids from the name and type, numbered when two share them", () => {
160
+ expect(contactPointJson(oncall).receivers.map((r) => r.uid)).toEqual(["oncall-slack", "oncall-slack-2", "mail"]);
161
+ });
162
+
163
+ test("entities referenced by variable are written by name, and lists are sorted", () => {
164
+ const built = buildAlerting([policy, b, template, a, weekends, oncall, prometheus])!;
165
+ const file = built.file;
166
+ expect(Object.keys(file)).toEqual(["apiVersion", "groups", "contactPoints", "policies", "muteTimes", "templates"]);
167
+ expect(file.groups!.map((g) => [g.name, g.interval, g.orgId])).toEqual([["a", "30s", 2], ["b", "1m", undefined]]);
168
+ expect(file.groups![1].rules[0].notification_settings).toEqual({ receiver: "oncall", mute_time_intervals: ["weekends"] });
169
+ expect(file.policies).toEqual([
170
+ {
171
+ receiver: "oncall",
172
+ group_by: ["alertname"],
173
+ routes: [{ receiver: "tickets", object_matchers: [["severity", "=", "ticket"]], mute_time_intervals: ["weekends"], routes: [{ matchers: ['team="a"'], active_time_intervals: ["nights"] }] }],
174
+ },
175
+ ]);
176
+ expect(built.index).toEqual({ ruleGroups: [{ name: "a", folder: "Z", rules: 0 }, { name: "b", folder: "Z", rules: 1 }], contactPoints: ["oncall"], policies: 1, muteTimings: ["weekends"], templates: ["t"] });
177
+ });
178
+
179
+ test("buildGrafana writes the file beside the datasources, and nothing when there is no alerting", () => {
180
+ const built = buildGrafana(new Map<string, never>([["prometheus", prometheus as never], ["b", b as never], ["oncall", oncall as never], ["weekends", weekends as never]]));
181
+ expect(built.index.files).toEqual([ALERTING_FILE, "provisioning/datasources/chant.yaml"]);
182
+ expect((load(built.files[ALERTING_FILE]) as AlertingFile).apiVersion).toBe(1);
183
+ expect(built.files[ALERTING_FILE].startsWith("# Grafana alerting provisioning, generated by chant.\n")).toBe(true);
184
+ expect(buildGrafana([prometheus]).files[ALERTING_FILE]).toBeUndefined();
185
+ expect(buildGrafana([prometheus]).index.alerting).toBeUndefined();
186
+ });
187
+ });
188
+
189
+ describe("detection", () => {
190
+ const promRules = join(fixturesDir, "..", "..", "..", "prometheus", "src", "import", "testdata");
191
+ const ruleFiles = [
192
+ ...["rules-full.yml"].map((f) => join(promRules, f)),
193
+ ...readdirSync(join(promRules, "upstream"))
194
+ .filter((f) => f.startsWith("prometheus-"))
195
+ .map((f) => join(promRules, "upstream", f)),
196
+ ];
197
+
198
+ test("every alerting fixture is an alerting file, and none is a Prometheus rule file or a dashboard", () => {
199
+ for (const f of ALERTING) {
200
+ const doc = load(read(f));
201
+ expect(looksLikeAlertingProvisioning(doc), f).toBe(true);
202
+ expect(looksLikeRuleFile(doc), f).toBe(false);
203
+ expect(looksLikeDashboard(doc), f).toBe(false);
204
+ }
205
+ });
206
+
207
+ test("a Prometheus rule file is not an alerting file", () => {
208
+ expect(ruleFiles.length).toBeGreaterThan(1);
209
+ for (const f of ruleFiles) {
210
+ const doc = load(readFileSync(f, "utf-8"));
211
+ expect(looksLikeRuleFile(doc), f).toBe(true);
212
+ expect(looksLikeAlertingProvisioning(doc), f).toBe(false);
213
+ }
214
+ // A PrometheusRule's spec is a rule file too.
215
+ expect(looksLikeAlertingProvisioning({ groups: [{ name: "g", rules: [{ alert: "A", expr: "up == 0" }] }] })).toBe(false);
216
+ });
217
+
218
+ test("other provisioning files and unrelated YAML are not alerting files", () => {
219
+ expect(looksLikeAlertingProvisioning({ apiVersion: 1, datasources: [] })).toBe(false);
220
+ expect(looksLikeAlertingProvisioning({ apiVersion: 1, providers: [] })).toBe(false);
221
+ expect(looksLikeAlertingProvisioning({ apiVersion: 1, policies: [], datasources: [] })).toBe(false);
222
+ expect(looksLikeAlertingProvisioning({ route: { receiver: "x" }, receivers: [] })).toBe(false);
223
+ expect(looksLikeAlertingProvisioning({ policies: [{ receiver: "x" }] })).toBe(false);
224
+ expect(looksLikeAlertingProvisioning({ apiVersion: 1, policies: [{ receiver: "x" }] })).toBe(true);
225
+ });
226
+ });
227
+
228
+ describe("SloAlertRules", () => {
229
+ const slo = Slo({
230
+ name: "checkout-with-a-rather-long-service-name",
231
+ objective: 0.999,
232
+ window: "30d",
233
+ sli: { errors: 'sum(rate(http_requests_total{code=~"5.."}[{{window}}]))', total: "sum(rate(http_requests_total[{{window}}]))" },
234
+ });
235
+
236
+ test("one rule per burn-rate pair, reading the Slo's recorded ratios, with its thresholds and labels", () => {
237
+ const m = sloMetrics(slo);
238
+ const oncall = new ContactPoint({ name: "oncall", receivers: [{ type: "email", settings: { addresses: "a@example.com" } }] });
239
+ const { rules } = SloAlertRules({ slo, datasource: prometheus, folder: "SLOs", contactPoint: oncall, for: "2m", labels: { team: "a" } });
240
+ expect(rules.props.name).toBe(m.group);
241
+ expect(rules.props.interval).toBe("1m");
242
+ const built = rules.props.rules.map(alertRuleJson);
243
+ expect(built).toHaveLength(m.burnRates.length);
244
+ const q = sloAlertQueries(m);
245
+ built.forEach((r, i) => {
246
+ const b = m.burnRates[i];
247
+ expect(r.uid!.length).toBeLessThanOrEqual(40);
248
+ expect(r.uid).toBe(q[i].uid);
249
+ expect(r.uid!.endsWith(`-${b.long}-${b.short}`)).toBe(true);
250
+ expect(r.data.map((d) => (d.model as Json).expr ?? (d.model as Json).expression)).toEqual([
251
+ `${b.longRecord}{slo="${m.name}"}`,
252
+ `${b.shortRecord}{slo="${m.name}"}`,
253
+ `$A > ${b.threshold} && $B > ${b.threshold}`,
254
+ ]);
255
+ expect(r.data.every((d) => d.datasourceUid === "prom" || d.datasourceUid === "__expr__")).toBe(true);
256
+ expect(r.condition).toBe("C");
257
+ expect(r.for).toBe("2m");
258
+ expect(r.labels).toEqual({ team: "a", ...b.labels });
259
+ expect(r.notification_settings).toEqual({ receiver: "oncall" });
260
+ expect(r.annotations!.description).toContain(b.exhaustsIn);
261
+ });
262
+ expect(new Set(built.map((r) => r.uid)).size).toBe(built.length);
263
+ });
264
+
265
+ test("reads sloMetrics() output too, and refuses an Slo without alerting, a missing folder or a non-Prometheus datasource", () => {
266
+ expect(SloAlertRules({ slo: sloMetrics(slo), datasource: { type: "prometheus", uid: "p" }, folder: "F" }).rules.props.rules).toHaveLength(4);
267
+ const quiet = Slo({ name: "quiet", objective: 0.99, window: "28d", sli: { errors: "sum(rate(e[{{window}}]))", total: "sum(rate(t[{{window}}]))" }, alerting: { page: false, ticket: false } });
268
+ expect(() => SloAlertRules({ slo: quiet, datasource: prometheus, folder: "F" })).toThrow(/alerting turned off/);
269
+ expect(() => SloAlertRules({ slo, datasource: prometheus, folder: "" })).toThrow(/folder is required/);
270
+ expect(() => SloAlertRules({ slo, datasource: new ExternalDatasource({ type: "loki", uid: "l" }) as never, folder: "F" })).toThrow(/must be a Prometheus datasource/);
271
+ });
272
+ });