@intentius/chant-lexicon-grafana 0.99.0 → 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +12 -6
  2. package/dist/annotations.d.ts +61 -0
  3. package/dist/annotations.d.ts.map +1 -0
  4. package/dist/api/apply.d.ts +8 -4
  5. package/dist/api/apply.d.ts.map +1 -1
  6. package/dist/api/fake-grafana.d.ts +2 -0
  7. package/dist/api/fake-grafana.d.ts.map +1 -1
  8. package/dist/api/folders.d.ts +53 -17
  9. package/dist/api/folders.d.ts.map +1 -1
  10. package/dist/build.d.ts +26 -5
  11. package/dist/build.d.ts.map +1 -1
  12. package/dist/catalog.d.ts +1 -1
  13. package/dist/catalog.d.ts.map +1 -1
  14. package/dist/codegen/docs.d.ts.map +1 -1
  15. package/dist/codegen/generate.d.ts +7 -1
  16. package/dist/codegen/generate.d.ts.map +1 -1
  17. package/dist/composites/shared.d.ts +4 -3
  18. package/dist/composites/shared.d.ts.map +1 -1
  19. package/dist/dashboard.d.ts +42 -14
  20. package/dist/dashboard.d.ts.map +1 -1
  21. package/dist/datasource-refs.d.ts +9 -5
  22. package/dist/datasource-refs.d.ts.map +1 -1
  23. package/dist/datasource-settings.d.ts +834 -0
  24. package/dist/datasource-settings.d.ts.map +1 -0
  25. package/dist/datasource.d.ts +53 -11
  26. package/dist/datasource.d.ts.map +1 -1
  27. package/dist/deep-observe-hooks.d.ts +1 -1
  28. package/dist/deep-observe-hooks.d.ts.map +1 -1
  29. package/dist/deep-observe.d.ts +9 -4
  30. package/dist/deep-observe.d.ts.map +1 -1
  31. package/dist/describe-resources.d.ts +1 -0
  32. package/dist/describe-resources.d.ts.map +1 -1
  33. package/dist/detect.d.ts +4 -2
  34. package/dist/detect.d.ts.map +1 -1
  35. package/dist/folder.d.ts +68 -0
  36. package/dist/folder.d.ts.map +1 -0
  37. package/dist/import/generator.d.ts.map +1 -1
  38. package/dist/import/mappings.d.ts +12 -6
  39. package/dist/import/mappings.d.ts.map +1 -1
  40. package/dist/import/model.d.ts +7 -0
  41. package/dist/import/model.d.ts.map +1 -1
  42. package/dist/import/normalize.d.ts.map +1 -1
  43. package/dist/import/parser.d.ts +16 -1
  44. package/dist/import/parser.d.ts.map +1 -1
  45. package/dist/import/testdata/fixtures.d.ts +6 -0
  46. package/dist/import/testdata/fixtures.d.ts.map +1 -1
  47. package/dist/import/v2.d.ts +4 -3
  48. package/dist/import/v2.d.ts.map +1 -1
  49. package/dist/index.d.ts +11 -9
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/init-templates.d.ts +27 -0
  52. package/dist/init-templates.d.ts.map +1 -0
  53. package/dist/integrity.json +11 -9
  54. package/dist/lint/audit-catalog.d.ts +1 -1
  55. package/dist/lint/audit-catalog.d.ts.map +1 -1
  56. package/dist/lint/post-synth/graf101.d.ts +1 -1
  57. package/dist/lint/post-synth/graf104.d.ts +2 -2
  58. package/dist/lint/post-synth/graf106.d.ts +2 -2
  59. package/dist/lint/post-synth/graf110.d.ts +8 -0
  60. package/dist/lint/post-synth/graf110.d.ts.map +1 -0
  61. package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -1
  62. package/dist/lint/post-synth/index.d.ts.map +1 -1
  63. package/dist/manifest.json +1 -1
  64. package/dist/meta.json +70 -0
  65. package/dist/okf/index.md +18 -3
  66. package/dist/okf/rules/GRAF101.md +2 -2
  67. package/dist/okf/rules/GRAF104.md +2 -2
  68. package/dist/okf/rules/GRAF106.md +2 -2
  69. package/dist/okf/rules/GRAF110.md +11 -0
  70. package/dist/okf/types/AdhocVariable.md +9 -0
  71. package/dist/okf/types/AzureMonitorQuery.md +9 -0
  72. package/dist/okf/types/BigQueryQuery.md +9 -0
  73. package/dist/okf/types/CloudMonitoringQuery.md +9 -0
  74. package/dist/okf/types/CloudWatchQuery.md +9 -0
  75. package/dist/okf/types/Dashboard.md +1 -1
  76. package/dist/okf/types/Datasource.md +1 -1
  77. package/dist/okf/types/DatasourceProvisioning.md +9 -0
  78. package/dist/okf/types/ElasticsearchQuery.md +9 -0
  79. package/dist/okf/types/ExternalDatasource.md +1 -1
  80. package/dist/okf/types/Folder.md +9 -0
  81. package/dist/okf/types/GroupByVariable.md +9 -0
  82. package/dist/okf/types/MSSQLQuery.md +9 -0
  83. package/dist/okf/types/MySQLQuery.md +9 -0
  84. package/dist/okf/types/PostgresQuery.md +9 -0
  85. package/dist/okf/types/PyroscopeQuery.md +9 -0
  86. package/dist/okf/types/SwitchVariable.md +9 -0
  87. package/dist/op/activities/grafana-apply.d.ts +11 -0
  88. package/dist/op/activities/grafana-apply.d.ts.map +1 -1
  89. package/dist/op/activities/index.d.ts +1 -1
  90. package/dist/op/activities/index.d.ts.map +1 -1
  91. package/dist/ownership.d.ts +11 -0
  92. package/dist/ownership.d.ts.map +1 -1
  93. package/dist/pin.d.ts +1 -1
  94. package/dist/pin.d.ts.map +1 -1
  95. package/dist/plugin.d.ts.map +1 -1
  96. package/dist/promql-check.d.ts.map +1 -1
  97. package/dist/query-models.d.ts +95 -0
  98. package/dist/query-models.d.ts.map +1 -0
  99. package/dist/query.d.ts +107 -10
  100. package/dist/query.d.ts.map +1 -1
  101. package/dist/rules/graf101.ts +2 -2
  102. package/dist/rules/graf104.ts +3 -3
  103. package/dist/rules/graf106.ts +3 -3
  104. package/dist/rules/graf110.ts +17 -0
  105. package/dist/rules/grafana-helpers.ts +13 -3
  106. package/dist/schema/azuremonitor.gen.d.ts +354 -0
  107. package/dist/schema/azuremonitor.gen.d.ts.map +1 -0
  108. package/dist/schema/bigquery.gen.d.ts +93 -0
  109. package/dist/schema/bigquery.gen.d.ts.map +1 -0
  110. package/dist/schema/cloudwatch.gen.d.ts +394 -0
  111. package/dist/schema/cloudwatch.gen.d.ts.map +1 -0
  112. package/dist/schema/elasticsearch.gen.d.ts +444 -0
  113. package/dist/schema/elasticsearch.gen.d.ts.map +1 -0
  114. package/dist/schema/googlecloudmonitoring.gen.d.ts +312 -0
  115. package/dist/schema/googlecloudmonitoring.gen.d.ts.map +1 -0
  116. package/dist/schema/grafanapyroscope.gen.d.ts +60 -0
  117. package/dist/schema/grafanapyroscope.gen.d.ts.map +1 -0
  118. package/dist/schema/index.d.ts +6 -0
  119. package/dist/schema/index.d.ts.map +1 -1
  120. package/dist/schema/table.gen.d.ts +13 -0
  121. package/dist/schema/table.gen.d.ts.map +1 -1
  122. package/dist/schema-validate.d.ts +9 -0
  123. package/dist/schema-validate.d.ts.map +1 -1
  124. package/dist/skill-defs.d.ts.map +1 -1
  125. package/dist/skills/chant-grafana-operations.md +108 -0
  126. package/dist/skills/chant-grafana.md +5 -4
  127. package/dist/spec/schemas.d.ts +9 -7
  128. package/dist/spec/schemas.d.ts.map +1 -1
  129. package/dist/spec/schemas.gen.d.ts +3 -0
  130. package/dist/spec/schemas.gen.d.ts.map +1 -0
  131. package/dist/validate-alerting.d.ts.map +1 -1
  132. package/dist/validate-output.d.ts +15 -2
  133. package/dist/validate-output.d.ts.map +1 -1
  134. package/dist/validate.d.ts.map +1 -1
  135. package/dist/validation.d.ts +14 -0
  136. package/dist/validation.d.ts.map +1 -0
  137. package/dist/variables.d.ts +102 -5
  138. package/dist/variables.d.ts.map +1 -1
  139. package/package.json +2 -2
  140. package/src/annotations.ts +84 -0
  141. package/src/api/apply.test.ts +19 -1
  142. package/src/api/apply.ts +18 -8
  143. package/src/api/fake-grafana.ts +6 -2
  144. package/src/api/folders.ts +106 -32
  145. package/src/build.test.ts +142 -6
  146. package/src/build.ts +126 -24
  147. package/src/catalog.ts +20 -2
  148. package/src/codegen/docs.ts +13 -6
  149. package/src/codegen/generate.ts +27 -1
  150. package/src/composites/shared.ts +3 -2
  151. package/src/dashboard.ts +59 -14
  152. package/src/datasource-refs.ts +30 -6
  153. package/src/datasource-settings.test.ts +106 -0
  154. package/src/datasource-settings.ts +941 -0
  155. package/src/datasource.ts +71 -11
  156. package/src/deep-observe-hooks.ts +16 -1
  157. package/src/deep-observe.test.ts +47 -1
  158. package/src/deep-observe.ts +36 -13
  159. package/src/describe-resources.test.ts +16 -0
  160. package/src/describe-resources.ts +36 -1
  161. package/src/detect.ts +3 -3
  162. package/src/folder.ts +118 -0
  163. package/src/generated/lexicon-grafana.json +70 -0
  164. package/src/import/generated-types.e2e.test.ts +15 -3
  165. package/src/import/generator.ts +2 -1
  166. package/src/import/mappings.ts +98 -26
  167. package/src/import/model.ts +7 -0
  168. package/src/import/normalize.ts +32 -3
  169. package/src/import/parser.test.ts +97 -21
  170. package/src/import/parser.ts +169 -18
  171. package/src/import/roundtrip.test.ts +104 -9
  172. package/src/import/testdata/fixtures.ts +8 -0
  173. package/src/import/v2.ts +4 -3
  174. package/src/index.ts +122 -13
  175. package/src/init-templates.test.ts +74 -0
  176. package/src/init-templates.ts +349 -0
  177. package/src/lint/audit-catalog.ts +9 -1
  178. package/src/lint/post-synth/graf101.ts +2 -2
  179. package/src/lint/post-synth/graf104.ts +3 -3
  180. package/src/lint/post-synth/graf106.ts +3 -3
  181. package/src/lint/post-synth/graf110.ts +17 -0
  182. package/src/lint/post-synth/grafana-helpers.ts +13 -3
  183. package/src/lint/post-synth/index.ts +2 -0
  184. package/src/lint/post-synth/post-synth.test.ts +99 -2
  185. package/src/load-cost.test.ts +35 -0
  186. package/src/op/activities/grafana-apply.e2e.test.ts +14 -5
  187. package/src/op/activities/grafana-apply.test.ts +16 -2
  188. package/src/op/activities/grafana-apply.ts +29 -3
  189. package/src/op/activities/index.ts +1 -1
  190. package/src/ownership.ts +19 -0
  191. package/src/panels.test.ts +3 -1
  192. package/src/pin.ts +12 -0
  193. package/src/plugin.test.ts +9 -2
  194. package/src/plugin.ts +9 -0
  195. package/src/promql-check.ts +5 -1
  196. package/src/query-models.ts +94 -0
  197. package/src/query.test.ts +166 -0
  198. package/src/query.ts +177 -12
  199. package/src/schema/azuremonitor.gen.ts +381 -0
  200. package/src/schema/bigquery.gen.ts +110 -0
  201. package/src/schema/cloudwatch.gen.ts +423 -0
  202. package/src/schema/elasticsearch.gen.ts +508 -0
  203. package/src/schema/googlecloudmonitoring.gen.ts +330 -0
  204. package/src/schema/grafanapyroscope.gen.ts +66 -0
  205. package/src/schema/index.ts +6 -0
  206. package/src/schema/table.gen.ts +14 -0
  207. package/src/schema-validate.ts +58 -7
  208. package/src/skill-defs.ts +17 -0
  209. package/src/skills/chant-grafana-operations.md +108 -0
  210. package/src/skills/chant-grafana.md +5 -4
  211. package/src/spec/overlay/table.overlay.json +19 -0
  212. package/src/spec/schemas/azuremonitor.jsonschema.json +679 -0
  213. package/src/spec/schemas/bigquery.jsonschema.json +290 -0
  214. package/src/spec/schemas/cloudwatch.jsonschema.json +721 -0
  215. package/src/spec/schemas/elasticsearch.jsonschema.json +1695 -0
  216. package/src/spec/schemas/googlecloudmonitoring.jsonschema.json +488 -0
  217. package/src/spec/schemas/grafanapyroscope.jsonschema.json +93 -0
  218. package/src/spec/schemas.gen.ts +40 -0
  219. package/src/spec/schemas.ts +9 -7
  220. package/src/validate-alerting.ts +4 -2
  221. package/src/validate-output.ts +96 -4
  222. package/src/validate.ts +14 -0
  223. package/src/validation.ts +27 -0
  224. package/src/variables.ts +113 -6
