@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.
- package/README.md +12 -6
- package/dist/annotations.d.ts +61 -0
- package/dist/annotations.d.ts.map +1 -0
- package/dist/api/apply.d.ts +8 -4
- package/dist/api/apply.d.ts.map +1 -1
- package/dist/api/fake-grafana.d.ts +2 -0
- package/dist/api/fake-grafana.d.ts.map +1 -1
- package/dist/api/folders.d.ts +53 -17
- package/dist/api/folders.d.ts.map +1 -1
- package/dist/build.d.ts +26 -5
- package/dist/build.d.ts.map +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/generate.d.ts +7 -1
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/composites/shared.d.ts +4 -3
- package/dist/composites/shared.d.ts.map +1 -1
- package/dist/dashboard.d.ts +42 -14
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/datasource-refs.d.ts +9 -5
- package/dist/datasource-refs.d.ts.map +1 -1
- package/dist/datasource-settings.d.ts +834 -0
- package/dist/datasource-settings.d.ts.map +1 -0
- package/dist/datasource.d.ts +53 -11
- package/dist/datasource.d.ts.map +1 -1
- package/dist/deep-observe-hooks.d.ts +1 -1
- package/dist/deep-observe-hooks.d.ts.map +1 -1
- package/dist/deep-observe.d.ts +9 -4
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +1 -0
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/detect.d.ts +4 -2
- package/dist/detect.d.ts.map +1 -1
- package/dist/folder.d.ts +68 -0
- package/dist/folder.d.ts.map +1 -0
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/mappings.d.ts +12 -6
- package/dist/import/mappings.d.ts.map +1 -1
- package/dist/import/model.d.ts +7 -0
- package/dist/import/model.d.ts.map +1 -1
- package/dist/import/normalize.d.ts.map +1 -1
- package/dist/import/parser.d.ts +16 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/import/testdata/fixtures.d.ts +6 -0
- package/dist/import/testdata/fixtures.d.ts.map +1 -1
- package/dist/import/v2.d.ts +4 -3
- package/dist/import/v2.d.ts.map +1 -1
- package/dist/index.d.ts +11 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/init-templates.d.ts +27 -0
- package/dist/init-templates.d.ts.map +1 -0
- package/dist/integrity.json +11 -9
- package/dist/lint/audit-catalog.d.ts +1 -1
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/graf101.d.ts +1 -1
- package/dist/lint/post-synth/graf104.d.ts +2 -2
- package/dist/lint/post-synth/graf106.d.ts +2 -2
- package/dist/lint/post-synth/graf110.d.ts +8 -0
- package/dist/lint/post-synth/graf110.d.ts.map +1 -0
- package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +70 -0
- package/dist/okf/index.md +18 -3
- package/dist/okf/rules/GRAF101.md +2 -2
- package/dist/okf/rules/GRAF104.md +2 -2
- package/dist/okf/rules/GRAF106.md +2 -2
- package/dist/okf/rules/GRAF110.md +11 -0
- package/dist/okf/types/AdhocVariable.md +9 -0
- package/dist/okf/types/AzureMonitorQuery.md +9 -0
- package/dist/okf/types/BigQueryQuery.md +9 -0
- package/dist/okf/types/CloudMonitoringQuery.md +9 -0
- package/dist/okf/types/CloudWatchQuery.md +9 -0
- package/dist/okf/types/Dashboard.md +1 -1
- package/dist/okf/types/Datasource.md +1 -1
- package/dist/okf/types/DatasourceProvisioning.md +9 -0
- package/dist/okf/types/ElasticsearchQuery.md +9 -0
- package/dist/okf/types/ExternalDatasource.md +1 -1
- package/dist/okf/types/Folder.md +9 -0
- package/dist/okf/types/GroupByVariable.md +9 -0
- package/dist/okf/types/MSSQLQuery.md +9 -0
- package/dist/okf/types/MySQLQuery.md +9 -0
- package/dist/okf/types/PostgresQuery.md +9 -0
- package/dist/okf/types/PyroscopeQuery.md +9 -0
- package/dist/okf/types/SwitchVariable.md +9 -0
- package/dist/op/activities/grafana-apply.d.ts +11 -0
- package/dist/op/activities/grafana-apply.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +1 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/ownership.d.ts +11 -0
- package/dist/ownership.d.ts.map +1 -1
- package/dist/pin.d.ts +1 -1
- package/dist/pin.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/promql-check.d.ts.map +1 -1
- package/dist/query-models.d.ts +95 -0
- package/dist/query-models.d.ts.map +1 -0
- package/dist/query.d.ts +107 -10
- package/dist/query.d.ts.map +1 -1
- package/dist/rules/graf101.ts +2 -2
- package/dist/rules/graf104.ts +3 -3
- package/dist/rules/graf106.ts +3 -3
- package/dist/rules/graf110.ts +17 -0
- package/dist/rules/grafana-helpers.ts +13 -3
- package/dist/schema/azuremonitor.gen.d.ts +354 -0
- package/dist/schema/azuremonitor.gen.d.ts.map +1 -0
- package/dist/schema/bigquery.gen.d.ts +93 -0
- package/dist/schema/bigquery.gen.d.ts.map +1 -0
- package/dist/schema/cloudwatch.gen.d.ts +394 -0
- package/dist/schema/cloudwatch.gen.d.ts.map +1 -0
- package/dist/schema/elasticsearch.gen.d.ts +444 -0
- package/dist/schema/elasticsearch.gen.d.ts.map +1 -0
- package/dist/schema/googlecloudmonitoring.gen.d.ts +312 -0
- package/dist/schema/googlecloudmonitoring.gen.d.ts.map +1 -0
- package/dist/schema/grafanapyroscope.gen.d.ts +60 -0
- package/dist/schema/grafanapyroscope.gen.d.ts.map +1 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/table.gen.d.ts +13 -0
- package/dist/schema/table.gen.d.ts.map +1 -1
- package/dist/schema-validate.d.ts +9 -0
- package/dist/schema-validate.d.ts.map +1 -1
- package/dist/skill-defs.d.ts.map +1 -1
- package/dist/skills/chant-grafana-operations.md +108 -0
- package/dist/skills/chant-grafana.md +5 -4
- package/dist/spec/schemas.d.ts +9 -7
- package/dist/spec/schemas.d.ts.map +1 -1
- package/dist/spec/schemas.gen.d.ts +3 -0
- package/dist/spec/schemas.gen.d.ts.map +1 -0
- package/dist/validate-alerting.d.ts.map +1 -1
- package/dist/validate-output.d.ts +15 -2
- package/dist/validate-output.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validation.d.ts +14 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/variables.d.ts +102 -5
- package/dist/variables.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/annotations.ts +84 -0
- package/src/api/apply.test.ts +19 -1
- package/src/api/apply.ts +18 -8
- package/src/api/fake-grafana.ts +6 -2
- package/src/api/folders.ts +106 -32
- package/src/build.test.ts +142 -6
- package/src/build.ts +126 -24
- package/src/catalog.ts +20 -2
- package/src/codegen/docs.ts +13 -6
- package/src/codegen/generate.ts +27 -1
- package/src/composites/shared.ts +3 -2
- package/src/dashboard.ts +59 -14
- package/src/datasource-refs.ts +30 -6
- package/src/datasource-settings.test.ts +106 -0
- package/src/datasource-settings.ts +941 -0
- package/src/datasource.ts +71 -11
- package/src/deep-observe-hooks.ts +16 -1
- package/src/deep-observe.test.ts +47 -1
- package/src/deep-observe.ts +36 -13
- package/src/describe-resources.test.ts +16 -0
- package/src/describe-resources.ts +36 -1
- package/src/detect.ts +3 -3
- package/src/folder.ts +118 -0
- package/src/generated/lexicon-grafana.json +70 -0
- package/src/import/generated-types.e2e.test.ts +15 -3
- package/src/import/generator.ts +2 -1
- package/src/import/mappings.ts +98 -26
- package/src/import/model.ts +7 -0
- package/src/import/normalize.ts +32 -3
- package/src/import/parser.test.ts +97 -21
- package/src/import/parser.ts +169 -18
- package/src/import/roundtrip.test.ts +104 -9
- package/src/import/testdata/fixtures.ts +8 -0
- package/src/import/v2.ts +4 -3
- package/src/index.ts +122 -13
- package/src/init-templates.test.ts +74 -0
- package/src/init-templates.ts +349 -0
- package/src/lint/audit-catalog.ts +9 -1
- package/src/lint/post-synth/graf101.ts +2 -2
- package/src/lint/post-synth/graf104.ts +3 -3
- package/src/lint/post-synth/graf106.ts +3 -3
- package/src/lint/post-synth/graf110.ts +17 -0
- package/src/lint/post-synth/grafana-helpers.ts +13 -3
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/post-synth.test.ts +99 -2
- package/src/load-cost.test.ts +35 -0
- package/src/op/activities/grafana-apply.e2e.test.ts +14 -5
- package/src/op/activities/grafana-apply.test.ts +16 -2
- package/src/op/activities/grafana-apply.ts +29 -3
- package/src/op/activities/index.ts +1 -1
- package/src/ownership.ts +19 -0
- package/src/panels.test.ts +3 -1
- package/src/pin.ts +12 -0
- package/src/plugin.test.ts +9 -2
- package/src/plugin.ts +9 -0
- package/src/promql-check.ts +5 -1
- package/src/query-models.ts +94 -0
- package/src/query.test.ts +166 -0
- package/src/query.ts +177 -12
- package/src/schema/azuremonitor.gen.ts +381 -0
- package/src/schema/bigquery.gen.ts +110 -0
- package/src/schema/cloudwatch.gen.ts +423 -0
- package/src/schema/elasticsearch.gen.ts +508 -0
- package/src/schema/googlecloudmonitoring.gen.ts +330 -0
- package/src/schema/grafanapyroscope.gen.ts +66 -0
- package/src/schema/index.ts +6 -0
- package/src/schema/table.gen.ts +14 -0
- package/src/schema-validate.ts +58 -7
- package/src/skill-defs.ts +17 -0
- package/src/skills/chant-grafana-operations.md +108 -0
- package/src/skills/chant-grafana.md +5 -4
- package/src/spec/overlay/table.overlay.json +19 -0
- package/src/spec/schemas/azuremonitor.jsonschema.json +679 -0
- package/src/spec/schemas/bigquery.jsonschema.json +290 -0
- package/src/spec/schemas/cloudwatch.jsonschema.json +721 -0
- package/src/spec/schemas/elasticsearch.jsonschema.json +1695 -0
- package/src/spec/schemas/googlecloudmonitoring.jsonschema.json +488 -0
- package/src/spec/schemas/grafanapyroscope.jsonschema.json +93 -0
- package/src/spec/schemas.gen.ts +40 -0
- package/src/spec/schemas.ts +9 -7
- package/src/validate-alerting.ts +4 -2
- package/src/validate-output.ts +96 -4
- package/src/validate.ts +14 -0
- package/src/validation.ts +27 -0
- package/src/variables.ts +113 -6
package/src/spec/schemas.ts
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Read the vendored Grafana schemas (`src/spec/schemas/*.jsonschema.json`), check
|
|
3
3
|
* them against the digests in `GRAFANA_SCHEMA_PIN`, and apply the correction
|
|
4
4
|
* overlay (`src/spec/overlay/`, see `./overlay.ts`) on top.
|
|
5
|
+
*
|
|
6
|
+
* This is the source side, for `npm run generate`, the lexicon's own
|
|
7
|
+
* validate step, the importer's tests and the fetch scripts: it reads files
|
|
8
|
+
* next to this module. Nothing a build or lint runs imports it. Validation
|
|
9
|
+
* reads the generated `./schemas.gen.ts`, which generate writes from
|
|
10
|
+
* {@link loadSchema}.
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
import { createHash } from "crypto";
|
|
@@ -11,12 +17,7 @@ import { fileURLToPath } from "url";
|
|
|
11
17
|
import { GRAFANA_SCHEMA_PIN, VENDORED_SCHEMA_NAMES, type SchemaName, type VendoredSchemaName } from "../pin";
|
|
12
18
|
import { applyOverlay, loadOverlay } from "./overlay";
|
|
13
19
|
|
|
14
|
-
/**
|
|
15
|
-
* Where the vendored files live: beside this module, under `src/`, so they
|
|
16
|
-
* ship with the source the package runs from and GRAF107 can read them.
|
|
17
|
-
* A function, not a module-scope constant, so edge bundles that import the
|
|
18
|
-
* lexicon never evaluate a filesystem path at load.
|
|
19
|
-
*/
|
|
20
|
+
/** Where the vendored files live: beside this module, under `src/`. */
|
|
20
21
|
export function schemasDir(): string {
|
|
21
22
|
return join(dirname(fileURLToPath(import.meta.url)), "schemas");
|
|
22
23
|
}
|
|
@@ -43,7 +44,8 @@ const parsed = new Map<SchemaName, Record<string, unknown>>();
|
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* One schema as the lexicon uses it: the vendored file with its overlay
|
|
46
|
-
* applied (cached per process).
|
|
47
|
+
* applied (cached per process). Generate writes the types and
|
|
48
|
+
* `./schemas.gen.ts` from this, so GRAF107 sees the same schema.
|
|
47
49
|
*/
|
|
48
50
|
export function loadSchema(name: SchemaName): Record<string, unknown> {
|
|
49
51
|
let schema = parsed.get(name);
|
package/src/validate-alerting.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { parseMatchers } from "@intentius/chant-lexicon-prometheus/matchers";
|
|
|
26
26
|
import { EXPRESSION_DATASOURCE_UID } from "./alerting";
|
|
27
27
|
import type { KnownDatasource } from "./datasource-refs";
|
|
28
28
|
import { isValidUid } from "./util";
|
|
29
|
-
import { validateExpressionSchema } from "./schema-validate";
|
|
29
|
+
import { schemaValidationUnavailable, validateExpressionSchema } from "./schema-validate";
|
|
30
30
|
import { checkGrafanaPromql } from "./promql-check";
|
|
31
31
|
|
|
32
32
|
type Json = Record<string, unknown>;
|
|
@@ -152,7 +152,9 @@ export function checkRuleQueries(docs: readonly AlertingDoc[]): AlertingIssue[]
|
|
|
152
152
|
if (q.datasourceUid !== EXPRESSION_DATASOURCE_UID) continue;
|
|
153
153
|
const model = modelOf(q);
|
|
154
154
|
const at = `expression ${String(q.refId ?? "?")}`;
|
|
155
|
-
|
|
155
|
+
const unavailable = schemaValidationUnavailable();
|
|
156
|
+
if (unavailable) push(`${at} was not checked against the Grafana expression schema: ${unavailable}.`, "warning");
|
|
157
|
+
for (const p of unavailable ? [] : validateExpressionSchema(model)) {
|
|
156
158
|
if (p.severity === "error") push(`${at} ${p.path === "/" ? "" : `${p.path.slice(1)} `}${p.message} (Grafana expression schema).`);
|
|
157
159
|
}
|
|
158
160
|
for (const input of expressionInputs(model)) {
|
package/src/validate-output.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The checks behind GRAF101-
|
|
2
|
+
* The checks behind GRAF101-GRAF110, GRAF111-GRAF114 and GRAF115, as plain
|
|
3
3
|
* functions over built Grafana output: dashboard JSON documents, provisioned
|
|
4
4
|
* datasources, dashboard providers and alerting provisioning files (the
|
|
5
5
|
* alerting checks are in `validate-alerting.ts`). The
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import type { ExternalDatasourceRecord, ProvisionedDatasource } from "./build";
|
|
18
18
|
import { DASHBOARDS_DIR, GRID_COLUMNS } from "./build";
|
|
19
19
|
import {
|
|
20
|
+
annotationsOf,
|
|
20
21
|
datasourceUses,
|
|
21
22
|
describePanel,
|
|
22
23
|
knownDatasources,
|
|
@@ -26,9 +27,9 @@ import {
|
|
|
26
27
|
type DatasourceRefJson,
|
|
27
28
|
type KnownDatasource,
|
|
28
29
|
} from "./datasource-refs";
|
|
29
|
-
import { isBuiltinVariable } from "./variables";
|
|
30
|
+
import { isBuiltinVariable, MULTI_VALUE_KINDS } from "./variables";
|
|
30
31
|
import { isValidUid } from "./util";
|
|
31
|
-
import { validateDashboardSchema } from "./schema-validate";
|
|
32
|
+
import { schemaValidationUnavailable, validateDashboardSchema } from "./schema-validate";
|
|
32
33
|
import { checkGrafanaPromql, prometheusQueries } from "./promql-check";
|
|
33
34
|
import { checkAlertingIdentity, checkNotificationRefs, checkRuleDatasources, checkRulePromql, checkRuleQueries, type AlertingDoc } from "./validate-alerting";
|
|
34
35
|
import { closestGrafanaUnit, isGrafanaUnit } from "./spec/units";
|
|
@@ -43,6 +44,7 @@ export type GrafanaIssueCode =
|
|
|
43
44
|
| "GRAF107"
|
|
44
45
|
| "GRAF108"
|
|
45
46
|
| "GRAF109"
|
|
47
|
+
| "GRAF110"
|
|
46
48
|
| "GRAF111"
|
|
47
49
|
| "GRAF112"
|
|
48
50
|
| "GRAF113"
|
|
@@ -73,6 +75,10 @@ export interface GrafanaArtifacts {
|
|
|
73
75
|
providers?: Array<Record<string, unknown>>;
|
|
74
76
|
/** Alerting provisioning files (rule groups, contact points, policies, mute timings, templates). */
|
|
75
77
|
alerting?: AlertingDoc[];
|
|
78
|
+
/** The folders the build resolved (its index's `folders`): each level of each dashboard's folder path, and every `Folder`. */
|
|
79
|
+
folders?: Array<{ uid: string; title: string; path?: string; parentUid?: string }>;
|
|
80
|
+
/** `deleteDatasources` entries of datasource provisioning files. */
|
|
81
|
+
deleteDatasources?: Array<{ name: string; orgId?: number }>;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
export type { AlertingDoc };
|
|
@@ -180,6 +186,19 @@ export function checkDatasourceRefs(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
|
180
186
|
}
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
// An ExternalDatasource the provisioning file deletes (and does not provision again) is gone once Grafana provisions it.
|
|
190
|
+
const deleted = new Set((a.deleteDatasources ?? []).filter((x) => (x.orgId ?? 1) === 1).map((x) => x.name));
|
|
191
|
+
const provisioned = new Set(a.datasources.map((x) => x.name));
|
|
192
|
+
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));
|
|
193
|
+
for (const { where, resolved } of uses) {
|
|
194
|
+
if (resolved.kind !== "declared" || !gone.has(resolved.datasource.uid)) continue;
|
|
195
|
+
push(
|
|
196
|
+
"GRAF101",
|
|
197
|
+
"error",
|
|
198
|
+
`${where} uses external datasource "${resolved.datasource.name}", which the datasource provisioning file deletes (deleteDatasources); Grafana removes it before the dashboard can use it.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
183
202
|
// A datasource variable offers the datasources of its plugin type; with none declared it has nothing to choose.
|
|
184
203
|
if (known.size > 0) {
|
|
185
204
|
for (const v of dsVariables) {
|
|
@@ -225,10 +244,22 @@ export function checkVariables(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
|
225
244
|
}
|
|
226
245
|
}
|
|
227
246
|
for (const v of variablesOf(d)) {
|
|
247
|
+
const ref = v.datasource as DatasourceRefJson | undefined;
|
|
248
|
+
if (ref && typeof ref === "object" && isVariableUid(ref.uid)) {
|
|
249
|
+
for (const n of variableReferences(ref.uid!)) if (n !== v.name) report(n, `variable "${String(v.name)}" datasource`);
|
|
250
|
+
}
|
|
228
251
|
if (v.type !== "query") continue;
|
|
229
|
-
const names = new Set(stringsIn(v.query, new Set()).flatMap(variableReferences));
|
|
252
|
+
const names = new Set(stringsIn(v.query, new Set(["refId"])).flatMap(variableReferences));
|
|
230
253
|
for (const n of names) if (n !== v.name) report(n, `variable "${String(v.name)}" query`);
|
|
231
254
|
}
|
|
255
|
+
for (const an of annotationsOf(d)) {
|
|
256
|
+
const where = `annotation "${String(an.name ?? "?")}"`;
|
|
257
|
+
const ref = an.datasource as DatasourceRefJson | undefined;
|
|
258
|
+
if (ref && typeof ref === "object" && isVariableUid(ref.uid)) for (const n of variableReferences(ref.uid!)) report(n, `${where} datasource`);
|
|
259
|
+
// Grafana interpolates the query (`target`, or Prometheus's legacy `expr`), not the name, colour or title formats.
|
|
260
|
+
const names = new Set(stringsIn([an.target, an.expr], TARGET_SKIP).flatMap(variableReferences));
|
|
261
|
+
for (const n of names) report(n, `${where} query`);
|
|
262
|
+
}
|
|
232
263
|
}
|
|
233
264
|
return issues;
|
|
234
265
|
}
|
|
@@ -253,6 +284,10 @@ export function checkDuplicates(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
|
253
284
|
const allUids = [...a.datasources.map((d) => d.uid), ...(a.externalDatasources ?? []).map((d) => d.uid)];
|
|
254
285
|
for (const [uid, n] of dupes(allUids, (u) => u)) push(`${n} datasources share the uid "${uid}".`, uid);
|
|
255
286
|
for (const [name, n] of dupes(a.datasources, (d) => d.name)) push(`${n} datasources share the name "${name}"; Grafana needs names to be unique.`, name);
|
|
287
|
+
for (const [uid, n] of dupes(a.folders ?? [], (f) => f.uid)) {
|
|
288
|
+
const paths = (a.folders ?? []).filter((f) => f.uid === uid).map((f) => `"${f.path ?? f.title}"`);
|
|
289
|
+
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);
|
|
290
|
+
}
|
|
256
291
|
for (const { json: d } of a.dashboards) {
|
|
257
292
|
const uid = String(d.uid ?? "");
|
|
258
293
|
const panels = panelsOf(d).map((p) => p.panel);
|
|
@@ -355,6 +390,19 @@ export function checkIdentity(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
|
355
390
|
});
|
|
356
391
|
}
|
|
357
392
|
}
|
|
393
|
+
for (const f of a.folders ?? []) {
|
|
394
|
+
const where = `Folder "${f.path ?? f.title}"`;
|
|
395
|
+
if (!isValidUid(f.uid)) {
|
|
396
|
+
issues.push({ code: "GRAF106", severity: "error", message: `${where} has uid "${f.uid}"; Grafana accepts 1-40 letters, digits, "-" and "_".`, entity: f.uid });
|
|
397
|
+
} else if (f.uid === "general") {
|
|
398
|
+
issues.push({
|
|
399
|
+
code: "GRAF106",
|
|
400
|
+
severity: "error",
|
|
401
|
+
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.`,
|
|
402
|
+
entity: f.uid,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
358
406
|
for (const ds of a.externalDatasources ?? []) {
|
|
359
407
|
if (!isValidUid(ds.uid)) {
|
|
360
408
|
issues.push({
|
|
@@ -377,6 +425,11 @@ export function checkIdentity(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
|
377
425
|
*/
|
|
378
426
|
export function checkSchema(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
379
427
|
const issues: GrafanaIssue[] = [];
|
|
428
|
+
if (a.dashboards.length === 0) return issues;
|
|
429
|
+
const unavailable = schemaValidationUnavailable();
|
|
430
|
+
if (unavailable) {
|
|
431
|
+
return [{ code: "GRAF107", severity: "warning", message: `Dashboards were not checked against the Grafana schema: ${unavailable}.` }];
|
|
432
|
+
}
|
|
380
433
|
for (const { json: d } of a.dashboards) {
|
|
381
434
|
for (const p of validateDashboardSchema(d)) {
|
|
382
435
|
issues.push({
|
|
@@ -412,6 +465,44 @@ export function checkPromqlSyntax(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
|
412
465
|
return issues;
|
|
413
466
|
}
|
|
414
467
|
|
|
468
|
+
// ── GRAF110: repeats ────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* A panel or row repeated over a variable Grafana cannot repeat over, or
|
|
472
|
+
* one that can only ever hold one value. Grafana repeats over a query,
|
|
473
|
+
* custom, datasource or group by variable (a `MultiValueVariable` in
|
|
474
|
+
* @grafana/scenes), once per selected value. Over any other kind it logs an
|
|
475
|
+
* error and shows the panel once; over a query, custom or datasource
|
|
476
|
+
* variable with neither `multi` nor `includeAll`, there is only ever one
|
|
477
|
+
* value to repeat for. A repeat naming no variable is GRAF103's.
|
|
478
|
+
*/
|
|
479
|
+
const KIND_NAMES: Readonly<Record<string, string>> = { adhoc: "an ad hoc", interval: "an interval", groupby: "a group by" };
|
|
480
|
+
|
|
481
|
+
export function checkRepeats(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
482
|
+
const issues: GrafanaIssue[] = [];
|
|
483
|
+
for (const { json: d } of a.dashboards) {
|
|
484
|
+
const variables = new Map(variablesOf(d).map((v) => [String(v.name), v]));
|
|
485
|
+
for (const { panel } of panelsOf(d)) {
|
|
486
|
+
if (typeof panel.repeat !== "string" || panel.repeat === "") continue;
|
|
487
|
+
const v = variables.get(panel.repeat);
|
|
488
|
+
if (!v) continue;
|
|
489
|
+
const where = panel.type === "row" ? `row "${String(panel.title ?? "")}"` : describePanel(panel);
|
|
490
|
+
const type = String(v.type);
|
|
491
|
+
const kind = `${KIND_NAMES[type] ?? `a ${type}`} variable`;
|
|
492
|
+
let why: string | undefined;
|
|
493
|
+
if (!MULTI_VALUE_KINDS.has(type)) {
|
|
494
|
+
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`;
|
|
495
|
+
} else if (type !== "groupby" && v.multi !== true && v.includeAll !== true) {
|
|
496
|
+
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`;
|
|
497
|
+
}
|
|
498
|
+
if (why) {
|
|
499
|
+
issues.push({ code: "GRAF110", severity: "warning", message: `${dashName(d)} ${where} repeats over $${panel.repeat}, ${why}.`, entity: String(d.uid ?? "") });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return issues;
|
|
504
|
+
}
|
|
505
|
+
|
|
415
506
|
// ── GRAF115: units ──────────────────────────────────────────────
|
|
416
507
|
|
|
417
508
|
/** Every unit a panel sets, with where: field defaults, `unit` overrides, heatmap axes and cells, and a legacy graph panel's y-axes. */
|
|
@@ -593,6 +684,7 @@ const BY_CODE: Record<GrafanaIssueCode, (a: GrafanaArtifacts) => GrafanaIssue[]>
|
|
|
593
684
|
GRAF107: checkSchema,
|
|
594
685
|
GRAF108: checkPromqlSyntax,
|
|
595
686
|
GRAF109: checkProvisioning,
|
|
687
|
+
GRAF110: checkRepeats,
|
|
596
688
|
GRAF111: (a) => checkRuleQueries(a.alerting ?? []),
|
|
597
689
|
GRAF112: (a) => checkRuleDatasources(a.alerting ?? [], knownDatasourcesOf(a)),
|
|
598
690
|
GRAF113: (a) => checkNotificationRefs(a.alerting ?? []),
|
package/src/validate.ts
CHANGED
|
@@ -25,6 +25,8 @@ export const REQUIRED_NAMES = [
|
|
|
25
25
|
"Datasource",
|
|
26
26
|
"ExternalDatasource",
|
|
27
27
|
"DashboardProvider",
|
|
28
|
+
"DatasourceProvisioning",
|
|
29
|
+
"Folder",
|
|
28
30
|
"Row",
|
|
29
31
|
"TimeSeriesPanel",
|
|
30
32
|
"StatPanel",
|
|
@@ -50,12 +52,24 @@ export const REQUIRED_NAMES = [
|
|
|
50
52
|
"PromQuery",
|
|
51
53
|
"TempoQuery",
|
|
52
54
|
"LokiQuery",
|
|
55
|
+
"ElasticsearchQuery",
|
|
56
|
+
"CloudWatchQuery",
|
|
57
|
+
"AzureMonitorQuery",
|
|
58
|
+
"CloudMonitoringQuery",
|
|
59
|
+
"BigQueryQuery",
|
|
60
|
+
"PyroscopeQuery",
|
|
61
|
+
"PostgresQuery",
|
|
62
|
+
"MySQLQuery",
|
|
63
|
+
"MSSQLQuery",
|
|
53
64
|
"QueryVariable",
|
|
54
65
|
"CustomVariable",
|
|
55
66
|
"IntervalVariable",
|
|
56
67
|
"DatasourceVariable",
|
|
57
68
|
"ConstantVariable",
|
|
58
69
|
"TextboxVariable",
|
|
70
|
+
"AdhocVariable",
|
|
71
|
+
"GroupByVariable",
|
|
72
|
+
"SwitchVariable",
|
|
59
73
|
];
|
|
60
74
|
|
|
61
75
|
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@intentius/chant-lexicon-grafana/validation`: the GRAF101-GRAF115 checks
|
|
3
|
+
* as plain functions over built Grafana output, and the schema validation
|
|
4
|
+
* behind GRAF107, for code that checks dashboards without a build.
|
|
5
|
+
*
|
|
6
|
+
* These are not on the package root, so importing the lexicon to declare
|
|
7
|
+
* dashboards does not load them. `chant build` and `chant lint` run the same
|
|
8
|
+
* checks through the plugin's post-synth checks. ajv is loaded the first time
|
|
9
|
+
* a schema is checked, and the schemas come from a generated module rather
|
|
10
|
+
* than files, so this works bundled.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
validateGrafanaOutput,
|
|
15
|
+
issuesFor,
|
|
16
|
+
knownDatasourcesOf,
|
|
17
|
+
type GrafanaIssue,
|
|
18
|
+
type GrafanaIssueCode,
|
|
19
|
+
type GrafanaArtifacts,
|
|
20
|
+
type DashboardDoc,
|
|
21
|
+
} from "./validate-output";
|
|
22
|
+
export {
|
|
23
|
+
validateDashboardSchema,
|
|
24
|
+
validateExpressionSchema,
|
|
25
|
+
schemaValidationUnavailable,
|
|
26
|
+
type SchemaProblem,
|
|
27
|
+
} from "./schema-validate";
|
package/src/variables.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dashboard variables (Grafana's "templating"): query, custom, interval,
|
|
3
|
-
* datasource, constant and
|
|
3
|
+
* datasource, constant, textbox, ad hoc filters, group by and switch.
|
|
4
4
|
*
|
|
5
5
|
* A variable is declared on its own and listed in a dashboard's
|
|
6
6
|
* `variables`. Queries reference it as `$name` or `${name}` in their
|
|
@@ -12,9 +12,12 @@
|
|
|
12
12
|
import { createProperty } from "@intentius/chant/runtime";
|
|
13
13
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
14
14
|
import type { DatasourceEntity, DatasourceRef, ExternalDatasourceEntity } from "./datasource";
|
|
15
|
-
import type { VariableOption } from "./schema/dashboard.gen";
|
|
15
|
+
import type { AdHocFilter, VariableOption } from "./schema/dashboard.gen";
|
|
16
16
|
|
|
17
|
-
export type VariableKind = "query" | "custom" | "interval" | "datasource" | "constant" | "textbox";
|
|
17
|
+
export type VariableKind = "query" | "custom" | "interval" | "datasource" | "constant" | "textbox" | "adhoc" | "groupby" | "switch";
|
|
18
|
+
|
|
19
|
+
/** The kinds Grafana can repeat a panel or row over: the ones that hold a list of values (`MultiValueVariable` in @grafana/scenes). */
|
|
20
|
+
export const MULTI_VALUE_KINDS: ReadonlySet<string> = new Set(["query", "custom", "datasource", "groupby"]);
|
|
18
21
|
|
|
19
22
|
/** Where the variable shows: with its label, without it, or not at all. */
|
|
20
23
|
export type VariableHide = "label" | "valueOnly" | "hidden";
|
|
@@ -42,10 +45,45 @@ interface MultiValueProps {
|
|
|
42
45
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
46
|
export type VariableDatasource = DatasourceEntity<any> | ExternalDatasourceEntity<any> | DatasourceRef | DatasourceVariableEntity<any>;
|
|
44
47
|
|
|
48
|
+
/**
|
|
49
|
+
* A variable query in the object form a datasource's variable editor
|
|
50
|
+
* writes. Grafana passes it to the datasource as it is, so any key the
|
|
51
|
+
* datasource reads may be here.
|
|
52
|
+
*/
|
|
53
|
+
export interface VariableQueryObject {
|
|
54
|
+
/** The query text, when the datasource has one (Prometheus, Loki, and most others). */
|
|
55
|
+
query?: string;
|
|
56
|
+
/** The editor's id for the query; Grafana writes e.g. `PrometheusVariableQueryEditor-VariableQuery`. */
|
|
57
|
+
refId?: string;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A Prometheus variable query as Grafana's Prometheus variable editor
|
|
63
|
+
* writes it. `query` is the text Grafana runs (`label_values(up, job)`);
|
|
64
|
+
* the other fields let the editor show the query in its form again.
|
|
65
|
+
*/
|
|
66
|
+
export interface PrometheusVariableQuery extends VariableQueryObject {
|
|
67
|
+
query: string;
|
|
68
|
+
/** The editor's query type: 0 label names, 1 label values, 2 metrics, 3 query result, 4 series query, 5 classic query. */
|
|
69
|
+
qryType?: 0 | 1 | 2 | 3 | 4 | 5;
|
|
70
|
+
label?: string;
|
|
71
|
+
metric?: string;
|
|
72
|
+
seriesQuery?: string;
|
|
73
|
+
varQueryResult?: string;
|
|
74
|
+
labelFilters?: Array<{ label: string; op: string; value: string }>;
|
|
75
|
+
}
|
|
76
|
+
|
|
45
77
|
export interface QueryVariableProps extends CommonVariableProps, MultiValueProps {
|
|
46
78
|
datasource: VariableDatasource;
|
|
47
|
-
/**
|
|
48
|
-
|
|
79
|
+
/**
|
|
80
|
+
* The datasource's variable query: a string (`label_values(up, job)` for
|
|
81
|
+
* Prometheus), or the object the datasource's variable editor writes
|
|
82
|
+
* (`{ query: "label_values(up, job)", qryType: 1 }`).
|
|
83
|
+
*/
|
|
84
|
+
query: string | VariableQueryObject;
|
|
85
|
+
/** The text Grafana shows for the query in the variable list. Defaults to the query string. */
|
|
86
|
+
definition?: string;
|
|
49
87
|
regex?: string;
|
|
50
88
|
/** When to re-run the query. Defaults to on dashboard load. */
|
|
51
89
|
refresh?: "never" | "onLoad" | "onTimeRangeChange";
|
|
@@ -85,6 +123,51 @@ export interface TextboxVariableProps extends CommonVariableProps {
|
|
|
85
123
|
value?: string;
|
|
86
124
|
}
|
|
87
125
|
|
|
126
|
+
/** One ad hoc filter: `key operator value`, e.g. `{ key: "namespace", operator: "=", value: "shop" }`. */
|
|
127
|
+
export type AdhocFilter = AdHocFilter;
|
|
128
|
+
|
|
129
|
+
/** A key an ad hoc or group by variable offers, in place of asking the datasource. */
|
|
130
|
+
export interface VariableKeyOption {
|
|
131
|
+
text: string;
|
|
132
|
+
value?: string | number;
|
|
133
|
+
[key: string]: unknown;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface AdhocVariableProps extends CommonVariableProps {
|
|
137
|
+
/** The datasource whose queries get the filters, and which is asked for keys and values. */
|
|
138
|
+
datasource: VariableDatasource;
|
|
139
|
+
/** The filters applied when the dashboard loads. */
|
|
140
|
+
filters?: AdhocFilter[];
|
|
141
|
+
/** Filters that narrow the key and value lookups, not shown or applied to queries. */
|
|
142
|
+
baseFilters?: AdhocFilter[];
|
|
143
|
+
/** A fixed set of keys to offer instead of the datasource's. */
|
|
144
|
+
defaultKeys?: VariableKeyOption[];
|
|
145
|
+
/** Whether a key or value not in the list can be typed in. Grafana's default is true. */
|
|
146
|
+
allowCustomValue?: boolean;
|
|
147
|
+
/** Offer the group-by operator in the filter box (Grafana 13, behind the `dashboardUnifiedDrilldownControls` feature toggle). */
|
|
148
|
+
enableGroupBy?: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface GroupByVariableProps extends CommonVariableProps {
|
|
152
|
+
/** The datasource whose queries are grouped, and which is asked for the keys. */
|
|
153
|
+
datasource: VariableDatasource;
|
|
154
|
+
/** A fixed set of keys to offer instead of the datasource's: a key, or `{ text, value }`. */
|
|
155
|
+
options?: Array<string | { text: string; value: string }>;
|
|
156
|
+
/** The keys selected when the dashboard loads with none in the URL. */
|
|
157
|
+
defaultValue?: string[] | VariableOption;
|
|
158
|
+
/** Whether a key not in the list can be typed in. Grafana's default is true. */
|
|
159
|
+
allowCustomValue?: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface SwitchVariableProps extends Omit<CommonVariableProps, "current"> {
|
|
163
|
+
/** Whether the switch is on when the dashboard loads. Defaults to off. */
|
|
164
|
+
enabled?: boolean;
|
|
165
|
+
/** The value `$name` has when the switch is on. Defaults to `"true"`. */
|
|
166
|
+
enabledValue?: string;
|
|
167
|
+
/** The value `$name` has when the switch is off. Defaults to `"false"`. */
|
|
168
|
+
disabledValue?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
88
171
|
export interface VariableEntity<P = CommonVariableProps> extends Declarable {
|
|
89
172
|
readonly props: P;
|
|
90
173
|
readonly variableKind: VariableKind;
|
|
@@ -99,7 +182,7 @@ export interface DatasourceVariableEntity<T extends string = string> extends Var
|
|
|
99
182
|
|
|
100
183
|
export const VARIABLE_TYPE_PREFIX = "Grafana::Variable::";
|
|
101
184
|
|
|
102
|
-
function variableClass<P extends CommonVariableProps | ConstantVariableProps>(kind: VariableKind, className: string) {
|
|
185
|
+
function variableClass<P extends CommonVariableProps | ConstantVariableProps | SwitchVariableProps>(kind: VariableKind, className: string) {
|
|
103
186
|
const Base = createProperty(`${VARIABLE_TYPE_PREFIX}${kind}`, "grafana") as unknown as (this: object, props: Record<string, unknown>) => void;
|
|
104
187
|
const Cls = function (this: object, props: P) {
|
|
105
188
|
Base.call(this, props as unknown as Record<string, unknown>);
|
|
@@ -143,6 +226,30 @@ export const TextboxVariable = variableClass<TextboxVariableProps>("textbox", "T
|
|
|
143
226
|
props: TextboxVariableProps,
|
|
144
227
|
) => VariableEntity<TextboxVariableProps>;
|
|
145
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Ad hoc filters: `key operator value` filters Grafana adds to every query
|
|
231
|
+
* sent to the variable's datasource (Prometheus, Loki, Elasticsearch,
|
|
232
|
+
* InfluxDB and others whose plugin supports them). Referenced by no query.
|
|
233
|
+
*/
|
|
234
|
+
export const AdhocVariable = variableClass<AdhocVariableProps>("adhoc", "AdhocVariable") as unknown as new (
|
|
235
|
+
props: AdhocVariableProps,
|
|
236
|
+
) => VariableEntity<AdhocVariableProps>;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Group by: a choice of label keys Grafana adds as a grouping to every query
|
|
240
|
+
* sent to the variable's datasource. In Grafana 12.4 and 13.x it is
|
|
241
|
+
* experimental: with the `groupByVariable` feature toggle off, Grafana
|
|
242
|
+
* drops it when the dashboard loads.
|
|
243
|
+
*/
|
|
244
|
+
export const GroupByVariable = variableClass<GroupByVariableProps>("groupby", "GroupByVariable") as unknown as new (
|
|
245
|
+
props: GroupByVariableProps,
|
|
246
|
+
) => VariableEntity<GroupByVariableProps>;
|
|
247
|
+
|
|
248
|
+
/** An on/off switch, `$name` being `enabledValue` or `disabledValue`. New in Grafana 12.3. */
|
|
249
|
+
export const SwitchVariable = variableClass<SwitchVariableProps>("switch", "SwitchVariable") as unknown as new (
|
|
250
|
+
props: SwitchVariableProps,
|
|
251
|
+
) => VariableEntity<SwitchVariableProps>;
|
|
252
|
+
|
|
146
253
|
export function isVariableEntity(value: unknown): value is VariableEntity {
|
|
147
254
|
return (
|
|
148
255
|
typeof value === "object" &&
|