@intentius/chant-lexicon-grafana 0.124.31 → 0.124.32

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 (166) hide show
  1. package/dist/actions/grafana.js +36 -0
  2. package/dist/actions/index.js +1 -0
  3. package/dist/alerting-build.js +262 -0
  4. package/dist/alerting.js +125 -0
  5. package/dist/annotations.js +47 -0
  6. package/dist/api/apply.js +320 -0
  7. package/dist/api/bind.js +44 -0
  8. package/dist/api/client.js +113 -0
  9. package/dist/api/converge.js +61 -0
  10. package/dist/api/dashboards.js +210 -0
  11. package/dist/api/datasources.js +24 -0
  12. package/dist/api/fake-grafana-writes.js +185 -0
  13. package/dist/api/fake-grafana.js +161 -0
  14. package/dist/api/folders.js +290 -0
  15. package/dist/api/library-panels.js +82 -0
  16. package/dist/build.js +767 -0
  17. package/dist/catalog.js +198 -0
  18. package/dist/codegen/docs-cli.js +3 -0
  19. package/dist/codegen/docs.js +130 -0
  20. package/dist/codegen/generate-cli.js +7 -0
  21. package/dist/codegen/generate.js +97 -0
  22. package/dist/codegen/package.js +28 -0
  23. package/dist/codegen/schema-types.js +159 -0
  24. package/dist/composites/agent-dashboard.js +460 -0
  25. package/dist/composites/catalog.js +306 -0
  26. package/dist/composites/index.js +8 -0
  27. package/dist/composites/red-dashboard.js +135 -0
  28. package/dist/composites/shared.js +74 -0
  29. package/dist/composites/slo-alert-rules.js +96 -0
  30. package/dist/composites/slo-dashboard.js +153 -0
  31. package/dist/config.js +110 -0
  32. package/dist/contact-point-check.js +61 -0
  33. package/dist/contact-point-secrets.js +32 -0
  34. package/dist/contact-point-settings.gen.js +421 -0
  35. package/dist/dashboard.js +60 -0
  36. package/dist/datasource-refs.js +170 -0
  37. package/dist/datasource-settings.js +57 -0
  38. package/dist/datasource.js +68 -0
  39. package/dist/deep-observe-hooks.js +266 -0
  40. package/dist/deep-observe.js +171 -0
  41. package/dist/describe-resources.js +249 -0
  42. package/dist/detect.js +119 -0
  43. package/dist/export-resources.js +73 -0
  44. package/dist/folder.js +86 -0
  45. package/dist/import/alerting.js +514 -0
  46. package/dist/import/edits.js +90 -0
  47. package/dist/import/embedded.js +60 -0
  48. package/dist/import/generator.js +656 -0
  49. package/dist/import/live-export.js +246 -0
  50. package/dist/import/mappings.js +317 -0
  51. package/dist/import/model.js +23 -0
  52. package/dist/import/normalize-alerting.js +106 -0
  53. package/dist/import/normalize.js +426 -0
  54. package/dist/import/operator.js +252 -0
  55. package/dist/import/parser.js +1480 -0
  56. package/dist/import/report.js +58 -0
  57. package/dist/import/testdata/fixtures.js +122 -0
  58. package/dist/import/v2.js +814 -0
  59. package/dist/index.js +59 -0
  60. package/dist/init-templates.js +305 -0
  61. package/dist/integrity.json +2 -2
  62. package/dist/k8s.js +548 -0
  63. package/dist/library-panel.js +83 -0
  64. package/dist/lint/audit-catalog.js +40 -0
  65. package/dist/lint/audit-lineage.js +9 -0
  66. package/dist/lint/post-synth/graf101.js +13 -0
  67. package/dist/lint/post-synth/graf102.js +13 -0
  68. package/dist/lint/post-synth/graf103.js +13 -0
  69. package/dist/lint/post-synth/graf104.js +13 -0
  70. package/dist/lint/post-synth/graf105.js +13 -0
  71. package/dist/lint/post-synth/graf106.js +13 -0
  72. package/dist/lint/post-synth/graf107.js +13 -0
  73. package/dist/lint/post-synth/graf108.js +13 -0
  74. package/dist/lint/post-synth/graf109.js +13 -0
  75. package/dist/lint/post-synth/graf110.js +13 -0
  76. package/dist/lint/post-synth/graf111.js +13 -0
  77. package/dist/lint/post-synth/graf112.js +13 -0
  78. package/dist/lint/post-synth/graf113.js +13 -0
  79. package/dist/lint/post-synth/graf114.js +13 -0
  80. package/dist/lint/post-synth/graf115.js +13 -0
  81. package/dist/lint/post-synth/graf116.js +13 -0
  82. package/dist/lint/post-synth/graf117.js +13 -0
  83. package/dist/lint/post-synth/graf118.js +37 -0
  84. package/dist/lint/post-synth/grafana-helpers.js +89 -0
  85. package/dist/lint/post-synth/index.js +38 -0
  86. package/dist/lint/rules/grafana-ast.js +70 -0
  87. package/dist/lint/rules/index.js +6 -0
  88. package/dist/lint/rules/literal-secret.js +109 -0
  89. package/dist/lint/rules/uid-syntax.js +53 -0
  90. package/dist/lsp/completions.js +11 -0
  91. package/dist/lsp/hover.js +29 -0
  92. package/dist/manifest.json +1 -1
  93. package/dist/op/activities/grafana-apply.js +147 -0
  94. package/dist/op/activities/index.js +7 -0
  95. package/dist/op/builders.js +19 -0
  96. package/dist/ownership.js +102 -0
  97. package/dist/package-cli.js +14 -0
  98. package/dist/panel-options.js +12 -0
  99. package/dist/panels.js +296 -0
  100. package/dist/pin.js +113 -0
  101. package/dist/plugin.js +172 -0
  102. package/dist/promql-check.js +176 -0
  103. package/dist/query-models.js +13 -0
  104. package/dist/query-syntax.js +129 -0
  105. package/dist/query.js +227 -0
  106. package/dist/schema/annotationslist.gen.js +5 -0
  107. package/dist/schema/azuremonitor.gen.js +5 -0
  108. package/dist/schema/barchart.gen.js +5 -0
  109. package/dist/schema/bargauge.gen.js +5 -0
  110. package/dist/schema/bigquery.gen.js +5 -0
  111. package/dist/schema/candlestick.gen.js +5 -0
  112. package/dist/schema/canvas.gen.js +5 -0
  113. package/dist/schema/cloudwatch.gen.js +5 -0
  114. package/dist/schema/dashboard.gen.js +6 -0
  115. package/dist/schema/dashboardlist.gen.js +5 -0
  116. package/dist/schema/datagrid.gen.js +5 -0
  117. package/dist/schema/elasticsearch.gen.js +5 -0
  118. package/dist/schema/expr.gen.js +5 -0
  119. package/dist/schema/gauge.gen.js +5 -0
  120. package/dist/schema/geomap.gen.js +5 -0
  121. package/dist/schema/googlecloudmonitoring.gen.js +5 -0
  122. package/dist/schema/grafanapyroscope.gen.js +5 -0
  123. package/dist/schema/heatmap.gen.js +5 -0
  124. package/dist/schema/histogram.gen.js +5 -0
  125. package/dist/schema/index.js +40 -0
  126. package/dist/schema/logs.gen.js +5 -0
  127. package/dist/schema/loki.gen.js +5 -0
  128. package/dist/schema/news.gen.js +5 -0
  129. package/dist/schema/nodegraph.gen.js +5 -0
  130. package/dist/schema/piechart.gen.js +5 -0
  131. package/dist/schema/prometheus.gen.js +5 -0
  132. package/dist/schema/stat.gen.js +5 -0
  133. package/dist/schema/statetimeline.gen.js +5 -0
  134. package/dist/schema/statushistory.gen.js +5 -0
  135. package/dist/schema/table.gen.js +5 -0
  136. package/dist/schema/tempo.gen.js +5 -0
  137. package/dist/schema/text.gen.js +5 -0
  138. package/dist/schema/timeseries.gen.js +5 -0
  139. package/dist/schema/trend.gen.js +5 -0
  140. package/dist/schema/xychart.gen.js +5 -0
  141. package/dist/schema-validate.js +265 -0
  142. package/dist/serializer.js +40 -0
  143. package/dist/skill-defs.js +60 -0
  144. package/dist/spec/fetch-cli.js +26 -0
  145. package/dist/spec/fetch-notifiers-cli.js +53 -0
  146. package/dist/spec/fetch-units-cli.js +40 -0
  147. package/dist/spec/notifiers.js +126 -0
  148. package/dist/spec/overlay.js +92 -0
  149. package/dist/spec/schemas.gen.js +42 -0
  150. package/dist/spec/schemas.js +62 -0
  151. package/dist/spec/units.gen.js +280 -0
  152. package/dist/spec/units.js +72 -0
  153. package/dist/transformations.js +127 -0
  154. package/dist/util.js +34 -0
  155. package/dist/validate-alerting.js +444 -0
  156. package/dist/validate-cli.js +7 -0
  157. package/dist/validate-output.js +635 -0
  158. package/dist/validate.js +122 -0
  159. package/dist/validation.js +13 -0
  160. package/dist/variables.js +70 -0
  161. package/package.json +10 -10
  162. package/src/codegen/package.ts +2 -2
  163. package/src/spec/fetch-notifiers-cli.ts +2 -2
  164. package/src/spec/fetch-units-cli.ts +2 -2
  165. package/src/spec/overlay.ts +2 -2
  166. package/src/spec/schemas.ts +2 -2
