@intentius/chant-lexicon-grafana 0.98.0 → 0.100.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 (532) hide show
  1. package/README.md +24 -8
  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/annotations.d.ts +61 -0
  11. package/dist/annotations.d.ts.map +1 -0
  12. package/dist/api/apply.d.ts +160 -0
  13. package/dist/api/apply.d.ts.map +1 -0
  14. package/dist/api/bind.d.ts +32 -0
  15. package/dist/api/bind.d.ts.map +1 -0
  16. package/dist/api/client.d.ts +76 -0
  17. package/dist/api/client.d.ts.map +1 -0
  18. package/dist/api/converge.d.ts +33 -0
  19. package/dist/api/converge.d.ts.map +1 -0
  20. package/dist/api/dashboards.d.ts +102 -0
  21. package/dist/api/dashboards.d.ts.map +1 -0
  22. package/dist/api/datasources.d.ts +26 -0
  23. package/dist/api/datasources.d.ts.map +1 -0
  24. package/dist/api/fake-grafana-writes.d.ts +41 -0
  25. package/dist/api/fake-grafana-writes.d.ts.map +1 -0
  26. package/dist/api/fake-grafana.d.ts +63 -0
  27. package/dist/api/fake-grafana.d.ts.map +1 -0
  28. package/dist/api/folders.d.ts +146 -0
  29. package/dist/api/folders.d.ts.map +1 -0
  30. package/dist/api/library-panels.d.ts +52 -0
  31. package/dist/api/library-panels.d.ts.map +1 -0
  32. package/dist/build.d.ts +55 -6
  33. package/dist/build.d.ts.map +1 -1
  34. package/dist/catalog.d.ts +1 -1
  35. package/dist/catalog.d.ts.map +1 -1
  36. package/dist/codegen/docs.d.ts.map +1 -1
  37. package/dist/codegen/generate.d.ts +7 -1
  38. package/dist/codegen/generate.d.ts.map +1 -1
  39. package/dist/codegen/schema-types.d.ts +3 -3
  40. package/dist/codegen/schema-types.d.ts.map +1 -1
  41. package/dist/composites/agent-dashboard.d.ts.map +1 -1
  42. package/dist/composites/catalog.d.ts.map +1 -1
  43. package/dist/composites/index.d.ts +4 -2
  44. package/dist/composites/index.d.ts.map +1 -1
  45. package/dist/composites/red-dashboard.d.ts +21 -2
  46. package/dist/composites/red-dashboard.d.ts.map +1 -1
  47. package/dist/composites/shared.d.ts +11 -3
  48. package/dist/composites/shared.d.ts.map +1 -1
  49. package/dist/composites/slo-alert-rules.d.ts +92 -0
  50. package/dist/composites/slo-alert-rules.d.ts.map +1 -0
  51. package/dist/config.d.ts +111 -0
  52. package/dist/config.d.ts.map +1 -0
  53. package/dist/contact-point-secrets.d.ts +17 -0
  54. package/dist/contact-point-secrets.d.ts.map +1 -0
  55. package/dist/dashboard.d.ts +51 -9
  56. package/dist/dashboard.d.ts.map +1 -1
  57. package/dist/datasource-refs.d.ts +99 -0
  58. package/dist/datasource-refs.d.ts.map +1 -0
  59. package/dist/datasource-settings.d.ts +834 -0
  60. package/dist/datasource-settings.d.ts.map +1 -0
  61. package/dist/datasource.d.ts +82 -11
  62. package/dist/datasource.d.ts.map +1 -1
  63. package/dist/deep-observe-hooks.d.ts +64 -0
  64. package/dist/deep-observe-hooks.d.ts.map +1 -0
  65. package/dist/deep-observe.d.ts +47 -0
  66. package/dist/deep-observe.d.ts.map +1 -0
  67. package/dist/describe-resources.d.ts +50 -0
  68. package/dist/describe-resources.d.ts.map +1 -0
  69. package/dist/detect.d.ts +32 -3
  70. package/dist/detect.d.ts.map +1 -1
  71. package/dist/export-resources.d.ts +38 -0
  72. package/dist/export-resources.d.ts.map +1 -0
  73. package/dist/folder.d.ts +68 -0
  74. package/dist/folder.d.ts.map +1 -0
  75. package/dist/import/alerting.d.ts +40 -0
  76. package/dist/import/alerting.d.ts.map +1 -0
  77. package/dist/import/edits.d.ts +47 -0
  78. package/dist/import/edits.d.ts.map +1 -0
  79. package/dist/import/embedded.d.ts +14 -0
  80. package/dist/import/embedded.d.ts.map +1 -0
  81. package/dist/import/generator.d.ts +60 -0
  82. package/dist/import/generator.d.ts.map +1 -0
  83. package/dist/import/live-export.d.ts +108 -0
  84. package/dist/import/live-export.d.ts.map +1 -0
  85. package/dist/import/mappings.d.ts +89 -0
  86. package/dist/import/mappings.d.ts.map +1 -0
  87. package/dist/import/model.d.ts +105 -0
  88. package/dist/import/model.d.ts.map +1 -0
  89. package/dist/import/normalize-alerting.d.ts +26 -0
  90. package/dist/import/normalize-alerting.d.ts.map +1 -0
  91. package/dist/import/normalize.d.ts +74 -0
  92. package/dist/import/normalize.d.ts.map +1 -0
  93. package/dist/import/parser.d.ts +125 -0
  94. package/dist/import/parser.d.ts.map +1 -0
  95. package/dist/import/report.d.ts +26 -0
  96. package/dist/import/report.d.ts.map +1 -0
  97. package/dist/import/testdata/fixtures.d.ts +41 -0
  98. package/dist/import/testdata/fixtures.d.ts.map +1 -0
  99. package/dist/import/v2.d.ts +73 -0
  100. package/dist/import/v2.d.ts.map +1 -0
  101. package/dist/index.d.ts +31 -12
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/init-templates.d.ts +27 -0
  104. package/dist/init-templates.d.ts.map +1 -0
  105. package/dist/integrity.json +22 -12
  106. package/dist/k8s.d.ts +107 -0
  107. package/dist/k8s.d.ts.map +1 -0
  108. package/dist/lint/audit-catalog.d.ts +2 -1
  109. package/dist/lint/audit-catalog.d.ts.map +1 -1
  110. package/dist/lint/post-synth/graf101.d.ts +2 -2
  111. package/dist/lint/post-synth/graf102.d.ts +1 -1
  112. package/dist/lint/post-synth/graf104.d.ts +2 -2
  113. package/dist/lint/post-synth/graf106.d.ts +2 -2
  114. package/dist/lint/post-synth/graf107.d.ts +1 -1
  115. package/dist/lint/post-synth/graf108.d.ts +8 -0
  116. package/dist/lint/post-synth/graf108.d.ts.map +1 -0
  117. package/dist/lint/post-synth/graf109.d.ts +8 -0
  118. package/dist/lint/post-synth/graf109.d.ts.map +1 -0
  119. package/dist/lint/post-synth/graf110.d.ts +8 -0
  120. package/dist/lint/post-synth/graf110.d.ts.map +1 -0
  121. package/dist/lint/post-synth/graf111.d.ts +8 -0
  122. package/dist/lint/post-synth/graf111.d.ts.map +1 -0
  123. package/dist/lint/post-synth/graf112.d.ts +8 -0
  124. package/dist/lint/post-synth/graf112.d.ts.map +1 -0
  125. package/dist/lint/post-synth/graf113.d.ts +8 -0
  126. package/dist/lint/post-synth/graf113.d.ts.map +1 -0
  127. package/dist/lint/post-synth/graf114.d.ts +8 -0
  128. package/dist/lint/post-synth/graf114.d.ts.map +1 -0
  129. package/dist/lint/post-synth/graf115.d.ts +8 -0
  130. package/dist/lint/post-synth/graf115.d.ts.map +1 -0
  131. package/dist/lint/post-synth/grafana-helpers.d.ts +7 -5
  132. package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -1
  133. package/dist/lint/post-synth/index.d.ts.map +1 -1
  134. package/dist/lint/rules/literal-secret.d.ts +7 -3
  135. package/dist/lint/rules/literal-secret.d.ts.map +1 -1
  136. package/dist/lsp/hover.d.ts.map +1 -1
  137. package/dist/manifest.json +1 -1
  138. package/dist/members.d.ts +27 -0
  139. package/dist/members.d.ts.map +1 -0
  140. package/dist/meta.json +205 -0
  141. package/dist/okf/index.md +53 -4
  142. package/dist/okf/rules/GRAF002.md +3 -2
  143. package/dist/okf/rules/GRAF101.md +3 -2
  144. package/dist/okf/rules/GRAF102.md +1 -0
  145. package/dist/okf/rules/GRAF104.md +2 -2
  146. package/dist/okf/rules/GRAF106.md +2 -2
  147. package/dist/okf/rules/GRAF108.md +11 -0
  148. package/dist/okf/rules/GRAF109.md +15 -0
  149. package/dist/okf/rules/GRAF110.md +11 -0
  150. package/dist/okf/rules/GRAF111.md +11 -0
  151. package/dist/okf/rules/GRAF112.md +16 -0
  152. package/dist/okf/rules/GRAF113.md +16 -0
  153. package/dist/okf/rules/GRAF114.md +11 -0
  154. package/dist/okf/rules/GRAF115.md +11 -0
  155. package/dist/okf/types/AdhocVariable.md +9 -0
  156. package/dist/okf/types/AlertListPanel.md +9 -0
  157. package/dist/okf/types/AlertQuery.md +9 -0
  158. package/dist/okf/types/AlertRule.md +9 -0
  159. package/dist/okf/types/AlertRuleGroup.md +9 -0
  160. package/dist/okf/types/AzureMonitorQuery.md +9 -0
  161. package/dist/okf/types/BarChartPanel.md +9 -0
  162. package/dist/okf/types/BarGaugePanel.md +9 -0
  163. package/dist/okf/types/BigQueryQuery.md +9 -0
  164. package/dist/okf/types/CanvasPanel.md +9 -0
  165. package/dist/okf/types/ClassicConditionsExpression.md +9 -0
  166. package/dist/okf/types/CloudMonitoringQuery.md +9 -0
  167. package/dist/okf/types/CloudWatchQuery.md +9 -0
  168. package/dist/okf/types/ContactPoint.md +14 -0
  169. package/dist/okf/types/Dashboard.md +2 -1
  170. package/dist/okf/types/Datasource.md +3 -2
  171. package/dist/okf/types/DatasourceProvisioning.md +9 -0
  172. package/dist/okf/types/ElasticsearchQuery.md +9 -0
  173. package/dist/okf/types/ExternalDatasource.md +15 -0
  174. package/dist/okf/types/FlameGraphPanel.md +9 -0
  175. package/dist/okf/types/Folder.md +9 -0
  176. package/dist/okf/types/GeomapPanel.md +9 -0
  177. package/dist/okf/types/GroupByVariable.md +9 -0
  178. package/dist/okf/types/HistogramPanel.md +9 -0
  179. package/dist/okf/types/MSSQLQuery.md +9 -0
  180. package/dist/okf/types/MathExpression.md +9 -0
  181. package/dist/okf/types/MuteTiming.md +13 -0
  182. package/dist/okf/types/MySQLQuery.md +9 -0
  183. package/dist/okf/types/NodeGraphPanel.md +9 -0
  184. package/dist/okf/types/NotificationPolicy.md +9 -0
  185. package/dist/okf/types/NotificationTemplate.md +9 -0
  186. package/dist/okf/types/PieChartPanel.md +9 -0
  187. package/dist/okf/types/PostgresQuery.md +9 -0
  188. package/dist/okf/types/PyroscopeQuery.md +9 -0
  189. package/dist/okf/types/ReduceExpression.md +9 -0
  190. package/dist/okf/types/ResampleExpression.md +9 -0
  191. package/dist/okf/types/SqlExpression.md +9 -0
  192. package/dist/okf/types/StateTimelinePanel.md +9 -0
  193. package/dist/okf/types/StatusHistoryPanel.md +9 -0
  194. package/dist/okf/types/SwitchVariable.md +9 -0
  195. package/dist/okf/types/ThresholdExpression.md +9 -0
  196. package/dist/okf/types/TrendPanel.md +9 -0
  197. package/dist/okf/types/XYChartPanel.md +9 -0
  198. package/dist/op/activities/grafana-apply.d.ts +86 -0
  199. package/dist/op/activities/grafana-apply.d.ts.map +1 -0
  200. package/dist/op/activities/index.d.ts +9 -0
  201. package/dist/op/activities/index.d.ts.map +1 -0
  202. package/dist/op/builders.d.ts +24 -0
  203. package/dist/op/builders.d.ts.map +1 -0
  204. package/dist/ownership.d.ts +78 -0
  205. package/dist/ownership.d.ts.map +1 -0
  206. package/dist/panel-options.d.ts +65 -0
  207. package/dist/panel-options.d.ts.map +1 -0
  208. package/dist/panels.d.ts +46 -9
  209. package/dist/panels.d.ts.map +1 -1
  210. package/dist/pin.d.ts +29 -7
  211. package/dist/pin.d.ts.map +1 -1
  212. package/dist/plugin.d.ts.map +1 -1
  213. package/dist/promql-check.d.ts +51 -0
  214. package/dist/promql-check.d.ts.map +1 -0
  215. package/dist/query-models.d.ts +95 -0
  216. package/dist/query-models.d.ts.map +1 -0
  217. package/dist/query.d.ts +116 -18
  218. package/dist/query.d.ts.map +1 -1
  219. package/dist/rules/graf101.ts +3 -3
  220. package/dist/rules/graf102.ts +1 -1
  221. package/dist/rules/graf104.ts +3 -3
  222. package/dist/rules/graf106.ts +3 -3
  223. package/dist/rules/graf107.ts +1 -1
  224. package/dist/rules/graf108.ts +17 -0
  225. package/dist/rules/graf109.ts +17 -0
  226. package/dist/rules/graf110.ts +17 -0
  227. package/dist/rules/graf111.ts +17 -0
  228. package/dist/rules/graf112.ts +17 -0
  229. package/dist/rules/graf113.ts +17 -0
  230. package/dist/rules/graf114.ts +17 -0
  231. package/dist/rules/graf115.ts +17 -0
  232. package/dist/rules/grafana-helpers.ts +27 -10
  233. package/dist/rules/literal-secret.ts +60 -6
  234. package/dist/schema/azuremonitor.gen.d.ts +354 -0
  235. package/dist/schema/azuremonitor.gen.d.ts.map +1 -0
  236. package/dist/schema/barchart.gen.d.ts +198 -0
  237. package/dist/schema/barchart.gen.d.ts.map +1 -0
  238. package/dist/schema/bargauge.gen.d.ts +97 -0
  239. package/dist/schema/bargauge.gen.d.ts.map +1 -0
  240. package/dist/schema/bigquery.gen.d.ts +93 -0
  241. package/dist/schema/bigquery.gen.d.ts.map +1 -0
  242. package/dist/schema/canvas.gen.d.ts +152 -0
  243. package/dist/schema/canvas.gen.d.ts.map +1 -0
  244. package/dist/schema/cloudwatch.gen.d.ts +394 -0
  245. package/dist/schema/cloudwatch.gen.d.ts.map +1 -0
  246. package/dist/schema/dashboard.gen.d.ts +196 -21
  247. package/dist/schema/dashboard.gen.d.ts.map +1 -1
  248. package/dist/schema/elasticsearch.gen.d.ts +444 -0
  249. package/dist/schema/elasticsearch.gen.d.ts.map +1 -0
  250. package/dist/schema/expr.gen.d.ts +606 -0
  251. package/dist/schema/expr.gen.d.ts.map +1 -0
  252. package/dist/schema/gauge.gen.d.ts.map +1 -1
  253. package/dist/schema/geomap.gen.d.ts +100 -0
  254. package/dist/schema/geomap.gen.d.ts.map +1 -0
  255. package/dist/schema/googlecloudmonitoring.gen.d.ts +312 -0
  256. package/dist/schema/googlecloudmonitoring.gen.d.ts.map +1 -0
  257. package/dist/schema/grafanapyroscope.gen.d.ts +60 -0
  258. package/dist/schema/grafanapyroscope.gen.d.ts.map +1 -0
  259. package/dist/schema/heatmap.gen.d.ts.map +1 -1
  260. package/dist/schema/histogram.gen.d.ts +132 -0
  261. package/dist/schema/histogram.gen.d.ts.map +1 -0
  262. package/dist/schema/index.d.ts +18 -0
  263. package/dist/schema/index.d.ts.map +1 -1
  264. package/dist/schema/logs.gen.d.ts +1 -0
  265. package/dist/schema/logs.gen.d.ts.map +1 -1
  266. package/dist/schema/loki.gen.d.ts.map +1 -1
  267. package/dist/schema/nodegraph.gen.d.ts +49 -0
  268. package/dist/schema/nodegraph.gen.d.ts.map +1 -0
  269. package/dist/schema/piechart.gen.d.ts +127 -0
  270. package/dist/schema/piechart.gen.d.ts.map +1 -0
  271. package/dist/schema/prometheus.gen.d.ts.map +1 -1
  272. package/dist/schema/stat.gen.d.ts.map +1 -1
  273. package/dist/schema/statetimeline.gen.d.ts +124 -0
  274. package/dist/schema/statetimeline.gen.d.ts.map +1 -0
  275. package/dist/schema/statushistory.gen.d.ts +114 -0
  276. package/dist/schema/statushistory.gen.d.ts.map +1 -0
  277. package/dist/schema/table.gen.d.ts +35 -3
  278. package/dist/schema/table.gen.d.ts.map +1 -1
  279. package/dist/schema/tempo.gen.d.ts.map +1 -1
  280. package/dist/schema/text.gen.d.ts.map +1 -1
  281. package/dist/schema/timeseries.gen.d.ts +5 -1
  282. package/dist/schema/timeseries.gen.d.ts.map +1 -1
  283. package/dist/schema/trend.gen.d.ts +180 -0
  284. package/dist/schema/trend.gen.d.ts.map +1 -0
  285. package/dist/schema/xychart.gen.d.ts +145 -0
  286. package/dist/schema/xychart.gen.d.ts.map +1 -0
  287. package/dist/schema-validate.d.ts +39 -4
  288. package/dist/schema-validate.d.ts.map +1 -1
  289. package/dist/serializer.d.ts +11 -3
  290. package/dist/serializer.d.ts.map +1 -1
  291. package/dist/skill-defs.d.ts.map +1 -1
  292. package/dist/skills/chant-grafana-alerting.md +49 -0
  293. package/dist/skills/chant-grafana-operations.md +108 -0
  294. package/dist/skills/chant-grafana-provisioning.md +3 -3
  295. package/dist/skills/chant-grafana.md +15 -8
  296. package/dist/spec/fetch-units-cli.d.ts +3 -0
  297. package/dist/spec/fetch-units-cli.d.ts.map +1 -0
  298. package/dist/spec/overlay.d.ts +43 -0
  299. package/dist/spec/overlay.d.ts.map +1 -0
  300. package/dist/spec/schemas.d.ts +21 -13
  301. package/dist/spec/schemas.d.ts.map +1 -1
  302. package/dist/spec/schemas.gen.d.ts +3 -0
  303. package/dist/spec/schemas.gen.d.ts.map +1 -0
  304. package/dist/spec/units.d.ts +35 -0
  305. package/dist/spec/units.d.ts.map +1 -0
  306. package/dist/spec/units.gen.d.ts +2 -0
  307. package/dist/spec/units.gen.d.ts.map +1 -0
  308. package/dist/transformations.d.ts +459 -0
  309. package/dist/transformations.d.ts.map +1 -0
  310. package/dist/util.d.ts +6 -0
  311. package/dist/util.d.ts.map +1 -1
  312. package/dist/validate-alerting.d.ts +65 -0
  313. package/dist/validate-alerting.d.ts.map +1 -0
  314. package/dist/validate-output.d.ts +62 -9
  315. package/dist/validate-output.d.ts.map +1 -1
  316. package/dist/validate.d.ts.map +1 -1
  317. package/dist/validation.d.ts +14 -0
  318. package/dist/validation.d.ts.map +1 -0
  319. package/dist/variables.d.ts +109 -7
  320. package/dist/variables.d.ts.map +1 -1
  321. package/package.json +12 -4
  322. package/src/actions/actions.test.ts +24 -0
  323. package/src/actions/grafana.ts +40 -0
  324. package/src/actions/index.ts +1 -0
  325. package/src/alerting-build.ts +376 -0
  326. package/src/alerting.e2e.test.ts +212 -0
  327. package/src/alerting.test.ts +272 -0
  328. package/src/alerting.ts +435 -0
  329. package/src/annotations.ts +84 -0
  330. package/src/api/apply.test.ts +256 -0
  331. package/src/api/apply.ts +394 -0
  332. package/src/api/bind.ts +58 -0
  333. package/src/api/client.ts +148 -0
  334. package/src/api/converge.ts +63 -0
  335. package/src/api/dashboards.ts +247 -0
  336. package/src/api/datasources.ts +33 -0
  337. package/src/api/fake-grafana-writes.ts +201 -0
  338. package/src/api/fake-grafana.ts +190 -0
  339. package/src/api/folders.ts +339 -0
  340. package/src/api/library-panels.ts +101 -0
  341. package/src/build.test.ts +351 -10
  342. package/src/build.ts +294 -55
  343. package/src/catalog.ts +104 -2
  344. package/src/codegen/docs.ts +50 -10
  345. package/src/codegen/generate.ts +27 -1
  346. package/src/codegen/schema-types.ts +16 -6
  347. package/src/composites/agent-dashboard.ts +6 -3
  348. package/src/composites/catalog.test.ts +2 -2
  349. package/src/composites/catalog.ts +82 -0
  350. package/src/composites/composites.test.ts +73 -9
  351. package/src/composites/index.ts +4 -2
  352. package/src/composites/queries.e2e.test.ts +374 -0
  353. package/src/composites/red-dashboard.ts +83 -12
  354. package/src/composites/shared.ts +14 -2
  355. package/src/composites/slo-alert-rules.ts +149 -0
  356. package/src/config.test.ts +61 -0
  357. package/src/config.ts +138 -0
  358. package/src/contact-point-secrets.ts +63 -0
  359. package/src/dashboard.ts +68 -9
  360. package/src/datasource-refs.test.ts +56 -0
  361. package/src/datasource-refs.ts +213 -0
  362. package/src/datasource-settings.test.ts +106 -0
  363. package/src/datasource-settings.ts +941 -0
  364. package/src/datasource.ts +123 -11
  365. package/src/deep-observe-hooks.ts +267 -0
  366. package/src/deep-observe.test.ts +311 -0
  367. package/src/deep-observe.ts +187 -0
  368. package/src/describe-resources.test.ts +271 -0
  369. package/src/describe-resources.ts +244 -0
  370. package/src/detect.ts +82 -5
  371. package/src/export-resources.test.ts +129 -0
  372. package/src/export-resources.ts +84 -0
  373. package/src/folder.ts +118 -0
  374. package/src/generated/lexicon-grafana.json +205 -0
  375. package/src/import/alerting-roundtrip.test.ts +161 -0
  376. package/src/import/alerting.ts +500 -0
  377. package/src/import/edits.ts +95 -0
  378. package/src/import/embedded.test.ts +45 -0
  379. package/src/import/embedded.ts +47 -0
  380. package/src/import/generated-types.e2e.test.ts +130 -0
  381. package/src/import/generator.test.ts +210 -0
  382. package/src/import/generator.ts +463 -0
  383. package/src/import/live-export.ts +264 -0
  384. package/src/import/mappings.ts +336 -0
  385. package/src/import/model.ts +117 -0
  386. package/src/import/normalize-alerting.ts +101 -0
  387. package/src/import/normalize.test.ts +117 -0
  388. package/src/import/normalize.ts +393 -0
  389. package/src/import/parser.test.ts +516 -0
  390. package/src/import/parser.ts +1387 -0
  391. package/src/import/report.ts +62 -0
  392. package/src/import/roundtrip.test.ts +563 -0
  393. package/src/import/testdata/fixtures.ts +137 -0
  394. package/src/import/v2.test.ts +292 -0
  395. package/src/import/v2.ts +804 -0
  396. package/src/import.e2e.test.ts +31 -84
  397. package/src/import.test.ts +9 -3
  398. package/src/index.ts +308 -14
  399. package/src/init-templates.test.ts +74 -0
  400. package/src/init-templates.ts +349 -0
  401. package/src/k8s.test.ts +98 -0
  402. package/src/k8s.ts +197 -0
  403. package/src/lint/audit-catalog.ts +68 -3
  404. package/src/lint/core-lint.test.ts +43 -0
  405. package/src/lint/post-synth/graf101.ts +3 -3
  406. package/src/lint/post-synth/graf102.ts +1 -1
  407. package/src/lint/post-synth/graf104.ts +3 -3
  408. package/src/lint/post-synth/graf106.ts +3 -3
  409. package/src/lint/post-synth/graf107.ts +1 -1
  410. package/src/lint/post-synth/graf108.ts +17 -0
  411. package/src/lint/post-synth/graf109.ts +17 -0
  412. package/src/lint/post-synth/graf110.ts +17 -0
  413. package/src/lint/post-synth/graf111.ts +17 -0
  414. package/src/lint/post-synth/graf112.ts +17 -0
  415. package/src/lint/post-synth/graf113.ts +17 -0
  416. package/src/lint/post-synth/graf114.ts +17 -0
  417. package/src/lint/post-synth/graf115.ts +17 -0
  418. package/src/lint/post-synth/grafana-helpers.ts +27 -10
  419. package/src/lint/post-synth/index.ts +16 -0
  420. package/src/lint/post-synth/post-synth.test.ts +333 -12
  421. package/src/lint/rules/literal-secret.ts +60 -6
  422. package/src/lint/rules/rules.test.ts +27 -0
  423. package/src/load-cost.test.ts +35 -0
  424. package/src/lsp/completions.test.ts +5 -0
  425. package/src/lsp/hover.test.ts +7 -0
  426. package/src/lsp/hover.ts +8 -1
  427. package/src/members.ts +68 -0
  428. package/src/observe.e2e.test.ts +255 -0
  429. package/src/op/activities/grafana-apply.e2e.test.ts +184 -0
  430. package/src/op/activities/grafana-apply.test.ts +243 -0
  431. package/src/op/activities/grafana-apply.ts +197 -0
  432. package/src/op/activities/index.ts +8 -0
  433. package/src/op/builders.test.ts +40 -0
  434. package/src/op/builders.ts +34 -0
  435. package/src/ownership.ts +122 -0
  436. package/src/panel-options.ts +66 -0
  437. package/src/panels.test.ts +53 -0
  438. package/src/panels.ts +148 -10
  439. package/src/pin.ts +69 -6
  440. package/src/plugin.test.ts +12 -2
  441. package/src/plugin.ts +75 -0
  442. package/src/promql-check.test.ts +108 -0
  443. package/src/promql-check.ts +193 -0
  444. package/src/query-models.ts +94 -0
  445. package/src/query.test.ts +166 -0
  446. package/src/query.ts +190 -20
  447. package/src/schema/azuremonitor.gen.ts +381 -0
  448. package/src/schema/barchart.gen.ts +221 -0
  449. package/src/schema/bargauge.gen.ts +111 -0
  450. package/src/schema/bigquery.gen.ts +110 -0
  451. package/src/schema/canvas.gen.ts +176 -0
  452. package/src/schema/cloudwatch.gen.ts +423 -0
  453. package/src/schema/dashboard.gen.ts +206 -22
  454. package/src/schema/elasticsearch.gen.ts +508 -0
  455. package/src/schema/expr.gen.ts +618 -0
  456. package/src/schema/gauge.gen.ts +2 -1
  457. package/src/schema/geomap.gen.ts +112 -0
  458. package/src/schema/googlecloudmonitoring.gen.ts +330 -0
  459. package/src/schema/grafanapyroscope.gen.ts +66 -0
  460. package/src/schema/heatmap.gen.ts +2 -1
  461. package/src/schema/histogram.gen.ts +151 -0
  462. package/src/schema/index.ts +18 -0
  463. package/src/schema/logs.gen.ts +3 -1
  464. package/src/schema/loki.gen.ts +2 -1
  465. package/src/schema/nodegraph.gen.ts +58 -0
  466. package/src/schema/piechart.gen.ts +146 -0
  467. package/src/schema/prometheus.gen.ts +2 -1
  468. package/src/schema/stat.gen.ts +2 -1
  469. package/src/schema/statetimeline.gen.ts +143 -0
  470. package/src/schema/statushistory.gen.ts +132 -0
  471. package/src/schema/table.gen.ts +41 -4
  472. package/src/schema/tempo.gen.ts +2 -1
  473. package/src/schema/text.gen.ts +2 -1
  474. package/src/schema/timeseries.gen.ts +7 -2
  475. package/src/schema/trend.gen.ts +208 -0
  476. package/src/schema/xychart.gen.ts +167 -0
  477. package/src/schema-overlay.test.ts +282 -0
  478. package/src/schema-validate.ts +199 -57
  479. package/src/schema.test.ts +4 -2
  480. package/src/serializer.test.ts +13 -1
  481. package/src/serializer.ts +11 -3
  482. package/src/skill-defs.ts +34 -0
  483. package/src/skills/chant-grafana-alerting.md +49 -0
  484. package/src/skills/chant-grafana-operations.md +108 -0
  485. package/src/skills/chant-grafana-provisioning.md +3 -3
  486. package/src/skills/chant-grafana.md +15 -8
  487. package/src/spec/fetch-cli.ts +3 -3
  488. package/src/spec/fetch-units-cli.ts +37 -0
  489. package/src/spec/overlay/canvas.overlay.json +142 -0
  490. package/src/spec/overlay/dashboard.overlay.json +378 -0
  491. package/src/spec/overlay/expr.overlay.json +14 -0
  492. package/src/spec/overlay/geomap.overlay.json +35 -0
  493. package/src/spec/overlay/logs.overlay.json +14 -0
  494. package/src/spec/overlay/nodegraph.overlay.json +31 -0
  495. package/src/spec/overlay/piechart.overlay.json +17 -0
  496. package/src/spec/overlay/table.overlay.json +103 -0
  497. package/src/spec/overlay/timeseries.overlay.json +21 -0
  498. package/src/spec/overlay/trend.overlay.json +17 -0
  499. package/src/spec/overlay/xychart.overlay.json +16 -0
  500. package/src/spec/overlay.ts +105 -0
  501. package/src/spec/schemas/azuremonitor.jsonschema.json +679 -0
  502. package/src/spec/schemas/barchart.jsonschema.json +419 -0
  503. package/src/spec/schemas/bargauge.jsonschema.json +224 -0
  504. package/src/spec/schemas/bigquery.jsonschema.json +290 -0
  505. package/src/spec/schemas/canvas.jsonschema.json +356 -0
  506. package/src/spec/schemas/cloudwatch.jsonschema.json +721 -0
  507. package/src/spec/schemas/dashboardv2.jsonschema.json +3387 -0
  508. package/src/spec/schemas/elasticsearch.jsonschema.json +1695 -0
  509. package/src/spec/schemas/expr.jsonschema.json +987 -0
  510. package/src/spec/schemas/geomap.jsonschema.json +220 -0
  511. package/src/spec/schemas/googlecloudmonitoring.jsonschema.json +488 -0
  512. package/src/spec/schemas/grafanapyroscope.jsonschema.json +93 -0
  513. package/src/spec/schemas/histogram.jsonschema.json +308 -0
  514. package/src/spec/schemas/nodegraph.jsonschema.json +77 -0
  515. package/src/spec/schemas/piechart.jsonschema.json +275 -0
  516. package/src/spec/schemas/statetimeline.jsonschema.json +324 -0
  517. package/src/spec/schemas/statushistory.jsonschema.json +292 -0
  518. package/src/spec/schemas/trend.jsonschema.json +449 -0
  519. package/src/spec/schemas/xychart.jsonschema.json +428 -0
  520. package/src/spec/schemas.gen.ts +40 -0
  521. package/src/spec/schemas.ts +30 -16
  522. package/src/spec/units.gen.ts +281 -0
  523. package/src/spec/units.ts +77 -0
  524. package/src/transformations.test.ts +65 -0
  525. package/src/transformations.ts +574 -0
  526. package/src/util.ts +8 -0
  527. package/src/validate-alerting.test.ts +255 -0
  528. package/src/validate-alerting.ts +430 -0
  529. package/src/validate-output.ts +427 -104
  530. package/src/validate.ts +28 -0
  531. package/src/validation.ts +27 -0
  532. package/src/variables.ts +120 -8
