@intentius/chant-lexicon-grafana 0.98.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 +24 -8
- 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/annotations.d.ts +61 -0
- package/dist/annotations.d.ts.map +1 -0
- package/dist/api/apply.d.ts +160 -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 +63 -0
- package/dist/api/fake-grafana.d.ts.map +1 -0
- package/dist/api/folders.d.ts +146 -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 +55 -6
- 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/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 +11 -3
- 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 +51 -9
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/datasource-refs.d.ts +99 -0
- package/dist/datasource-refs.d.ts.map +1 -0
- package/dist/datasource-settings.d.ts +834 -0
- package/dist/datasource-settings.d.ts.map +1 -0
- package/dist/datasource.d.ts +82 -11
- 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 +47 -0
- package/dist/deep-observe.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +50 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/detect.d.ts +32 -3
- 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/folder.d.ts +68 -0
- package/dist/folder.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 +89 -0
- package/dist/import/mappings.d.ts.map +1 -0
- package/dist/import/model.d.ts +105 -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 +125 -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 +41 -0
- package/dist/import/testdata/fixtures.d.ts.map +1 -0
- package/dist/import/v2.d.ts +73 -0
- package/dist/import/v2.d.ts.map +1 -0
- package/dist/index.d.ts +31 -12
- 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 +22 -12
- 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/graf104.d.ts +2 -2
- package/dist/lint/post-synth/graf106.d.ts +2 -2
- 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/graf110.d.ts +8 -0
- package/dist/lint/post-synth/graf110.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 +205 -0
- package/dist/okf/index.md +53 -4
- 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/GRAF104.md +2 -2
- package/dist/okf/rules/GRAF106.md +2 -2
- package/dist/okf/rules/GRAF108.md +11 -0
- package/dist/okf/rules/GRAF109.md +15 -0
- package/dist/okf/rules/GRAF110.md +11 -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/AdhocVariable.md +9 -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/AzureMonitorQuery.md +9 -0
- package/dist/okf/types/BarChartPanel.md +9 -0
- package/dist/okf/types/BarGaugePanel.md +9 -0
- package/dist/okf/types/BigQueryQuery.md +9 -0
- package/dist/okf/types/CanvasPanel.md +9 -0
- package/dist/okf/types/ClassicConditionsExpression.md +9 -0
- package/dist/okf/types/CloudMonitoringQuery.md +9 -0
- package/dist/okf/types/CloudWatchQuery.md +9 -0
- package/dist/okf/types/ContactPoint.md +14 -0
- package/dist/okf/types/Dashboard.md +2 -1
- package/dist/okf/types/Datasource.md +3 -2
- package/dist/okf/types/DatasourceProvisioning.md +9 -0
- package/dist/okf/types/ElasticsearchQuery.md +9 -0
- package/dist/okf/types/ExternalDatasource.md +15 -0
- package/dist/okf/types/FlameGraphPanel.md +9 -0
- package/dist/okf/types/Folder.md +9 -0
- package/dist/okf/types/GeomapPanel.md +9 -0
- package/dist/okf/types/GroupByVariable.md +9 -0
- package/dist/okf/types/HistogramPanel.md +9 -0
- package/dist/okf/types/MSSQLQuery.md +9 -0
- package/dist/okf/types/MathExpression.md +9 -0
- package/dist/okf/types/MuteTiming.md +13 -0
- package/dist/okf/types/MySQLQuery.md +9 -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/PostgresQuery.md +9 -0
- package/dist/okf/types/PyroscopeQuery.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/SwitchVariable.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 +86 -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 +78 -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-models.d.ts +95 -0
- package/dist/query-models.d.ts.map +1 -0
- package/dist/query.d.ts +116 -18
- 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/graf104.ts +3 -3
- package/dist/rules/graf106.ts +3 -3
- package/dist/rules/graf107.ts +1 -1
- package/dist/rules/graf108.ts +17 -0
- package/dist/rules/graf109.ts +17 -0
- package/dist/rules/graf110.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 +27 -10
- package/dist/rules/literal-secret.ts +60 -6
- package/dist/schema/azuremonitor.gen.d.ts +354 -0
- package/dist/schema/azuremonitor.gen.d.ts.map +1 -0
- 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/bigquery.gen.d.ts +93 -0
- package/dist/schema/bigquery.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/cloudwatch.gen.d.ts +394 -0
- package/dist/schema/cloudwatch.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/elasticsearch.gen.d.ts +444 -0
- package/dist/schema/elasticsearch.gen.d.ts.map +1 -0
- 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/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/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 +18 -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 +35 -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 +39 -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-operations.md +108 -0
- package/dist/skills/chant-grafana-provisioning.md +3 -3
- package/dist/skills/chant-grafana.md +15 -8
- 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 +21 -13
- 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/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 +62 -9
- 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 +109 -7
- 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/annotations.ts +84 -0
- package/src/api/apply.test.ts +256 -0
- package/src/api/apply.ts +394 -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 +190 -0
- package/src/api/folders.ts +339 -0
- package/src/api/library-panels.ts +101 -0
- package/src/build.test.ts +351 -10
- package/src/build.ts +294 -55
- package/src/catalog.ts +104 -2
- package/src/codegen/docs.ts +50 -10
- package/src/codegen/generate.ts +27 -1
- 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 +14 -2
- 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 +68 -9
- package/src/datasource-refs.test.ts +56 -0
- package/src/datasource-refs.ts +213 -0
- package/src/datasource-settings.test.ts +106 -0
- package/src/datasource-settings.ts +941 -0
- package/src/datasource.ts +123 -11
- package/src/deep-observe-hooks.ts +267 -0
- package/src/deep-observe.test.ts +311 -0
- package/src/deep-observe.ts +187 -0
- package/src/describe-resources.test.ts +271 -0
- package/src/describe-resources.ts +244 -0
- package/src/detect.ts +82 -5
- package/src/export-resources.test.ts +129 -0
- package/src/export-resources.ts +84 -0
- package/src/folder.ts +118 -0
- package/src/generated/lexicon-grafana.json +205 -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 +130 -0
- package/src/import/generator.test.ts +210 -0
- package/src/import/generator.ts +463 -0
- package/src/import/live-export.ts +264 -0
- package/src/import/mappings.ts +336 -0
- package/src/import/model.ts +117 -0
- package/src/import/normalize-alerting.ts +101 -0
- package/src/import/normalize.test.ts +117 -0
- package/src/import/normalize.ts +393 -0
- package/src/import/parser.test.ts +516 -0
- package/src/import/parser.ts +1387 -0
- package/src/import/report.ts +62 -0
- package/src/import/roundtrip.test.ts +563 -0
- package/src/import/testdata/fixtures.ts +137 -0
- package/src/import/v2.test.ts +292 -0
- package/src/import/v2.ts +804 -0
- package/src/import.e2e.test.ts +31 -84
- package/src/import.test.ts +9 -3
- package/src/index.ts +308 -14
- package/src/init-templates.test.ts +74 -0
- package/src/init-templates.ts +349 -0
- package/src/k8s.test.ts +98 -0
- package/src/k8s.ts +197 -0
- package/src/lint/audit-catalog.ts +68 -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/graf104.ts +3 -3
- package/src/lint/post-synth/graf106.ts +3 -3
- 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/graf110.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 +27 -10
- package/src/lint/post-synth/index.ts +16 -0
- package/src/lint/post-synth/post-synth.test.ts +333 -12
- package/src/lint/rules/literal-secret.ts +60 -6
- package/src/lint/rules/rules.test.ts +27 -0
- package/src/load-cost.test.ts +35 -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 +184 -0
- package/src/op/activities/grafana-apply.test.ts +243 -0
- package/src/op/activities/grafana-apply.ts +197 -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 +122 -0
- package/src/panel-options.ts +66 -0
- package/src/panels.test.ts +53 -0
- package/src/panels.ts +148 -10
- package/src/pin.ts +69 -6
- package/src/plugin.test.ts +12 -2
- package/src/plugin.ts +75 -0
- package/src/promql-check.test.ts +108 -0
- package/src/promql-check.ts +193 -0
- package/src/query-models.ts +94 -0
- package/src/query.test.ts +166 -0
- package/src/query.ts +190 -20
- package/src/schema/azuremonitor.gen.ts +381 -0
- package/src/schema/barchart.gen.ts +221 -0
- package/src/schema/bargauge.gen.ts +111 -0
- package/src/schema/bigquery.gen.ts +110 -0
- package/src/schema/canvas.gen.ts +176 -0
- package/src/schema/cloudwatch.gen.ts +423 -0
- package/src/schema/dashboard.gen.ts +206 -22
- package/src/schema/elasticsearch.gen.ts +508 -0
- 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/googlecloudmonitoring.gen.ts +330 -0
- package/src/schema/grafanapyroscope.gen.ts +66 -0
- package/src/schema/heatmap.gen.ts +2 -1
- package/src/schema/histogram.gen.ts +151 -0
- package/src/schema/index.ts +18 -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 +41 -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 +199 -57
- 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 +34 -0
- package/src/skills/chant-grafana-alerting.md +49 -0
- package/src/skills/chant-grafana-operations.md +108 -0
- package/src/skills/chant-grafana-provisioning.md +3 -3
- package/src/skills/chant-grafana.md +15 -8
- 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 +103 -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/azuremonitor.jsonschema.json +679 -0
- package/src/spec/schemas/barchart.jsonschema.json +419 -0
- package/src/spec/schemas/bargauge.jsonschema.json +224 -0
- package/src/spec/schemas/bigquery.jsonschema.json +290 -0
- package/src/spec/schemas/canvas.jsonschema.json +356 -0
- package/src/spec/schemas/cloudwatch.jsonschema.json +721 -0
- package/src/spec/schemas/dashboardv2.jsonschema.json +3387 -0
- package/src/spec/schemas/elasticsearch.jsonschema.json +1695 -0
- package/src/spec/schemas/expr.jsonschema.json +987 -0
- package/src/spec/schemas/geomap.jsonschema.json +220 -0
- package/src/spec/schemas/googlecloudmonitoring.jsonschema.json +488 -0
- package/src/spec/schemas/grafanapyroscope.jsonschema.json +93 -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.gen.ts +40 -0
- package/src/spec/schemas.ts +30 -16
- 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 +430 -0
- package/src/validate-output.ts +427 -104
- package/src/validate.ts +28 -0
- package/src/validation.ts +27 -0
- package/src/variables.ts +120 -8
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SloAlertRules`: an `Slo`'s multiwindow, multi-burn-rate alerts as
|
|
3
|
+
* Grafana-managed alert rules, for teams that page from Grafana rather
|
|
4
|
+
* than from Prometheus and Alertmanager.
|
|
5
|
+
*
|
|
6
|
+
* The windows, factors, thresholds and labels come from the prometheus
|
|
7
|
+
* lexicon's `sloMetrics()`, the same numbers the `Slo`'s own Prometheus
|
|
8
|
+
* alerts use. Each burn-rate pair becomes one rule that reads the two
|
|
9
|
+
* error ratios the `Slo` records (`slo:sli_error:ratio_rate<window>`) as
|
|
10
|
+
* instant Prometheus queries, and a math expression that holds while both
|
|
11
|
+
* are above the pair's threshold:
|
|
12
|
+
*
|
|
13
|
+
* A = slo:sli_error:ratio_rate1h{slo="checkout"}
|
|
14
|
+
* B = slo:sli_error:ratio_rate5m{slo="checkout"}
|
|
15
|
+
* C = $A > 0.0144 && $B > 0.0144 (condition)
|
|
16
|
+
*
|
|
17
|
+
* So the `Slo`'s rule group must be loaded into the Prometheus the rules
|
|
18
|
+
* query: the recording rules are what make a 3-day window cheap to
|
|
19
|
+
* evaluate every minute.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Composite, type CompositeInstance } from "@intentius/chant/composite";
|
|
23
|
+
import { sloMetrics, type SloInstance, type SloMetrics } from "@intentius/chant-lexicon-prometheus/composites/slo";
|
|
24
|
+
import type { RuleGroupEntity } from "@intentius/chant-lexicon-prometheus/rules";
|
|
25
|
+
import {
|
|
26
|
+
AlertRule,
|
|
27
|
+
AlertRuleGroup,
|
|
28
|
+
MathExpression,
|
|
29
|
+
type AlertRuleGroupEntity,
|
|
30
|
+
type ContactPointEntity,
|
|
31
|
+
type ExecErrState,
|
|
32
|
+
type NoDataState,
|
|
33
|
+
} from "../alerting";
|
|
34
|
+
import { PromQuery, type DatasourceInput } from "../query";
|
|
35
|
+
import { slugUid } from "../util";
|
|
36
|
+
import { num, requireDatasource } from "./shared";
|
|
37
|
+
|
|
38
|
+
export interface SloAlertRulesProps {
|
|
39
|
+
/** The `Slo(...)` the rules alert on, its rule group, or what `sloMetrics()` returned for it. It must have alerting on. */
|
|
40
|
+
slo: SloInstance | RuleGroupEntity | SloMetrics;
|
|
41
|
+
/** The Prometheus holding the `Slo`'s recorded series: a declared `Datasource`, an `ExternalDatasource`, or `{ type: "prometheus", uid }`. */
|
|
42
|
+
datasource: DatasourceInput<"prometheus">;
|
|
43
|
+
/** The Grafana folder the rules are stored in, by title. */
|
|
44
|
+
folder: string;
|
|
45
|
+
/** The rule group's name. Defaults to the `Slo`'s group name, `slo-<name>`. */
|
|
46
|
+
group?: string;
|
|
47
|
+
/** How often the rules are evaluated, a multiple of 10s. Defaults to `1m`. */
|
|
48
|
+
interval?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Send the alerts straight to this contact point. Left out, they go
|
|
51
|
+
* through the notification policy tree, which can route on the
|
|
52
|
+
* `severity` label (`page` or `ticket` by default).
|
|
53
|
+
*/
|
|
54
|
+
contactPoint?: ContactPointEntity | string;
|
|
55
|
+
/** How long a pair must hold before its alert fires. Default: none; the short window already filters blips. */
|
|
56
|
+
for?: string;
|
|
57
|
+
/** More labels on every rule, e.g. `team`. */
|
|
58
|
+
labels?: Record<string, string>;
|
|
59
|
+
/** More annotations on every rule, e.g. `runbook_url`. */
|
|
60
|
+
annotations?: Record<string, string>;
|
|
61
|
+
/** What no data does. Defaults to `OK`: no recorded ratio means no traffic, so no budget is burning. */
|
|
62
|
+
noDataState?: NoDataState;
|
|
63
|
+
/** What a query error does. Defaults to `Error`. */
|
|
64
|
+
execErrState?: ExecErrState;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type SloAlertRulesMembers = {
|
|
68
|
+
/** One rule per burn-rate pair, page tier first. */
|
|
69
|
+
rules: AlertRuleGroupEntity;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** What `SloAlertRules(...)` returns: its rule group, as `rules`. */
|
|
73
|
+
export type SloAlertRulesInstance = CompositeInstance<SloAlertRulesMembers> & SloAlertRulesMembers;
|
|
74
|
+
|
|
75
|
+
function isMetrics(x: unknown): x is SloMetrics {
|
|
76
|
+
return typeof x === "object" && x !== null && "errorBudgetRemaining" in x && "burnRates" in x && "selector" in x;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A rule uid for one pair: the SLO's name cut to fit, then the windows, at most 40 characters. */
|
|
80
|
+
function pairUid(name: string, long: string, short: string): string {
|
|
81
|
+
const suffix = `-${long}-${short}`.toLowerCase();
|
|
82
|
+
return `${slugUid(`slo ${name}`).slice(0, 40 - suffix.length).replace(/-$/, "")}${suffix}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The queries and condition of each burn-rate rule, from `sloMetrics()`. Exposed for tests and for rules of your own. */
|
|
86
|
+
export function sloAlertQueries(m: SloMetrics) {
|
|
87
|
+
return m.burnRates.map((b) => ({
|
|
88
|
+
...b,
|
|
89
|
+
uid: pairUid(m.name, b.long, b.short),
|
|
90
|
+
longExpr: `${b.longRecord}${m.selector}`,
|
|
91
|
+
shortExpr: `${b.shortRecord}${m.selector}`,
|
|
92
|
+
condition: `$A > ${num(b.threshold)} && $B > ${num(b.threshold)}`,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Grafana-managed burn-rate alerts for an `Slo`: one rule per window pair,
|
|
98
|
+
* with the labels and annotations the `Slo`'s Prometheus alerts carry.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* import { Slo } from "@intentius/chant-lexicon-prometheus";
|
|
103
|
+
* import { SloAlertRules } from "@intentius/chant-lexicon-grafana";
|
|
104
|
+
*
|
|
105
|
+
* export const checkout = Slo({ name: "checkout", objective: 0.999, window: "30d", sli: { ... } });
|
|
106
|
+
* export const checkoutAlerts = SloAlertRules({ slo: checkout, datasource: prometheus, folder: "SLOs" });
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export const SloAlertRules = Composite<SloAlertRulesProps, SloAlertRulesMembers>((props) => {
|
|
110
|
+
requireDatasource("SloAlertRules", props.datasource);
|
|
111
|
+
if (!props.slo) throw new Error("SloAlertRules: slo is required (an Slo(...) declaration)");
|
|
112
|
+
if (typeof props.folder !== "string" || props.folder.trim() === "") throw new Error("SloAlertRules: folder is required (the Grafana folder title)");
|
|
113
|
+
const m = isMetrics(props.slo) ? props.slo : sloMetrics(props.slo);
|
|
114
|
+
const pairs = sloAlertQueries(m);
|
|
115
|
+
if (pairs.length === 0) {
|
|
116
|
+
throw new Error(`SloAlertRules: the Slo "${m.name}" has alerting turned off, so it has no burn-rate windows to alert on`);
|
|
117
|
+
}
|
|
118
|
+
const ds = props.datasource;
|
|
119
|
+
const budget = num(m.errorBudget);
|
|
120
|
+
const rules = pairs.map(
|
|
121
|
+
(p) =>
|
|
122
|
+
new AlertRule({
|
|
123
|
+
title: `SLO ${m.name} error budget burn ${p.long}/${p.short} (${p.severity})`,
|
|
124
|
+
uid: p.uid,
|
|
125
|
+
data: [
|
|
126
|
+
new PromQuery({ datasource: ds, expr: p.longExpr, instant: true, range: false }),
|
|
127
|
+
new PromQuery({ datasource: ds, expr: p.shortExpr, instant: true, range: false }),
|
|
128
|
+
new MathExpression({ expression: p.condition }),
|
|
129
|
+
],
|
|
130
|
+
condition: "C",
|
|
131
|
+
noDataState: props.noDataState ?? "OK",
|
|
132
|
+
execErrState: props.execErrState ?? "Error",
|
|
133
|
+
...(props.for !== undefined ? { for: props.for } : {}),
|
|
134
|
+
labels: { ...(props.labels ?? {}), ...p.labels },
|
|
135
|
+
annotations: {
|
|
136
|
+
summary: `SLO ${m.name} is burning its error budget more than ${num(p.factor)}x too fast (${p.long} and ${p.short} windows)`,
|
|
137
|
+
description:
|
|
138
|
+
`The error ratio over both the last ${p.long} and the last ${p.short} is above ${num(p.threshold)} ` +
|
|
139
|
+
`(${num(p.factor)} times the ${budget} budget of a ${num(m.objective)} objective). ` +
|
|
140
|
+
`At that rate the ${m.window} error budget is gone in ${p.exhaustsIn}.`,
|
|
141
|
+
...(props.annotations ?? {}),
|
|
142
|
+
},
|
|
143
|
+
...(props.contactPoint !== undefined ? { notification_settings: { receiver: props.contactPoint } } : {}),
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
return {
|
|
147
|
+
rules: new AlertRuleGroup({ name: props.group ?? m.group, folder: props.folder, interval: props.interval ?? "1m", rules }),
|
|
148
|
+
};
|
|
149
|
+
}, "SloAlertRules");
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { grafanaConfigSchema, resolveGrafanaTarget } from "./config";
|
|
3
|
+
import { namespaceOf } from "./api/client";
|
|
4
|
+
import { grafanaPlugin } from "./plugin";
|
|
5
|
+
|
|
6
|
+
const config = {
|
|
7
|
+
grafana: {
|
|
8
|
+
profiles: {
|
|
9
|
+
prod: { url: "https://grafana.example.com/", token: { env: "GF_PROD" }, orgId: 2 },
|
|
10
|
+
local: { url: "http://localhost:3000", basicAuth: { user: { env: "GF_USER" }, password: { env: "GF_PASS" } } },
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("grafana config namespace", () => {
|
|
16
|
+
it("is declared on the plugin and rejects an unknown key in a profile", () => {
|
|
17
|
+
expect(grafanaPlugin.configSchema).toBe(grafanaConfigSchema);
|
|
18
|
+
expect(grafanaConfigSchema.safeParse({ profiles: { prod: { url: "x", tokn: { env: "T" } } } }).success).toBe(false);
|
|
19
|
+
expect(grafanaConfigSchema.safeParse(config.grafana).success).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("resolveGrafanaTarget", () => {
|
|
24
|
+
it("binds an environment's profile, with the token from the variable it names", () => {
|
|
25
|
+
expect(resolveGrafanaTarget({ environment: "prod", config, env: { GF_PROD: "glsa_x" } })).toEqual({
|
|
26
|
+
url: "https://grafana.example.com",
|
|
27
|
+
auth: { token: "glsa_x" },
|
|
28
|
+
orgId: 2,
|
|
29
|
+
source: "grafana.profiles.prod",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("basic auth, from two variables", () => {
|
|
34
|
+
expect(resolveGrafanaTarget({ environment: "local", config, env: { GF_USER: "admin", GF_PASS: "admin" } })).toMatchObject({ auth: { user: "admin", password: "admin" } });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("a profile whose credential variable is unset is no-credentials, not an anonymous read", () => {
|
|
38
|
+
expect(resolveGrafanaTarget({ environment: "prod", config, env: {} })).toEqual({ reason: "no-credentials", detail: expect.stringContaining("GF_PROD") });
|
|
39
|
+
expect(resolveGrafanaTarget({ environment: "local", config, env: { GF_USER: "a" } })).toEqual({ reason: "no-credentials", detail: expect.stringContaining("GF_PASS") });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("falls back to GRAFANA_URL and GRAFANA_TOKEN, or GRAFANA_USER and GRAFANA_PASSWORD", () => {
|
|
43
|
+
expect(resolveGrafanaTarget({ environment: "dev", config, env: { GRAFANA_URL: "http://g:3000", GRAFANA_TOKEN: "t", GRAFANA_ORG_ID: "3" } })).toEqual({
|
|
44
|
+
url: "http://g:3000",
|
|
45
|
+
auth: { token: "t" },
|
|
46
|
+
orgId: 3,
|
|
47
|
+
source: "env GRAFANA_URL",
|
|
48
|
+
});
|
|
49
|
+
expect(resolveGrafanaTarget({ env: { GRAFANA_URL: "http://g", GRAFANA_USER: "u", GRAFANA_PASSWORD: "p" } })).toMatchObject({ auth: { user: "u", password: "p" } });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("no profile and no GRAFANA_URL is no-binding", () => {
|
|
53
|
+
expect(resolveGrafanaTarget({ environment: "dev", config, env: {} })).toEqual({ reason: "no-binding", detail: expect.stringContaining("grafana.profiles.dev") });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("the dashboard API namespace follows the organisation", () => {
|
|
57
|
+
expect(namespaceOf({})).toBe("default");
|
|
58
|
+
expect(namespaceOf({ orgId: 4 })).toBe("org-4");
|
|
59
|
+
expect(namespaceOf({ orgId: 4, namespace: "stacks-12" })).toBe("stacks-12");
|
|
60
|
+
});
|
|
61
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `grafana` namespace in `chant.config.ts` (#2946): which Grafana each
|
|
3
|
+
* chant environment observes, exports from and (#2948) applies to.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import type { ChantConfig } from "@intentius/chant/config";
|
|
7
|
+
* import "@intentius/chant-lexicon-grafana"; // brings the `grafana` key into ChantConfig
|
|
8
|
+
*
|
|
9
|
+
* export default {
|
|
10
|
+
* lexicons: ["grafana"],
|
|
11
|
+
* grafana: {
|
|
12
|
+
* profiles: {
|
|
13
|
+
* staging: { url: "https://grafana.staging.example.com", token: { env: "GRAFANA_STAGING_TOKEN" } },
|
|
14
|
+
* prod: { url: "https://grafana.example.com", token: { env: "GRAFANA_PROD_TOKEN" }, orgId: 2 },
|
|
15
|
+
* },
|
|
16
|
+
* },
|
|
17
|
+
* } satisfies ChantConfig;
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* A profile is keyed by chant environment, as `k8s.profiles.<env>` is.
|
|
21
|
+
* Credentials are named by their environment variable, never written in
|
|
22
|
+
* the config. An environment with no profile falls back to `GRAFANA_URL`
|
|
23
|
+
* with `GRAFANA_TOKEN`, or `GRAFANA_USER` and `GRAFANA_PASSWORD` (see
|
|
24
|
+
* `resolveGrafanaTarget`).
|
|
25
|
+
*
|
|
26
|
+
* One schema, three consumers, as in the fountain lexicon: core validates the
|
|
27
|
+
* namespace against it at load, the type is derived from it, and that type
|
|
28
|
+
* augments `ChantConfig`.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import type { ChantConfig } from "@intentius/chant/config";
|
|
33
|
+
import type { GrafanaAuth, GrafanaTarget } from "./api/client";
|
|
34
|
+
|
|
35
|
+
const envRef = z.strictObject({ env: z.string() });
|
|
36
|
+
|
|
37
|
+
export const grafanaProfileSchema = z.strictObject({
|
|
38
|
+
/** Base URL of the Grafana instance. */
|
|
39
|
+
url: z.string(),
|
|
40
|
+
/** A service account token, named by its environment variable. */
|
|
41
|
+
token: envRef.optional(),
|
|
42
|
+
/** Basic auth, each half named by its environment variable. For a local instance; use a token elsewhere. */
|
|
43
|
+
basicAuth: z.strictObject({ user: envRef, password: envRef }).optional(),
|
|
44
|
+
/** Organisation id. Defaults to 1. */
|
|
45
|
+
orgId: z.number().int().positive().optional(),
|
|
46
|
+
/** The `dashboard.grafana.app` namespace, when it is not `default` / `org-<id>` (Grafana Cloud: `stacks-<id>`). */
|
|
47
|
+
namespace: z.string().optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const grafanaConfigSchema = z.strictObject({
|
|
51
|
+
/** One Grafana per chant environment. */
|
|
52
|
+
profiles: z.record(z.string(), grafanaProfileSchema).optional(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export type GrafanaProfile = z.infer<typeof grafanaProfileSchema>;
|
|
56
|
+
export type GrafanaConfig = z.infer<typeof grafanaConfigSchema>;
|
|
57
|
+
|
|
58
|
+
declare module "@intentius/chant/config" {
|
|
59
|
+
interface ChantConfig {
|
|
60
|
+
grafana?: GrafanaConfig;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Compile-time proof that the augmentation reaches `ChantConfig`. */
|
|
65
|
+
export type GrafanaConfigNamespace = NonNullable<ChantConfig["grafana"]>;
|
|
66
|
+
|
|
67
|
+
/** Why no target could be resolved, in the observation vocabulary. */
|
|
68
|
+
export interface UnresolvedTarget {
|
|
69
|
+
readonly reason: "no-binding" | "no-credentials";
|
|
70
|
+
readonly detail: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isUnresolved(v: GrafanaTarget | UnresolvedTarget): v is UnresolvedTarget {
|
|
74
|
+
return "reason" in v;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { isUnresolved as isUnresolvedTarget };
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The Grafana an environment reads from. Pure: `config` and `env` are
|
|
81
|
+
* passed in.
|
|
82
|
+
*
|
|
83
|
+
* 1. `grafana.profiles.<environment>`. A profile that names a credential
|
|
84
|
+
* variable which is not set is `no-credentials`, not an anonymous read:
|
|
85
|
+
* the profile said how to authenticate.
|
|
86
|
+
* 2. `GRAFANA_URL`, with `GRAFANA_TOKEN`, or `GRAFANA_USER` and
|
|
87
|
+
* `GRAFANA_PASSWORD`, or neither (an instance with anonymous access; a
|
|
88
|
+
* refused read is then `no-credentials`).
|
|
89
|
+
* 3. Otherwise `no-binding`.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveGrafanaTarget(input: {
|
|
92
|
+
environment?: string;
|
|
93
|
+
config?: Pick<ChantConfig, "grafana">;
|
|
94
|
+
env?: Record<string, string | undefined>;
|
|
95
|
+
}): GrafanaTarget | UnresolvedTarget {
|
|
96
|
+
const env = input.env ?? process.env;
|
|
97
|
+
const profile = input.environment !== undefined ? input.config?.grafana?.profiles?.[input.environment] : undefined;
|
|
98
|
+
if (profile) {
|
|
99
|
+
const source = `grafana.profiles.${input.environment}`;
|
|
100
|
+
let auth: GrafanaAuth | undefined;
|
|
101
|
+
if (profile.token) {
|
|
102
|
+
const token = env[profile.token.env];
|
|
103
|
+
if (!token) return { reason: "no-credentials", detail: `${source}.token names ${profile.token.env}, which is not set` };
|
|
104
|
+
auth = { token };
|
|
105
|
+
} else if (profile.basicAuth) {
|
|
106
|
+
const user = env[profile.basicAuth.user.env];
|
|
107
|
+
const password = env[profile.basicAuth.password.env];
|
|
108
|
+
if (!user || !password) {
|
|
109
|
+
const missing = [!user ? profile.basicAuth.user.env : undefined, !password ? profile.basicAuth.password.env : undefined].filter(Boolean).join(" and ");
|
|
110
|
+
return { reason: "no-credentials", detail: `${source}.basicAuth names ${missing}, which ${missing.includes(" and ") ? "are" : "is"} not set` };
|
|
111
|
+
}
|
|
112
|
+
auth = { user, password };
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
url: profile.url.replace(/\/+$/, ""),
|
|
116
|
+
...(auth ? { auth } : {}),
|
|
117
|
+
...(profile.orgId !== undefined ? { orgId: profile.orgId } : {}),
|
|
118
|
+
...(profile.namespace ? { namespace: profile.namespace } : {}),
|
|
119
|
+
source,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const url = env.GRAFANA_URL;
|
|
124
|
+
if (!url) {
|
|
125
|
+
const where = input.environment !== undefined ? `grafana.profiles.${input.environment} is not declared and ` : "";
|
|
126
|
+
return { reason: "no-binding", detail: `${where}GRAFANA_URL is not set, so there is no Grafana to read` };
|
|
127
|
+
}
|
|
128
|
+
let auth: GrafanaAuth | undefined;
|
|
129
|
+
if (env.GRAFANA_TOKEN) auth = { token: env.GRAFANA_TOKEN };
|
|
130
|
+
else if (env.GRAFANA_USER && env.GRAFANA_PASSWORD) auth = { user: env.GRAFANA_USER, password: env.GRAFANA_PASSWORD };
|
|
131
|
+
const orgId = env.GRAFANA_ORG_ID ? Number(env.GRAFANA_ORG_ID) : undefined;
|
|
132
|
+
return {
|
|
133
|
+
url: url.replace(/\/+$/, ""),
|
|
134
|
+
...(auth ? { auth } : {}),
|
|
135
|
+
...(orgId !== undefined && Number.isInteger(orgId) && orgId > 0 ? { orgId } : {}),
|
|
136
|
+
source: "env GRAFANA_URL",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contact point settings Grafana stores encrypted, per integration: the
|
|
3
|
+
* options marked `secure` by `GET /api/alert-notifiers?version=2` on
|
|
4
|
+
* `grafana/grafana:13.2.2` (commit 1bea008f; the same list on 12.4.11).
|
|
5
|
+
* Nested settings are dotted paths (`tlsConfig.clientKey`).
|
|
6
|
+
*
|
|
7
|
+
* GRAF002 flags a literal at one of these paths, and the importer replaces
|
|
8
|
+
* the `[REDACTED]` that an export without secrets writes there.
|
|
9
|
+
*/
|
|
10
|
+
export const CONTACT_POINT_SECRET_SETTINGS: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
11
|
+
dingding: ["url"],
|
|
12
|
+
discord: ["url"],
|
|
13
|
+
email: [],
|
|
14
|
+
googlechat: ["url"],
|
|
15
|
+
jira: ["user", "password", "api_token"],
|
|
16
|
+
kafka: ["password"],
|
|
17
|
+
line: ["token"],
|
|
18
|
+
mqtt: ["password", "tlsConfig.caCertificate", "tlsConfig.clientCertificate", "tlsConfig.clientKey"],
|
|
19
|
+
oncall: ["password", "authorization_credentials"],
|
|
20
|
+
opsgenie: ["apiKey"],
|
|
21
|
+
pagerduty: ["integrationKey"],
|
|
22
|
+
"prometheus-alertmanager": ["basicAuthPassword"],
|
|
23
|
+
pushover: ["apiToken", "userKey"],
|
|
24
|
+
sensugo: ["apikey"],
|
|
25
|
+
slack: ["token", "url"],
|
|
26
|
+
sns: ["sigv4.access_key", "sigv4.secret_key"],
|
|
27
|
+
teams: [],
|
|
28
|
+
telegram: ["bottoken"],
|
|
29
|
+
threema: ["api_secret"],
|
|
30
|
+
victorops: ["url"],
|
|
31
|
+
webex: ["bot_token"],
|
|
32
|
+
webhook: [
|
|
33
|
+
"password",
|
|
34
|
+
"authorization_credentials",
|
|
35
|
+
"tlsConfig.caCertificate",
|
|
36
|
+
"tlsConfig.clientCertificate",
|
|
37
|
+
"tlsConfig.clientKey",
|
|
38
|
+
"hmacConfig.secret",
|
|
39
|
+
"http_config.oauth2.client_secret",
|
|
40
|
+
"http_config.oauth2.tls_config.caCertificate",
|
|
41
|
+
"http_config.oauth2.tls_config.clientCertificate",
|
|
42
|
+
"http_config.oauth2.tls_config.clientKey",
|
|
43
|
+
],
|
|
44
|
+
wechat: ["api_secret", "http_config.basic_auth.password", "http_config.authorization.credentials", "http_config.oauth2.client_secret"],
|
|
45
|
+
wecom: ["url", "secret"],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** How Grafana provisioning reads a value from outside the file: `$VAR`, `${VAR}`, `$__env{…}`, `$__file{…}`, `$__vault{…}`. */
|
|
49
|
+
export const EXPANDED_VALUE = /\$(\{[A-Za-z_][A-Za-z0-9_]*\}|[A-Za-z_][A-Za-z0-9_]*|__(env|file|vault)\{[^}]+\})/;
|
|
50
|
+
|
|
51
|
+
/** What an export without secrets writes in place of each one. */
|
|
52
|
+
export const REDACTED = "[REDACTED]";
|
|
53
|
+
|
|
54
|
+
/** The secret settings of one receiver present in `settings`, with their values: `[path, value]`. */
|
|
55
|
+
export function secretSettings(type: string, settings: Record<string, unknown>): Array<[string, unknown]> {
|
|
56
|
+
const out: Array<[string, unknown]> = [];
|
|
57
|
+
for (const path of CONTACT_POINT_SECRET_SETTINGS[type] ?? []) {
|
|
58
|
+
let node: unknown = settings;
|
|
59
|
+
for (const seg of path.split(".")) node = node && typeof node === "object" ? (node as Record<string, unknown>)[seg] : undefined;
|
|
60
|
+
if (node !== undefined) out.push([path, node]);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
package/src/dashboard.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type { Declarable } from "@intentius/chant/declarable";
|
|
|
12
12
|
import type { PanelEntity, RowEntity } from "./panels";
|
|
13
13
|
import type { VariableEntity } from "./variables";
|
|
14
14
|
import type { DashboardLink, TimePickerConfig } from "./schema/dashboard.gen";
|
|
15
|
+
import { isFolderEntity, type FolderEntity } from "./folder";
|
|
16
|
+
import type { AnnotationInput } from "./annotations";
|
|
15
17
|
|
|
16
18
|
/** A dashboard link: `title` and `type` required, the rest defaulted as Grafana's editor would. */
|
|
17
19
|
export type DashboardLinkInput = Partial<DashboardLink> & Pick<DashboardLink, "title" | "type">;
|
|
@@ -40,6 +42,13 @@ export interface DashboardProps {
|
|
|
40
42
|
graphTooltip?: "default" | "sharedCrosshair" | "sharedTooltip";
|
|
41
43
|
/** Whether users may edit it in the UI. Defaults to true, as in Grafana. */
|
|
42
44
|
editable?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The dashboard schema version the JSON is written at. Leave it out for
|
|
47
|
+
* the pinned one (`DASHBOARD_SCHEMA_VERSION`). `chant import` sets it for
|
|
48
|
+
* a dashboard saved by an older Grafana, so Grafana still runs its
|
|
49
|
+
* migrations when it loads the rebuilt JSON.
|
|
50
|
+
*/
|
|
51
|
+
schemaVersion?: number;
|
|
43
52
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
53
|
variables?: VariableEntity<any>[];
|
|
45
54
|
/** Panels and rows, top to bottom. A row's own `panels` follow it. */
|
|
@@ -47,23 +56,50 @@ export interface DashboardProps {
|
|
|
47
56
|
panels?: Array<PanelEntity<any, any> | RowEntity>;
|
|
48
57
|
links?: DashboardLinkInput[];
|
|
49
58
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
59
|
+
* Annotation queries: events drawn on the dashboard's time series panels
|
|
60
|
+
* (deploys, incidents), each from a datasource. Grafana adds its own
|
|
61
|
+
* "Annotations & Alerts" query to every dashboard that has none with
|
|
62
|
+
* `builtIn: 1`, so leave that one out unless you change it.
|
|
53
63
|
*/
|
|
54
|
-
|
|
64
|
+
annotations?: AnnotationInput[];
|
|
65
|
+
/**
|
|
66
|
+
* The Grafana folder: a path (`"Platform/Kubernetes"`) or a `Folder`,
|
|
67
|
+
* which pins the folder's uid. Written as a subdirectory of `dashboards/`,
|
|
68
|
+
* which the provider maps to folders (`foldersFromFilesStructure`); a path
|
|
69
|
+
* nests one directory in another, which Grafana 13.1 and later make a
|
|
70
|
+
* Kubernetes folder inside Platform, and earlier versions use only the
|
|
71
|
+
* last level (GRAF109 warns). The API applier creates each level with its
|
|
72
|
+
* uid. Leave it out for the General folder.
|
|
73
|
+
*/
|
|
74
|
+
folder?: string | FolderEntity;
|
|
55
75
|
}
|
|
56
76
|
|
|
77
|
+
/** A dashboard's props as the entity holds them: `folder` is always the path, and a `Folder` given for it is `folderEntity`. */
|
|
78
|
+
export type DashboardEntityProps = Omit<DashboardProps, "folder"> & { folder?: string };
|
|
79
|
+
|
|
57
80
|
export interface DashboardEntity extends Declarable {
|
|
58
|
-
readonly props:
|
|
81
|
+
readonly props: DashboardEntityProps;
|
|
82
|
+
/** The `Folder` the dashboard was given, when it was given one rather than a path. */
|
|
83
|
+
readonly folderEntity?: FolderEntity;
|
|
59
84
|
}
|
|
60
85
|
|
|
61
86
|
export const DASHBOARD_TYPE = "Grafana::Dashboard";
|
|
62
87
|
|
|
63
88
|
const DashboardBase = createResource(DASHBOARD_TYPE, "grafana", {}) as unknown as (this: object, props: Record<string, unknown>) => void;
|
|
64
89
|
|
|
65
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* A Grafana dashboard. A `Folder` given as its `folder` is held as the
|
|
92
|
+
* folder's path, so everything that reads the dashboard's props (the build,
|
|
93
|
+
* the checks, observation's diff) sees one form; the `Folder` itself, for its
|
|
94
|
+
* uid, is `folderEntity`.
|
|
95
|
+
*/
|
|
66
96
|
export const Dashboard = function (this: object, props: DashboardProps) {
|
|
97
|
+
const folder = props.folder;
|
|
98
|
+
if (isFolderEntity(folder)) {
|
|
99
|
+
DashboardBase.call(this, { ...props, folder: folder.path } as unknown as Record<string, unknown>);
|
|
100
|
+
Object.defineProperty(this, "folderEntity", { value: folder, enumerable: false });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
67
103
|
DashboardBase.call(this, props as unknown as Record<string, unknown>);
|
|
68
104
|
} as unknown as new (props: DashboardProps) => DashboardEntity;
|
|
69
105
|
Object.defineProperty(Dashboard, "name", { value: "Dashboard" });
|
|
@@ -84,8 +120,15 @@ export interface DashboardProviderProps {
|
|
|
84
120
|
/** Provider name, unique in the provisioning file. */
|
|
85
121
|
name: string;
|
|
86
122
|
orgId?: number;
|
|
87
|
-
/**
|
|
88
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Put every dashboard this provider loads in this folder: a title, or a
|
|
125
|
+
* root-level `Folder`, whose title and uid are written as `folder` and
|
|
126
|
+
* `folderUid` (Grafana creates a provider's folder at the root, so a
|
|
127
|
+
* nested `Folder` is refused). Setting it (or `folderUid`) turns
|
|
128
|
+
* `foldersFromFilesStructure` off by default, so each dashboard's own
|
|
129
|
+
* `folder` is ignored (GRAF109 warns).
|
|
130
|
+
*/
|
|
131
|
+
folder?: string | FolderEntity;
|
|
89
132
|
folderUid?: string;
|
|
90
133
|
/** Where the `dashboards/` output is mounted in the Grafana container. Defaults to `/var/lib/grafana/dashboards`. */
|
|
91
134
|
path?: string;
|
|
@@ -96,8 +139,11 @@ export interface DashboardProviderProps {
|
|
|
96
139
|
updateIntervalSeconds?: number;
|
|
97
140
|
}
|
|
98
141
|
|
|
142
|
+
/** A provider's props as the entity holds them: a `Folder` given for `folder` is written out as `folder` and `folderUid`. */
|
|
143
|
+
export type DashboardProviderEntityProps = Omit<DashboardProviderProps, "folder"> & { folder?: string };
|
|
144
|
+
|
|
99
145
|
export interface DashboardProviderEntity extends Declarable {
|
|
100
|
-
readonly props:
|
|
146
|
+
readonly props: DashboardProviderEntityProps;
|
|
101
147
|
}
|
|
102
148
|
|
|
103
149
|
export const DASHBOARD_PROVIDER_TYPE = "Grafana::DashboardProvider";
|
|
@@ -109,6 +155,19 @@ const ProviderBase = createResource(DASHBOARD_PROVIDER_TYPE, "grafana", {}) as u
|
|
|
109
155
|
|
|
110
156
|
/** An entry in the dashboard provisioning file. Optional: a default one is written when none is declared. */
|
|
111
157
|
export const DashboardProvider = function (this: object, props: DashboardProviderProps) {
|
|
158
|
+
const folder = props.folder;
|
|
159
|
+
if (isFolderEntity(folder)) {
|
|
160
|
+
if (folder.props.parent) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`grafana: DashboardProvider "${props.name}" puts its dashboards in the Folder "${folder.path}", which is nested; Grafana creates a provider's folder at the root. Give it a root-level Folder, or leave folder out and set each dashboard's folder`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (props.folderUid !== undefined && props.folderUid !== folder.uid) {
|
|
166
|
+
throw new Error(`grafana: DashboardProvider "${props.name}" has folderUid "${props.folderUid}", but its Folder's uid is "${folder.uid}"`);
|
|
167
|
+
}
|
|
168
|
+
ProviderBase.call(this, { ...props, folder: folder.props.title, folderUid: folder.uid } as unknown as Record<string, unknown>);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
112
171
|
ProviderBase.call(this, props as unknown as Record<string, unknown>);
|
|
113
172
|
} as unknown as new (props: DashboardProviderProps) => DashboardProviderEntity;
|
|
114
173
|
Object.defineProperty(DashboardProvider, "name", { value: "DashboardProvider" });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { datasourceUses, knownDatasources, resolveDatasourceRef } from "./datasource-refs";
|
|
3
|
+
|
|
4
|
+
const known = knownDatasources([{ name: "Prometheus", type: "prometheus", uid: "prom" }], [{ type: "loki", uid: "logs", name: "Logs" }]);
|
|
5
|
+
|
|
6
|
+
function dashboard(panels: unknown[], variables: unknown[] = []) {
|
|
7
|
+
return { uid: "d", title: "D", panels, templating: { list: variables } };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("resolveDatasourceRef", () => {
|
|
11
|
+
const vars = new Map([["ds", { type: "datasource", name: "ds", query: "prometheus" }]]);
|
|
12
|
+
|
|
13
|
+
test("resolves provisioned and external datasources to their plugin type", () => {
|
|
14
|
+
expect(resolveDatasourceRef({ uid: "prom" }, vars, known)).toMatchObject({ kind: "declared", type: "prometheus", datasource: { external: false } });
|
|
15
|
+
expect(resolveDatasourceRef({ type: "loki", uid: "logs" }, vars, known)).toMatchObject({ kind: "declared", type: "loki", datasource: { external: true, name: "Logs" } });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("resolves a datasource variable to its plugin type", () => {
|
|
19
|
+
expect(resolveDatasourceRef({ uid: "${ds}" }, vars, known)).toEqual({ kind: "variable", variable: "ds", declared: true, type: "prometheus" });
|
|
20
|
+
expect(resolveDatasourceRef({ type: "tempo", uid: "$other" }, vars, known)).toEqual({ kind: "variable", variable: "other", declared: false, type: "tempo" });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("separates Grafana's pseudo-datasources, unknown uids and no ref", () => {
|
|
24
|
+
expect(resolveDatasourceRef({ type: "datasource", uid: "-- Mixed --" }, vars, known).kind).toBe("builtin");
|
|
25
|
+
expect(resolveDatasourceRef({ type: "prometheus", uid: "gone" }, vars, known)).toEqual({ kind: "undeclared", uid: "gone", type: "prometheus" });
|
|
26
|
+
expect(resolveDatasourceRef(undefined, vars, known)).toEqual({ kind: "default" });
|
|
27
|
+
expect(resolveDatasourceRef({ type: "prometheus" }, vars, known)).toEqual({ kind: "default", type: "prometheus" });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("datasourceUses", () => {
|
|
32
|
+
test("a query without its own ref goes where its panel's goes", () => {
|
|
33
|
+
const d = dashboard([{ type: "stat", id: 1, title: "p", datasource: { uid: "prom" }, targets: [{ refId: "A", expr: "up" }, { refId: "B", datasource: { uid: "logs" } }] }]);
|
|
34
|
+
const queries = datasourceUses(d, known).filter((u) => u.kind === "query");
|
|
35
|
+
expect(queries.map((u) => [u.target?.refId, u.ref === undefined, u.resolved.type])).toEqual([
|
|
36
|
+
["A", true, "prometheus"],
|
|
37
|
+
["B", false, "loki"],
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("a query under a -- Mixed -- panel with no ref goes to the default datasource", () => {
|
|
42
|
+
const d = dashboard([{ type: "table", id: 1, datasource: { type: "datasource", uid: "-- Mixed --" }, targets: [{ refId: "A" }] }]);
|
|
43
|
+
expect(datasourceUses(d, known).find((u) => u.kind === "query")?.resolved).toEqual({ kind: "default" });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("covers panels in collapsed rows and query variables, and skips rows", () => {
|
|
47
|
+
const d = dashboard(
|
|
48
|
+
[{ type: "row", id: 1, collapsed: true, datasource: { uid: "prom" }, panels: [{ type: "stat", id: 2, datasource: { uid: "logs" }, targets: [] }] }],
|
|
49
|
+
[{ type: "query", name: "job", datasource: { uid: "prom" }, query: "label_values(job)" }],
|
|
50
|
+
);
|
|
51
|
+
expect(datasourceUses(d, known).map((u) => [u.kind, u.where, u.resolved.type])).toEqual([
|
|
52
|
+
["panel", 'panel (untitled) (id 2)', "loki"],
|
|
53
|
+
["variable", 'variable "job"', "prometheus"],
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
});
|