@intentius/chant-lexicon-grafana 0.97.0 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -5
- package/dist/actions/grafana.d.ts +24 -0
- package/dist/actions/grafana.d.ts.map +1 -0
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/alerting-build.d.ts +128 -0
- package/dist/alerting-build.d.ts.map +1 -0
- package/dist/alerting.d.ts +273 -0
- package/dist/alerting.d.ts.map +1 -0
- package/dist/api/apply.d.ts +156 -0
- package/dist/api/apply.d.ts.map +1 -0
- package/dist/api/bind.d.ts +32 -0
- package/dist/api/bind.d.ts.map +1 -0
- package/dist/api/client.d.ts +76 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/converge.d.ts +33 -0
- package/dist/api/converge.d.ts.map +1 -0
- package/dist/api/dashboards.d.ts +102 -0
- package/dist/api/dashboards.d.ts.map +1 -0
- package/dist/api/datasources.d.ts +26 -0
- package/dist/api/datasources.d.ts.map +1 -0
- package/dist/api/fake-grafana-writes.d.ts +41 -0
- package/dist/api/fake-grafana-writes.d.ts.map +1 -0
- package/dist/api/fake-grafana.d.ts +61 -0
- package/dist/api/fake-grafana.d.ts.map +1 -0
- package/dist/api/folders.d.ts +110 -0
- package/dist/api/folders.d.ts.map +1 -0
- package/dist/api/library-panels.d.ts +52 -0
- package/dist/api/library-panels.d.ts.map +1 -0
- package/dist/build.d.ts +30 -2
- 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/schema-types.d.ts +3 -3
- package/dist/codegen/schema-types.d.ts.map +1 -1
- package/dist/composites/agent-dashboard.d.ts.map +1 -1
- package/dist/composites/catalog.d.ts.map +1 -1
- package/dist/composites/index.d.ts +4 -2
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/red-dashboard.d.ts +21 -2
- package/dist/composites/red-dashboard.d.ts.map +1 -1
- package/dist/composites/shared.d.ts +7 -0
- package/dist/composites/shared.d.ts.map +1 -1
- package/dist/composites/slo-alert-rules.d.ts +92 -0
- package/dist/composites/slo-alert-rules.d.ts.map +1 -0
- package/dist/config.d.ts +111 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/contact-point-secrets.d.ts +17 -0
- package/dist/contact-point-secrets.d.ts.map +1 -0
- package/dist/dashboard.d.ts +16 -2
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/datasource-refs.d.ts +95 -0
- package/dist/datasource-refs.d.ts.map +1 -0
- package/dist/datasource.d.ts +29 -0
- package/dist/datasource.d.ts.map +1 -1
- package/dist/deep-observe-hooks.d.ts +64 -0
- package/dist/deep-observe-hooks.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +42 -0
- package/dist/deep-observe.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +49 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/detect.d.ts +28 -1
- package/dist/detect.d.ts.map +1 -1
- package/dist/export-resources.d.ts +38 -0
- package/dist/export-resources.d.ts.map +1 -0
- package/dist/import/alerting.d.ts +40 -0
- package/dist/import/alerting.d.ts.map +1 -0
- package/dist/import/edits.d.ts +47 -0
- package/dist/import/edits.d.ts.map +1 -0
- package/dist/import/embedded.d.ts +14 -0
- package/dist/import/embedded.d.ts.map +1 -0
- package/dist/import/generator.d.ts +60 -0
- package/dist/import/generator.d.ts.map +1 -0
- package/dist/import/live-export.d.ts +108 -0
- package/dist/import/live-export.d.ts.map +1 -0
- package/dist/import/mappings.d.ts +83 -0
- package/dist/import/mappings.d.ts.map +1 -0
- package/dist/import/model.d.ts +98 -0
- package/dist/import/model.d.ts.map +1 -0
- package/dist/import/normalize-alerting.d.ts +26 -0
- package/dist/import/normalize-alerting.d.ts.map +1 -0
- package/dist/import/normalize.d.ts +74 -0
- package/dist/import/normalize.d.ts.map +1 -0
- package/dist/import/parser.d.ts +110 -0
- package/dist/import/parser.d.ts.map +1 -0
- package/dist/import/report.d.ts +26 -0
- package/dist/import/report.d.ts.map +1 -0
- package/dist/import/testdata/fixtures.d.ts +35 -0
- package/dist/import/testdata/fixtures.d.ts.map +1 -0
- package/dist/import/v2.d.ts +72 -0
- package/dist/import/v2.d.ts.map +1 -0
- package/dist/index.d.ts +26 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +18 -10
- package/dist/k8s.d.ts +107 -0
- package/dist/k8s.d.ts.map +1 -0
- package/dist/lint/audit-catalog.d.ts +2 -1
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/graf101.d.ts +2 -2
- package/dist/lint/post-synth/graf102.d.ts +1 -1
- package/dist/lint/post-synth/graf107.d.ts +1 -1
- package/dist/lint/post-synth/graf108.d.ts +8 -0
- package/dist/lint/post-synth/graf108.d.ts.map +1 -0
- package/dist/lint/post-synth/graf109.d.ts +8 -0
- package/dist/lint/post-synth/graf109.d.ts.map +1 -0
- package/dist/lint/post-synth/graf111.d.ts +8 -0
- package/dist/lint/post-synth/graf111.d.ts.map +1 -0
- package/dist/lint/post-synth/graf112.d.ts +8 -0
- package/dist/lint/post-synth/graf112.d.ts.map +1 -0
- package/dist/lint/post-synth/graf113.d.ts +8 -0
- package/dist/lint/post-synth/graf113.d.ts.map +1 -0
- package/dist/lint/post-synth/graf114.d.ts +8 -0
- package/dist/lint/post-synth/graf114.d.ts.map +1 -0
- package/dist/lint/post-synth/graf115.d.ts +8 -0
- package/dist/lint/post-synth/graf115.d.ts.map +1 -0
- package/dist/lint/post-synth/grafana-helpers.d.ts +7 -5
- 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/lint/rules/literal-secret.d.ts +7 -3
- package/dist/lint/rules/literal-secret.d.ts.map +1 -1
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/members.d.ts +27 -0
- package/dist/members.d.ts.map +1 -0
- package/dist/meta.json +135 -0
- package/dist/okf/index.md +36 -2
- package/dist/okf/rules/GRAF002.md +3 -2
- package/dist/okf/rules/GRAF101.md +3 -2
- package/dist/okf/rules/GRAF102.md +1 -0
- package/dist/okf/rules/GRAF108.md +11 -0
- package/dist/okf/rules/GRAF109.md +15 -0
- package/dist/okf/rules/GRAF111.md +11 -0
- package/dist/okf/rules/GRAF112.md +16 -0
- package/dist/okf/rules/GRAF113.md +16 -0
- package/dist/okf/rules/GRAF114.md +11 -0
- package/dist/okf/rules/GRAF115.md +11 -0
- package/dist/okf/types/AlertListPanel.md +9 -0
- package/dist/okf/types/AlertQuery.md +9 -0
- package/dist/okf/types/AlertRule.md +9 -0
- package/dist/okf/types/AlertRuleGroup.md +9 -0
- package/dist/okf/types/BarChartPanel.md +9 -0
- package/dist/okf/types/BarGaugePanel.md +9 -0
- package/dist/okf/types/CanvasPanel.md +9 -0
- package/dist/okf/types/ClassicConditionsExpression.md +9 -0
- package/dist/okf/types/ContactPoint.md +14 -0
- package/dist/okf/types/Dashboard.md +1 -0
- package/dist/okf/types/Datasource.md +3 -2
- package/dist/okf/types/ExternalDatasource.md +15 -0
- package/dist/okf/types/FlameGraphPanel.md +9 -0
- package/dist/okf/types/GeomapPanel.md +9 -0
- package/dist/okf/types/HistogramPanel.md +9 -0
- package/dist/okf/types/MathExpression.md +9 -0
- package/dist/okf/types/MuteTiming.md +13 -0
- package/dist/okf/types/NodeGraphPanel.md +9 -0
- package/dist/okf/types/NotificationPolicy.md +9 -0
- package/dist/okf/types/NotificationTemplate.md +9 -0
- package/dist/okf/types/PieChartPanel.md +9 -0
- package/dist/okf/types/ReduceExpression.md +9 -0
- package/dist/okf/types/ResampleExpression.md +9 -0
- package/dist/okf/types/SqlExpression.md +9 -0
- package/dist/okf/types/StateTimelinePanel.md +9 -0
- package/dist/okf/types/StatusHistoryPanel.md +9 -0
- package/dist/okf/types/ThresholdExpression.md +9 -0
- package/dist/okf/types/TrendPanel.md +9 -0
- package/dist/okf/types/XYChartPanel.md +9 -0
- package/dist/op/activities/grafana-apply.d.ts +75 -0
- package/dist/op/activities/grafana-apply.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +9 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/builders.d.ts +24 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/ownership.d.ts +67 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/panel-options.d.ts +65 -0
- package/dist/panel-options.d.ts.map +1 -0
- package/dist/panels.d.ts +46 -9
- package/dist/panels.d.ts.map +1 -1
- package/dist/pin.d.ts +29 -7
- package/dist/pin.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/promql-check.d.ts +51 -0
- package/dist/promql-check.d.ts.map +1 -0
- package/dist/query.d.ts +9 -8
- package/dist/query.d.ts.map +1 -1
- package/dist/rules/graf101.ts +3 -3
- package/dist/rules/graf102.ts +1 -1
- package/dist/rules/graf107.ts +1 -1
- package/dist/rules/graf108.ts +17 -0
- package/dist/rules/graf109.ts +17 -0
- package/dist/rules/graf111.ts +17 -0
- package/dist/rules/graf112.ts +17 -0
- package/dist/rules/graf113.ts +17 -0
- package/dist/rules/graf114.ts +17 -0
- package/dist/rules/graf115.ts +17 -0
- package/dist/rules/grafana-helpers.ts +15 -8
- package/dist/rules/literal-secret.ts +60 -6
- package/dist/schema/barchart.gen.d.ts +198 -0
- package/dist/schema/barchart.gen.d.ts.map +1 -0
- package/dist/schema/bargauge.gen.d.ts +97 -0
- package/dist/schema/bargauge.gen.d.ts.map +1 -0
- package/dist/schema/canvas.gen.d.ts +152 -0
- package/dist/schema/canvas.gen.d.ts.map +1 -0
- package/dist/schema/dashboard.gen.d.ts +196 -21
- package/dist/schema/dashboard.gen.d.ts.map +1 -1
- package/dist/schema/expr.gen.d.ts +606 -0
- package/dist/schema/expr.gen.d.ts.map +1 -0
- package/dist/schema/gauge.gen.d.ts.map +1 -1
- package/dist/schema/geomap.gen.d.ts +100 -0
- package/dist/schema/geomap.gen.d.ts.map +1 -0
- package/dist/schema/heatmap.gen.d.ts.map +1 -1
- package/dist/schema/histogram.gen.d.ts +132 -0
- package/dist/schema/histogram.gen.d.ts.map +1 -0
- package/dist/schema/index.d.ts +12 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/logs.gen.d.ts +1 -0
- package/dist/schema/logs.gen.d.ts.map +1 -1
- package/dist/schema/loki.gen.d.ts.map +1 -1
- package/dist/schema/nodegraph.gen.d.ts +49 -0
- package/dist/schema/nodegraph.gen.d.ts.map +1 -0
- package/dist/schema/piechart.gen.d.ts +127 -0
- package/dist/schema/piechart.gen.d.ts.map +1 -0
- package/dist/schema/prometheus.gen.d.ts.map +1 -1
- package/dist/schema/stat.gen.d.ts.map +1 -1
- package/dist/schema/statetimeline.gen.d.ts +124 -0
- package/dist/schema/statetimeline.gen.d.ts.map +1 -0
- package/dist/schema/statushistory.gen.d.ts +114 -0
- package/dist/schema/statushistory.gen.d.ts.map +1 -0
- package/dist/schema/table.gen.d.ts +22 -3
- package/dist/schema/table.gen.d.ts.map +1 -1
- package/dist/schema/tempo.gen.d.ts.map +1 -1
- package/dist/schema/text.gen.d.ts.map +1 -1
- package/dist/schema/timeseries.gen.d.ts +5 -1
- package/dist/schema/timeseries.gen.d.ts.map +1 -1
- package/dist/schema/trend.gen.d.ts +180 -0
- package/dist/schema/trend.gen.d.ts.map +1 -0
- package/dist/schema/xychart.gen.d.ts +145 -0
- package/dist/schema/xychart.gen.d.ts.map +1 -0
- package/dist/schema-validate.d.ts +30 -4
- package/dist/schema-validate.d.ts.map +1 -1
- package/dist/serializer.d.ts +11 -3
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skill-defs.d.ts.map +1 -1
- package/dist/skills/chant-grafana-alerting.md +49 -0
- package/dist/skills/chant-grafana-provisioning.md +3 -3
- package/dist/skills/chant-grafana.md +12 -6
- package/dist/spec/fetch-units-cli.d.ts +3 -0
- package/dist/spec/fetch-units-cli.d.ts.map +1 -0
- package/dist/spec/overlay.d.ts +43 -0
- package/dist/spec/overlay.d.ts.map +1 -0
- package/dist/spec/schemas.d.ts +13 -7
- package/dist/spec/schemas.d.ts.map +1 -1
- package/dist/spec/units.d.ts +35 -0
- package/dist/spec/units.d.ts.map +1 -0
- package/dist/spec/units.gen.d.ts +2 -0
- package/dist/spec/units.gen.d.ts.map +1 -0
- package/dist/transformations.d.ts +459 -0
- package/dist/transformations.d.ts.map +1 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/validate-alerting.d.ts +65 -0
- package/dist/validate-alerting.d.ts.map +1 -0
- package/dist/validate-output.d.ts +49 -9
- package/dist/validate-output.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/variables.d.ts +7 -2
- package/dist/variables.d.ts.map +1 -1
- package/package.json +12 -4
- package/src/actions/actions.test.ts +24 -0
- package/src/actions/grafana.ts +40 -0
- package/src/actions/index.ts +1 -0
- package/src/alerting-build.ts +376 -0
- package/src/alerting.e2e.test.ts +212 -0
- package/src/alerting.test.ts +272 -0
- package/src/alerting.ts +435 -0
- package/src/api/apply.test.ts +238 -0
- package/src/api/apply.ts +384 -0
- package/src/api/bind.ts +58 -0
- package/src/api/client.ts +148 -0
- package/src/api/converge.ts +63 -0
- package/src/api/dashboards.ts +247 -0
- package/src/api/datasources.ts +33 -0
- package/src/api/fake-grafana-writes.ts +201 -0
- package/src/api/fake-grafana.ts +186 -0
- package/src/api/folders.ts +265 -0
- package/src/api/library-panels.ts +101 -0
- package/src/build.test.ts +210 -5
- package/src/build.ts +180 -43
- package/src/catalog.ts +86 -2
- package/src/codegen/docs.ts +42 -9
- package/src/codegen/schema-types.ts +16 -6
- package/src/composites/agent-dashboard.ts +6 -3
- package/src/composites/catalog.test.ts +2 -2
- package/src/composites/catalog.ts +82 -0
- package/src/composites/composites.test.ts +73 -9
- package/src/composites/index.ts +4 -2
- package/src/composites/queries.e2e.test.ts +374 -0
- package/src/composites/red-dashboard.ts +83 -12
- package/src/composites/shared.ts +11 -0
- package/src/composites/slo-alert-rules.ts +149 -0
- package/src/config.test.ts +61 -0
- package/src/config.ts +138 -0
- package/src/contact-point-secrets.ts +63 -0
- package/src/dashboard.ts +16 -2
- package/src/datasource-refs.test.ts +56 -0
- package/src/datasource-refs.ts +189 -0
- package/src/datasource.ts +52 -0
- package/src/deep-observe-hooks.ts +252 -0
- package/src/deep-observe.test.ts +265 -0
- package/src/deep-observe.ts +164 -0
- package/src/describe-resources.test.ts +255 -0
- package/src/describe-resources.ts +209 -0
- package/src/detect.ts +79 -2
- package/src/export-resources.test.ts +129 -0
- package/src/export-resources.ts +84 -0
- package/src/generated/lexicon-grafana.json +135 -0
- package/src/import/alerting-roundtrip.test.ts +161 -0
- package/src/import/alerting.ts +500 -0
- package/src/import/edits.ts +95 -0
- package/src/import/embedded.test.ts +45 -0
- package/src/import/embedded.ts +47 -0
- package/src/import/generated-types.e2e.test.ts +118 -0
- package/src/import/generator.test.ts +210 -0
- package/src/import/generator.ts +462 -0
- package/src/import/live-export.ts +264 -0
- package/src/import/mappings.ts +264 -0
- package/src/import/model.ts +110 -0
- package/src/import/normalize-alerting.ts +101 -0
- package/src/import/normalize.test.ts +117 -0
- package/src/import/normalize.ts +364 -0
- package/src/import/parser.test.ts +440 -0
- package/src/import/parser.ts +1236 -0
- package/src/import/report.ts +62 -0
- package/src/import/roundtrip.test.ts +468 -0
- package/src/import/testdata/fixtures.ts +129 -0
- package/src/import/v2.test.ts +292 -0
- package/src/import/v2.ts +803 -0
- package/src/import.e2e.test.ts +31 -84
- package/src/import.test.ts +9 -3
- package/src/index.ts +190 -5
- package/src/k8s.test.ts +98 -0
- package/src/k8s.ts +197 -0
- package/src/lint/audit-catalog.ts +60 -3
- package/src/lint/core-lint.test.ts +43 -0
- package/src/lint/post-synth/graf101.ts +3 -3
- package/src/lint/post-synth/graf102.ts +1 -1
- package/src/lint/post-synth/graf107.ts +1 -1
- package/src/lint/post-synth/graf108.ts +17 -0
- package/src/lint/post-synth/graf109.ts +17 -0
- package/src/lint/post-synth/graf111.ts +17 -0
- package/src/lint/post-synth/graf112.ts +17 -0
- package/src/lint/post-synth/graf113.ts +17 -0
- package/src/lint/post-synth/graf114.ts +17 -0
- package/src/lint/post-synth/graf115.ts +17 -0
- package/src/lint/post-synth/grafana-helpers.ts +15 -8
- package/src/lint/post-synth/index.ts +14 -0
- package/src/lint/post-synth/post-synth.test.ts +235 -11
- package/src/lint/rules/literal-secret.ts +60 -6
- package/src/lint/rules/rules.test.ts +27 -0
- package/src/lsp/completions.test.ts +5 -0
- package/src/lsp/hover.test.ts +7 -0
- package/src/lsp/hover.ts +8 -1
- package/src/members.ts +68 -0
- package/src/observe.e2e.test.ts +255 -0
- package/src/op/activities/grafana-apply.e2e.test.ts +175 -0
- package/src/op/activities/grafana-apply.test.ts +229 -0
- package/src/op/activities/grafana-apply.ts +171 -0
- package/src/op/activities/index.ts +8 -0
- package/src/op/builders.test.ts +40 -0
- package/src/op/builders.ts +34 -0
- package/src/ownership.ts +103 -0
- package/src/panel-options.ts +66 -0
- package/src/panels.test.ts +51 -0
- package/src/panels.ts +148 -10
- package/src/pin.ts +57 -6
- package/src/plugin.test.ts +5 -2
- package/src/plugin.ts +66 -0
- package/src/promql-check.test.ts +108 -0
- package/src/promql-check.ts +189 -0
- package/src/query.ts +13 -8
- package/src/schema/barchart.gen.ts +221 -0
- package/src/schema/bargauge.gen.ts +111 -0
- package/src/schema/canvas.gen.ts +176 -0
- package/src/schema/dashboard.gen.ts +206 -22
- package/src/schema/expr.gen.ts +618 -0
- package/src/schema/gauge.gen.ts +2 -1
- package/src/schema/geomap.gen.ts +112 -0
- package/src/schema/heatmap.gen.ts +2 -1
- package/src/schema/histogram.gen.ts +151 -0
- package/src/schema/index.ts +12 -0
- package/src/schema/logs.gen.ts +3 -1
- package/src/schema/loki.gen.ts +2 -1
- package/src/schema/nodegraph.gen.ts +58 -0
- package/src/schema/piechart.gen.ts +146 -0
- package/src/schema/prometheus.gen.ts +2 -1
- package/src/schema/stat.gen.ts +2 -1
- package/src/schema/statetimeline.gen.ts +143 -0
- package/src/schema/statushistory.gen.ts +132 -0
- package/src/schema/table.gen.ts +27 -4
- package/src/schema/tempo.gen.ts +2 -1
- package/src/schema/text.gen.ts +2 -1
- package/src/schema/timeseries.gen.ts +7 -2
- package/src/schema/trend.gen.ts +208 -0
- package/src/schema/xychart.gen.ts +167 -0
- package/src/schema-overlay.test.ts +282 -0
- package/src/schema-validate.ts +141 -50
- package/src/schema.test.ts +4 -2
- package/src/serializer.test.ts +13 -1
- package/src/serializer.ts +11 -3
- package/src/skill-defs.ts +17 -0
- package/src/skills/chant-grafana-alerting.md +49 -0
- package/src/skills/chant-grafana-provisioning.md +3 -3
- package/src/skills/chant-grafana.md +12 -6
- package/src/spec/fetch-cli.ts +3 -3
- package/src/spec/fetch-units-cli.ts +37 -0
- package/src/spec/overlay/canvas.overlay.json +142 -0
- package/src/spec/overlay/dashboard.overlay.json +378 -0
- package/src/spec/overlay/expr.overlay.json +14 -0
- package/src/spec/overlay/geomap.overlay.json +35 -0
- package/src/spec/overlay/logs.overlay.json +14 -0
- package/src/spec/overlay/nodegraph.overlay.json +31 -0
- package/src/spec/overlay/piechart.overlay.json +17 -0
- package/src/spec/overlay/table.overlay.json +84 -0
- package/src/spec/overlay/timeseries.overlay.json +21 -0
- package/src/spec/overlay/trend.overlay.json +17 -0
- package/src/spec/overlay/xychart.overlay.json +16 -0
- package/src/spec/overlay.ts +105 -0
- package/src/spec/schemas/barchart.jsonschema.json +419 -0
- package/src/spec/schemas/bargauge.jsonschema.json +224 -0
- package/src/spec/schemas/canvas.jsonschema.json +356 -0
- package/src/spec/schemas/dashboardv2.jsonschema.json +3387 -0
- package/src/spec/schemas/expr.jsonschema.json +987 -0
- package/src/spec/schemas/geomap.jsonschema.json +220 -0
- package/src/spec/schemas/histogram.jsonschema.json +308 -0
- package/src/spec/schemas/nodegraph.jsonschema.json +77 -0
- package/src/spec/schemas/piechart.jsonschema.json +275 -0
- package/src/spec/schemas/statetimeline.jsonschema.json +324 -0
- package/src/spec/schemas/statushistory.jsonschema.json +292 -0
- package/src/spec/schemas/trend.jsonschema.json +449 -0
- package/src/spec/schemas/xychart.jsonschema.json +428 -0
- package/src/spec/schemas.ts +22 -10
- package/src/spec/units.gen.ts +281 -0
- package/src/spec/units.ts +77 -0
- package/src/transformations.test.ts +65 -0
- package/src/transformations.ts +574 -0
- package/src/util.ts +8 -0
- package/src/validate-alerting.test.ts +255 -0
- package/src/validate-alerting.ts +428 -0
- package/src/validate-output.ts +332 -101
- package/src/validate.ts +14 -0
- package/src/variables.ts +7 -2
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The checks behind GRAF111-GRAF114, and the alert-rule half of GRAF108, as
|
|
3
|
+
* plain functions over alerting provisioning files (parsed). The
|
|
4
|
+
* post-synth checks run them over a build's output; a test or another
|
|
5
|
+
* lexicon holding the same YAML can call them directly.
|
|
6
|
+
*
|
|
7
|
+
* - GRAF111: a rule's queries and expressions fit together: its condition,
|
|
8
|
+
* each expression's inputs and a recording rule's `record.from` name a
|
|
9
|
+
* refId of the rule, refIds are unique, and each expression model is
|
|
10
|
+
* valid for the pinned `expr` schema.
|
|
11
|
+
* - GRAF112: each query's datasource is one the build knows (provisioned or
|
|
12
|
+
* `ExternalDatasource`), and of the type the query model says.
|
|
13
|
+
* - GRAF113: the policy tree and rules route to contact points and mute
|
|
14
|
+
* timings the build declares, and their matchers parse.
|
|
15
|
+
* - GRAF114: uids, names, titles and intervals Grafana accepts, and nothing
|
|
16
|
+
* declared twice (a duplicate stops Grafana provisioning every alerting
|
|
17
|
+
* file at startup).
|
|
18
|
+
*
|
|
19
|
+
* Like GRAF101, a reference check only reports an error when the build
|
|
20
|
+
* declares things of that kind; with none declared it warns once that it
|
|
21
|
+
* cannot check, since the thing may already exist in Grafana.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { durationMs, isValidDuration } from "@intentius/chant-lexicon-prometheus/duration";
|
|
25
|
+
import { parseMatchers } from "@intentius/chant-lexicon-prometheus/matchers";
|
|
26
|
+
import { EXPRESSION_DATASOURCE_UID } from "./alerting";
|
|
27
|
+
import type { KnownDatasource } from "./datasource-refs";
|
|
28
|
+
import { isValidUid } from "./util";
|
|
29
|
+
import { validateExpressionSchema } from "./schema-validate";
|
|
30
|
+
import { checkGrafanaPromql } from "./promql-check";
|
|
31
|
+
|
|
32
|
+
type Json = Record<string, unknown>;
|
|
33
|
+
|
|
34
|
+
/** One alerting provisioning file, parsed, and where it came from. */
|
|
35
|
+
export interface AlertingDoc {
|
|
36
|
+
source?: string;
|
|
37
|
+
json: Json;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type AlertingIssueCode = "GRAF108" | "GRAF111" | "GRAF112" | "GRAF113" | "GRAF114";
|
|
41
|
+
|
|
42
|
+
export interface AlertingIssue {
|
|
43
|
+
code: AlertingIssueCode;
|
|
44
|
+
severity: "error" | "warning";
|
|
45
|
+
message: string;
|
|
46
|
+
/** The rule uid, contact point or group the issue is about. */
|
|
47
|
+
entity?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Contact point names Grafana has without provisioning: the default email contact point. */
|
|
51
|
+
export const BUILTIN_CONTACT_POINTS: ReadonlySet<string> = new Set(["grafana-default-email"]);
|
|
52
|
+
|
|
53
|
+
function isObject(v: unknown): v is Json {
|
|
54
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function listOf(doc: Json, key: string): Json[] {
|
|
58
|
+
const v = doc[key];
|
|
59
|
+
return Array.isArray(v) ? v.filter(isObject) : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function orgOf(o: Json): number {
|
|
63
|
+
return typeof o.orgId === "number" && o.orgId >= 1 ? o.orgId : 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface RuleInfo {
|
|
67
|
+
rule: Json;
|
|
68
|
+
group: Json;
|
|
69
|
+
/** `rule "<title>" (uid <uid>)`. */
|
|
70
|
+
where: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Every rule of every group across the files. */
|
|
74
|
+
export function alertRules(docs: readonly AlertingDoc[]): RuleInfo[] {
|
|
75
|
+
const out: RuleInfo[] = [];
|
|
76
|
+
for (const { json } of docs) {
|
|
77
|
+
for (const group of listOf(json, "groups")) {
|
|
78
|
+
for (const rule of Array.isArray(group.rules) ? group.rules.filter(isObject) : []) {
|
|
79
|
+
out.push({ rule, group, where: `alert rule "${String(rule.title ?? "?")}" (uid ${String(rule.uid ?? "?")})` });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function queriesOf(rule: Json): Json[] {
|
|
87
|
+
return Array.isArray(rule.data) ? rule.data.filter(isObject) : [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function modelOf(q: Json): Json {
|
|
91
|
+
return isObject(q.model) ? q.model : {};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── GRAF111: queries and expressions ────────────────────────────
|
|
95
|
+
|
|
96
|
+
/** The refIds an expression reads. */
|
|
97
|
+
export function expressionInputs(model: Json): string[] {
|
|
98
|
+
const refs = new Set<string>();
|
|
99
|
+
const one = (v: unknown) => {
|
|
100
|
+
if (typeof v === "string" && v.trim() !== "") refs.add(v.trim().replace(/^\$\{?([^}]*)\}?$/, "$1"));
|
|
101
|
+
};
|
|
102
|
+
switch (model.type) {
|
|
103
|
+
case "reduce":
|
|
104
|
+
case "threshold":
|
|
105
|
+
case "resample":
|
|
106
|
+
one(model.expression);
|
|
107
|
+
break;
|
|
108
|
+
case "math":
|
|
109
|
+
if (typeof model.expression === "string") {
|
|
110
|
+
for (const m of model.expression.matchAll(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g)) refs.add(m[1] ?? m[2]);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
case "classic_conditions":
|
|
114
|
+
for (const c of Array.isArray(model.conditions) ? model.conditions : []) {
|
|
115
|
+
const params = isObject(c) && isObject(c.query) && Array.isArray(c.query.params) ? c.query.params : [];
|
|
116
|
+
one(params[0]);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
return [...refs];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function checkRuleQueries(docs: readonly AlertingDoc[]): AlertingIssue[] {
|
|
124
|
+
const issues: AlertingIssue[] = [];
|
|
125
|
+
for (const { rule, where } of alertRules(docs)) {
|
|
126
|
+
const push = (message: string, severity: AlertingIssue["severity"] = "error") =>
|
|
127
|
+
issues.push({ code: "GRAF111", severity, message: `${where} ${message}`, entity: String(rule.uid ?? rule.title ?? "") });
|
|
128
|
+
const data = queriesOf(rule);
|
|
129
|
+
if (data.length === 0) {
|
|
130
|
+
push("has no queries; Grafana refuses to provision it.");
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const refIds = data.map((q) => q.refId);
|
|
134
|
+
const seen = new Set<unknown>();
|
|
135
|
+
for (const r of refIds) {
|
|
136
|
+
if (typeof r !== "string" || r === "") push("has a query with no refId.");
|
|
137
|
+
else if (seen.has(r)) push(`has two queries with refId "${r}".`);
|
|
138
|
+
seen.add(r);
|
|
139
|
+
}
|
|
140
|
+
const known = new Set(refIds.filter((r): r is string => typeof r === "string"));
|
|
141
|
+
const list = [...known].join(", ");
|
|
142
|
+
const record = isObject(rule.record) ? rule.record : undefined;
|
|
143
|
+
if (record) {
|
|
144
|
+
if (typeof record.metric !== "string" || record.metric === "") push("is a recording rule with no record.metric.");
|
|
145
|
+
if (typeof record.from !== "string" || !known.has(record.from)) push(`records from "${String(record.from)}", which is not one of its refIds (${list}).`);
|
|
146
|
+
} else if (typeof rule.condition !== "string" || rule.condition === "") {
|
|
147
|
+
push("has no condition; an alert rule needs the refId whose result fires it.");
|
|
148
|
+
} else if (!known.has(rule.condition)) {
|
|
149
|
+
push(`has condition "${rule.condition}", which is not one of its refIds (${list}).`);
|
|
150
|
+
}
|
|
151
|
+
for (const q of data) {
|
|
152
|
+
if (q.datasourceUid !== EXPRESSION_DATASOURCE_UID) continue;
|
|
153
|
+
const model = modelOf(q);
|
|
154
|
+
const at = `expression ${String(q.refId ?? "?")}`;
|
|
155
|
+
for (const p of validateExpressionSchema(model)) {
|
|
156
|
+
if (p.severity === "error") push(`${at} ${p.path === "/" ? "" : `${p.path.slice(1)} `}${p.message} (Grafana expression schema).`);
|
|
157
|
+
}
|
|
158
|
+
for (const input of expressionInputs(model)) {
|
|
159
|
+
if (input === q.refId) push(`${at} reads its own result.`);
|
|
160
|
+
else if (!known.has(input)) push(`${at} reads "${input}", which is not one of the rule's refIds (${list}).`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return issues;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── GRAF112 and GRAF108: datasources ────────────────────────────
|
|
168
|
+
|
|
169
|
+
/** Where one query goes: the known datasource, or the type its model states. */
|
|
170
|
+
function resolveQuery(q: Json, known: ReadonlyMap<string, KnownDatasource>): { uid: string; declared?: KnownDatasource; statedType?: string } | undefined {
|
|
171
|
+
const uid = q.datasourceUid;
|
|
172
|
+
if (typeof uid !== "string" || uid === EXPRESSION_DATASOURCE_UID) return undefined;
|
|
173
|
+
const ds = modelOf(q).datasource;
|
|
174
|
+
const statedType = isObject(ds) && typeof ds.type === "string" && ds.type !== "" ? ds.type : undefined;
|
|
175
|
+
return { uid, declared: known.get(uid), ...(statedType ? { statedType } : {}) };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function checkRuleDatasources(docs: readonly AlertingDoc[], known: ReadonlyMap<string, KnownDatasource>): AlertingIssue[] {
|
|
179
|
+
const issues: AlertingIssue[] = [];
|
|
180
|
+
const unchecked = new Set<string>();
|
|
181
|
+
for (const { rule, where } of alertRules(docs)) {
|
|
182
|
+
const entity = String(rule.uid ?? rule.title ?? "");
|
|
183
|
+
for (const q of queriesOf(rule)) {
|
|
184
|
+
const r = resolveQuery(q, known);
|
|
185
|
+
if (!r) continue;
|
|
186
|
+
const at = `${where} query ${String(q.refId ?? "?")}`;
|
|
187
|
+
if (!r.declared) {
|
|
188
|
+
if (known.size === 0) unchecked.add(r.uid);
|
|
189
|
+
else {
|
|
190
|
+
issues.push({
|
|
191
|
+
code: "GRAF112",
|
|
192
|
+
severity: "error",
|
|
193
|
+
message: `${at} uses datasource uid "${r.uid}", which no declared Datasource or ExternalDatasource has. Declared: ${[...known.keys()].map((k) => `"${k}"`).join(", ")}.`,
|
|
194
|
+
entity,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} else if (r.statedType && r.statedType !== r.declared.type) {
|
|
198
|
+
issues.push({
|
|
199
|
+
code: "GRAF112",
|
|
200
|
+
severity: "error",
|
|
201
|
+
message: `${at} has a ${r.statedType} query model, but datasource "${r.declared.name ?? r.uid}" is ${r.declared.type}.`,
|
|
202
|
+
entity,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const record = isObject(rule.record) ? rule.record : undefined;
|
|
207
|
+
const target = record?.targetDatasourceUid;
|
|
208
|
+
if (typeof target === "string" && target !== "" && known.size > 0) {
|
|
209
|
+
const ds = known.get(target);
|
|
210
|
+
if (!ds) issues.push({ code: "GRAF112", severity: "error", message: `${where} writes to datasource uid "${target}", which no declared Datasource or ExternalDatasource has.`, entity });
|
|
211
|
+
else if (ds.type !== "prometheus") issues.push({ code: "GRAF112", severity: "error", message: `${where} writes its metric to datasource "${ds.name ?? target}", which is ${ds.type}; Grafana writes recorded metrics to Prometheus.`, entity });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (unchecked.size > 0) {
|
|
215
|
+
const uids = [...unchecked].map((u) => `"${u}"`).join(", ");
|
|
216
|
+
issues.push({
|
|
217
|
+
code: "GRAF112",
|
|
218
|
+
severity: "warning",
|
|
219
|
+
message: `Alert rules query datasource uid${unchecked.size > 1 ? "s" : ""} ${uids}, but the build declares no datasource, so GRAF112 cannot check ${unchecked.size > 1 ? "them" : "it"}. Declare a Datasource, or an ExternalDatasource for one that already exists in Grafana.`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return issues;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Every PromQL expression an alert rule sends to Prometheus, and where it is written. */
|
|
226
|
+
export function alertRulePromql(docs: readonly AlertingDoc[], known: ReadonlyMap<string, KnownDatasource>): Array<{ where: string; expr: string; entity: string }> {
|
|
227
|
+
const out: Array<{ where: string; expr: string; entity: string }> = [];
|
|
228
|
+
for (const { rule, where } of alertRules(docs)) {
|
|
229
|
+
for (const q of queriesOf(rule)) {
|
|
230
|
+
const r = resolveQuery(q, known);
|
|
231
|
+
if (!r) continue;
|
|
232
|
+
const type = r.declared?.type ?? r.statedType;
|
|
233
|
+
const expr = modelOf(q).expr;
|
|
234
|
+
if (type !== "prometheus" || typeof expr !== "string" || expr.trim() === "") continue;
|
|
235
|
+
out.push({ where: `${where} query ${String(q.refId ?? "?")}`, expr, entity: String(rule.uid ?? rule.title ?? "") });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** GRAF108 over alert rules: the PromQL of each query that reaches Prometheus parses. */
|
|
242
|
+
export function checkRulePromql(docs: readonly AlertingDoc[], known: ReadonlyMap<string, KnownDatasource>): AlertingIssue[] {
|
|
243
|
+
const issues: AlertingIssue[] = [];
|
|
244
|
+
for (const { where, expr, entity } of alertRulePromql(docs, known)) {
|
|
245
|
+
const checked = checkGrafanaPromql(expr);
|
|
246
|
+
if (!checked.ok) issues.push({ code: "GRAF108", severity: "error", message: `${where} is not valid PromQL: ${checked.message}.`, entity });
|
|
247
|
+
}
|
|
248
|
+
return issues;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── GRAF113: notification references ────────────────────────────
|
|
252
|
+
|
|
253
|
+
interface Named {
|
|
254
|
+
contactPoints: Set<string>;
|
|
255
|
+
timings: Set<string>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function declaredNames(docs: readonly AlertingDoc[]): Named {
|
|
259
|
+
const contactPoints = new Set<string>();
|
|
260
|
+
const timings = new Set<string>();
|
|
261
|
+
for (const { json } of docs) {
|
|
262
|
+
for (const c of listOf(json, "contactPoints")) if (typeof c.name === "string") contactPoints.add(c.name);
|
|
263
|
+
for (const t of listOf(json, "muteTimes")) if (typeof t.name === "string") timings.add(t.name);
|
|
264
|
+
}
|
|
265
|
+
return { contactPoints, timings };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const OPS = new Set(["=", "!=", "=~", "!~"]);
|
|
269
|
+
|
|
270
|
+
export function checkNotificationRefs(docs: readonly AlertingDoc[]): AlertingIssue[] {
|
|
271
|
+
const issues: AlertingIssue[] = [];
|
|
272
|
+
const named = declaredNames(docs);
|
|
273
|
+
const uncheckedReceivers = new Set<string>();
|
|
274
|
+
const uncheckedTimings = new Set<string>();
|
|
275
|
+
|
|
276
|
+
const receiver = (name: unknown, where: string, entity: string) => {
|
|
277
|
+
if (typeof name !== "string" || name === "" || BUILTIN_CONTACT_POINTS.has(name) || named.contactPoints.has(name)) return;
|
|
278
|
+
if (named.contactPoints.size === 0) uncheckedReceivers.add(name);
|
|
279
|
+
else issues.push({ code: "GRAF113", severity: "error", message: `${where} sends to contact point "${name}", which the build does not declare. Declared: ${[...named.contactPoints].map((n) => `"${n}"`).join(", ")}.`, entity });
|
|
280
|
+
};
|
|
281
|
+
const timings = (list: unknown, key: string, where: string, entity: string) => {
|
|
282
|
+
for (const name of Array.isArray(list) ? list : []) {
|
|
283
|
+
if (typeof name !== "string" || named.timings.has(name)) continue;
|
|
284
|
+
if (named.timings.size === 0) uncheckedTimings.add(name);
|
|
285
|
+
else issues.push({ code: "GRAF113", severity: "error", message: `${where} ${key} names "${name}", which is not a declared mute timing. Declared: ${[...named.timings].map((n) => `"${n}"`).join(", ")}.`, entity });
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const route = (r: Json, where: string, entity: string, root: boolean) => {
|
|
289
|
+
receiver(r.receiver, where, entity);
|
|
290
|
+
if (root && (typeof r.receiver !== "string" || r.receiver === "")) {
|
|
291
|
+
issues.push({ code: "GRAF113", severity: "error", message: `${where} has no receiver; the root policy must name the contact point every unmatched alert goes to.`, entity });
|
|
292
|
+
}
|
|
293
|
+
timings(r.mute_time_intervals, "mute_time_intervals", where, entity);
|
|
294
|
+
timings(r.active_time_intervals, "active_time_intervals", where, entity);
|
|
295
|
+
for (const m of Array.isArray(r.object_matchers) ? r.object_matchers : []) {
|
|
296
|
+
if (!Array.isArray(m) || m.length !== 3 || typeof m[0] !== "string" || !OPS.has(m[1] as string) || typeof m[2] !== "string") {
|
|
297
|
+
issues.push({ code: "GRAF113", severity: "error", message: `${where} has object matcher ${JSON.stringify(m)}; each is [label, op, value] with op one of =, !=, =~, !~.`, entity });
|
|
298
|
+
} else if ((m[1] === "=~" || m[1] === "!~") && !validRegex(m[2])) {
|
|
299
|
+
issues.push({ code: "GRAF113", severity: "error", message: `${where} matches ${m[0]} against "${m[2]}", which is not a valid regular expression.`, entity });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
for (const m of Array.isArray(r.matchers) ? r.matchers : []) {
|
|
303
|
+
const parsed = typeof m === "string" ? parseMatchers(m) : { ok: false as const, error: `${JSON.stringify(m)} is not a string` };
|
|
304
|
+
if (!parsed.ok) issues.push({ code: "GRAF113", severity: "error", message: `${where} has matcher ${JSON.stringify(m)}: ${parsed.error}.`, entity });
|
|
305
|
+
}
|
|
306
|
+
(Array.isArray(r.routes) ? r.routes.filter(isObject) : []).forEach((c, i) => route(c, `${where} > route ${i + 1}`, entity, false));
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
for (const { json } of docs) {
|
|
310
|
+
for (const p of listOf(json, "policies")) route(p, `The notification policy tree of org ${orgOf(p)}`, `policies:${orgOf(p)}`, true);
|
|
311
|
+
}
|
|
312
|
+
for (const { rule, where } of alertRules(docs)) {
|
|
313
|
+
const ns = isObject(rule.notification_settings) ? rule.notification_settings : undefined;
|
|
314
|
+
if (!ns) continue;
|
|
315
|
+
const entity = String(rule.uid ?? rule.title ?? "");
|
|
316
|
+
receiver(ns.receiver, where, entity);
|
|
317
|
+
timings(ns.mute_time_intervals, "notification_settings.mute_time_intervals", where, entity);
|
|
318
|
+
timings(ns.active_time_intervals, "notification_settings.active_time_intervals", where, entity);
|
|
319
|
+
}
|
|
320
|
+
if (uncheckedReceivers.size > 0) {
|
|
321
|
+
issues.push({
|
|
322
|
+
code: "GRAF113",
|
|
323
|
+
severity: "warning",
|
|
324
|
+
message: `Alerting routes to contact point${uncheckedReceivers.size > 1 ? "s" : ""} ${[...uncheckedReceivers].map((n) => `"${n}"`).join(", ")}, but the build declares no ContactPoint, so GRAF113 cannot check that ${uncheckedReceivers.size > 1 ? "they exist" : "it exists"} in Grafana.`,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (uncheckedTimings.size > 0) {
|
|
328
|
+
issues.push({
|
|
329
|
+
code: "GRAF113",
|
|
330
|
+
severity: "warning",
|
|
331
|
+
message: `Alerting names mute timing${uncheckedTimings.size > 1 ? "s" : ""} ${[...uncheckedTimings].map((n) => `"${n}"`).join(", ")}, but the build declares no MuteTiming, so GRAF113 cannot check that ${uncheckedTimings.size > 1 ? "they exist" : "it exists"} in Grafana.`,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return issues;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function validRegex(re: string): boolean {
|
|
338
|
+
try {
|
|
339
|
+
new RegExp(`^(?:${re})$`);
|
|
340
|
+
return true;
|
|
341
|
+
} catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── GRAF114: identity, duplicates and intervals ─────────────────
|
|
347
|
+
|
|
348
|
+
/** Grafana's scheduler ticks every 10 seconds; a group's interval must be a positive multiple of it. */
|
|
349
|
+
export const BASE_INTERVAL_SECONDS = 10;
|
|
350
|
+
|
|
351
|
+
const DURATION_FIELDS = ["for", "keepFiringFor"] as const;
|
|
352
|
+
|
|
353
|
+
export function checkAlertingIdentity(docs: readonly AlertingDoc[]): AlertingIssue[] {
|
|
354
|
+
const issues: AlertingIssue[] = [];
|
|
355
|
+
const push = (message: string, entity?: string, severity: AlertingIssue["severity"] = "error") => issues.push({ code: "GRAF114", severity, message, ...(entity ? { entity } : {}) });
|
|
356
|
+
const count = (map: Map<string, number>, key: string) => map.set(key, (map.get(key) ?? 0) + 1);
|
|
357
|
+
|
|
358
|
+
const ruleUids = new Map<string, number>();
|
|
359
|
+
const groups = new Map<string, number>();
|
|
360
|
+
const cpNames = new Map<string, number>();
|
|
361
|
+
const receiverUids = new Map<string, number>();
|
|
362
|
+
const policyOrgs = new Map<string, number>();
|
|
363
|
+
const timingNames = new Map<string, number>();
|
|
364
|
+
const templateNames = new Map<string, number>();
|
|
365
|
+
|
|
366
|
+
for (const { json } of docs) {
|
|
367
|
+
for (const g of listOf(json, "groups")) {
|
|
368
|
+
const name = typeof g.name === "string" ? g.name : "";
|
|
369
|
+
const where = `Rule group "${name}"`;
|
|
370
|
+
if (name.trim() === "") push(`A rule group has no name; Grafana refuses the file.`);
|
|
371
|
+
if (typeof g.folder !== "string" || g.folder.trim() === "") push(`${where} has no folder; Grafana refuses the file.`, name);
|
|
372
|
+
count(groups, `${orgOf(g)}\u0000${String(g.folder)}\u0000${name}`);
|
|
373
|
+
const interval = g.interval;
|
|
374
|
+
if (typeof interval !== "string" || !isValidDuration(interval)) push(`${where} has interval ${JSON.stringify(interval)}, not a duration like 1m.`, name);
|
|
375
|
+
else {
|
|
376
|
+
const ms = durationMs(interval)!;
|
|
377
|
+
if (ms <= 0 || ms % (BASE_INTERVAL_SECONDS * 1000) !== 0) push(`${where} has interval ${interval}; Grafana evaluates rules every ${BASE_INTERVAL_SECONDS}s, so it must be a positive multiple of ${BASE_INTERVAL_SECONDS}s.`, name);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const c of listOf(json, "contactPoints")) {
|
|
381
|
+
const name = typeof c.name === "string" ? c.name : "";
|
|
382
|
+
if (name.trim() === "") push("A contact point has no name; Grafana refuses the file.");
|
|
383
|
+
count(cpNames, `${orgOf(c)}\u0000${name}`);
|
|
384
|
+
const receivers = Array.isArray(c.receivers) ? c.receivers.filter(isObject) : [];
|
|
385
|
+
if (receivers.length === 0) push(`Contact point "${name}" has no receivers.`, name, "warning");
|
|
386
|
+
for (const r of receivers) {
|
|
387
|
+
const uid = r.uid;
|
|
388
|
+
if (typeof uid !== "string" || !isValidUid(uid)) push(`Contact point "${name}" has a ${String(r.type)} receiver with uid ${JSON.stringify(uid)}; Grafana needs 1-40 letters, digits, "-" and "_".`, name);
|
|
389
|
+
else count(receiverUids, `${orgOf(c)}\u0000${uid}`);
|
|
390
|
+
if (!isObject(r.settings) || Object.keys(r.settings).length === 0) push(`Contact point "${name}" has a ${String(r.type)} receiver with no settings; Grafana refuses the file.`, name);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (const p of listOf(json, "policies")) count(policyOrgs, String(orgOf(p)));
|
|
394
|
+
for (const t of listOf(json, "muteTimes")) count(timingNames, `${orgOf(t)}\u0000${String(t.name)}`);
|
|
395
|
+
for (const t of listOf(json, "templates")) count(templateNames, `${orgOf(t)}\u0000${String(t.name)}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (const { rule, group, where } of alertRules(docs)) {
|
|
399
|
+
const uid = rule.uid;
|
|
400
|
+
const entity = String(uid ?? rule.title ?? "");
|
|
401
|
+
if (typeof uid !== "string" || !isValidUid(uid)) {
|
|
402
|
+
push(`${where} has uid ${JSON.stringify(uid)}; Grafana needs 1-40 letters, digits, "-" and "_", and one bad rule stops it provisioning every alerting file.`, entity);
|
|
403
|
+
} else count(ruleUids, `${orgOf(group)}\u0000${uid}`);
|
|
404
|
+
if (typeof rule.title !== "string" || rule.title.trim() === "") push(`${where} has no title; Grafana refuses the file.`, entity);
|
|
405
|
+
else if (rule.title.length > 190) push(`${where} has a title of ${rule.title.length} characters; Grafana allows 190.`, entity);
|
|
406
|
+
for (const key of DURATION_FIELDS) {
|
|
407
|
+
const v = rule[key];
|
|
408
|
+
if (v !== undefined && (typeof v !== "string" || !isValidDuration(v))) push(`${where} has ${key} ${JSON.stringify(v)}, not a duration like 5m.`, entity);
|
|
409
|
+
}
|
|
410
|
+
for (const s of ["noDataState", "execErrState"] as const) {
|
|
411
|
+
const allowed = s === "noDataState" ? ["NoData", "Alerting", "OK", "KeepLast"] : ["Error", "Alerting", "OK", "KeepLast"];
|
|
412
|
+
const v = rule[s];
|
|
413
|
+
if (v !== undefined && v !== "" && !allowed.includes(v as string)) push(`${where} has ${s} ${JSON.stringify(v)}; Grafana accepts ${allowed.join(", ")}.`, entity);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const report = (map: Map<string, number>, what: (key: string[]) => string) => {
|
|
418
|
+
for (const [key, n] of map) if (n > 1) push(`${n} ${what(key.split("\u0000"))}`, key.split("\u0000").slice(-1)[0]);
|
|
419
|
+
};
|
|
420
|
+
report(ruleUids, ([org, uid]) => `alert rules share the uid "${uid}" in org ${org}; Grafana stops provisioning.`);
|
|
421
|
+
report(groups, ([org, folder, name]) => `rule groups are named "${name}" in folder "${folder}" (org ${org}); Grafana keeps one.`);
|
|
422
|
+
report(cpNames, ([org, name]) => `contact points are named "${name}" in org ${org}; put the integrations in one contact point's receivers.`);
|
|
423
|
+
report(receiverUids, ([org, uid]) => `contact point receivers share the uid "${uid}" in org ${org}.`);
|
|
424
|
+
report(policyOrgs, ([org]) => `notification policy trees are declared for org ${org}; each replaces the whole tree, so only the last one would stand.`);
|
|
425
|
+
report(timingNames, ([org, name]) => `mute timings are named "${name}" in org ${org}.`);
|
|
426
|
+
report(templateNames, ([org, name]) => `notification templates are named "${name}" in org ${org}.`);
|
|
427
|
+
return issues;
|
|
428
|
+
}
|