package/src/index.ts CHANGED
@@ -14,6 +14,12 @@ export {
14
14
  isDatasourceEntity,
15
15
  isExternalDatasource,
16
16
  isDatasourceDeclaration,
17
+ DatasourceProvisioning,
18
+ DATASOURCE_PROVISIONING_TYPE,
19
+ isDatasourceProvisioningEntity,
20
+ type DatasourceProvisioningProps,
21
+ type DatasourceProvisioningEntity,
22
+ type DeletedDatasource,
17
23
  type ExternalDatasourceProps,
18
24
  type ExternalDatasourceEntity,
19
25
  type ExternalDatasourceConstructor,
@@ -25,6 +31,64 @@ export {
25
31
  type KnownDatasourceType,
26
32
  } from "./datasource";
27
33
 
34
+ // Typed datasource settings (jsonData, secureJsonData keys) per plugin
35
+ export {
36
+ JSONDATA_LINKS,
37
+ TRACING_DATASOURCE_TYPES,
38
+ TRACE_TO_LOGS_DATASOURCE_TYPES,
39
+ TRACE_TO_METRICS_DATASOURCE_TYPES,
40
+ type JsonDataLink,
41
+ type LinkedDatasource,
42
+ type TracingDatasourceType,
43
+ type TraceToLogsDatasourceType,
44
+ type TraceToMetricsDatasourceType,
45
+ type TypedDatasourceType,
46
+ type DatasourceJsonData,
47
+ type DatasourceSecureJsonData,
48
+ type DatasourceJsonDataTypes,
49
+ type DatasourceSecureJsonKeys,
50
+ type CommonJsonData,
51
+ type HttpJsonData,
52
+ type HttpSecureJsonKey,
53
+ type PrometheusJsonData,
54
+ type ExemplarTraceIdDestination,
55
+ type LokiJsonData,
56
+ type DerivedFieldConfig,
57
+ type TempoJsonData,
58
+ type TraceToLogsTag,
59
+ type TraceToLogsOptions,
60
+ type TraceToLogsOptionsV2,
61
+ type TraceToMetricsOptions,
62
+ type TraceToProfilesOptions,
63
+ type ElasticsearchJsonData,
64
+ type ElasticsearchDataLink,
65
+ type ElasticsearchSecureJsonKey,
66
+ type AwsAuthJsonData,
67
+ type AwsAuthSecureJsonKey,
68
+ type CloudWatchJsonData,
69
+ type CloudWatchLogGroup,
70
+ type CloudWatchSecureJsonKey,
71
+ type AzureCloudName,
72
+ type AzureCredentials,
73
+ type AzureMonitorJsonData,
74
+ type AzureMonitorSecureJsonKey,
75
+ type GoogleAuthJsonData,
76
+ type GoogleAuthSecureJsonKey,
77
+ type CloudMonitoringJsonData,
78
+ type CloudMonitoringSecureJsonKey,
79
+ type BigQueryJsonData,
80
+ type BigQuerySecureJsonKey,
81
+ type PyroscopeJsonData,
82
+ type PyroscopeSecureJsonKey,
83
+ type SqlJsonData,
84
+ type PostgresJsonData,
85
+ type PostgresSecureJsonKey,
86
+ type MySQLJsonData,
87
+ type MySQLSecureJsonKey,
88
+ type MSSQLJsonData,
89
+ type MSSQLSecureJsonKey,
90
+ } from "./datasource-settings";
91
+
28
92
  // Dashboards and provisioning
29
93
  export {
30
94
  Dashboard,
@@ -39,8 +103,14 @@ export {
39
103
  type DashboardLinkInput,
40
104
  type DashboardProviderProps,
41
105
  type DashboardProviderEntity,
106
+ type DashboardEntityProps,
107
+ type DashboardProviderEntityProps,
42
108
  } from "./dashboard";
43
109
 
110
+ // Folders and annotations
111
+ export { Folder, FOLDER_TYPE, isFolderEntity, folderLevels, type FolderProps, type FolderEntity } from "./folder";
112
+ export { annotationJson, ANNOTATION_REF_ID, type AnnotationInput } from "./annotations";
113
+
44
114
  // Grafana-managed alerting
45
115
  export {
46
116
  AlertRuleGroup,
@@ -179,6 +249,15 @@ export {
179
249
  PromQuery,
180
250
  TempoQuery,
181
251
  LokiQuery,
252
+ ElasticsearchQuery,
253
+ CloudWatchQuery,
254
+ AzureMonitorQuery,
255
+ CloudMonitoringQuery,
256
+ BigQueryQuery,
257
+ PyroscopeQuery,
258
+ PostgresQuery,
259
+ MySQLQuery,
260
+ MSSQLQuery,
182
261
  defineQuery,
183
262
  registeredQueries,
184
263
  queryDefinitionFor,
@@ -186,6 +265,8 @@ export {
186
265
  QUERY_TYPE_PREFIX,
187
266
  type DatasourceInput,
188
267
  type QueryModel,
268
+ type Loosen,
269
+ type MergedModel,
189
270
  type QueryProps,
190
271
  type QueryDefinition,
191
272
  type QueryEntity,
@@ -193,8 +274,30 @@ export {
193
274
  type PromQueryEntity,
194
275
  type TempoQueryEntity,
195
276
  type LokiQueryEntity,
277
+ type ElasticsearchQueryEntity,
278
+ type CloudWatchQueryEntity,
279
+ type AzureMonitorQueryEntity,
280
+ type CloudMonitoringQueryEntity,
281
+ type BigQueryQueryEntity,
282
+ type PyroscopeQueryEntity,
283
+ type PostgresQueryEntity,
284
+ type MySQLQueryEntity,
285
+ type MSSQLQueryEntity,
196
286
  } from "./query";
197
287
 
288
+ // Hand-written query models for datasources with no schema
289
+ export type {
290
+ SqlQueryModel,
291
+ SqlBuilderQuery,
292
+ SqlQueryFormat,
293
+ SqlExpressionType,
294
+ SqlProperty,
295
+ SqlFunctionExpression,
296
+ SqlFunctionParameterExpression,
297
+ SqlGroupByExpression,
298
+ SqlPropertyExpression,
299
+ } from "./query-models";
300
+
198
301
  // Variables
199
302
  export {
200
303
  QueryVariable,
@@ -219,6 +322,17 @@ export {
219
322
  type DatasourceVariableProps,
220
323
  type ConstantVariableProps,
221
324
  type TextboxVariableProps,
325
+ AdhocVariable,
326
+ GroupByVariable,
327
+ SwitchVariable,
328
+ MULTI_VALUE_KINDS,
329
+ type AdhocVariableProps,
330
+ type GroupByVariableProps,
331
+ type SwitchVariableProps,
332
+ type AdhocFilter,
333
+ type VariableKeyOption,
334
+ type VariableQueryObject,
335
+ type PrometheusVariableQuery,
222
336
  } from "./variables";
223
337
 
224
338
  // The generated schema types and their pin
@@ -251,7 +365,7 @@ export {
251
365
  type ReducerId,
252
366
  } from "./transformations";
253
367
 
254
- // Plain-data API: render, check and detect without a build
368
+ // Plain-data API: render and detect without a build
255
369
  export {
256
370
  buildGrafana,
257
371
  grafanaFiles,
@@ -267,6 +381,7 @@ export {
267
381
  externalDatasourceRecord,
268
382
  provisionedProvider,
269
383
  datasourcesYaml,
384
+ datasourceFileSettings,
270
385
  dashboardProvidersYaml,
271
386
  GRID_COLUMNS,
272
387
  DATASOURCES_FILE,
@@ -278,24 +393,19 @@ export {
278
393
  type ProvisionedDatasource,
279
394
  type ExternalDatasourceRecord,
280
395
  type ProvisionedProvider,
396
+ type DatasourceFileSettings,
281
397
  type DashboardJson,
282
398
  type PanelJson,
283
399
  type RowPanelJson,
284
400
  type VariableModel,
285
401
  type DataSourceRef,
286
402
  } from "./build";
287
- export {
288
- validateGrafanaOutput,
289
- issuesFor,
290
- knownDatasourcesOf,
291
- variableReferences,
292
- type GrafanaIssue,
293
- type GrafanaIssueCode,
294
- type GrafanaArtifacts,
295
- type DashboardDoc,
296
- } from "./validate-output";
403
+ // The checks as plain functions (validateGrafanaOutput, validateDashboardSchema)
404
+ // are on the `@intentius/chant-lexicon-grafana/validation` subpath (#2958), so
405
+ // declaring dashboards does not load them.
297
406
  export {
298
407
  datasourceUses,
408
+ variableReferences,
299
409
  resolveDatasourceRef,
300
410
  knownDatasources,
301
411
  type DatasourceUse,
@@ -303,7 +413,6 @@ export {
303
413
  type KnownDatasource,
304
414
  type DatasourceRefJson,
305
415
  } from "./datasource-refs";
306
- export { validateDashboardSchema, validateExpressionSchema, type SchemaProblem } from "./schema-validate";
307
416
  export { looksLikeDashboard, looksLikeDatasourceProvisioning, looksLikeDashboardProvisioning, looksLikeAlertingProvisioning } from "./detect";
308
417
  export { slugUid, isValidUid, UID_PATTERN, type DeepPartial, type PropsOf } from "./util";
309
418
 
@@ -357,7 +466,7 @@ export { bindGrafana, classifyGrafanaFailure, GrafanaBindingError } from "./api/
357
466
  export { grafanaApply } from "./op/builders";
358
467
  export { applyGrafana, planFromDashboards, GRAFANA_APPLY_KINDS } from "./api/apply";
359
468
  export type { GrafanaApplyPlan, GrafanaApplyOutcome, DashboardPlan, BuiltDashboardInput } from "./api/apply";
360
- export { folderUidFor, foldersForDashboards, type FolderPlan } from "./api/folders";
469
+ export { folderUidFor, foldersForDashboards, resolveFolders, liveFolderPath, type FolderPlan, type ResolvedFolders } from "./api/folders";
361
470
 
362
471
  // The Grafana RBAC actions a service account needs to observe, apply and prune.
363
472
  export { GrafanaActions, grafanaActionsFor, type GrafanaAccessLevel } from "./actions/index";
@@ -0,0 +1,74 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { build } from "@intentius/chant/build";
5
+ import type { Serializer } from "@intentius/chant/serializer";
6
+ import { lintCommand } from "@intentius/chant/cli/commands/lint";
7
+ import { runPostSynthChecks } from "@intentius/chant/lint/post-synth";
8
+ import { k8sPlugin } from "@intentius/chant-lexicon-k8s";
9
+ import { prometheusPlugin } from "@intentius/chant-lexicon-prometheus";
10
+ import { grafanaPlugin } from "./plugin";
11
+ import { postSynthChecks } from "./lint/post-synth";
12
+
13
+ // Every template builds with the lexicons its chant.config.ts names, and its
14
+ // output passes every GRAF check, and the other lexicon's checks, with
15
+ // nothing to report.
16
+ const cases: Array<[string | undefined, Serializer[], string[]]> = [
17
+ [undefined, [grafanaPlugin.serializer], ["grafana"]],
18
+ ["red", [grafanaPlugin.serializer], ["grafana"]],
19
+ ["k8s-pods", [k8sPlugin.serializer, grafanaPlugin.serializer], ["k8s", "grafana"]],
20
+ ["slo", [prometheusPlugin.serializer, grafanaPlugin.serializer], ["prometheus", "grafana"]],
21
+ ];
22
+
23
+ describe("init templates", () => {
24
+ test.each(cases)("%s builds, lints clean and passes every check", async (name, serializers, outputs) => {
25
+ const set = grafanaPlugin.initTemplates!(name);
26
+ const dir = mkdtempSync(join(import.meta.dirname, "..", ".init-template-"));
27
+ try {
28
+ mkdirSync(join(dir, "src"));
29
+ for (const [file, text] of Object.entries(set.src)) writeFileSync(join(dir, "src", file), text);
30
+ for (const [file, text] of Object.entries(set.root ?? {})) writeFileSync(join(dir, file), text);
31
+ const result = await build(join(dir, "src"), serializers);
32
+ expect(result.errors).toEqual([]);
33
+ for (const lexicon of outputs) expect(result.outputs.get(lexicon), lexicon).toBeTruthy();
34
+
35
+ expect(runPostSynthChecks(postSynthChecks, result)).toEqual([]);
36
+ const other = name === "k8s-pods" ? k8sPlugin : name === "slo" ? prometheusPlugin : undefined;
37
+ if (other) {
38
+ expect(runPostSynthChecks(other.postSynthChecks!(), result)).toEqual([]);
39
+ }
40
+
41
+ const lint = await lintCommand({ path: join(dir, "src"), format: "stylish", fix: false });
42
+ expect(lint.errorCount + lint.warningCount, lint.output).toBe(0);
43
+ } finally {
44
+ rmSync(dir, { recursive: true, force: true });
45
+ }
46
+ });
47
+
48
+ test("the RED template puts its dashboard in the pinned folder and counts served spans only", async () => {
49
+ const dir = mkdtempSync(join(import.meta.dirname, "..", ".init-template-"));
50
+ try {
51
+ mkdirSync(join(dir, "src"));
52
+ for (const [file, text] of Object.entries(grafanaPlugin.initTemplates!("red").src)) writeFileSync(join(dir, "src", file), text);
53
+ const result = await build(join(dir, "src"), [grafanaPlugin.serializer]);
54
+ const out = result.outputs.get("grafana") as { primary: string; files: Record<string, string> };
55
+ const index = JSON.parse(out.primary) as { dashboards: Array<{ uid: string; folderUid?: string }>; datasources: unknown[] };
56
+ expect(index.dashboards).toEqual([expect.objectContaining({ uid: "services-red", folderUid: "observability" })]);
57
+ // An ExternalDatasource is checked against, never provisioned.
58
+ expect(index.datasources).toEqual([]);
59
+ const dashboard = Object.entries(out.files).find(([path]) => path.endsWith("services-red.json"))![1];
60
+ expect(dashboard).toContain("SPAN_KIND_SERVER|SPAN_KIND_CONSUMER");
61
+ } finally {
62
+ rmSync(dir, { recursive: true, force: true });
63
+ }
64
+ });
65
+
66
+ test("the k8s-pods and slo templates build every lexicon their config names", () => {
67
+ for (const name of ["k8s-pods", "slo"]) {
68
+ const set = grafanaPlugin.initTemplates!(name);
69
+ expect(set.root?.["chant.config.ts"]).toContain('"grafana"');
70
+ expect(set.scripts?.build).toBe("chant build src");
71
+ }
72
+ expect(grafanaPlugin.initTemplates!("no-such-template")).toBe(grafanaPlugin.initTemplates!());
73
+ });
74
+ });
@@ -0,0 +1,349 @@
1
+ /**
2
+ * `chant init --lexicon grafana` scaffolds.
3
+ *
4
+ * - default: a Prometheus datasource and a job overview dashboard with a
5
+ * typed variable, in a folder.
6
+ * - `red`: rate, errors and duration per service from the OpenTelemetry
7
+ * collector's span metrics, over a Prometheus Grafana already has
8
+ * (`ExternalDatasource`), in a pinned `Folder`.
9
+ * - `k8s-pods`: CPU, memory, restarts and pod phase per namespace, delivered
10
+ * as ConfigMaps the Grafana Helm chart's sidecar loads, from the `/k8s`
11
+ * subpath. Builds with the k8s lexicon.
12
+ * - `slo`: an SLO's recording rules, its dashboard and its burn-rate alerts
13
+ * as Grafana-managed rules. Builds with the prometheus lexicon.
14
+ *
15
+ * The otel, prometheus and k8s lexicons these import are dependencies of
16
+ * the grafana lexicon, so they are installed with it. The templates that
17
+ * build with a second lexicon ship their own `chant.config.ts` and a build
18
+ * script without `--lexicon`, so every lexicon's output and checks run.
19
+ */
20
+ import type { InitTemplateSet } from "@intentius/chant/lexicon";
21
+
22
+ // ── default ────────────────────────────────────────────────────────────
23
+
24
+ const DEFAULT_DATASOURCES = `/**
25
+ * The datasource the dashboards query, declared in the same build root so
26
+ * GRAF101 and GRAF102 check every panel's reference against it.
27
+ */
28
+ import { Datasource } from "@intentius/chant-lexicon-grafana";
29
+
30
+ const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090", isDefault: true });
31
+
32
+ export { prometheus };
33
+ `;
34
+
35
+ const DEFAULT_DASHBOARD = `/**
36
+ * One dashboard: which of a job's targets are up, and how long their
37
+ * scrapes take. Panels without a gridPos are laid out left to right.
38
+ */
39
+ import { Dashboard, PromQuery, QueryVariable, StatPanel, TimeSeriesPanel } from "@intentius/chant-lexicon-grafana";
40
+ import { prometheus } from "./datasources";
41
+
42
+ const jobQuery = { query: "label_values(up, job)", qryType: 1 as const, label: "job", metric: "up" };
43
+ const job = new QueryVariable({ name: "job", label: "Job", datasource: prometheus, query: jobQuery, refresh: "onTimeRangeChange", sort: 1 });
44
+
45
+ const up = new PromQuery({ expr: 'sum(up{job="$job"})', instant: true });
46
+ const targetsUp = new StatPanel({ title: "Targets up", datasource: prometheus, targets: [up] });
47
+
48
+ const scrape = new PromQuery({ expr: 'avg by (instance) (scrape_duration_seconds{job="$job"})', legendFormat: "{{instance}}" });
49
+ const seconds = { defaults: { unit: "s" } };
50
+ const scrapeDuration = new TimeSeriesPanel({ title: "Scrape duration", datasource: prometheus, targets: [scrape], fieldConfig: seconds });
51
+
52
+ const lastHour = { from: "now-1h", to: "now" };
53
+
54
+ const overview = new Dashboard({
55
+ title: "Overview",
56
+ uid: "overview",
57
+ tags: ["chant"],
58
+ time: lastHour,
59
+ refresh: "30s",
60
+ variables: [job],
61
+ panels: [targetsUp, scrapeDuration],
62
+ folder: "Services",
63
+ });
64
+
65
+ export { overview };
66
+ `;
67
+
68
+ // ── red ────────────────────────────────────────────────────────────────
69
+
70
+ const RED_DATASOURCES = `/**
71
+ * The Prometheus the dashboard reads, one Grafana already has. It is checked
72
+ * against (GRAF101, GRAF102) and never provisioned: set \`uid\` to its uid,
73
+ * from Connections > Data sources in Grafana.
74
+ */
75
+ import { ExternalDatasource } from "@intentius/chant-lexicon-grafana";
76
+
77
+ const prometheus = new ExternalDatasource({ type: "prometheus", uid: "prometheus", name: "Prometheus" });
78
+
79
+ export { prometheus };
80
+ `;
81
+
82
+ const RED_DASHBOARD = `/**
83
+ * Rate, errors and duration per service, from the span metrics the
84
+ * OpenTelemetry collector's \`spanmetrics\` connector writes. The metric and
85
+ * label names come from the connector's settings, so change them here to
86
+ * match your collector (or pass the connector itself, if the collector is
87
+ * declared with the otel lexicon) and every query follows.
88
+ */
89
+ import { Folder, RedDashboard, type SpanKind } from "@intentius/chant-lexicon-grafana";
90
+ import { spanMetricsNames } from "@intentius/chant-lexicon-otel";
91
+ import { prometheus } from "./datasources";
92
+
93
+ const milliseconds = { unit: "ms" };
94
+ const connector = { namespace: "traces.span.metrics", histogram: milliseconds };
95
+ const names = spanMetricsNames(connector);
96
+
97
+ // The spans that serve a request or consume a message, so a service's own
98
+ // outgoing calls don't count toward its rate. [] counts every kind.
99
+ const served: SpanKind[] = ["SPAN_KIND_SERVER", "SPAN_KIND_CONSUMER"];
100
+
101
+ // A Folder pins the uid, for links and alert rules that name it.
102
+ const observability = new Folder({ title: "Observability", uid: "observability" });
103
+
104
+ const services = RedDashboard({ spanMetrics: names, datasource: prometheus, spanKinds: served, folder: observability, uid: "services-red" });
105
+
106
+ export { observability, services };
107
+ `;
108
+
109
+ // ── k8s-pods ───────────────────────────────────────────────────────────
110
+
111
+ const K8S_CONFIG = `import type { ChantConfig } from "@intentius/chant";
112
+
113
+ // The dashboard and the ConfigMaps that deliver it share one build root: the
114
+ // grafana output is checked by the GRAF checks, the ConfigMaps are applied
115
+ // with kubectl.
116
+ export default { lexicons: ["k8s", "grafana"] } satisfies ChantConfig;
117
+ `;
118
+
119
+ const K8S_DATASOURCES = `/**
120
+ * The Prometheus that scrapes cAdvisor and kube-state-metrics, as Grafana
121
+ * reaches it inside the cluster.
122
+ */
123
+ import { Datasource } from "@intentius/chant-lexicon-grafana";
124
+
125
+ const prometheus = new Datasource({
126
+ name: "Prometheus",
127
+ type: "prometheus",
128
+ uid: "prometheus",
129
+ url: "http://prometheus-server.monitoring.svc:80",
130
+ isDefault: true,
131
+ });
132
+
133
+ export { prometheus };
134
+ `;
135
+
136
+ const K8S_VARIABLES = `/**
137
+ * Which namespace, and which of its pods. Queries use them as $namespace
138
+ * and $pod; \`pod\` takes several values, or all of them.
139
+ */
140
+ import { QueryVariable } from "@intentius/chant-lexicon-grafana";
141
+ import { prometheus } from "./datasources";
142
+
143
+ const namespace = new QueryVariable({
144
+ name: "namespace",
145
+ label: "Namespace",
146
+ datasource: prometheus,
147
+ query: "label_values(kube_pod_info, namespace)",
148
+ refresh: "onTimeRangeChange",
149
+ sort: 1,
150
+ });
151
+
152
+ const pod = new QueryVariable({
153
+ name: "pod",
154
+ label: "Pod",
155
+ datasource: prometheus,
156
+ query: 'label_values(kube_pod_info{namespace="$namespace"}, pod)',
157
+ refresh: "onTimeRangeChange",
158
+ sort: 1,
159
+ multi: true,
160
+ includeAll: true,
161
+ });
162
+
163
+ export { namespace, pod };
164
+ `;
165
+
166
+ const K8S_QUERIES = `/**
167
+ * PromQL over cAdvisor (container_*) and kube-state-metrics (kube_*).
168
+ */
169
+ import { PromQuery } from "@intentius/chant-lexicon-grafana";
170
+
171
+ const scope = 'namespace="$namespace", pod=~"$pod"';
172
+
173
+ const cpu = new PromQuery({
174
+ expr: \`sum by (pod) (rate(container_cpu_usage_seconds_total{\${scope}, container!=""}[$__rate_interval]))\`,
175
+ legendFormat: "{{pod}}",
176
+ });
177
+
178
+ const memory = new PromQuery({
179
+ expr: \`sum by (pod) (container_memory_working_set_bytes{\${scope}, container!=""})\`,
180
+ legendFormat: "{{pod}}",
181
+ });
182
+
183
+ const restarts = new PromQuery({
184
+ expr: \`sum by (pod) (increase(kube_pod_container_status_restarts_total{\${scope}}[1h]))\`,
185
+ legendFormat: "{{pod}}",
186
+ });
187
+
188
+ const phase = new PromQuery({
189
+ expr: \`sum by (pod, phase) (kube_pod_status_phase{\${scope}}) > 0\`,
190
+ instant: true,
191
+ format: "table",
192
+ });
193
+
194
+ export { cpu, memory, restarts, phase };
195
+ `;
196
+
197
+ const K8S_DASHBOARD = `/**
198
+ * Pods in a namespace: what they use, how often they restart, and which
199
+ * phase each is in.
200
+ */
201
+ import { Dashboard, TablePanel, TimeSeriesPanel, transformation } from "@intentius/chant-lexicon-grafana";
202
+ import { prometheus } from "./datasources";
203
+ import { namespace, pod } from "./variables";
204
+ import { cpu, memory, restarts, phase } from "./queries";
205
+
206
+ const cores = { defaults: { unit: "suffix: cores" } };
207
+ const cpuPanel = new TimeSeriesPanel({ title: "CPU", datasource: prometheus, targets: [cpu], fieldConfig: cores });
208
+
209
+ const bytes = { defaults: { unit: "bytes" } };
210
+ const memoryPanel = new TimeSeriesPanel({ title: "Memory working set", datasource: prometheus, targets: [memory], fieldConfig: bytes });
211
+
212
+ const count = { defaults: { unit: "short", decimals: 0 } };
213
+ const restartPanel = new TimeSeriesPanel({ title: "Restarts in the last hour", datasource: prometheus, targets: [restarts], fieldConfig: count });
214
+
215
+ // The query returns one row per pod with its phase; drop the time and value columns.
216
+ const hidden = { Time: true, Value: true };
217
+ const renamed = { pod: "Pod", phase: "Phase" };
218
+ const phaseColumns = [transformation("organize", { excludeByName: hidden, renameByName: renamed })];
219
+ const phasePanel = new TablePanel({ title: "Pod phase", datasource: prometheus, targets: [phase], transformations: phaseColumns });
220
+
221
+ const pods = new Dashboard({
222
+ title: "Kubernetes pods",
223
+ uid: "k8s-pods",
224
+ tags: ["chant", "kubernetes"],
225
+ refresh: "30s",
226
+ variables: [namespace, pod],
227
+ panels: [cpuPanel, memoryPanel, restartPanel, phasePanel],
228
+ folder: "Kubernetes",
229
+ });
230
+
231
+ export { pods };
232
+ `;
233
+
234
+ const K8S_DELIVERY = `/**
235
+ * The dashboard and the datasource as ConfigMaps, labelled the way the
236
+ * Grafana Helm chart's sidecar (also in kube-prometheus-stack) finds them:
237
+ * \`grafana_dashboard: "1"\` and \`grafana_datasource: "1"\`, the folder in the
238
+ * \`k8s-sidecar-target-directory\` annotation. \`kubectl apply -f\` the k8s
239
+ * output into the namespace Grafana watches.
240
+ */
241
+ import { GrafanaConfigMaps } from "@intentius/chant-lexicon-grafana/k8s";
242
+ import { prometheus } from "./datasources";
243
+ import { pods } from "./dashboard";
244
+
245
+ /** The namespace Grafana's sidecar watches for ConfigMaps. */
246
+ export const GRAFANA_NAMESPACE = "monitoring";
247
+
248
+ const delivered = [prometheus, pods];
249
+ const labels = { "app.kubernetes.io/part-of": "grafana" };
250
+
251
+ const grafanaConfigMaps = GrafanaConfigMaps({ entities: delivered, namespace: GRAFANA_NAMESPACE, labels });
252
+
253
+ export { grafanaConfigMaps };
254
+ `;
255
+
256
+ // ── slo ────────────────────────────────────────────────────────────────
257
+
258
+ const SLO_CONFIG = `import type { ChantConfig } from "@intentius/chant";
259
+
260
+ // The SLO's Prometheus rules, and the Grafana dashboard and alert rules that
261
+ // read what they record, share one build root: GRAF108 and GRAF112 check the
262
+ // queries against the datasource declared here.
263
+ export default { lexicons: ["prometheus", "grafana"] } satisfies ChantConfig;
264
+ `;
265
+
266
+ const SLO_DATASOURCES = `/**
267
+ * The Prometheus that evaluates the SLO's recording rules.
268
+ */
269
+ import { Datasource } from "@intentius/chant-lexicon-grafana";
270
+
271
+ const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", uid: "prometheus", url: "http://prometheus:9090", isDefault: true });
272
+
273
+ export { prometheus };
274
+ `;
275
+
276
+ const SLO_DECLARATION = `/**
277
+ * The SLO: 99.9% of checkout requests answer without a 5xx over 30 days.
278
+ * The prometheus lexicon builds its recording rules; the dashboard and the
279
+ * alert rules read the series they record, by the same names.
280
+ */
281
+ import { Slo } from "@intentius/chant-lexicon-prometheus";
282
+
283
+ const sli = {
284
+ errors: 'sum(rate(http_requests_total{job="checkout",code=~"5.."}[{{window}}]))',
285
+ total: 'sum(rate(http_requests_total{job="checkout"}[{{window}}]))',
286
+ };
287
+ const team = { team: "payments" };
288
+
289
+ const checkout = Slo({
290
+ name: "checkout",
291
+ objective: 0.999,
292
+ window: "30d",
293
+ description: "Checkout requests answer without a 5xx.",
294
+ sli,
295
+ labels: team,
296
+ });
297
+
298
+ export { checkout };
299
+ `;
300
+
301
+ const SLO_DASHBOARD = `/**
302
+ * The SLO's dashboard (SLI against the objective, error budget left, each
303
+ * burn-rate pair against the rate it fires at), and the same burn rates as
304
+ * Grafana-managed alert rules in the same folder.
305
+ */
306
+ import { SloAlertRules, SloDashboard } from "@intentius/chant-lexicon-grafana";
307
+ import { prometheus } from "./datasources";
308
+ import { checkout } from "./slo";
309
+
310
+ const checkoutSlo = SloDashboard({ slo: checkout, datasource: prometheus, folder: "SLOs" });
311
+
312
+ const team = { team: "payments" };
313
+ const runbook = { runbook_url: "https://runbooks.example.com/checkout-slo" };
314
+ const checkoutBurn = SloAlertRules({ slo: checkout, datasource: prometheus, folder: "SLOs", labels: team, annotations: runbook });
315
+
316
+ export { checkoutSlo, checkoutBurn };
317
+ `;
318
+
319
+ // Build every lexicon the config names, not just grafana.
320
+ const ALL_LEXICONS = { build: "chant build src", dev: "chant build src --watch" };
321
+
322
+ export const DEFAULT_TEMPLATE: InitTemplateSet = {
323
+ src: { "datasources.ts": DEFAULT_DATASOURCES, "dashboard.ts": DEFAULT_DASHBOARD },
324
+ };
325
+
326
+ export const RED_TEMPLATE: InitTemplateSet = {
327
+ src: { "datasources.ts": RED_DATASOURCES, "red.ts": RED_DASHBOARD },
328
+ };
329
+
330
+ export const K8S_PODS_TEMPLATE: InitTemplateSet = {
331
+ src: {
332
+ "datasources.ts": K8S_DATASOURCES,
333
+ "variables.ts": K8S_VARIABLES,
334
+ "queries.ts": K8S_QUERIES,
335
+ "dashboard.ts": K8S_DASHBOARD,
336
+ "k8s.ts": K8S_DELIVERY,
337
+ },
338
+ root: { "chant.config.ts": K8S_CONFIG },
339
+ scripts: ALL_LEXICONS,
340
+ };
341
+
342
+ export const SLO_TEMPLATE: InitTemplateSet = {
343
+ src: { "datasources.ts": SLO_DATASOURCES, "slo.ts": SLO_DECLARATION, "dashboards.ts": SLO_DASHBOARD },
344
+ root: { "chant.config.ts": SLO_CONFIG },
345
+ scripts: ALL_LEXICONS,
346
+ };
347
+
348
+ /** The template names `chant init --lexicon grafana --template <name>` takes, besides the default. */
349
+ export const TEMPLATE_NAMES = ["red", "k8s-pods", "slo"] as const;
@@ -2,7 +2,7 @@
2
2
  * The grafana lexicon's chant audit catalog, contributed via
3
3
  * `grafanaPlugin.auditCatalog()` (#687, #1346).
4
4
  *
5
- * GRAF101-GRAF109, GRAF111-GRAF114 and GRAF115 read the emitted dashboard
5
+ * GRAF101-GRAF110, GRAF111-GRAF114 and GRAF115 read the emitted dashboard
6
6
  * JSON and provisioning files (alerting included), so
7
7
  * they fire on an audit of files chant didn't build too, and are
8
8
  * `yamlBased`. GRAF001 and GRAF002 read TypeScript source, so they are
@@ -101,6 +101,14 @@ export const grafanaAuditCatalog: Record<string, RuleMeta> = {
101
101
  "Give each dashboard provider its own path; set foldersFromFilesStructure: true (and no folder) on the provider when dashboards declare a folder; for a nested folder, run Grafana 13.1 or later or use a single-level folder.",
102
102
  { category: "correctness" },
103
103
  ),
104
+ GRAF110: auditRule(
105
+ "GRAF110",
106
+ "merge-worthy",
107
+ "guidance",
108
+ "Panel or row repeats over a variable that cannot give it more than one value",
109
+ "Repeat over a query, custom, datasource or group by variable, and set multi or includeAll on a query, custom or datasource one; Grafana shows a panel repeated over anything else once.",
110
+ { category: "correctness" },
111
+ ),
104
112
  GRAF111: auditRule(
105
113
  "GRAF111",
106
114
  "merge-worthy",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * GRAF101: A panel, query or variable uses a datasource no Datasource or ExternalDatasource in the build declares
2
+ * GRAF101: A panel, query, variable or annotation uses a datasource no Datasource or ExternalDatasource in the build declares, or one the build deletes
3
3
  *
4
4
  * Grafana shows "datasource not found" on the panel. Typed references cannot go wrong this way, so it mostly catches a { type, uid } ref or a hand-edited dashboard. A datasource variable whose plugin type no declared datasource has is an error too. A panel with queries and no datasource at all is a warning: Grafana sends them to its default datasource. This check joins dashboards against the datasources of the same build root (chant #1939): declare one that exists in Grafana but is provisioned elsewhere with ExternalDatasource, which the check counts and the build never provisions. A build that declares no datasource at all gets one warning per dashboard that references any, since nothing can be checked.
5
5
  */
@@ -9,7 +9,7 @@ import { grafanaDiagnostics } from "./grafana-helpers";
9
9
 
10
10
  export const graf101: PostSynthCheck = {
11
11
  id: "GRAF101",
12
- description: "A panel, query or variable uses a datasource no Datasource or ExternalDatasource in the build declares",
12
+ description: "A panel, query, variable or annotation uses a datasource no Datasource or ExternalDatasource in the build declares, or one the build deletes",
13
13
 
14
14
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
15
15
  return grafanaDiagnostics(ctx, "GRAF101");