@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
@@ -18,6 +18,7 @@ import { slugUid } from "../util";
18
18
  import {
19
19
  dashboardProps,
20
20
  durationUnit,
21
+ errorRatio,
21
22
  legend,
22
23
  quantile,
23
24
  quantileName,
@@ -75,8 +76,7 @@ export function agentQueries(m: GenAiMetrics, q = 0.95) {
75
76
  const err: Matcher = [status, "=", SPAN_STATUS_ERROR];
76
77
  const calls = m.calls.prometheus;
77
78
  const buckets = `${m.duration.prometheus}_bucket`;
78
- const ratio = (matchers: Matcher[], by: string[]) =>
79
- `${sumRate(selector(calls, [...matchers, err]), by)}\n/\n${sumRate(selector(calls, matchers), by)}`;
79
+ const ratio = (matchers: Matcher[], by: string[]) => errorRatio(selector(calls, [...matchers, err]), selector(calls, matchers), by);
80
80
  const tokens: Matcher[] = [[tokenModel, "=~", "$model"]];
81
81
 
82
82
  return {
@@ -191,13 +191,16 @@ export const AgentDashboard = Composite<AgentDashboardProps, AgentDashboardMembe
191
191
  }),
192
192
  ],
193
193
  });
194
+ // One instant query at the end of the range: `increase(...[$__range])`
195
+ // is the total over the whole range, so evaluating it at every step of a
196
+ // range query would only repeat that work for points the stat throws away.
194
197
  const tokenStat = (title: string, expr: string) =>
195
198
  new StatPanel({
196
199
  title,
197
200
  description: "Over the dashboard's time range.",
198
201
  datasource: ds,
199
202
  gridPos: { w: 4, h: 8 },
200
- targets: [new PromQuery({ expr, legendFormat: title })],
203
+ targets: [new PromQuery({ expr, legendFormat: title, instant: true, range: false })],
201
204
  options: { graphMode: "none", colorMode: "none", reduceOptions: { calcs: ["lastNotNull"] } },
202
205
  fieldConfig: { defaults: { unit: "short", decimals: 0 } },
203
206
  });
@@ -19,7 +19,7 @@ const catalogued = compositeCatalog.map((entry) => entry.name).sort();
19
19
 
