@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
@@ -6,6 +6,10 @@
6
6
  * lexicon's `spanMetricsNames()`: its namespace, its histogram unit, the
7
7
  * default dimensions it keeps, and optionally the `prometheus` exporter's
8
8
  * namespace. Renaming the connector's namespace moves every query here.
9
+ *
10
+ * The queries count server and consumer spans only by default, the spans
11
+ * that handle a request or a message, so a service's outgoing calls and
12
+ * its internal spans don't inflate its rate or dilute its error ratio.
9
13
  */
10
14
 
11
15
  import { Composite, type CompositeInstance } from "@intentius/chant/composite";
@@ -21,6 +25,7 @@ import { slugUid } from "../util";
21
25
  import {
22
26
  dashboardProps,
23
27
  durationUnit,
28
+ errorRatio,
24
29
  legend,
25
30
  quantile,
26
31
  quantileName,
@@ -41,8 +46,36 @@ export interface RedDashboardProps extends DashboardOptions {
41
46
  exporter?: OTelComponent<"exporter", "prometheus", PrometheusExporterConfig> | PrometheusNaming;
42
47
  /** Duration quantiles, one panel each (default p50, p95 and p99). */
43
48
  quantiles?: number[];
49
+ /**
50
+ * The span kinds the queries count (default server and consumer spans).
51
+ * `[]` counts every kind. With the default, a connector that excludes
52
+ * `span.kind` gets no kind filter; naming kinds for such a connector is
53
+ * an error.
54
+ */
55
+ spanKinds?: SpanKind[];
44
56
  }
45
57
 
58
+ /** The `span_kind` label values the spanmetrics connector writes. */
59
+ export type SpanKind =
60
+ | "SPAN_KIND_UNSPECIFIED"
61
+ | "SPAN_KIND_INTERNAL"
62
+ | "SPAN_KIND_SERVER"
63
+ | "SPAN_KIND_CLIENT"
64
+ | "SPAN_KIND_PRODUCER"
65
+ | "SPAN_KIND_CONSUMER";
66
+
67
+ const SPAN_KINDS: readonly SpanKind[] = [
68
+ "SPAN_KIND_UNSPECIFIED",
69
+ "SPAN_KIND_INTERNAL",
70
+ "SPAN_KIND_SERVER",
71
+ "SPAN_KIND_CLIENT",
72
+ "SPAN_KIND_PRODUCER",
73
+ "SPAN_KIND_CONSUMER",
74
+ ];
75
+
76
+ /** The kinds `RedDashboard` counts unless told otherwise: the spans that serve a request or consume a message. */
77
+ export const RED_DEFAULT_SPAN_KINDS: readonly SpanKind[] = Object.freeze(["SPAN_KIND_SERVER", "SPAN_KIND_CONSUMER"]);
78
+
46
79
  export type RedDashboardMembers = { dashboard: DashboardEntity };
47
80
 
48
81
  /** What `RedDashboard(...)` returns: its dashboard, as `dashboard`. */
@@ -50,28 +83,65 @@ export type RedDashboardInstance = CompositeInstance<RedDashboardMembers> & RedD
50
83
 
51
84
  const DEFAULT_QUANTILES = [0.5, 0.95, 0.99];
52
85
 
86
+ function cap(s: string): string {
87
+ return s.charAt(0).toUpperCase() + s.slice(1);
88
+ }
89
+
53
90
  function isNames(x: unknown): x is SpanMetricsNames {
54
91
  return typeof x === "object" && x !== null && "calls" in x && "labels" in x && "errorStatus" in x;
55
92
  }
56
93
 
57
- /** The PromQL the RED dashboard runs, from the connector's names. Exposed for tests and for panels of your own. */
58
- export function redQueries(names: SpanMetricsNames, quantiles: number[] = DEFAULT_QUANTILES, serviceFilter = "$service") {
94
+ /**
95
+ * The span-kind matcher for `kinds`, or none: none for `[]`, and none for
96
+ * the default kinds when the connector excludes `span.kind`.
97
+ */
98
+ function spanKindMatchers(names: SpanMetricsNames, kinds: readonly SpanKind[] | undefined): Matcher[] {
99
+ const label = names.labels.spanKind;
100
+ if (kinds === undefined) return label ? [[label, "=~", RED_DEFAULT_SPAN_KINDS.join("|")]] : [];
101
+ for (const k of kinds) {
102
+ if (!SPAN_KINDS.includes(k)) throw new Error(`RedDashboard: unknown span kind ${JSON.stringify(k)}; use one of ${SPAN_KINDS.join(", ")}`);
103
+ }
104
+ if (kinds.length === 0) return [];
105
+ if (!label) throw new Error("RedDashboard: the connector excludes span.kind, so the metrics can't be filtered by spanKinds");
106
+ return [[label, "=~", [...new Set(kinds)].join("|")]];
107
+ }
108
+
109
+ /**
110
+ * The PromQL the RED dashboard runs, from the connector's names. Exposed for
111
+ * tests and for panels of your own. `spanKinds` defaults to server and
112
+ * consumer spans; `[]` counts every kind.
113
+ */
114
+ export function redQueries(
115
+ names: SpanMetricsNames,
116
+ quantiles: number[] = DEFAULT_QUANTILES,
117
+ serviceFilter = "$service",
118
+ spanKinds?: readonly SpanKind[],
119
+ ) {
59
120
  const svc = names.labels.service;
60
121
  const status = names.labels.statusCode;
61
122
  if (!svc) throw new Error("RedDashboard: the connector excludes service.name, so there is no service to break the metrics down by");
62
123
  if (!status) throw new Error("RedDashboard: the connector excludes status.code, so errors can't be told from successes");
63
- const bySvc: Matcher[] = [[svc, "=~", serviceFilter]];
64
- const calls = selector(names.calls.prometheus, bySvc);
65
- const errors = selector(names.calls.prometheus, [...bySvc, [status, "=", names.errorStatus]]);
66
- const buckets = names.duration ? selector(`${names.duration.prometheus}_bucket`, bySvc) : undefined;
124
+ const kind = spanKindMatchers(names, spanKinds);
125
+ const scope: Matcher[] = [[svc, "=~", serviceFilter], ...kind];
126
+ const calls = selector(names.calls.prometheus, scope);
127
+ const errors = selector(names.calls.prometheus, [...scope, [status, "=", names.errorStatus]]);
128
+ const buckets = names.duration ? selector(`${names.duration.prometheus}_bucket`, scope) : undefined;
67
129
  return {
68
130
  rate: sumRate(calls, [svc]),
69
- errorRatio: `${sumRate(errors, [svc])}\n/\n${sumRate(calls, [svc])}`,
131
+ errorRatio: errorRatio(errors, calls, [svc]),
70
132
  duration: buckets ? quantiles.map((q) => ({ quantile: q, expr: quantile(q, buckets, [svc]) })) : [],
71
- services: `label_values(${names.calls.prometheus}, ${svc})`,
133
+ services: `label_values(${selector(names.calls.prometheus, kind)}, ${svc})`,
72
134
  };
73
135
  }
74
136
 
137
+ /** "server and consumer spans", for panel descriptions. */
138
+ function kindsText(names: SpanMetricsNames, kinds: readonly SpanKind[] | undefined): string {
139
+ const counted = kinds ?? (names.labels.spanKind ? RED_DEFAULT_SPAN_KINDS : []);
140
+ if (counted.length === 0) return "spans";
141
+ const words = [...new Set(counted)].map((k) => k.replace(/^SPAN_KIND_/, "").toLowerCase());
142
+ return `${words.length > 1 ? `${words.slice(0, -1).join(", ")} and ${words[words.length - 1]}` : words[0]} spans`;
143
+ }
144
+
75
145
  /**
76
146
  * A RED dashboard (rate, errors, duration) per service, built from a
77
147
  * `spanmetrics` connector declaration.
@@ -94,7 +164,8 @@ export const RedDashboard = Composite<RedDashboardProps, RedDashboardMembers>((p
94
164
  for (const q of quantiles) {
95
165
  if (!(typeof q === "number" && q > 0 && q < 1)) throw new Error(`RedDashboard: a quantile must be between 0 and 1, got ${String(q)}`);
96
166
  }
97
- const q = redQueries(names, quantiles);
167
+ const q = redQueries(names, quantiles, "$service", props.spanKinds);
168
+ const counted = kindsText(names, props.spanKinds);
98
169
  const svc = names.labels.service!;
99
170
  const ds = props.datasource;
100
171
 
@@ -112,7 +183,7 @@ export const RedDashboard = Composite<RedDashboardProps, RedDashboardMembers>((p
112
183
 
113
184
  const rate = new TimeSeriesPanel({
114
185
  title: "Rate",
115
- description: `Spans per second by service, from ${names.calls.prometheus}.`,
186
+ description: `${cap(counted)} per second by service, from ${names.calls.prometheus}.`,
116
187
  datasource: ds,
117
188
  gridPos: { w: 12, h: 8 },
118
189
  targets: [new PromQuery({ expr: q.rate, legendFormat: legend(svc) })],
@@ -120,7 +191,7 @@ export const RedDashboard = Composite<RedDashboardProps, RedDashboardMembers>((p
120
191
  });
121
192
  const errors = new TimeSeriesPanel({
122
193
  title: "Errors",
123
- description: `Share of spans with ${names.labels.statusCode}="${names.errorStatus}", by service.`,
194
+ description: `Share of ${counted} with ${names.labels.statusCode}="${names.errorStatus}", by service.`,
124
195
  datasource: ds,
125
196
  gridPos: { w: 12, h: 8 },
126
197
  targets: [new PromQuery({ expr: q.errorRatio, legendFormat: legend(svc) })],
@@ -135,7 +206,7 @@ export const RedDashboard = Composite<RedDashboardProps, RedDashboardMembers>((p
135
206
  (d) =>
136
207
  new TimeSeriesPanel({
137
208
  title: `Duration ${quantileName(d.quantile)}`,
138
- description: `${quantileName(d.quantile)} span duration by service, from ${names.duration!.prometheus}.`,
209
+ description: `${quantileName(d.quantile)} duration of ${counted} by service, from ${names.duration!.prometheus}.`,
139
210
  datasource: ds,
140
211
  gridPos: { w: width, h: 8 },
141
212
  targets: [new PromQuery({ expr: d.expr, legendFormat: legend(svc) })],
@@ -39,6 +39,17 @@ export function sumRate(sel: string, by: string[], range = "$__rate_interval"):
39
39
  return `sum${by.length ? ` by (${by.join(", ")})` : ""} (rate(${sel}[${range}]))`;
40
40
  }
41
41
 
42
+ /**
43
+ * `errors / all` as rates summed by `by`, with the numerator padded to zero:
44
+ * `(errors or all * 0) / all`. A group with calls but no error series gets
45
+ * 0 instead of dropping out of the result, so a panel shows 0% rather than
46
+ * "No data" for a service that has had no errors.
47
+ */
48
+ export function errorRatio(errorSel: string, allSel: string, by: string[]): string {
49
+ const all = sumRate(allSel, by);
50
+ return `(\n${sumRate(errorSel, by)}\nor\n${all} * 0\n)\n/\n${all}`;
51
+ }
52
+
42
53
  /** `histogram_quantile(q, sum by (le, labels) (rate(bucket[range])))`. */
43
54
  export function quantile(q: number, bucketSel: string, by: string[]): string {
44
55
  return `histogram_quantile(${num(q)}, ${sumRate(bucketSel, ["le", ...by])})`;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * `SloAlertRules`: an `Slo`'s multiwindow, multi-burn-rate alerts as
3
+ * Grafana-managed alert rules, for teams that page from Grafana rather
4
+ * than from Prometheus and Alertmanager.
5
+ *
6
+ * The windows, factors, thresholds and labels come from the prometheus
7
+ * lexicon's `sloMetrics()`, the same numbers the `Slo`'s own Prometheus
8
+ * alerts use. Each burn-rate pair becomes one rule that reads the two
9
+ * error ratios the `Slo` records (`slo:sli_error:ratio_rate<window>`) as
10
+ * instant Prometheus queries, and a math expression that holds while both
11
+ * are above the pair's threshold:
12
+ *
13
+ * A = slo:sli_error:ratio_rate1h{slo="checkout"}
14
+ * B = slo:sli_error:ratio_rate5m{slo="checkout"}
15
+ * C = $A > 0.0144 && $B > 0.0144 (condition)
16
+ *
17
+ * So the `Slo`'s rule group must be loaded into the Prometheus the rules
18
+ * query: the recording rules are what make a 3-day window cheap to
19
+ * evaluate every minute.
20
+ */
21
+
22
+ import { Composite, type CompositeInstance } from "@intentius/chant/composite";
23
+ import { sloMetrics, type SloInstance, type SloMetrics } from "@intentius/chant-lexicon-prometheus/composites/slo";
24
+ import type { RuleGroupEntity } from "@intentius/chant-lexicon-prometheus/rules";
25
+ import {
26
+ AlertRule,
27
+ AlertRuleGroup,
28
+ MathExpression,
29
+ type AlertRuleGroupEntity,
30
+ type ContactPointEntity,
31
+ type ExecErrState,
32
+ type NoDataState,
33
+ } from "../alerting";
34
+ import { PromQuery, type DatasourceInput } from "../query";
35
+ import { slugUid } from "../util";
36
+ import { num, requireDatasource } from "./shared";
37
+
38
+ export interface SloAlertRulesProps {
39
+ /** The `Slo(...)` the rules alert on, its rule group, or what `sloMetrics()` returned for it. It must have alerting on. */
40
+ slo: SloInstance | RuleGroupEntity | SloMetrics;
41
+ /** The Prometheus holding the `Slo`'s recorded series: a declared `Datasource`, an `ExternalDatasource`, or `{ type: "prometheus", uid }`. */
42
+ datasource: DatasourceInput<"prometheus">;
43
+ /** The Grafana folder the rules are stored in, by title. */
44
+ folder: string;
45
+ /** The rule group's name. Defaults to the `Slo`'s group name, `slo-<name>`. */
46
+ group?: string;
47
+ /** How often the rules are evaluated, a multiple of 10s. Defaults to `1m`. */
48
+ interval?: string;
49
+ /**
50
+ * Send the alerts straight to this contact point. Left out, they go
51
+ * through the notification policy tree, which can route on the
52
+ * `severity` label (`page` or `ticket` by default).
53
+ */
54
+ contactPoint?: ContactPointEntity | string;
55
+ /** How long a pair must hold before its alert fires. Default: none; the short window already filters blips. */
56
+ for?: string;
57
+ /** More labels on every rule, e.g. `team`. */
58
+ labels?: Record<string, string>;
59
+ /** More annotations on every rule, e.g. `runbook_url`. */
60
+ annotations?: Record<string, string>;
61
+ /** What no data does. Defaults to `OK`: no recorded ratio means no traffic, so no budget is burning. */
62
+ noDataState?: NoDataState;
63
+ /** What a query error does. Defaults to `Error`. */
64
+ execErrState?: ExecErrState;
65
+ }
66
+
67
+ export type SloAlertRulesMembers = {
68
+ /** One rule per burn-rate pair, page tier first. */
69
+ rules: AlertRuleGroupEntity;
70
+ };
71
+
72
+ /** What `SloAlertRules(...)` returns: its rule group, as `rules`. */
73
+ export type SloAlertRulesInstance = CompositeInstance<SloAlertRulesMembers> & SloAlertRulesMembers;
74
+
75
+ function isMetrics(x: unknown): x is SloMetrics {
76
+ return typeof x === "object" && x !== null && "errorBudgetRemaining" in x && "burnRates" in x && "selector" in x;
77
+ }
78
+
79
+ /** A rule uid for one pair: the SLO's name cut to fit, then the windows, at most 40 characters. */
80
+ function pairUid(name: string, long: string, short: string): string {
81
+ const suffix = `-${long}-${short}`.toLowerCase();
82
+ return `${slugUid(`slo ${name}`).slice(0, 40 - suffix.length).replace(/-$/, "")}${suffix}`;
83
+ }
84
+
85
+ /** The queries and condition of each burn-rate rule, from `sloMetrics()`. Exposed for tests and for rules of your own. */
86
+ export function sloAlertQueries(m: SloMetrics) {
87
+ return m.burnRates.map((b) => ({
88
+ ...b,
89
+ uid: pairUid(m.name, b.long, b.short),
90
+ longExpr: `${b.longRecord}${m.selector}`,
91
+ shortExpr: `${b.shortRecord}${m.selector}`,
92
+ condition: `$A > ${num(b.threshold)} && $B > ${num(b.threshold)}`,
93
+ }));
94
+ }
95
+
96
+ /**
97
+ * Grafana-managed burn-rate alerts for an `Slo`: one rule per window pair,
98
+ * with the labels and annotations the `Slo`'s Prometheus alerts carry.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * import { Slo } from "@intentius/chant-lexicon-prometheus";
103
+ * import { SloAlertRules } from "@intentius/chant-lexicon-grafana";
104
+ *
105
+ * export const checkout = Slo({ name: "checkout", objective: 0.999, window: "30d", sli: { ... } });
106
+ * export const checkoutAlerts = SloAlertRules({ slo: checkout, datasource: prometheus, folder: "SLOs" });
107
+ * ```
108
+ */
109
+ export const SloAlertRules = Composite<SloAlertRulesProps, SloAlertRulesMembers>((props) => {
110
+ requireDatasource("SloAlertRules", props.datasource);
111
+ if (!props.slo) throw new Error("SloAlertRules: slo is required (an Slo(...) declaration)");
112
+ if (typeof props.folder !== "string" || props.folder.trim() === "") throw new Error("SloAlertRules: folder is required (the Grafana folder title)");
113
+ const m = isMetrics(props.slo) ? props.slo : sloMetrics(props.slo);
114
+ const pairs = sloAlertQueries(m);
115
+ if (pairs.length === 0) {
116
+ throw new Error(`SloAlertRules: the Slo "${m.name}" has alerting turned off, so it has no burn-rate windows to alert on`);
117
+ }
118
+ const ds = props.datasource;
119
+ const budget = num(m.errorBudget);
120
+ const rules = pairs.map(
121
+ (p) =>
122
+ new AlertRule({
123
+ title: `SLO ${m.name} error budget burn ${p.long}/${p.short} (${p.severity})`,
124
+ uid: p.uid,
125
+ data: [
126
+ new PromQuery({ datasource: ds, expr: p.longExpr, instant: true, range: false }),
127
+ new PromQuery({ datasource: ds, expr: p.shortExpr, instant: true, range: false }),
128
+ new MathExpression({ expression: p.condition }),
129
+ ],
130
+ condition: "C",
131
+ noDataState: props.noDataState ?? "OK",
132
+ execErrState: props.execErrState ?? "Error",
133
+ ...(props.for !== undefined ? { for: props.for } : {}),
134
+ labels: { ...(props.labels ?? {}), ...p.labels },
135
+ annotations: {
136
+ summary: `SLO ${m.name} is burning its error budget more than ${num(p.factor)}x too fast (${p.long} and ${p.short} windows)`,
137
+ description:
138
+ `The error ratio over both the last ${p.long} and the last ${p.short} is above ${num(p.threshold)} ` +
139
+ `(${num(p.factor)} times the ${budget} budget of a ${num(m.objective)} objective). ` +
140
+ `At that rate the ${m.window} error budget is gone in ${p.exhaustsIn}.`,
141
+ ...(props.annotations ?? {}),
142
+ },
143
+ ...(props.contactPoint !== undefined ? { notification_settings: { receiver: props.contactPoint } } : {}),
144
+ }),
145
+ );
146
+ return {
147
+ rules: new AlertRuleGroup({ name: props.group ?? m.group, folder: props.folder, interval: props.interval ?? "1m", rules }),
148
+ };
149
+ }, "SloAlertRules");
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { grafanaConfigSchema, resolveGrafanaTarget } from "./config";
3
+ import { namespaceOf } from "./api/client";
4
+ import { grafanaPlugin } from "./plugin";
5
+
6
+ const config = {
7
+ grafana: {
8
+ profiles: {
9
+ prod: { url: "https://grafana.example.com/", token: { env: "GF_PROD" }, orgId: 2 },
10
+ local: { url: "http://localhost:3000", basicAuth: { user: { env: "GF_USER" }, password: { env: "GF_PASS" } } },
11
+ },
12
+ },
13
+ };
14
+
15
+ describe("grafana config namespace", () => {
16
+ it("is declared on the plugin and rejects an unknown key in a profile", () => {
17
+ expect(grafanaPlugin.configSchema).toBe(grafanaConfigSchema);
18
+ expect(grafanaConfigSchema.safeParse({ profiles: { prod: { url: "x", tokn: { env: "T" } } } }).success).toBe(false);
19
+ expect(grafanaConfigSchema.safeParse(config.grafana).success).toBe(true);
20
+ });
21
+ });
22
+
23
+ describe("resolveGrafanaTarget", () => {
24
+ it("binds an environment's profile, with the token from the variable it names", () => {
25
+ expect(resolveGrafanaTarget({ environment: "prod", config, env: { GF_PROD: "glsa_x" } })).toEqual({
26
+ url: "https://grafana.example.com",
27
+ auth: { token: "glsa_x" },
28
+ orgId: 2,
29
+ source: "grafana.profiles.prod",
30
+ });
31
+ });
32
+
33
+ it("basic auth, from two variables", () => {
34
+ expect(resolveGrafanaTarget({ environment: "local", config, env: { GF_USER: "admin", GF_PASS: "admin" } })).toMatchObject({ auth: { user: "admin", password: "admin" } });
35
+ });
36
+
37
+ it("a profile whose credential variable is unset is no-credentials, not an anonymous read", () => {
38
+ expect(resolveGrafanaTarget({ environment: "prod", config, env: {} })).toEqual({ reason: "no-credentials", detail: expect.stringContaining("GF_PROD") });
39
+ expect(resolveGrafanaTarget({ environment: "local", config, env: { GF_USER: "a" } })).toEqual({ reason: "no-credentials", detail: expect.stringContaining("GF_PASS") });
40
+ });
41
+
42
+ it("falls back to GRAFANA_URL and GRAFANA_TOKEN, or GRAFANA_USER and GRAFANA_PASSWORD", () => {
43
+ expect(resolveGrafanaTarget({ environment: "dev", config, env: { GRAFANA_URL: "http://g:3000", GRAFANA_TOKEN: "t", GRAFANA_ORG_ID: "3" } })).toEqual({
44
+ url: "http://g:3000",
45
+ auth: { token: "t" },
46
+ orgId: 3,
47
+ source: "env GRAFANA_URL",
48
+ });
49
+ expect(resolveGrafanaTarget({ env: { GRAFANA_URL: "http://g", GRAFANA_USER: "u", GRAFANA_PASSWORD: "p" } })).toMatchObject({ auth: { user: "u", password: "p" } });
50
+ });
51
+
52
+ it("no profile and no GRAFANA_URL is no-binding", () => {
53
+ expect(resolveGrafanaTarget({ environment: "dev", config, env: {} })).toEqual({ reason: "no-binding", detail: expect.stringContaining("grafana.profiles.dev") });
54
+ });
55
+
56
+ it("the dashboard API namespace follows the organisation", () => {
57
+ expect(namespaceOf({})).toBe("default");
58
+ expect(namespaceOf({ orgId: 4 })).toBe("org-4");
59
+ expect(namespaceOf({ orgId: 4, namespace: "stacks-12" })).toBe("stacks-12");
60
+ });
61
+ });
package/src/config.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The `grafana` namespace in `chant.config.ts` (#2946): which Grafana each
3
+ * chant environment observes, exports from and (#2948) applies to.
4
+ *
5
+ * ```ts
6
+ * import type { ChantConfig } from "@intentius/chant/config";
7
+ * import "@intentius/chant-lexicon-grafana"; // brings the `grafana` key into ChantConfig
8
+ *
9
+ * export default {
10
+ * lexicons: ["grafana"],
11
+ * grafana: {
12
+ * profiles: {
13
+ * staging: { url: "https://grafana.staging.example.com", token: { env: "GRAFANA_STAGING_TOKEN" } },
14
+ * prod: { url: "https://grafana.example.com", token: { env: "GRAFANA_PROD_TOKEN" }, orgId: 2 },
15
+ * },
16
+ * },
17
+ * } satisfies ChantConfig;
18
+ * ```
19
+ *
20
+ * A profile is keyed by chant environment, as `k8s.profiles.<env>` is.
21
+ * Credentials are named by their environment variable, never written in
22
+ * the config. An environment with no profile falls back to `GRAFANA_URL`
23
+ * with `GRAFANA_TOKEN`, or `GRAFANA_USER` and `GRAFANA_PASSWORD` (see
24
+ * `resolveGrafanaTarget`).
25
+ *
26
+ * One schema, three consumers, as in the fountain lexicon: core validates the
27
+ * namespace against it at load, the type is derived from it, and that type
28
+ * augments `ChantConfig`.
29
+ */
30
+
31
+ import { z } from "zod";
32
+ import type { ChantConfig } from "@intentius/chant/config";
33
+ import type { GrafanaAuth, GrafanaTarget } from "./api/client";
34
+
35
+ const envRef = z.strictObject({ env: z.string() });
36
+
37
+ export const grafanaProfileSchema = z.strictObject({
38
+ /** Base URL of the Grafana instance. */
39
+ url: z.string(),
40
+ /** A service account token, named by its environment variable. */
41
+ token: envRef.optional(),
42
+ /** Basic auth, each half named by its environment variable. For a local instance; use a token elsewhere. */
43
+ basicAuth: z.strictObject({ user: envRef, password: envRef }).optional(),
44
+ /** Organisation id. Defaults to 1. */
45
+ orgId: z.number().int().positive().optional(),
46
+ /** The `dashboard.grafana.app` namespace, when it is not `default` / `org-<id>` (Grafana Cloud: `stacks-<id>`). */
47
+ namespace: z.string().optional(),
48
+ });
49
+
50
+ export const grafanaConfigSchema = z.strictObject({
51
+ /** One Grafana per chant environment. */
52
+ profiles: z.record(z.string(), grafanaProfileSchema).optional(),
53
+ });
54
+
55
+ export type GrafanaProfile = z.infer<typeof grafanaProfileSchema>;
56
+ export type GrafanaConfig = z.infer<typeof grafanaConfigSchema>;
57
+
58
+ declare module "@intentius/chant/config" {
59
+ interface ChantConfig {
60
+ grafana?: GrafanaConfig;
61
+ }
62
+ }
63
+
64
+ /** Compile-time proof that the augmentation reaches `ChantConfig`. */
65
+ export type GrafanaConfigNamespace = NonNullable<ChantConfig["grafana"]>;
66
+
67
+ /** Why no target could be resolved, in the observation vocabulary. */
68
+ export interface UnresolvedTarget {
69
+ readonly reason: "no-binding" | "no-credentials";
70
+ readonly detail: string;
71
+ }
72
+
73
+ function isUnresolved(v: GrafanaTarget | UnresolvedTarget): v is UnresolvedTarget {
74
+ return "reason" in v;
75
+ }
76
+
77
+ export { isUnresolved as isUnresolvedTarget };
78
+
79
+ /**
80
+ * The Grafana an environment reads from. Pure: `config` and `env` are
81
+ * passed in.
82
+ *
83
+ * 1. `grafana.profiles.<environment>`. A profile that names a credential
84
+ * variable which is not set is `no-credentials`, not an anonymous read:
85
+ * the profile said how to authenticate.
86
+ * 2. `GRAFANA_URL`, with `GRAFANA_TOKEN`, or `GRAFANA_USER` and
87
+ * `GRAFANA_PASSWORD`, or neither (an instance with anonymous access; a
88
+ * refused read is then `no-credentials`).
89
+ * 3. Otherwise `no-binding`.
90
+ */
91
+ export function resolveGrafanaTarget(input: {
92
+ environment?: string;
93
+ config?: Pick<ChantConfig, "grafana">;
94
+ env?: Record<string, string | undefined>;
95
+ }): GrafanaTarget | UnresolvedTarget {
96
+ const env = input.env ?? process.env;
97
+ const profile = input.environment !== undefined ? input.config?.grafana?.profiles?.[input.environment] : undefined;
98
+ if (profile) {
99
+ const source = `grafana.profiles.${input.environment}`;
100
+ let auth: GrafanaAuth | undefined;
101
+ if (profile.token) {
102
+ const token = env[profile.token.env];
103
+ if (!token) return { reason: "no-credentials", detail: `${source}.token names ${profile.token.env}, which is not set` };
104
+ auth = { token };
105
+ } else if (profile.basicAuth) {
106
+ const user = env[profile.basicAuth.user.env];
107
+ const password = env[profile.basicAuth.password.env];
108
+ if (!user || !password) {
109
+ const missing = [!user ? profile.basicAuth.user.env : undefined, !password ? profile.basicAuth.password.env : undefined].filter(Boolean).join(" and ");
110
+ return { reason: "no-credentials", detail: `${source}.basicAuth names ${missing}, which ${missing.includes(" and ") ? "are" : "is"} not set` };
111
+ }
112
+ auth = { user, password };
113
+ }
114
+ return {
115
+ url: profile.url.replace(/\/+$/, ""),
116
+ ...(auth ? { auth } : {}),
117
+ ...(profile.orgId !== undefined ? { orgId: profile.orgId } : {}),
118
+ ...(profile.namespace ? { namespace: profile.namespace } : {}),
119
+ source,
120
+ };
121
+ }
122
+
123
+ const url = env.GRAFANA_URL;
124
+ if (!url) {
125
+ const where = input.environment !== undefined ? `grafana.profiles.${input.environment} is not declared and ` : "";
126
+ return { reason: "no-binding", detail: `${where}GRAFANA_URL is not set, so there is no Grafana to read` };
127
+ }
128
+ let auth: GrafanaAuth | undefined;
129
+ if (env.GRAFANA_TOKEN) auth = { token: env.GRAFANA_TOKEN };
130
+ else if (env.GRAFANA_USER && env.GRAFANA_PASSWORD) auth = { user: env.GRAFANA_USER, password: env.GRAFANA_PASSWORD };
131
+ const orgId = env.GRAFANA_ORG_ID ? Number(env.GRAFANA_ORG_ID) : undefined;
132
+ return {
133
+ url: url.replace(/\/+$/, ""),
134
+ ...(auth ? { auth } : {}),
135
+ ...(orgId !== undefined && Number.isInteger(orgId) && orgId > 0 ? { orgId } : {}),
136
+ source: "env GRAFANA_URL",
137
+ };
138
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The contact point settings Grafana stores encrypted, per integration: the
3
+ * options marked `secure` by `GET /api/alert-notifiers?version=2` on
4
+ * `grafana/grafana:13.2.2` (commit 1bea008f; the same list on 12.4.11).
5
+ * Nested settings are dotted paths (`tlsConfig.clientKey`).
6
+ *
7
+ * GRAF002 flags a literal at one of these paths, and the importer replaces
8
+ * the `[REDACTED]` that an export without secrets writes there.
9
+ */
10
+ export const CONTACT_POINT_SECRET_SETTINGS: Readonly<Record<string, readonly string[]>> = Object.freeze({
11
+ dingding: ["url"],
12
+ discord: ["url"],
13
+ email: [],
14
+ googlechat: ["url"],
15
+ jira: ["user", "password", "api_token"],
16
+ kafka: ["password"],
17
+ line: ["token"],
18
+ mqtt: ["password", "tlsConfig.caCertificate", "tlsConfig.clientCertificate", "tlsConfig.clientKey"],
19
+ oncall: ["password", "authorization_credentials"],
20
+ opsgenie: ["apiKey"],
21
+ pagerduty: ["integrationKey"],
22
+ "prometheus-alertmanager": ["basicAuthPassword"],
23
+ pushover: ["apiToken", "userKey"],
24
+ sensugo: ["apikey"],
25
+ slack: ["token", "url"],
26
+ sns: ["sigv4.access_key", "sigv4.secret_key"],
27
+ teams: [],
28
+ telegram: ["bottoken"],
29
+ threema: ["api_secret"],
30
+ victorops: ["url"],
31
+ webex: ["bot_token"],
32
+ webhook: [
33
+ "password",
34
+ "authorization_credentials",
35
+ "tlsConfig.caCertificate",
36
+ "tlsConfig.clientCertificate",
37
+ "tlsConfig.clientKey",
38
+ "hmacConfig.secret",
39
+ "http_config.oauth2.client_secret",
40
+ "http_config.oauth2.tls_config.caCertificate",
41
+ "http_config.oauth2.tls_config.clientCertificate",
42
+ "http_config.oauth2.tls_config.clientKey",
43
+ ],
44
+ wechat: ["api_secret", "http_config.basic_auth.password", "http_config.authorization.credentials", "http_config.oauth2.client_secret"],
45
+ wecom: ["url", "secret"],
46
+ });
47
+
48
+ /** How Grafana provisioning reads a value from outside the file: `$VAR`, `${VAR}`, `$__env{…}`, `$__file{…}`, `$__vault{…}`. */
49
+ export const EXPANDED_VALUE = /\$(\{[A-Za-z_][A-Za-z0-9_]*\}|[A-Za-z_][A-Za-z0-9_]*|__(env|file|vault)\{[^}]+\})/;
50
+
51
+ /** What an export without secrets writes in place of each one. */
52
+ export const REDACTED = "[REDACTED]";
53
+
54
+ /** The secret settings of one receiver present in `settings`, with their values: `[path, value]`. */
55
+ export function secretSettings(type: string, settings: Record<string, unknown>): Array<[string, unknown]> {
56
+ const out: Array<[string, unknown]> = [];
57
+ for (const path of CONTACT_POINT_SECRET_SETTINGS[type] ?? []) {
58
+ let node: unknown = settings;
59
+ for (const seg of path.split(".")) node = node && typeof node === "object" ? (node as Record<string, unknown>)[seg] : undefined;
60
+ if (node !== undefined) out.push([path, node]);
61
+ }
62
+ return out;
63
+ }
package/src/dashboard.ts CHANGED
@@ -40,6 +40,13 @@ export interface DashboardProps {
40
40
  graphTooltip?: "default" | "sharedCrosshair" | "sharedTooltip";
41
41
  /** Whether users may edit it in the UI. Defaults to true, as in Grafana. */
42
42
  editable?: boolean;
43
+ /**
44
+ * The dashboard schema version the JSON is written at. Leave it out for
45
+ * the pinned one (`DASHBOARD_SCHEMA_VERSION`). `chant import` sets it for
46
+ * a dashboard saved by an older Grafana, so Grafana still runs its
47
+ * migrations when it loads the rebuilt JSON.
48
+ */
49
+ schemaVersion?: number;
43
50
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
51
  variables?: VariableEntity<any>[];
45
52
  /** Panels and rows, top to bottom. A row's own `panels` follow it. */
@@ -49,7 +56,10 @@ export interface DashboardProps {
49
56
  /**
50
57
  * The Grafana folder to provision it into. Written as a subdirectory of
51
58
  * `dashboards/`, which the provider maps to folders
52
- * (`foldersFromFilesStructure`). Leave it out for the General folder.
59
+ * (`foldersFromFilesStructure`). `"Platform/Kubernetes"` nests one
60
+ * directory in another, which Grafana 13.1 and later make a Kubernetes
61
+ * folder inside Platform; earlier versions use only the last level
62
+ * (GRAF109 warns). Leave it out for the General folder.
53
63
  */
54
64
  folder?: string;
55
65
  }
@@ -84,7 +94,11 @@ export interface DashboardProviderProps {
84
94
  /** Provider name, unique in the provisioning file. */
85
95
  name: string;
86
96
  orgId?: number;
87
- /** Folder for dashboards not in a subdirectory. */
97
+ /**
98
+ * Put every dashboard this provider loads in this folder. Setting it (or
99
+ * `folderUid`) turns `foldersFromFilesStructure` off by default, so each
100
+ * dashboard's own `folder` is ignored (GRAF109 warns).
101
+ */
88
102
  folder?: string;
89
103
  folderUid?: string;
90
104
  /** Where the `dashboards/` output is mounted in the Grafana container. Defaults to `/var/lib/grafana/dashboards`. */