@@ -0,0 +1,635 @@
1
+ /**
2
+ * The checks behind GRAF101-GRAF110, GRAF111-GRAF114 and GRAF115-GRAF117, as plain
3
+ * functions over built Grafana output: dashboard JSON documents, provisioned
4
+ * datasources, dashboard providers and alerting provisioning files (the
5
+ * alerting checks are in `validate-alerting.ts`). The
6
+ * post-synth checks run them over a build; anything else holding the same
7
+ * JSON (a test, another lexicon embedding dashboards) can call them directly.
8
+ *
9
+ * Checks that join a dashboard against datasources (GRAF101, GRAF102) only
10
+ * see the datasources passed in: the ones provisioned and the ones declared
11
+ * with `ExternalDatasource`. In a build that is the build root being built
12
+ * (chant #1939). With none declared they warn that references cannot be
13
+ * checked; a datasource that exists in Grafana but is not declared looks
14
+ * undeclared, so declare it with `ExternalDatasource`.
15
+ */
16
+ import { DASHBOARDS_DIR, GRID_COLUMNS } from "./build.js";
17
+ import { annotationsOf, datasourceUses, describePanel, knownDatasources, panelsOf, variableReferences, variablesOf, } from "./datasource-refs.js";
18
+ import { isBuiltinVariable, MULTI_VALUE_KINDS } from "./variables.js";
19
+ import { isValidUid } from "./util.js";
20
+ import { schemaValidationUnavailable, validateDashboardSchema } from "./schema-validate.js";
21
+ import { checkGrafanaPromql, prometheusQueries } from "./promql-check.js";
22
+ import { checkGrafanaLogql, checkGrafanaTraceql, lokiQueries, tempoQueries } from "./query-syntax.js";
23
+ import { checkAlertingIdentity, checkNotificationRefs, checkRuleDatasources, checkRuleLogql, checkRulePromql, checkRuleQueries } from "./validate-alerting.js";
24
+ import { closestGrafanaUnit, isGrafanaUnit } from "./spec/units.js";
25
+ export { variableReferences };
26
+ function dashName(d) {
27
+ return `Dashboard "${String(d.title ?? d.uid ?? "?")}"`;
28
+ }
29
+ function isVariableUid(uid) {
30
+ return typeof uid === "string" && uid.includes("$");
31
+ }
32
+ function stringsIn(value, skip, out = []) {
33
+ if (typeof value === "string")
34
+ out.push(value);
35
+ else if (Array.isArray(value))
36
+ for (const v of value)
37
+ stringsIn(v, skip, out);
38
+ else if (value && typeof value === "object") {
39
+ for (const [k, v] of Object.entries(value))
40
+ if (!skip.has(k))
41
+ stringsIn(v, skip, out);
42
+ }
43
+ return out;
44
+ }
45
+ const TARGET_SKIP = new Set(["refId", "datasource"]);
46
+ /** Every datasource ref a panel or row writes itself, with where it is written. */
47
+ function panelRefs(panel) {
48
+ const out = [];
49
+ const panelRef = panel.datasource;
50
+ if (panelRef && typeof panelRef === "object")
51
+ out.push({ ref: panelRef, where: describePanel(panel) });
52
+ const targets = Array.isArray(panel.targets) ? panel.targets : [];
53
+ for (const t of targets) {
54
+ const r = t?.datasource;
55
+ if (r && typeof r === "object")
56
+ out.push({ ref: r, where: `${describePanel(panel)} query ${String(t.refId ?? "?")}` });
57
+ }
58
+ return out;
59
+ }
60
+ /** The datasources a set of artifacts knows by uid: provisioned and external. */
61
+ export function knownDatasourcesOf(a) {
62
+ return knownDatasources(a.datasources, a.externalDatasources ?? []);
63
+ }
64
+ function listUids(known) {
65
+ return [...known.keys()].map((k) => `"${k}"`).join(", ");
66
+ }
67
+ // ── GRAF101, GRAF102: datasource references ─────────────────────
68
+ export function checkDatasourceRefs(a) {
69
+ const issues = [];
70
+ const known = knownDatasourcesOf(a);
71
+ const types = new Set([...known.values()].map((k) => k.type));
72
+ for (const { json: d } of a.dashboards) {
73
+ const uid = String(d.uid ?? "");
74
+ const push = (code, severity, message) => issues.push({ code, severity, message: `${dashName(d)} ${message}`, entity: uid });
75
+ for (const { panel } of panelsOf(d)) {
76
+ if (panel.type === "row")
77
+ continue;
78
+ const targets = Array.isArray(panel.targets) ? panel.targets : [];
79
+ if (targets.length > 0 && !panel.datasource && targets.every((t) => !t?.datasource)) {
80
+ push("GRAF101", "warning", `${describePanel(panel)} has queries but no datasource; Grafana will send them to whichever datasource is its default.`);
81
+ }
82
+ }
83
+ const uses = datasourceUses(d, known).filter((u) => u.ref !== undefined);
84
+ const dsVariables = variablesOf(d).filter((v) => v.type === "datasource" && typeof v.query === "string" && v.query);
85
+ // Nothing declared: the references can't be checked. Say so once per dashboard rather than staying silent.
86
+ if (known.size === 0) {
87
+ const uids = [...new Set(uses.flatMap((u) => (u.resolved.kind === "undeclared" ? [u.resolved.uid] : [])))];
88
+ const plugins = [...new Set(dsVariables.map((v) => String(v.query)))];
89
+ const what = [
90
+ ...(uids.length > 0 ? [`datasource uid${uids.length > 1 ? "s" : ""} ${uids.map((u) => `"${u}"`).join(", ")}`] : []),
91
+ ...(plugins.length > 0 ? [`datasource variables of type ${plugins.join(", ")}`] : []),
92
+ ];
93
+ if (what.length > 0) {
94
+ const them = uids.length + plugins.length > 1 ? "them" : "it";
95
+ push("GRAF101", "warning", `uses ${what.join(" and ")}, but the build declares no datasource, so GRAF101 and GRAF102 cannot check ${them}. Declare a Datasource to provision one, or an ExternalDatasource for one that already exists in Grafana.`);
96
+ }
97
+ }
98
+ for (const { ref, where, resolved } of uses) {
99
+ if (resolved.kind === "variable") {
100
+ const v = variablesOf(d).find((x) => x.name === resolved.variable);
101
+ if (v && v.type === "datasource" && ref.type && v.query && v.query !== ref.type) {
102
+ push("GRAF102", "error", `${where} expects a ${ref.type} datasource, but variable "${resolved.variable}" chooses among ${String(v.query)} datasources.`);
103
+ }
104
+ }
105
+ else if (resolved.kind === "undeclared" && known.size > 0) {
106
+ push("GRAF101", "error", `${where} uses datasource uid "${resolved.uid}"${ref.type ? ` (${ref.type})` : ""}, which no declared Datasource or ExternalDatasource has. Declared: ${listUids(known)}.`);
107
+ }
108
+ else if (resolved.kind === "declared" && ref.type && ref.type !== resolved.type) {
109
+ const ds = resolved.datasource;
110
+ push("GRAF102", "error", `${where} sends a ${ref.type} query to ${ds.external ? "external " : ""}datasource "${ds.name ?? ds.uid}", which is ${ds.type}.`);
111
+ }
112
+ }
113
+ // An ExternalDatasource the provisioning file deletes (and does not provision again) is gone once Grafana provisions it.
114
+ const deleted = new Set((a.deleteDatasources ?? []).filter((x) => (x.orgId ?? 1) === 1).map((x) => x.name));
115
+ const provisioned = new Set(a.datasources.map((x) => x.name));
116
+ const gone = new Set([...known.values()].filter((k) => k.external && k.name !== undefined && deleted.has(k.name) && !provisioned.has(k.name)).map((k) => k.uid));
117
+ for (const { where, resolved } of uses) {
118
+ if (resolved.kind !== "declared" || !gone.has(resolved.datasource.uid))
119
+ continue;
120
+ push("GRAF101", "error", `${where} uses external datasource "${resolved.datasource.name}", which the datasource provisioning file deletes (deleteDatasources); Grafana removes it before the dashboard can use it.`);
121
+ }
122
+ // A datasource variable offers the datasources of its plugin type; with none declared it has nothing to choose.
123
+ if (known.size > 0) {
124
+ for (const v of dsVariables) {
125
+ if (types.has(String(v.query)))
126
+ continue;
127
+ push("GRAF101", "error", `variable "${String(v.name)}" chooses among ${String(v.query)} datasources, but no declared Datasource or ExternalDatasource is of type ${String(v.query)}. Declared types: ${[...types].join(", ")}.`);
128
+ }
129
+ }
130
+ }
131
+ return issues;
132
+ }
133
+ // ── GRAF103: variables ──────────────────────────────────────────
134
+ export function checkVariables(a) {
135
+ const issues = [];
136
+ for (const { json: d } of a.dashboards) {
137
+ const uid = String(d.uid ?? "");
138
+ const declared = new Set(variablesOf(d).map((v) => String(v.name)));
139
+ const report = (name, where) => {
140
+ if (declared.has(name) || isBuiltinVariable(name))
141
+ return;
142
+ issues.push({
143
+ code: "GRAF103",
144
+ severity: "error",
145
+ message: `${dashName(d)} ${where} uses $${name}, which is not one of its variables${declared.size > 0 ? ` (${[...declared].map((n) => `$${n}`).join(", ")})` : ""}.`,
146
+ entity: uid,
147
+ });
148
+ };
149
+ for (const { panel } of panelsOf(d)) {
150
+ const where = panel.type === "row" ? `row "${String(panel.title ?? "")}"` : describePanel(panel);
151
+ if (typeof panel.repeat === "string" && panel.repeat)
152
+ report(panel.repeat, `${where} repeat`);
153
+ if (typeof panel.title === "string")
154
+ for (const n of new Set(variableReferences(panel.title)))
155
+ report(n, `${where} title`);
156
+ for (const { ref, where: w } of panelRefs(panel)) {
157
+ if (isVariableUid(ref.uid))
158
+ for (const n of variableReferences(ref.uid))
159
+ report(n, `${w} datasource`);
160
+ }
161
+ const targets = Array.isArray(panel.targets) ? panel.targets : [];
162
+ for (const t of targets) {
163
+ const names = new Set(stringsIn(t, TARGET_SKIP).flatMap(variableReferences));
164
+ for (const n of names)
165
+ report(n, `${where} query ${String(t?.refId ?? "?")}`);
166
+ }
167
+ }
168
+ for (const v of variablesOf(d)) {
169
+ const ref = v.datasource;
170
+ if (ref && typeof ref === "object" && isVariableUid(ref.uid)) {
171
+ for (const n of variableReferences(ref.uid))
172
+ if (n !== v.name)
173
+ report(n, `variable "${String(v.name)}" datasource`);
174
+ }
175
+ if (v.type !== "query")
176
+ continue;
177
+ const names = new Set(stringsIn(v.query, new Set(["refId"])).flatMap(variableReferences));
178
+ for (const n of names)
179
+ if (n !== v.name)
180
+ report(n, `variable "${String(v.name)}" query`);
181
+ }
182
+ for (const an of annotationsOf(d)) {
183
+ const where = `annotation "${String(an.name ?? "?")}"`;
184
+ const ref = an.datasource;
185
+ if (ref && typeof ref === "object" && isVariableUid(ref.uid))
186
+ for (const n of variableReferences(ref.uid))
187
+ report(n, `${where} datasource`);
188
+ // Grafana interpolates the query (`target`, or Prometheus's legacy `expr`), not the name, colour or title formats.
189
+ const names = new Set(stringsIn([an.target, an.expr], TARGET_SKIP).flatMap(variableReferences));
190
+ for (const n of names)
191
+ report(n, `${where} query`);
192
+ }
193
+ }
194
+ return issues;
195
+ }
196
+ // ── GRAF104: duplicates ─────────────────────────────────────────
197
+ function dupes(items, key) {
198
+ const counts = new Map();
199
+ for (const i of items) {
200
+ const k = key(i);
201
+ if (k !== undefined)
202
+ counts.set(k, (counts.get(k) ?? 0) + 1);
203
+ }
204
+ return new Map([...counts].filter(([, n]) => n > 1));
205
+ }
206
+ export function checkDuplicates(a) {
207
+ const issues = [];
208
+ const push = (message, entity) => issues.push({ code: "GRAF104", severity: "error", message, entity });
209
+ for (const [uid, n] of dupes(a.dashboards, (d) => (typeof d.json.uid === "string" ? d.json.uid : undefined))) {
210
+ push(`${n} dashboards share the uid "${uid}"; Grafana keeps one of them.`, uid);
211
+ }
212
+ // Two ExternalDatasources with the same uid, type and name describe the one datasource in Grafana
213
+ // (two imported dashboards each declare the datasource they name), so they count once.
214
+ const externals = new Map();
215
+ for (const d of a.externalDatasources ?? [])
216
+ externals.set(JSON.stringify([d.uid, d.type, d.name ?? null]), d);
217
+ const allUids = [...a.datasources.map((d) => d.uid), ...[...externals.values()].map((d) => d.uid)];
218
+ for (const [uid, n] of dupes(allUids, (u) => u))
219
+ push(`${n} datasources share the uid "${uid}".`, uid);
220
+ for (const [name, n] of dupes(a.datasources, (d) => d.name))
221
+ push(`${n} datasources share the name "${name}"; Grafana needs names to be unique.`, name);
222
+ for (const [uid, n] of dupes(a.folders ?? [], (f) => f.uid)) {
223
+ const paths = (a.folders ?? []).filter((f) => f.uid === uid).map((f) => `"${f.path ?? f.title}"`);
224
+ push(`${n} folders share the uid "${uid}" (${paths.join(", ")}); Grafana keeps one folder per uid, so the API applier refuses the build. Give one a Folder with its own uid.`, uid);
225
+ }
226
+ for (const { json: d } of a.dashboards) {
227
+ const uid = String(d.uid ?? "");
228
+ const panels = panelsOf(d).map((p) => p.panel);
229
+ for (const [id, n] of dupes(panels, (p) => (typeof p.id === "number" ? String(p.id) : undefined))) {
230
+ push(`${dashName(d)} has ${n} panels with id ${id}.`, uid);
231
+ }
232
+ for (const [name, n] of dupes(variablesOf(d), (v) => (typeof v.name === "string" ? v.name : undefined))) {
233
+ push(`${dashName(d)} declares the variable "${name}" ${n} times.`, uid);
234
+ }
235
+ for (const p of panels) {
236
+ const targets = Array.isArray(p.targets) ? p.targets : [];
237
+ for (const [refId, n] of dupes(targets, (t) => (typeof t?.refId === "string" ? t.refId : undefined))) {
238
+ push(`${dashName(d)} ${describePanel(p)} has ${n} queries with refId "${refId}".`, uid);
239
+ }
240
+ }
241
+ }
242
+ return issues;
243
+ }
244
+ function boxOf(panel) {
245
+ const g = panel.gridPos;
246
+ if (!g || typeof g !== "object")
247
+ return undefined;
248
+ if ([g.x, g.y, g.w, g.h].some((v) => typeof v !== "number"))
249
+ return undefined;
250
+ return g;
251
+ }
252
+ function overlaps(a, b) {
253
+ return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
254
+ }
255
+ export function checkGrid(a) {
256
+ const issues = [];
257
+ for (const { json: d } of a.dashboards) {
258
+ const uid = String(d.uid ?? "");
259
+ const groups = new Map();
260
+ for (const { panel, group } of panelsOf(d)) {
261
+ const box = boxOf(panel);
262
+ if (!box)
263
+ continue;
264
+ if (box.x < 0 || box.y < 0 || box.w < 1 || box.h < 1 || box.x + box.w > GRID_COLUMNS) {
265
+ issues.push({
266
+ code: "GRAF105",
267
+ severity: "error",
268
+ message: `${dashName(d)} ${describePanel(panel)} at x=${box.x} y=${box.y} w=${box.w} h=${box.h} does not fit Grafana's ${GRID_COLUMNS}-column grid.`,
269
+ entity: uid,
270
+ });
271
+ }
272
+ groups.set(group, [...(groups.get(group) ?? []), { panel, box }]);
273
+ }
274
+ for (const members of groups.values()) {
275
+ for (let i = 0; i < members.length; i++) {
276
+ for (let j = i + 1; j < members.length; j++) {
277
+ if (!overlaps(members[i].box, members[j].box))
278
+ continue;
279
+ issues.push({
280
+ code: "GRAF105",
281
+ severity: "warning",
282
+ message: `${dashName(d)} ${describePanel(members[i].panel)} overlaps ${describePanel(members[j].panel)}; Grafana will move one of them.`,
283
+ entity: uid,
284
+ });
285
+ }
286
+ }
287
+ }
288
+ }
289
+ return issues;
290
+ }
291
+ // ── GRAF106: uids and titles Grafana rejects ────────────────────
292
+ export function checkIdentity(a) {
293
+ const issues = [];
294
+ for (const { json: d } of a.dashboards) {
295
+ const uid = d.uid;
296
+ if (typeof uid !== "string" || !isValidUid(uid)) {
297
+ issues.push({
298
+ code: "GRAF106",
299
+ severity: "error",
300
+ message: `${dashName(d)} has uid ${JSON.stringify(uid)}; Grafana accepts 1-40 letters, digits, "-" and "_".`,
301
+ entity: typeof uid === "string" ? uid : undefined,
302
+ });
303
+ }
304
+ if (typeof d.title !== "string" || d.title.trim() === "") {
305
+ issues.push({ code: "GRAF106", severity: "error", message: `Dashboard ${JSON.stringify(uid)} has no title; Grafana refuses to save it.`, entity: String(uid) });
306
+ }
307
+ }
308
+ for (const ds of a.datasources) {
309
+ if (!isValidUid(ds.uid)) {
310
+ issues.push({
311
+ code: "GRAF106",
312
+ severity: "error",
313
+ message: `Datasource "${ds.name}" has uid "${ds.uid}"; Grafana accepts 1-40 letters, digits, "-" and "_".`,
314
+ entity: ds.name,
315
+ });
316
+ }
317
+ }
318
+ for (const f of a.folders ?? []) {
319
+ const where = `Folder "${f.path ?? f.title}"`;
320
+ if (!isValidUid(f.uid)) {
321
+ issues.push({ code: "GRAF106", severity: "error", message: `${where} has uid "${f.uid}"; Grafana accepts 1-40 letters, digits, "-" and "_".`, entity: f.uid });
322
+ }
323
+ else if (f.uid === "general") {
324
+ issues.push({
325
+ code: "GRAF106",
326
+ severity: "error",
327
+ message: `${where} has uid "general", which Grafana keeps for its root (the General folder) and refuses for a folder. Leave folder out to put a dashboard in General, or give the folder another uid.`,
328
+ entity: f.uid,
329
+ });
330
+ }
331
+ }
332
+ for (const ds of a.externalDatasources ?? []) {
333
+ if (!isValidUid(ds.uid)) {
334
+ issues.push({
335
+ code: "GRAF106",
336
+ severity: "error",
337
+ message: `ExternalDatasource "${ds.name ?? ds.uid}" has uid "${ds.uid}"; Grafana accepts 1-40 letters, digits, "-" and "_".`,
338
+ entity: ds.name ?? ds.uid,
339
+ });
340
+ }
341
+ }
342
+ return issues;
343
+ }
344
+ // ── GRAF107: the pinned schemas ─────────────────────────────────
345
+ /**
346
+ * A value the pinned schemas (with the correction overlay) do not allow is
347
+ * an error; a key they do not know is a warning, since newer Grafana
348
+ * versions add keys the pin has not caught up with.
349
+ */
350
+ export function checkSchema(a) {
351
+ const issues = [];
352
+ if (a.dashboards.length === 0)
353
+ return issues;
354
+ const unavailable = schemaValidationUnavailable();
355
+ if (unavailable) {
356
+ return [{ code: "GRAF107", severity: "warning", message: `Dashboards were not checked against the Grafana schema: ${unavailable}.` }];
357
+ }
358
+ for (const { json: d } of a.dashboards) {
359
+ for (const p of validateDashboardSchema(d)) {
360
+ issues.push({
361
+ code: "GRAF107",
362
+ severity: p.severity,
363
+ message: `${dashName(d)} ${p.path}: ${p.message} (Grafana schema).`,
364
+ entity: String(d.uid ?? ""),
365
+ });
366
+ }
367
+ }
368
+ return issues;
369
+ }
370
+ // ── GRAF108: PromQL syntax ──────────────────────────────────────
371
+ /**
372
+ * Each panel query, query variable and alert rule query that reaches a
373
+ * Prometheus datasource is parsed as PromQL, with its template variables replaced by placeholders
374
+ * first (see `promql-check.ts`). A query whose datasource can't be told is
375
+ * not parsed.
376
+ */
377
+ export function checkPromqlSyntax(a) {
378
+ const issues = [];
379
+ const known = knownDatasourcesOf(a);
380
+ for (const { json: d } of a.dashboards) {
381
+ for (const { where, expr } of prometheusQueries(d, known)) {
382
+ const checked = checkGrafanaPromql(expr);
383
+ if (checked.ok)
384
+ continue;
385
+ issues.push({ code: "GRAF108", severity: "error", message: `${dashName(d)} ${where} is not valid PromQL: ${checked.message}.`, entity: String(d.uid ?? "") });
386
+ }
387
+ }
388
+ issues.push(...checkRulePromql(a.alerting ?? [], known));
389
+ return issues;
390
+ }
391
+ // ── GRAF116 and GRAF117: LogQL and TraceQL syntax ──────────────
392
+ /**
393
+ * Each panel query, annotation query and query variable stream selector
394
+ * that reaches a Loki datasource is parsed as LogQL, and each alert rule
395
+ * query to Loki too (GRAF116, an error). Each panel query that reaches a
396
+ * Tempo datasource is parsed as TraceQL (GRAF117, a warning: Grafana's
397
+ * TraceQL grammar trails Tempo's own parser). See `query-syntax.ts`.
398
+ */
399
+ export function checkLogqlSyntax(a) {
400
+ const issues = [];
401
+ const known = knownDatasourcesOf(a);
402
+ for (const { json: d } of a.dashboards) {
403
+ for (const { where, expr } of lokiQueries(d, known)) {
404
+ const checked = checkGrafanaLogql(expr);
405
+ if (checked.ok)
406
+ continue;
407
+ issues.push({ code: "GRAF116", severity: "error", message: `${dashName(d)} ${where} is not valid LogQL: ${checked.message}.`, entity: String(d.uid ?? "") });
408
+ }
409
+ }
410
+ issues.push(...checkRuleLogql(a.alerting ?? [], known));
411
+ return issues;
412
+ }
413
+ export function checkTraceqlSyntax(a) {
414
+ const issues = [];
415
+ const known = knownDatasourcesOf(a);
416
+ for (const { json: d } of a.dashboards) {
417
+ for (const { where, expr } of tempoQueries(d, known)) {
418
+ const checked = checkGrafanaTraceql(expr);
419
+ if (checked.ok)
420
+ continue;
421
+ issues.push({ code: "GRAF117", severity: "warning", message: `${dashName(d)} ${where} is not valid TraceQL: ${checked.message}.`, entity: String(d.uid ?? "") });
422
+ }
423
+ }
424
+ return issues;
425
+ }
426
+ // ── GRAF110: repeats ────────────────────────────────────────────
427
+ /**
428
+ * A panel or row repeated over a variable Grafana cannot repeat over, or
429
+ * one that can only ever hold one value. Grafana repeats over a query,
430
+ * custom, datasource or group by variable (a `MultiValueVariable` in
431
+ * @grafana/scenes), once per selected value. Over any other kind it logs an
432
+ * error and shows the panel once; over a query, custom or datasource
433
+ * variable with neither `multi` nor `includeAll`, there is only ever one
434
+ * value to repeat for. A repeat naming no variable is GRAF103's.
435
+ */
436
+ const KIND_NAMES = { adhoc: "an ad hoc", interval: "an interval", groupby: "a group by" };
437
+ export function checkRepeats(a) {
438
+ const issues = [];
439
+ for (const { json: d } of a.dashboards) {
440
+ const variables = new Map(variablesOf(d).map((v) => [String(v.name), v]));
441
+ for (const { panel } of panelsOf(d)) {
442
+ if (typeof panel.repeat !== "string" || panel.repeat === "")
443
+ continue;
444
+ const v = variables.get(panel.repeat);
445
+ if (!v)
446
+ continue;
447
+ const where = panel.type === "row" ? `row "${String(panel.title ?? "")}"` : describePanel(panel);
448
+ const type = String(v.type);
449
+ const kind = `${KIND_NAMES[type] ?? `a ${type}`} variable`;
450
+ let why;
451
+ if (!MULTI_VALUE_KINDS.has(type)) {
452
+ why = `${kind}, which Grafana cannot repeat over: it shows the ${panel.type === "row" ? "row" : "panel"} once. Repeat over a query, custom, datasource or group by variable`;
453
+ }
454
+ else if (type !== "groupby" && v.multi !== true && v.includeAll !== true) {
455
+ why = `${kind} with neither multi nor includeAll, so it only ever holds one value and the ${panel.type === "row" ? "row" : "panel"} shows once. Set multi or includeAll on it`;
456
+ }
457
+ if (why) {
458
+ issues.push({ code: "GRAF110", severity: "warning", message: `${dashName(d)} ${where} repeats over $${panel.repeat}, ${why}.`, entity: String(d.uid ?? "") });
459
+ }
460
+ }
461
+ }
462
+ return issues;
463
+ }
464
+ // ── GRAF115: units ──────────────────────────────────────────────
465
+ /** Every unit a panel sets, with where: field defaults, `unit` overrides, heatmap axes and cells, and a legacy graph panel's y-axes. */
466
+ export function panelUnits(panel) {
467
+ const out = [];
468
+ const add = (where, unit) => {
469
+ if (typeof unit === "string")
470
+ out.push({ where, unit });
471
+ };
472
+ const fieldConfig = panel.fieldConfig;
473
+ add("fieldConfig.defaults.unit", fieldConfig?.defaults?.unit);
474
+ const overrides = Array.isArray(fieldConfig?.overrides) ? fieldConfig.overrides : [];
475
+ overrides.forEach((o, i) => {
476
+ const props = Array.isArray(o?.properties) ? o.properties : [];
477
+ props.forEach((p, j) => {
478
+ if (p?.id === "unit")
479
+ add(`fieldConfig.overrides[${i}].properties[${j}]`, p.value);
480
+ });
481
+ });
482
+ const options = panel.options;
483
+ add("options.yAxis.unit", options?.yAxis?.unit);
484
+ add("options.cellValues.unit", options?.cellValues?.unit);
485
+ if (Array.isArray(panel.yaxes))
486
+ panel.yaxes.forEach((y, i) => add(`yaxes[${i}].format`, y?.format));
487
+ return out;
488
+ }
489
+ /**
490
+ * A unit Grafana doesn't register (at v13.2.2) and that isn't a custom
491
+ * `<kind>:` unit is shown as a literal suffix, so `"byte"` renders `5 byte`.
492
+ * A warning: it renders, just not as meant.
493
+ */
494
+ export function checkUnits(a) {
495
+ const issues = [];
496
+ for (const { json: d } of a.dashboards) {
497
+ const panels = panelsOf(d).map(({ panel }) => ({ panel, label: describePanel(panel) }));
498
+ const elements = d.__elements;
499
+ if (elements && typeof elements === "object" && !Array.isArray(elements)) {
500
+ for (const [key, element] of Object.entries(elements)) {
501
+ const model = element && typeof element === "object" ? element.model : undefined;
502
+ if (model && typeof model === "object" && !Array.isArray(model))
503
+ panels.push({ panel: model, label: `library panel "${key}"` });
504
+ }
505
+ }
506
+ for (const { panel, label } of panels) {
507
+ if (panel.type === "row")
508
+ continue;
509
+ for (const { where, unit } of panelUnits(panel)) {
510
+ if (isGrafanaUnit(unit))
511
+ continue;
512
+ const guess = closestGrafanaUnit(unit);
513
+ const hint = guess ? `Did you mean "${guess}"? ` : "";
514
+ issues.push({
515
+ code: "GRAF115",
516
+ severity: "warning",
517
+ message: `${dashName(d)} ${label} ${where}: unit "${unit}" is not one Grafana knows, so it is shown as a literal suffix. ${hint}For a custom unit write "suffix:${unit}" (or prefix:, si:, count:, currency:, time:).`,
518
+ entity: String(d.uid ?? ""),
519
+ });
520
+ }
521
+ }
522
+ }
523
+ return issues;
524
+ }
525
+ // ── GRAF109: where dashboard provisioning puts dashboards ───────
526
+ /** Grafana's default `[folder] max_nested_folder_depth`, and the most it can be raised to. */
527
+ const DEFAULT_FOLDER_DEPTH = 4;
528
+ const MAX_FOLDER_DEPTH = 7;
529
+ /** The folder levels a dashboard file sits in under `dashboards/`, e.g. `["Platform", "Kubernetes"]`. */
530
+ function folderLevels(source) {
531
+ if (!source?.startsWith(`${DASHBOARDS_DIR}/`))
532
+ return [];
533
+ return source.slice(DASHBOARDS_DIR.length + 1).split("/").slice(0, -1);
534
+ }
535
+ function providerView(p) {
536
+ const options = (p.options && typeof p.options === "object" ? p.options : {});
537
+ const path = typeof options.path === "string" ? options.path : typeof options.folder === "string" ? options.folder : "";
538
+ return {
539
+ name: typeof p.name === "string" ? p.name : "?",
540
+ orgId: typeof p.orgId === "number" && p.orgId !== 0 ? p.orgId : 1,
541
+ path: path.replace(/\/+$/, "") || "/",
542
+ folder: typeof p.folder === "string" ? p.folder : "",
543
+ folderUid: typeof p.folderUid === "string" ? p.folderUid : "",
544
+ fromFiles: options.foldersFromFilesStructure === true,
545
+ };
546
+ }
547
+ /** Whether a provider reading `a` also reads `b`: Grafana walks a provider's path recursively. */
548
+ function contains(a, b) {
549
+ return a === b || a === "/" || b.startsWith(`${a}/`);
550
+ }
551
+ function listDashboards(ds) {
552
+ const shown = ds.slice(0, 3).map(({ d, levels }) => `"${String(d.title ?? d.uid ?? "?")}" (${levels.join("/")})`);
553
+ return ds.length > 3 ? `${shown.join(", ")} and ${ds.length - 3} more` : shown.join(", ");
554
+ }
555
+ export function checkProvisioning(a) {
556
+ const issues = [];
557
+ const push = (severity, message, entity) => issues.push({ code: "GRAF109", severity, message, entity });
558
+ const providers = (a.providers ?? []).map(providerView);
559
+ const foldered = a.dashboards.map(({ source, json }) => ({ d: json, levels: folderLevels(source) })).filter((x) => x.levels.length > 0);
560
+ // Two providers reading the same files, in the same org.
561
+ for (let i = 0; i < providers.length; i++) {
562
+ for (let j = i + 1; j < providers.length; j++) {
563
+ const [p, q] = [providers[i], providers[j]];
564
+ if (p.orgId !== q.orgId)
565
+ continue;
566
+ const shared = contains(p.path, q.path) ? q.path : contains(q.path, p.path) ? p.path : undefined;
567
+ if (shared === undefined)
568
+ continue;
569
+ push("error", `Dashboard providers "${p.name}" and "${q.name}" both load the dashboards under ${shared}. Grafana provisions each of them twice, then takes database writes away from both providers, so later changes to the files never reach Grafana. Give each provider its own path.`, q.name);
570
+ }
571
+ }
572
+ // A provider mapping directories to folders that also names a folder.
573
+ for (const p of providers) {
574
+ if (!p.fromFiles || (p.folder === "" && p.folderUid === ""))
575
+ continue;
576
+ if (p.folder !== "" && p.folderUid !== "") {
577
+ push("error", `Dashboard provider "${p.name}" sets folder, folderUid and foldersFromFilesStructure; Grafana refuses to start it. Drop folder and folderUid, or foldersFromFilesStructure.`, p.name);
578
+ }
579
+ else {
580
+ const which = p.folder !== "" ? `folder "${p.folder}"` : `folderUid "${p.folderUid}"`;
581
+ push("warning", `Dashboard provider "${p.name}" sets ${which} and foldersFromFilesStructure. Grafana files each dashboard by its directory and the top-level ones in General, so ${which} is not used${p.folderUid !== "" ? " (before Grafana 13.1 every directory resolves to that one folder uid instead)" : ""}. Drop one of the two.`, p.name);
582
+ }
583
+ }
584
+ // Dashboards with a folder that no provider maps from directories.
585
+ if (foldered.length > 0 && providers.length > 0 && !providers.some((p) => p.fromFiles)) {
586
+ const where = providers.map((p) => `"${p.name}" puts every dashboard in ${p.folder !== "" ? `folder "${p.folder}"` : p.folderUid !== "" ? `folder uid "${p.folderUid}"` : "General"}`).join("; ");
587
+ push("warning", `${foldered.length === 1 ? "A dashboard declares a folder" : `${foldered.length} dashboards declare a folder`}, ${listDashboards(foldered)}, but no dashboard provider sets foldersFromFilesStructure: ${where}. Set foldersFromFilesStructure: true on the provider (and drop its folder), or leave the dashboards' folder out.`);
588
+ }
589
+ // Nested folders, where a provider does map directories to folders.
590
+ if (providers.some((p) => p.fromFiles)) {
591
+ for (const { d, levels } of foldered) {
592
+ if (levels.length < 2)
593
+ continue;
594
+ const uid = String(d.uid ?? "");
595
+ const path = levels.join("/");
596
+ if (levels.length > MAX_FOLDER_DEPTH) {
597
+ push("error", `${dashName(d)} is in folder "${path}", ${levels.length} levels deep; Grafana nests at most ${MAX_FOLDER_DEPTH}, and the provider stops with an error when it reaches it.`, uid);
598
+ }
599
+ else {
600
+ const depth = levels.length > DEFAULT_FOLDER_DEPTH
601
+ ? ` It is ${levels.length} levels deep, past Grafana's default max_nested_folder_depth of ${DEFAULT_FOLDER_DEPTH}: raise it in [folder], or the provider stops with an error when it reaches it.`
602
+ : "";
603
+ push("warning", `${dashName(d)} is in nested folder "${path}". Grafana 13.1 and later create the folders inside one another; Grafana 12.4 and 13.0 use only the last level and put it in a top-level folder "${levels[levels.length - 1]}".${depth}`, uid);
604
+ }
605
+ }
606
+ }
607
+ return issues;
608
+ }
609
+ const BY_CODE = {
610
+ GRAF101: (a) => checkDatasourceRefs(a).filter((i) => i.code === "GRAF101"),
611
+ GRAF102: (a) => checkDatasourceRefs(a).filter((i) => i.code === "GRAF102"),
612
+ GRAF103: checkVariables,
613
+ GRAF104: checkDuplicates,
614
+ GRAF105: checkGrid,
615
+ GRAF106: checkIdentity,
616
+ GRAF107: checkSchema,
617
+ GRAF108: checkPromqlSyntax,
618
+ GRAF109: checkProvisioning,
619
+ GRAF110: checkRepeats,
620
+ GRAF111: (a) => checkRuleQueries(a.alerting ?? []),
621
+ GRAF112: (a) => checkRuleDatasources(a.alerting ?? [], knownDatasourcesOf(a)),
622
+ GRAF113: (a) => checkNotificationRefs(a.alerting ?? []),
623
+ GRAF114: (a) => checkAlertingIdentity(a.alerting ?? []),
624
+ GRAF115: checkUnits,
625
+ GRAF116: checkLogqlSyntax,
626
+ GRAF117: checkTraceqlSyntax,
627
+ };
628
+ /** The issues one check finds. */
629
+ export function issuesFor(code, a) {
630
+ return BY_CODE[code](a);
631
+ }
632
+ /** Every issue every check finds, in code order. */
633
+ export function validateGrafanaOutput(a) {
634
+ return Object.keys(BY_CODE).flatMap((c) => issuesFor(c, a));
635
+ }