20
20
  describe("the grafana composite catalog", () => {
21
21
  test("every exported composite has an entry, and every entry names an exported composite", () => {
22
- expect(exported).toEqual(["AgentDashboard", "RedDashboard", "SloDashboard"]);
22
+ expect(exported).toEqual(["AgentDashboard", "RedDashboard", "SloAlertRules", "SloDashboard"]);
23
23
  expect(catalogued, "regenerate with `npm run generate:composite-catalogs -- grafana`").toEqual(exported);
24
24
  });
25
25
 
@@ -27,7 +27,7 @@ describe("the grafana composite catalog", () => {
27
27
  for (const entry of compositeCatalog) {
28
28
  expect(entry.lexicon, entry.name).toBe("grafana");
29
29
  expect(entry.description.length, entry.name).toBeGreaterThan(0);
30
- expect(entry.bundles, entry.name).toEqual(["Dashboard"]);
30
+ expect(entry.bundles, entry.name).toEqual(entry.name === "SloAlertRules" ? ["AlertRuleGroup"] : ["Dashboard"]);
31
31
  }
32
32
  });
33
33
 
@@ -102,6 +102,12 @@ export const compositeCatalog: CompositeEntry[] = [
102
102
  "required": false,
103
103
  "description": "Duration quantiles, one panel each (default p50, p95 and p99)."
104
104
  },
105
+ {
106
+ "name": "spanKinds",
107
+ "type": "SpanKind[]",
108
+ "required": false,
109
+ "description": "The span kinds the queries count (default server and consumer spans)."
110
+ },
105
111
  {
106
112
  "name": "datasource",
107
113
  "type": "DatasourceInput<\"prometheus\">",
@@ -154,6 +160,82 @@ export const compositeCatalog: CompositeEntry[] = [
154
160
  }
155
161
  ]
156
162
  },
163
+ {
164
+ "name": "SloAlertRules",
165
+ "lexicon": "grafana",
166
+ "description": "Grafana-managed burn-rate alerts for an `Slo`: one rule per window pair, with the labels and annotations the `Slo`'s Prometheus alerts carry.",
167
+ "bundles": [
168
+ "AlertRuleGroup"
169
+ ],
170
+ "params": [
171
+ {
172
+ "name": "slo",
173
+ "type": "SloInstance | RuleGroupEntity | SloMetrics",
174
+ "required": true,
175
+ "description": "The `Slo(...)` the rules alert on, its rule group, or what `sloMetrics()` returned for it."
176
+ },
177
+ {
178
+ "name": "datasource",
179
+ "type": "DatasourceInput<\"prometheus\">",
180
+ "required": true,
181
+ "description": "The Prometheus holding the `Slo`'s recorded series: a declared `Datasource`, an `ExternalDatasource`, or `{ type: \"prometheus\", uid }`."
182
+ },
183
+ {
184
+ "name": "folder",
185
+ "type": "string",
186
+ "required": true,
187
+ "description": "The Grafana folder the rules are stored in, by title."
188
+ },
189
+ {
190
+ "name": "group",
191
+ "type": "string",
192
+ "required": false,
193
+ "description": "The rule group's name."
194
+ },
195
+ {
196
+ "name": "interval",
197
+ "type": "string",
198
+ "required": false,
199
+ "description": "How often the rules are evaluated, a multiple of 10s."
200
+ },
201
+ {
202
+ "name": "contactPoint",
203
+ "type": "ContactPointEntity | string",
204
+ "required": false,
205
+ "description": "Send the alerts straight to this contact point."
206
+ },
207
+ {
208
+ "name": "for",
209
+ "type": "string",
210
+ "required": false,
211
+ "description": "How long a pair must hold before its alert fires."
212
+ },
213
+ {
214
+ "name": "labels",
215
+ "type": "Record<string, string>",
216
+ "required": false,
217
+ "description": "More labels on every rule, e.g. `team`."
218
+ },
219
+ {
220
+ "name": "annotations",
221
+ "type": "Record<string, string>",
222
+ "required": false,
223
+ "description": "More annotations on every rule, e.g. `runbook_url`."
224
+ },
225
+ {
226
+ "name": "noDataState",
227
+ "type": "NoDataState",
228
+ "required": false,
229
+ "description": "What no data does."
230
+ },
231
+ {
232
+ "name": "execErrState",
233
+ "type": "ExecErrState",
234
+ "required": false,
235
+ "description": "What a query error does."
236
+ }
237
+ ]
238
+ },
157
239
  {
158
240
  "name": "SloDashboard",
159
241
  "lexicon": "grafana",
@@ -15,7 +15,7 @@ import { Datasource } from "../datasource";
15
15
  import { buildGrafana, type DashboardJson } from "../build";
16
16
  import { validateGrafanaOutput } from "../validate-output";
17
17
  import { validateDashboardSchema } from "../schema-validate";
18
- import { AgentDashboard, RedDashboard, SloDashboard } from "./index";
18
+ import { AgentDashboard, RedDashboard, redQueries, SloDashboard } from "./index";
19
19
 
20
20
  const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090" });
21
21
 
@@ -78,16 +78,64 @@ describe("RedDashboard", () => {
78
78
  expectClean(json, out);
79
79
  expect(json.uid).toBe("red-traces-span-metrics");
80
80
  expect(panels(json).map((p) => p.title)).toEqual(["Rate", "Errors", "Duration p50", "Duration p95", "Duration p99"]);
81
- expect(panelByTitle(json, "Rate").targets).toMatchObject([
82
- { expr: 'sum by (service_name) (rate(traces_span_metrics_calls_total{service_name=~"$service"}[$__rate_interval]))' },
83
- ]);
84
- expect((panelByTitle(json, "Errors").targets as Json[])[0].expr).toContain('status_code="STATUS_CODE_ERROR"');
81
+ const calls = 'traces_span_metrics_calls_total{service_name=~"$service", span_kind=~"SPAN_KIND_SERVER|SPAN_KIND_CONSUMER"}';
82
+ const errors = 'traces_span_metrics_calls_total{service_name=~"$service", span_kind=~"SPAN_KIND_SERVER|SPAN_KIND_CONSUMER", status_code="STATUS_CODE_ERROR"}';
83
+ expect(panelByTitle(json, "Rate").targets).toMatchObject([{ expr: `sum by (service_name) (rate(${calls}[$__rate_interval]))` }]);
84
+ expect(panelByTitle(json, "Rate").description).toBe("Server and consumer spans per second by service, from traces_span_metrics_calls_total.");
85
+ expect((panelByTitle(json, "Errors").targets as Json[])[0].expr).toBe(
86
+ "(\n" +
87
+ `sum by (service_name) (rate(${errors}[$__rate_interval]))\n` +
88
+ "or\n" +
89
+ `sum by (service_name) (rate(${calls}[$__rate_interval])) * 0\n` +
90
+ ")\n/\n" +
91
+ `sum by (service_name) (rate(${calls}[$__rate_interval]))`,
92
+ );
85
93
  expect((panelByTitle(json, "Duration p95").targets as Json[])[0].expr).toBe(
86
- 'histogram_quantile(0.95, sum by (le, service_name) (rate(traces_span_metrics_duration_milliseconds_bucket{service_name=~"$service"}[$__rate_interval])))',
94
+ 'histogram_quantile(0.95, sum by (le, service_name) (rate(traces_span_metrics_duration_milliseconds_bucket{service_name=~"$service", span_kind=~"SPAN_KIND_SERVER|SPAN_KIND_CONSUMER"}[$__rate_interval])))',
87
95
  );
88
96
  expect((panelByTitle(json, "Duration p95").fieldConfig as { defaults: Json }).defaults.unit).toBe("ms");
89
97
  const templating = json.templating as unknown as { list: Json[] };
90
- expect(templating.list.map((v) => [v.name, v.query])).toEqual([["service", "label_values(traces_span_metrics_calls_total, service_name)"]]);
98
+ expect(templating.list.map((v) => [v.name, v.query])).toEqual([
99
+ ["service", 'label_values(traces_span_metrics_calls_total{span_kind=~"SPAN_KIND_SERVER|SPAN_KIND_CONSUMER"}, service_name)'],
100
+ ]);
101
+ });
102
+
103
+ test("spanKinds picks the kinds counted; [] counts every kind", () => {
104
+ const names = spanMetricsNames(new SpanMetricsConnector({ namespace: "shop" }));
105
+ const all = redQueries(names, [0.5], "$service", []);
106
+ expect(all.rate).toBe('sum by (service_name) (rate(shop_calls_total{service_name=~"$service"}[$__rate_interval]))');
107
+ expect(all.services).toBe("label_values(shop_calls_total, service_name)");
108
+
109
+ const client = redQueries(names, [0.5], "checkout", ["SPAN_KIND_CLIENT", "SPAN_KIND_PRODUCER", "SPAN_KIND_CLIENT"]);
110
+ expect(client.rate).toBe(
111
+ 'sum by (service_name) (rate(shop_calls_total{service_name=~"checkout", span_kind=~"SPAN_KIND_CLIENT|SPAN_KIND_PRODUCER"}[$__rate_interval]))',
112
+ );
113
+ for (const e of [client.rate, client.errorRatio, ...client.duration.map((d) => d.expr)]) {
114
+ expect(checkPromql(asPromql(e)), e).toEqual({ ok: true });
115
+ }
116
+
117
+ const { json, out } = built(
118
+ RedDashboard({ spanMetrics: new SpanMetricsConnector({ namespace: "shop" }), spanKinds: ["SPAN_KIND_SERVER"], datasource: prometheus }).dashboard,
119
+ );
120
+ expectClean(json, out);
121
+ for (const e of exprs(json)) expect(e).toContain('span_kind=~"SPAN_KIND_SERVER"');
122
+ expect(panelByTitle(json, "Errors").description).toBe('Share of server spans with status_code="STATUS_CODE_ERROR", by service.');
123
+
124
+ const every = built(RedDashboard({ spanMetrics: new SpanMetricsConnector({ namespace: "shop" }), spanKinds: [], datasource: prometheus }).dashboard);
125
+ for (const e of exprs(every.json)) expect(e).not.toContain("span_kind");
126
+ expect(panelByTitle(every.json, "Rate").description).toBe("Spans per second by service, from shop_calls_total.");
127
+ });
128
+
129
+ test("a connector that excludes span.kind gets no kind filter by default, and naming kinds for it is an error", () => {
130
+ const connector = new SpanMetricsConnector({ exclude_dimensions: ["span.kind"] });
131
+ const { json, out } = built(RedDashboard({ spanMetrics: connector, datasource: prometheus }).dashboard);
132
+ expectClean(json, out);
133
+ for (const e of exprs(json)) expect(e).not.toContain("span_kind");
134
+ expect(() => RedDashboard({ spanMetrics: connector, spanKinds: ["SPAN_KIND_SERVER"], datasource: prometheus })).toThrow(/excludes span\.kind/);
135
+ expect(() => RedDashboard({ spanMetrics: connector, spanKinds: [], datasource: prometheus })).not.toThrow();
136
+ expect(() =>
137
+ RedDashboard({ spanMetrics: new SpanMetricsConnector({}), spanKinds: ["SERVER" as never], datasource: prometheus }),
138
+ ).toThrow(/unknown span kind "SERVER"/);
91
139
  });
92
140
 
93
141
  test("renaming the connector's namespace moves every query; its histogram unit moves the duration metric", () => {
@@ -259,10 +307,26 @@ describe("AgentDashboard", () => {
259
307
  "Input tokens",
260
308
  "Output tokens",
261
309
  ]);
310
+ const calls = 'genai_calls_total{service_name=~"$service", gen_ai_request_model=~"$model"}';
311
+ const errors = 'genai_calls_total{service_name=~"$service", gen_ai_request_model=~"$model", status_code="STATUS_CODE_ERROR"}';
262
312
  expect((panelByTitle(json, "Errors by model").targets as Json[])[0].expr).toBe(
263
- 'sum by (gen_ai_request_model) (rate(genai_calls_total{service_name=~"$service", gen_ai_request_model=~"$model", status_code="STATUS_CODE_ERROR"}[$__rate_interval]))\n/\n' +
264
- 'sum by (gen_ai_request_model) (rate(genai_calls_total{service_name=~"$service", gen_ai_request_model=~"$model"}[$__rate_interval]))',
313
+ "(\n" +
314
+ `sum by (gen_ai_request_model) (rate(${errors}[$__rate_interval]))\n` +
315
+ "or\n" +
316
+ `sum by (gen_ai_request_model) (rate(${calls}[$__rate_interval])) * 0\n` +
317
+ ")\n/\n" +
318
+ `sum by (gen_ai_request_model) (rate(${calls}[$__rate_interval]))`,
265
319
  );
320
+ expect((panelByTitle(json, "Errors by tool").targets as Json[])[0].expr).toMatch(/^\(\nsum by \(gen_ai_tool_name\) .*\nor\n.* \* 0\n\)\n\/\n/s);
321
+ // The token totals are one instant query each, not a range query of full-range increases.
322
+ for (const [title, metric] of [["Input tokens", "genai_tokens_input_total"], ["Output tokens", "genai_tokens_output_total"]]) {
323
+ expect(panelByTitle(json, title).targets).toMatchObject([
324
+ { expr: `sum(increase(${metric}{gen_ai_request_model=~"$model"}[$__range]))`, instant: true, range: false },
325
+ ]);
326
+ }
327
+ for (const title of ["Input tokens by model", "Errors by model"]) {
328
+ expect((panelByTitle(json, title).targets as Json[])[0].instant).toBeUndefined();
329
+ }
266
330
  expect((panelByTitle(json, "Latency p95 by tool").targets as Json[])[0].expr).toBe(
267
331
  'histogram_quantile(0.95, sum by (le, gen_ai_tool_name) (rate(genai_duration_seconds_bucket{service_name=~"$service", gen_ai_request_model=~"$model", gen_ai_tool_name!=""}[$__rate_interval])))',
268
332
  );
@@ -3,10 +3,12 @@
3
3
  * read, so renaming a metric at its source moves it in the panels.
4
4
  */
5
5
 
6
- export { RedDashboard, redQueries } from "./red-dashboard";
7
- export type { RedDashboardProps, RedDashboardMembers, RedDashboardInstance } from "./red-dashboard";
6
+ export { RedDashboard, redQueries, RED_DEFAULT_SPAN_KINDS } from "./red-dashboard";
7
+ export type { RedDashboardProps, RedDashboardMembers, RedDashboardInstance, SpanKind } from "./red-dashboard";
8
8
  export { SloDashboard, sloQueries } from "./slo-dashboard";
9
9
  export type { SloDashboardProps, SloDashboardMembers, SloDashboardInstance } from "./slo-dashboard";
10
10
  export { AgentDashboard, agentQueries } from "./agent-dashboard";
11
11
  export type { AgentDashboardProps, AgentDashboardMembers, AgentDashboardInstance } from "./agent-dashboard";
12
12
  export type { DashboardOptions } from "./shared";
13
+ export { SloAlertRules, sloAlertQueries } from "./slo-alert-rules";
14
+ export type { SloAlertRulesProps, SloAlertRulesMembers, SloAlertRulesInstance } from "./slo-alert-rules";
@@ -0,0 +1,374 @@
1
+ /**
2
+ * The composites' PromQL against real series, through Grafana.
3
+ *
4
+ * Builds the dashboards-from-declarations example (RedDashboard,
5
+ * SloDashboard and AgentDashboard over its spanmetrics connector, its Slo
6
+ * and the GenAI preset), backfills a Prometheus with series named the way
7
+ * those declarations name them, provisions the build into each Grafana
8
+ * release in `GRAFANA_IMAGES` on the same Docker network, and runs every
9
+ * panel's target through `/api/ds/query` the way the dashboard would, with
10
+ * each variable at its "All" value.
11
+ *
12
+ * The series grow at constant rates, so the expected values are exact:
13
+ *
14
+ * - checkout: server spans at 1/s OK and 0.1/s errors, plus client spans
15
+ * erroring at 10/s. Rate 1.1, error ratio 0.1/1.1; the client errors
16
+ * are not counted.
17
+ * - cart: server spans at 2/s, no error series. Rate 2, error ratio 0.
18
+ * - worker: consumer spans at 0.5/s, counted.
19
+ * - frontend: client spans only. Absent from every panel and from the
20
+ * service picker.
21
+ * - idle: a server-span counter that doesn't move. Rate 0, and error ratio
22
+ * 0/0 = NaN, which is what a group with no traffic should give.
23
+ * - agents: two models, two tools, an idle model, token counters; the
24
+ * token totals are one instant value each.
25
+ * - SLO: the recording rules' outputs, seeded as plain series (the rules
26
+ * themselves are the prometheus lexicon's, checked there).
27
+ *
28
+ * Skipped, with the reason in the suite name, without Docker.
29
+ * On demand: `npx vitest run --project e2e lexicons/grafana/src/composites/queries.e2e.test.ts`.
30
+ */
31
+ import { mkdirSync, writeFileSync } from "node:fs";
32
+ import { dirname, join } from "node:path";
33
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
34
+ import { build } from "@intentius/chant/build";
35
+ import type { SerializerResult } from "@intentius/chant/serializer";
36
+ import { otelSerializer } from "@intentius/chant-lexicon-otel/serializer";
37
+ import { prometheusSerializer } from "@intentius/chant-lexicon-prometheus/serializer";
38
+ import { grafanaSerializer } from "../serializer";
39
+ import { DockerScope, GRAFANA_IMAGES, dockerAvailable, waitFor, type GrafanaContainer } from "../../test/e2e/containers";
40
+ import { dsQuery, lastBy, type Target } from "../../test/e2e/ds-query";
41
+ import { openMetrics, type Family } from "../../test/e2e/openmetrics";
42
+
43
+ const hasDocker = dockerAvailable();
44
+ const skipReason = hasDocker ? "" : " (skipped: Docker is not running)";
45
+ const example = join(import.meta.dirname, "..", "..", "examples", "dashboards-from-declarations", "src");
46
+
47
+ const STEP = 15;
48
+ /** The last sample, on a step boundary a minute back so no query reaches past the data. */
49
+ const END = Math.floor(Date.now() / 1000 / STEP) * STEP - 60;
50
+ const START = END - 3600;
51
+ /** Every query covers the last 30 minutes of the data. */
52
+ const FROM_MS = (END - 1800) * 1000;
53
+ const TO_MS = END * 1000;
54
+ const RANGE_S = 1800;
55
+
56
+ const SERVER = "SPAN_KIND_SERVER";
57
+ const CLIENT = "SPAN_KIND_CLIENT";
58
+ const CONSUMER = "SPAN_KIND_CONSUMER";
59
+ const OK = "STATUS_CODE_UNSET";
60
+ const ERROR = "STATUS_CODE_ERROR";
61
+
62
+ const span = (service: string, kind: string, status: string) => ({ service_name: service, span_name: `${service}-op`, span_kind: kind, status_code: status });
63
+
64
+ const agent = (model: string, tool: string, status: string, errorType = "") => ({
65
+ service_name: "agent",
66
+ gen_ai_request_model: model,
67
+ ...(tool ? { gen_ai_tool_name: tool } : {}),
68
+ status_code: status,
69
+ ...(errorType ? { error_type: errorType } : {}),
70
+ });
71
+
72
+ const FAMILIES: Family[] = [
73
+ {
74
+ type: "counter",
75
+ name: "shop_calls",
76
+ series: [
77
+ { labels: span("checkout", SERVER, OK), perSecond: 1 },
78
+ { labels: span("checkout", SERVER, ERROR), perSecond: 0.1 },
79
+ { labels: span("checkout", CLIENT, ERROR), perSecond: 10 },
80
+ { labels: span("cart", SERVER, OK), perSecond: 2 },
81
+ { labels: span("worker", CONSUMER, OK), perSecond: 0.5 },
82
+ { labels: span("frontend", CLIENT, OK), perSecond: 3 },
83
+ { labels: span("frontend", CLIENT, ERROR), perSecond: 1 },
84
+ { labels: span("idle", SERVER, OK), perSecond: 0 },
85
+ ],
86
+ },
87
+ {
88
+ type: "histogram",
89
+ name: "shop_duration_milliseconds",
90
+ buckets: [5, 10, 25, 50, 100, 250, 500, 1000],
91
+ series: [
92
+ { labels: span("checkout", SERVER, OK), perSecond: 1.1, within: 100 },
93
+ { labels: span("checkout", CLIENT, ERROR), perSecond: 10, within: 1000 },
94
+ { labels: span("cart", SERVER, OK), perSecond: 2, within: 10 },
95
+ { labels: span("worker", CONSUMER, OK), perSecond: 0.5, within: 250 },
96
+ { labels: span("frontend", CLIENT, OK), perSecond: 4, within: 500 },
97
+ ],
98
+ },
99
+ {
100
+ type: "counter",
101
+ name: "agents_calls",
102
+ series: [
103
+ { labels: agent("big", "", OK), perSecond: 1 },
104
+ { labels: agent("big", "", ERROR, "timeout"), perSecond: 0.25 },
105
+ { labels: agent("big", "search", OK), perSecond: 0.2 },
106
+ { labels: agent("big", "search", ERROR, "tool_error"), perSecond: 0.05 },
107
+ { labels: agent("small", "", OK), perSecond: 0.5 },
108
+ { labels: agent("small", "fetch", OK), perSecond: 0.1 },
109
+ { labels: agent("idle-model", "", OK), perSecond: 0 },
110
+ ],
111
+ },
112
+ {
113
+ type: "histogram",
114
+ name: "agents_duration_seconds",
115
+ buckets: [0.1, 0.5, 1, 2.5, 5],
116
+ series: [
117
+ { labels: { service_name: "agent", gen_ai_request_model: "big" }, perSecond: 1.25, within: 2.5 },
118
+ { labels: { service_name: "agent", gen_ai_request_model: "big", gen_ai_tool_name: "search" }, perSecond: 0.25, within: 2.5 },
119
+ { labels: { service_name: "agent", gen_ai_request_model: "small" }, perSecond: 0.5, within: 0.5 },
120
+ { labels: { service_name: "agent", gen_ai_request_model: "small", gen_ai_tool_name: "fetch" }, perSecond: 0.1, within: 0.5 },
121
+ ],
122
+ },
123
+ {
124
+ type: "counter",
125
+ name: "agents_tokens_input",
126
+ series: [
127
+ { labels: { gen_ai_request_model: "big" }, perSecond: 100 },
128
+ { labels: { gen_ai_request_model: "small" }, perSecond: 10 },
129
+ ],
130
+ },
131
+ {
132
+ type: "counter",
133
+ name: "agents_tokens_output",
134
+ series: [
135
+ { labels: { gen_ai_request_model: "big" }, perSecond: 20 },
136
+ { labels: { gen_ai_request_model: "small" }, perSecond: 5 },
137
+ ],
138
+ },
139
+ ...(
140
+ [
141
+ ["slo:sli_error:ratio_rate30d", 0.0005],
142
+ ["slo:objective:ratio", 0.999],
143
+ ["slo:error_budget:remaining", 0.5],
144
+ ["slo:sli_error:ratio_rate5m", 0.004],
145
+ ["slo:sli_error:ratio_rate30m", 0.003],
146
+ ["slo:sli_error:ratio_rate1h", 0.002],
147
+ ["slo:sli_error:ratio_rate2h", 0.0015],
148
+ ["slo:sli_error:ratio_rate6h", 0.001],
149
+ ["slo:sli_error:ratio_rate1d", 0.0008],
150
+ ["slo:sli_error:ratio_rate3d", 0.0006],
151
+ ] as const
152
+ ).map(([name, value]): Family => ({ type: "gauge", name, series: [{ labels: { slo: "checkout" }, value }] })),
153
+ ];
154
+
155
+ /** `histogram_quantile` inside the bucket `(lower, upper]` that holds every observation. */
156
+ const interpolated = (q: number, lower: number, upper: number) => lower + q * (upper - lower);
157
+
158
+ interface Panel {
159
+ title: string;
160
+ type: string;
161
+ targets?: Target[];
162
+ panels?: Panel[];
163
+ }
164
+
165
+ interface Built {
166
+ uid: string;
167
+ templating: { list: Array<{ name: string; query: string | { query: string }; allValue?: string }> };
168
+ panels: Panel[];
169
+ }
170
+
171
+ function panelsOf(d: Built): Panel[] {
172
+ const out: Panel[] = [];
173
+ const walk = (ps: Panel[]) => {
174
+ for (const p of ps) {
175
+ if (p.panels) walk(p.panels);
176
+ if (p.targets) out.push(p);
177
+ }
178
+ };
179
+ walk(d.panels);
180
+ return out;
181
+ }
182
+
183
+ /** Each variable at its "All" value, as the dashboard opens. */
184
+ function allValues(d: Built): Record<string, string> {
185
+ return Object.fromEntries(d.templating.list.map((v) => [v.name, v.allValue ?? ".*"]));
186
+ }
187
+
188
+ describe.skipIf(!hasDocker)(`composite queries against a seeded Prometheus, through Grafana${skipReason}`, () => {
189
+ const scope = new DockerScope("grafana-queries");
190
+ const dashboards = new Map<string, Built>();
191
+ let network = "";
192
+ let dir = "";
193
+
194
+ beforeAll(async () => {
195
+ const result = await build(example, [otelSerializer, prometheusSerializer, grafanaSerializer]);
196
+ expect(result.errors).toEqual([]);
197
+ const out = result.outputs.get("grafana") as SerializerResult;
198
+ dir = scope.tempDir();
199
+ for (const [file, content] of Object.entries(out.files ?? {})) {
200
+ mkdirSync(dirname(join(dir, file)), { recursive: true });
201
+ writeFileSync(join(dir, file), content);
202
+ if (file.endsWith(".json")) {
203
+ const d = JSON.parse(content) as Built;
204
+ dashboards.set(d.uid, d);
205
+ }
206
+ }
207
+ network = scope.network();
208
+ await scope.prometheus({ network, alias: "prometheus", openMetrics: openMetrics({ start: START, end: END, step: STEP }, FAMILIES) });
209
+ }, 300_000);
210
+
211
+ afterAll(() => scope.cleanup());
212
+
213
+ describe.each(GRAFANA_IMAGES)("%s", (image) => {
214
+ let grafana: GrafanaContainer;
215
+ const dashboard = (uid: string) => {
216
+ const d = dashboards.get(uid);
217
+ if (!d) throw new Error(`the example built no dashboard ${uid}; it built ${[...dashboards.keys()].join(", ")}`);
218
+ return d;
219
+ };
220
+ const panel = (uid: string, title: string, nth = 0) => {
221
+ const p = panelsOf(dashboard(uid)).filter((x) => x.title === title)[nth];
222
+ if (!p) throw new Error(`${uid} has no panel ${title}`);
223
+ return p;
224
+ };
225
+ const run = async (uid: string, target: Target) => {
226
+ const r = await dsQuery(grafana, target, { from: FROM_MS, to: TO_MS, vars: allValues(dashboard(uid)) });
227
+ expect(r.error, `${target.expr}\n${r.error}`).toBeUndefined();
228
+ return r;
229
+ };
230
+ const runPanel = async (uid: string, title: string, nth = 0) => run(uid, panel(uid, title, nth).targets![0]);
231
+
232
+ beforeAll(async () => {
233
+ grafana = await scope.grafana({ image, network, provisioningDir: join(dir, "provisioning"), dashboardsDir: join(dir, "dashboards") });
234
+ // The datasource reaches the seeded Prometheus before any panel runs.
235
+ await waitFor("Grafana to reach Prometheus", async () => {
236
+ const r = await grafana.api("/api/datasources/uid/prometheus/health");
237
+ return r.status === 200 && r.body?.status === "OK" ? true : undefined;
238
+ });
239
+ }, 300_000);
240
+
241
+ it("every panel query of every composite dashboard runs, and returns data", { timeout: 120_000 }, async () => {
242
+ for (const [uid, d] of dashboards) {
243
+ for (const p of panelsOf(d)) {
244
+ for (const t of p.targets!) {
245
+ const r = await run(uid, t);
246
+ expect(r.series.length, `${uid} / ${p.title} / ${t.refId}: ${t.expr}`).toBeGreaterThan(0);
247
+ }
248
+ }
249
+ }
250
+ });
251
+
252
+ describe("RedDashboard", () => {
253
+ it("rate counts server and consumer spans only; client-only services are absent", async () => {
254
+ const rate = lastBy((await runPanel("red-shop", "Rate")).series, "service_name");
255
+ expect(Object.keys(rate).sort()).toEqual(["cart", "checkout", "idle", "worker"]);
256
+ expect(rate.checkout).toBeCloseTo(1.1, 6);
257
+ expect(rate.cart).toBeCloseTo(2, 6);
258
+ expect(rate.worker).toBeCloseTo(0.5, 6);
259
+ expect(rate.idle).toBe(0);
260
+ });
261
+
262
+ it("error ratio ignores client errors, reads 0 for a service with calls and no errors, and NaN for one with no traffic", async () => {
263
+ const ratio = lastBy((await runPanel("red-shop", "Errors")).series, "service_name");
264
+ expect(Object.keys(ratio).sort()).toEqual(["cart", "checkout", "idle", "worker"]);
265
+ expect(ratio.checkout).toBeCloseTo(0.1 / 1.1, 6);
266
+ expect(ratio.cart).toBe(0);
267
+ expect(ratio.worker).toBe(0);
268
+ expect(ratio.idle).toBeNaN();
269
+ });
270
+
271
+ it("duration quantiles come from server and consumer spans only", async () => {
272
+ for (const [title, q] of [
273
+ ["Duration p50", 0.5],
274
+ ["Duration p95", 0.95],
275
+ ["Duration p99", 0.99],
276
+ ] as const) {
277
+ const d = lastBy((await runPanel("red-shop", title)).series, "service_name");
278
+ expect(Object.keys(d).sort(), title).toEqual(["cart", "checkout", "worker"]);
279
+ expect(d.checkout, title).toBeCloseTo(interpolated(q, 50, 100), 6);
280
+ expect(d.cart, title).toBeCloseTo(interpolated(q, 5, 10), 6);
281
+ expect(d.worker, title).toBeCloseTo(interpolated(q, 100, 250), 6);
282
+ }
283
+ });
284
+
285
+ it("the service picker lists the services that serve, not the client-only one", async () => {
286
+ const v = dashboard("red-shop").templating.list.find((x) => x.name === "service")!;
287
+ const query = typeof v.query === "string" ? v.query : v.query.query;
288
+ const m = /^label_values\((.*), (\w+)\)$/.exec(query);
289
+ expect(m, query).not.toBeNull();
290
+ const params = new URLSearchParams({ "match[]": m![1], start: String(FROM_MS / 1000), end: String(TO_MS / 1000) });
291
+ const r = await grafana.api(`/api/datasources/uid/prometheus/resources/api/v1/label/${m![2]}/values?${params}`);
292
+ expect(r.status).toBe(200);
293
+ expect([...r.body.data].sort()).toEqual(["cart", "checkout", "idle", "worker"]);
294
+ });
295
+ });
296
+
297
+ describe("AgentDashboard", () => {
298
+ it("calls, errors and latency per model", async () => {
299
+ const calls = lastBy((await runPanel("agents-agents", "Calls by model")).series, "gen_ai_request_model");
300
+ expect(calls.big).toBeCloseTo(1.5, 6);
301
+ expect(calls.small).toBeCloseTo(0.6, 6);
302
+ expect(calls["idle-model"]).toBe(0);
303
+ const errors = lastBy((await runPanel("agents-agents", "Errors by model")).series, "gen_ai_request_model");
304
+ expect(errors.big).toBeCloseTo(0.3 / 1.5, 6);
305
+ expect(errors.small).toBe(0);
306
+ expect(errors["idle-model"]).toBeNaN();
307
+ const latency = lastBy((await runPanel("agents-agents", "Latency p95 by model")).series, "gen_ai_request_model");
308
+ expect(latency.big).toBeCloseTo(interpolated(0.95, 1, 2.5), 6);
309
+ expect(latency.small).toBeCloseTo(interpolated(0.95, 0.1, 0.5), 6);
310
+ });
311
+
312
+ it("calls, errors and latency per tool, leaving out calls with no tool", async () => {
313
+ const calls = lastBy((await runPanel("agents-agents", "Tool calls")).series, "gen_ai_tool_name");
314
+ expect(Object.keys(calls).sort()).toEqual(["fetch", "search"]);
315
+ expect(calls.search).toBeCloseTo(0.25, 6);
316
+ expect(calls.fetch).toBeCloseTo(0.1, 6);
317
+ const errors = lastBy((await runPanel("agents-agents", "Errors by tool")).series, "gen_ai_tool_name");
318
+ expect(errors.search).toBeCloseTo(0.2, 6);
319
+ expect(errors.fetch).toBe(0);
320
+ const latency = lastBy((await runPanel("agents-agents", "Latency p95 by tool")).series, "gen_ai_tool_name");
321
+ expect(Object.keys(latency).sort()).toEqual(["fetch", "search"]);
322
+ expect(latency.search).toBeCloseTo(interpolated(0.95, 1, 2.5), 6);
323
+ expect(latency.fetch).toBeCloseTo(interpolated(0.95, 0.1, 0.5), 6);
324
+ });
325
+
326
+ it("errors by type", async () => {
327
+ const byType = lastBy((await runPanel("agents-agents", "Errors by type")).series, "error_type");
328
+ expect(Object.keys(byType).sort()).toEqual(["timeout", "tool_error"]);
329
+ expect(byType.timeout).toBeCloseTo(0.25, 6);
330
+ expect(byType.tool_error).toBeCloseTo(0.05, 6);
331
+ });
332
+
333
+ it("token rates per model", async () => {
334
+ const input = lastBy((await runPanel("agents-agents", "Input tokens by model")).series, "gen_ai_request_model");
335
+ expect(input.big).toBeCloseTo(100, 6);
336
+ expect(input.small).toBeCloseTo(10, 6);
337
+ const output = lastBy((await runPanel("agents-agents", "Output tokens by model")).series, "gen_ai_request_model");
338
+ expect(output.big).toBeCloseTo(20, 6);
339
+ expect(output.small).toBeCloseTo(5, 6);
340
+ });
341
+
342
+ it("token totals come back as one instant value over the dashboard's range", async () => {
343
+ for (const [title, perSecond] of [
344
+ ["Input tokens", 110],
345
+ ["Output tokens", 25],
346
+ ] as const) {
347
+ const r = await runPanel("agents-agents", title);
348
+ expect(r.series.length, title).toBe(1);
349
+ expect(r.series[0].values.length, title).toBe(1);
350
+ expect(r.series[0].values[0]!, title).toBeCloseTo(perSecond * RANGE_S, 3);
351
+ }
352
+ });
353
+ });
354
+
355
+ describe("SloDashboard", () => {
356
+ it("reads the recorded SLI, objective, budget and burn rates", async () => {
357
+ const last = async (title: string, nth = 0, refId = "A") => {
358
+ const p = panel("slo-checkout", title, nth);
359
+ const r = await run("slo-checkout", p.targets!.find((t) => t.refId === refId)!);
360
+ expect(r.series.length, `${title} ${refId}`).toBe(1);
361
+ return [...r.series[0].values].reverse().find((v) => v !== null)!;
362
+ };
363
+ expect(await last("SLI over 30d")).toBeCloseTo(0.9995, 9);
364
+ expect(await last("Objective")).toBeCloseTo(0.999, 9);
365
+ expect(await last("Error budget remaining")).toBeCloseTo(0.5, 9);
366
+ // No alert is firing, so `or vector(0)` gives 0.
367
+ expect(await last("Burn-rate alerts firing")).toBe(0);
368
+ expect(await last("Burn rate 1h / 5m (page)", 0, "A")).toBeCloseTo(2, 6);
369
+ expect(await last("Burn rate 1h / 5m (page)", 0, "B")).toBeCloseTo(4, 6);
370
+ expect(await last("Burn rate 3d / 6h (ticket)", 0, "A")).toBeCloseTo(0.6, 6);
371
+ });
372
+ });
373
+ });
374
+ });