@@ -0,0 +1,376 @@
1
+ /**
2
+ * From alerting entities to Grafana's alerting provisioning file,
3
+ * `provisioning/alerting/chant.yaml`.
4
+ *
5
+ * The file is what Grafana's file provisioner reads
6
+ * (`pkg/services/provisioning/alerting`, the same at v12.4.11 and v13.2.2)
7
+ * and what `GET /api/v1/provisioning/*\/export` writes: `apiVersion: 1`
8
+ * and the lists `groups`, `contactPoints`, `policies`, `muteTimes` and
9
+ * `templates`. Mount `provisioning/` at `/etc/grafana/provisioning`, as for
10
+ * datasources and dashboards.
11
+ *
12
+ * Plain functions, so a test or another lexicon can render the same file
13
+ * with `alertingYaml()` without a build.
14
+ */
15
+
16
+ import { dump } from "js-yaml";
17
+ import type { Declarable } from "@intentius/chant/declarable";
18
+ import { durationMs, isValidDuration } from "@intentius/chant-lexicon-prometheus/duration";
19
+ import {
20
+ EXPRESSION_DATASOURCE_UID,
21
+ isAlertQueryEntity,
22
+ isAlertRuleGroupEntity,
23
+ isContactPointEntity,
24
+ isExpressionEntity,
25
+ isMuteTimingEntity,
26
+ isNotificationPolicyEntity,
27
+ isNotificationTemplateEntity,
28
+ type AlertDuration,
29
+ type AlertRuleData,
30
+ type AlertRuleEntity,
31
+ type AlertRuleGroupEntity,
32
+ type ContactPointEntity,
33
+ type MuteTimingEntity,
34
+ type NotificationPolicyEntity,
35
+ type NotificationTemplateEntity,
36
+ type PolicyRoute,
37
+ type RelativeTimeRange,
38
+ } from "./alerting";
39
+ import { isDashboardEntity } from "./dashboard";
40
+ import { isQueryEntity, type QueryEntity } from "./query";
41
+ import { isDatasourceVariable } from "./variables";
42
+ import { compact, slugUid } from "./util";
43
+ // build.ts imports this module too; only functions cross the cycle, and only at call time.
44
+ import { datasourceRef, targetJson } from "./build";
45
+
46
+ /** Where the alerting provisioning file goes, relative to the build output directory. */
47
+ export const ALERTING_FILE = "provisioning/alerting/chant.yaml";
48
+
49
+ /** The time range a query reads when neither it nor its rule sets one: the last 10 minutes, Grafana's editor default. */
50
+ export const DEFAULT_RELATIVE_TIME_RANGE = { from: 600, to: 0 } as const;
51
+
52
+ /** A rule group's evaluation interval when it sets none. */
53
+ export const DEFAULT_GROUP_INTERVAL = "1m";
54
+
55
+ type Json = Record<string, unknown>;
56
+
57
+ /** One entry of a rule's `data` in the file. */
58
+ export interface ProvisionedAlertQuery {
59
+ refId: string;
60
+ queryType?: string;
61
+ relativeTimeRange?: { from: number; to: number };
62
+ datasourceUid: string;
63
+ model: Json;
64
+ }
65
+
66
+ export interface ProvisionedAlertRule {
67
+ uid: string;
68
+ title: string;
69
+ condition?: string;
70
+ data: ProvisionedAlertQuery[];
71
+ dashboardUid?: string;
72
+ panelId?: number;
73
+ noDataState?: string;
74
+ execErrState?: string;
75
+ for?: string;
76
+ keepFiringFor?: string;
77
+ missing_series_evals_to_resolve?: number;
78
+ annotations?: Record<string, string>;
79
+ labels?: Record<string, string>;
80
+ isPaused?: boolean;
81
+ notification_settings?: Json;
82
+ record?: Json;
83
+ }
84
+
85
+ export interface ProvisionedRuleGroup {
86
+ orgId?: number;
87
+ name: string;
88
+ folder: string;
89
+ interval: string;
90
+ rules: ProvisionedAlertRule[];
91
+ }
92
+
93
+ export interface ProvisionedContactPoint {
94
+ orgId?: number;
95
+ name: string;
96
+ receivers: Array<{ uid: string; type: string; settings: Json; disableResolveMessage?: boolean }>;
97
+ }
98
+
99
+ export interface ProvisionedMuteTiming {
100
+ orgId?: number;
101
+ name: string;
102
+ time_intervals: Json[];
103
+ }
104
+
105
+ export interface ProvisionedTemplate {
106
+ orgId?: number;
107
+ name: string;
108
+ template: string;
109
+ }
110
+
111
+ /** The alerting provisioning file, parsed. */
112
+ export interface AlertingFile {
113
+ apiVersion: 1;
114
+ groups?: ProvisionedRuleGroup[];
115
+ contactPoints?: ProvisionedContactPoint[];
116
+ policies?: Json[];
117
+ muteTimes?: ProvisionedMuteTiming[];
118
+ templates?: ProvisionedTemplate[];
119
+ }
120
+
121
+ /** The keys an alerting provisioning file can hold, from `AlertingFileV1`. */
122
+ export const ALERTING_FILE_KEYS = [
123
+ "apiVersion",
124
+ "groups",
125
+ "deleteRules",
126
+ "contactPoints",
127
+ "deleteContactPoints",
128
+ "policies",
129
+ "resetPolicies",
130
+ "muteTimes",
131
+ "deleteMuteTimes",
132
+ "templates",
133
+ "deleteTemplates",
134
+ ] as const;
135
+
136
+ // ── Durations ───────────────────────────────────────────────────
137
+
138
+ /** A duration as whole seconds: a number is taken as seconds, a string as a Prometheus duration. */
139
+ export function durationSeconds(value: AlertDuration, what: string): number {
140
+ if (typeof value === "number") {
141
+ if (!Number.isInteger(value) || value < 0) throw new Error(`grafana: ${what} must be a whole number of seconds, got ${value}`);
142
+ return value;
143
+ }
144
+ if (!isValidDuration(value)) throw new Error(`grafana: ${what} "${value}" is not a duration like 10m or 1h`);
145
+ return Math.floor(durationMs(value)! / 1000);
146
+ }
147
+
148
+ function timeRange(range: RelativeTimeRange, what: string): { from: number; to: number } {
149
+ return { from: durationSeconds(range.from, `${what} from`), to: durationSeconds(range.to ?? 0, `${what} to`) };
150
+ }
151
+
152
+ // ── References ──────────────────────────────────────────────────
153
+
154
+ function receiverName(receiver: ContactPointEntity | string): string {
155
+ return typeof receiver === "string" ? receiver : receiver.props.name;
156
+ }
157
+
158
+ function timingNames(list: Array<MuteTimingEntity | string> | undefined): string[] | undefined {
159
+ return list?.map((t) => (typeof t === "string" ? t : t.props.name));
160
+ }
161
+
162
+ // ── Rules ───────────────────────────────────────────────────────
163
+
164
+ const REF_IDS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
165
+ function refIdAt(i: number): string {
166
+ return i < REF_IDS.length ? REF_IDS[i] : `${REF_IDS[Math.floor(i / REF_IDS.length) - 1]}${REF_IDS[i % REF_IDS.length]}`;
167
+ }
168
+
169
+ /** The uid a query's datasource resolves to, and its type when known. */
170
+ function datasourceOf(input: unknown, where: string): { uid: string; type?: string } {
171
+ if (input === undefined || input === null) throw new Error(`grafana: ${where} has no datasource; an alert query must name one`);
172
+ if (typeof input === "string") return { uid: input };
173
+ if (isDatasourceVariable(input)) {
174
+ throw new Error(`grafana: ${where} uses a DatasourceVariable; alert rules are evaluated without dashboard variables, so name the datasource itself`);
175
+ }
176
+ const ref = datasourceRef(input as Parameters<typeof datasourceRef>[0])!;
177
+ return { uid: ref.uid!, ...(ref.type ? { type: ref.type } : {}) };
178
+ }
179
+
180
+ function typedQueryModel(query: QueryEntity, refId: string, where: string): { uid: string; model: Json } {
181
+ const ds = datasourceOf((query.props as Json).datasource, where);
182
+ const model = targetJson(query, 0, ds.type ? { type: ds.type, uid: ds.uid } : undefined);
183
+ return { uid: ds.uid, model: { ...model, refId } };
184
+ }
185
+
186
+ /** One entry of `data`, as the file holds it. */
187
+ export function alertQueryJson(item: AlertRuleData, index: number, ruleRange: RelativeTimeRange | undefined, rule: string): ProvisionedAlertQuery {
188
+ const where = `alert rule "${rule}" data[${index}]`;
189
+ const fallbackRange = ruleRange ? timeRange(ruleRange, `${where} relativeTimeRange`) : { ...DEFAULT_RELATIVE_TIME_RANGE };
190
+ if (isExpressionEntity(item)) {
191
+ const { refId: ownRefId, relativeTimeRange, ...rest } = item.props as Json & { refId?: string; relativeTimeRange?: RelativeTimeRange };
192
+ const refId = ownRefId ?? refIdAt(index);
193
+ return compact({
194
+ refId,
195
+ relativeTimeRange: relativeTimeRange ? timeRange(relativeTimeRange, `${where} relativeTimeRange`) : undefined,
196
+ datasourceUid: EXPRESSION_DATASOURCE_UID,
197
+ model: { datasource: { type: EXPRESSION_DATASOURCE_UID, uid: EXPRESSION_DATASOURCE_UID }, refId, type: item.expressionKind, ...rest },
198
+ });
199
+ }
200
+ if (isAlertQueryEntity(item)) {
201
+ const p = item.props;
202
+ const refId = p.refId ?? refIdAt(index);
203
+ const range = p.relativeTimeRange ? timeRange(p.relativeTimeRange, `${where} relativeTimeRange`) : fallbackRange;
204
+ let uid: string;
205
+ let model: Json;
206
+ if (p.query) {
207
+ const typed = typedQueryModel(p.query, refId, where);
208
+ const own = p.datasource !== undefined ? datasourceOf(p.datasource, where) : undefined;
209
+ uid = own?.uid ?? typed.uid;
210
+ model = { ...typed.model, ...(p.model ?? {}) };
211
+ } else {
212
+ uid = datasourceOf(p.datasource, where).uid;
213
+ model = p.model ?? {};
214
+ }
215
+ return compact({ refId, queryType: p.queryType, relativeTimeRange: range, datasourceUid: uid, model });
216
+ }
217
+ if (isQueryEntity(item)) {
218
+ const refId = ((item.props as Json).refId as string | undefined) ?? refIdAt(index);
219
+ const typed = typedQueryModel(item, refId, where);
220
+ return { refId, relativeTimeRange: fallbackRange, datasourceUid: typed.uid, model: typed.model };
221
+ }
222
+ throw new Error(`grafana: ${where} must be a query (PromQuery, AlertQuery, ...) or an expression (ReduceExpression, ...)`);
223
+ }
224
+
225
+ /** One rule as the file holds it. */
226
+ export function alertRuleJson(rule: AlertRuleEntity): ProvisionedAlertRule {
227
+ const p = rule.props;
228
+ const data = (p.data ?? []).map((d, i) => alertQueryJson(d, i, p.relativeTimeRange, p.title));
229
+ const condition = p.condition ?? (p.record ? undefined : data[data.length - 1]?.refId);
230
+ const dashboardUid = p.dashboardUid === undefined ? undefined : isDashboardEntity(p.dashboardUid) ? (p.dashboardUid.props.uid ?? slugUid(p.dashboardUid.props.title)) : p.dashboardUid;
231
+ const ns = p.notification_settings;
232
+ return compact({
233
+ uid: p.uid ?? slugUid(p.title),
234
+ title: p.title,
235
+ condition,
236
+ data,
237
+ dashboardUid,
238
+ panelId: p.panelId,
239
+ noDataState: p.noDataState,
240
+ execErrState: p.execErrState,
241
+ for: p.for,
242
+ keepFiringFor: p.keepFiringFor,
243
+ missing_series_evals_to_resolve: p.missing_series_evals_to_resolve,
244
+ annotations: p.annotations,
245
+ labels: p.labels,
246
+ isPaused: p.isPaused,
247
+ notification_settings: ns
248
+ ? {
249
+ receiver: receiverName(ns.receiver),
250
+ group_by: ns.group_by,
251
+ group_wait: ns.group_wait,
252
+ group_interval: ns.group_interval,
253
+ repeat_interval: ns.repeat_interval,
254
+ mute_time_intervals: timingNames(ns.mute_time_intervals),
255
+ active_time_intervals: timingNames(ns.active_time_intervals),
256
+ }
257
+ : undefined,
258
+ record: p.record ? { ...p.record } : undefined,
259
+ });
260
+ }
261
+
262
+ /** One rule group as the file holds it. */
263
+ export function ruleGroupJson(group: AlertRuleGroupEntity): ProvisionedRuleGroup {
264
+ const p = group.props;
265
+ return compact({
266
+ orgId: p.orgId,
267
+ name: p.name,
268
+ folder: p.folder,
269
+ interval: p.interval ?? DEFAULT_GROUP_INTERVAL,
270
+ rules: (p.rules ?? []).map(alertRuleJson),
271
+ });
272
+ }
273
+
274
+ // ── Notifications ───────────────────────────────────────────────
275
+
276
+ /** One contact point as the file holds it. A receiver without a uid gets one from the name and the integration type. */
277
+ export function contactPointJson(cp: ContactPointEntity): ProvisionedContactPoint {
278
+ const p = cp.props;
279
+ const taken = new Set<string>();
280
+ const receivers = (p.receivers ?? []).map((r) => {
281
+ let uid = r.uid ?? slugUid(`${p.name} ${r.type}`);
282
+ if (r.uid === undefined) for (let n = 2; taken.has(uid); n++) uid = slugUid(`${p.name} ${r.type} ${n}`);
283
+ taken.add(uid);
284
+ return compact({ uid, type: r.type, settings: r.settings ?? {}, disableResolveMessage: r.disableResolveMessage });
285
+ });
286
+ return compact({ orgId: p.orgId, name: p.name, receivers });
287
+ }
288
+
289
+ function routeJson(route: PolicyRoute): Json {
290
+ return compact({
291
+ receiver: route.receiver === undefined ? undefined : receiverName(route.receiver),
292
+ group_by: route.group_by,
293
+ object_matchers: route.object_matchers?.map((m) => [...m]),
294
+ matchers: route.matchers,
295
+ match: route.match,
296
+ match_re: route.match_re,
297
+ continue: route.continue,
298
+ group_wait: route.group_wait,
299
+ group_interval: route.group_interval,
300
+ repeat_interval: route.repeat_interval,
301
+ mute_time_intervals: timingNames(route.mute_time_intervals),
302
+ active_time_intervals: timingNames(route.active_time_intervals),
303
+ routes: route.routes?.map(routeJson),
304
+ });
305
+ }
306
+
307
+ /** One organisation's policy tree as the file holds it. */
308
+ export function notificationPolicyJson(policy: NotificationPolicyEntity): Json {
309
+ const p = policy.props;
310
+ return compact({
311
+ orgId: p.orgId,
312
+ receiver: receiverName(p.receiver),
313
+ group_by: p.group_by,
314
+ group_wait: p.group_wait,
315
+ group_interval: p.group_interval,
316
+ repeat_interval: p.repeat_interval,
317
+ mute_time_intervals: timingNames(p.mute_time_intervals),
318
+ active_time_intervals: timingNames(p.active_time_intervals),
319
+ routes: p.routes?.map(routeJson),
320
+ });
321
+ }
322
+
323
+ export function muteTimingJson(t: MuteTimingEntity): ProvisionedMuteTiming {
324
+ return compact({ orgId: t.props.orgId, name: t.props.name, time_intervals: (t.props.time_intervals ?? []) as unknown as Json[] });
325
+ }
326
+
327
+ export function notificationTemplateJson(t: NotificationTemplateEntity): ProvisionedTemplate {
328
+ return compact({ orgId: t.props.orgId, name: t.props.name, template: t.props.template });
329
+ }
330
+
331
+ // ── The file ────────────────────────────────────────────────────
332
+
333
+ /** The alerting provisioning file as YAML text. */
334
+ export function alertingYaml(file: AlertingFile): string {
335
+ return `# Grafana alerting provisioning, generated by chant.\n${dump(file, { lineWidth: -1, noRefs: true, quotingType: '"' })}`;
336
+ }
337
+
338
+ /** Summary of the alerting a build provisions, for the build index. */
339
+ export interface AlertingIndex {
340
+ ruleGroups: Array<{ name: string; folder: string; rules: number }>;
341
+ contactPoints: string[];
342
+ policies: number;
343
+ muteTimings: string[];
344
+ templates: string[];
345
+ }
346
+
347
+ /** The alerting provisioning file for the alerting entities among these, or undefined when there are none. */
348
+ export function buildAlerting(entities: Iterable<Declarable>): { file: AlertingFile; index: AlertingIndex } | undefined {
349
+ const all = [...entities];
350
+ const byName = <T extends { name: string }>(a: T, b: T) => a.name.localeCompare(b.name);
351
+ const groups = all
352
+ .filter(isAlertRuleGroupEntity)
353
+ .map(ruleGroupJson)
354
+ .sort((a, b) => a.folder.localeCompare(b.folder) || a.name.localeCompare(b.name));
355
+ const contactPoints = all.filter(isContactPointEntity).map(contactPointJson).sort(byName);
356
+ const policies = all.filter(isNotificationPolicyEntity).map(notificationPolicyJson);
357
+ const muteTimes = all.filter(isMuteTimingEntity).map(muteTimingJson).sort(byName);
358
+ const templates = all.filter(isNotificationTemplateEntity).map(notificationTemplateJson).sort(byName);
359
+ if (groups.length + contactPoints.length + policies.length + muteTimes.length + templates.length === 0) return undefined;
360
+ const file: AlertingFile = compact({
361
+ apiVersion: 1 as const,
362
+ groups: groups.length > 0 ? groups : undefined,
363
+ contactPoints: contactPoints.length > 0 ? contactPoints : undefined,
364
+ policies: policies.length > 0 ? policies : undefined,
365
+ muteTimes: muteTimes.length > 0 ? muteTimes : undefined,
366
+ templates: templates.length > 0 ? templates : undefined,
367
+ });
368
+ const index: AlertingIndex = {
369
+ ruleGroups: groups.map((g) => ({ name: g.name, folder: g.folder, rules: g.rules.length })),
370
+ contactPoints: contactPoints.map((c) => c.name),
371
+ policies: policies.length,
372
+ muteTimings: muteTimes.map((m) => m.name),
373
+ templates: templates.map((t) => t.name),
374
+ };
375
+ return { file, index };
376
+ }
@@ -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
+ });