@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
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* lexicon's `spanMetricsNames()`: its namespace, its histogram unit, the
|
|
7
7
|
* default dimensions it keeps, and optionally the `prometheus` exporter's
|
|
8
8
|
* namespace. Renaming the connector's namespace moves every query here.
|
|
9
|
+
*
|
|
10
|
+
* The queries count server and consumer spans only by default, the spans
|
|
11
|
+
* that handle a request or a message, so a service's outgoing calls and
|
|
12
|
+
* its internal spans don't inflate its rate or dilute its error ratio.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
import { Composite, type CompositeInstance } from "@intentius/chant/composite";
|
|
@@ -21,6 +25,7 @@ import { slugUid } from "../util";
|
|
|
21
25
|
import {
|
|
22
26
|
dashboardProps,
|
|
23
27
|
durationUnit,
|
|
28
|
+
errorRatio,
|
|
24
29
|
legend,
|
|
25
30
|
quantile,
|
|
26
31
|
quantileName,
|
|
@@ -41,8 +46,36 @@ export interface RedDashboardProps extends DashboardOptions {
|
|
|
41
46
|
exporter?: OTelComponent<"exporter", "prometheus", PrometheusExporterConfig> | PrometheusNaming;
|
|
42
47
|
/** Duration quantiles, one panel each (default p50, p95 and p99). */
|
|
43
48
|
quantiles?: number[];
|
|
49
|
+
/**
|
|
50
|
+
* The span kinds the queries count (default server and consumer spans).
|
|
51
|
+
* `[]` counts every kind. With the default, a connector that excludes
|
|
52
|
+
* `span.kind` gets no kind filter; naming kinds for such a connector is
|
|
53
|
+
* an error.
|
|
54
|
+
*/
|
|
55
|
+
spanKinds?: SpanKind[];
|
|
44
56
|
}
|
|
45
57
|
|
|
58
|
+
/** The `span_kind` label values the spanmetrics connector writes. */
|
|
59
|
+
export type SpanKind =
|
|
60
|
+
| "SPAN_KIND_UNSPECIFIED"
|
|
61
|
+
| "SPAN_KIND_INTERNAL"
|
|
62
|
+
| "SPAN_KIND_SERVER"
|
|
63
|
+
| "SPAN_KIND_CLIENT"
|
|
64
|
+
| "SPAN_KIND_PRODUCER"
|
|
65
|
+
| "SPAN_KIND_CONSUMER";
|
|
66
|
+
|
|
67
|
+
const SPAN_KINDS: readonly SpanKind[] = [
|
|
68
|
+
"SPAN_KIND_UNSPECIFIED",
|
|
69
|
+
"SPAN_KIND_INTERNAL",
|
|
70
|
+
"SPAN_KIND_SERVER",
|
|
71
|
+
"SPAN_KIND_CLIENT",
|
|
72
|
+
"SPAN_KIND_PRODUCER",
|
|
73
|
+
"SPAN_KIND_CONSUMER",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/** The kinds `RedDashboard` counts unless told otherwise: the spans that serve a request or consume a message. */
|
|
77
|
+
export const RED_DEFAULT_SPAN_KINDS: readonly SpanKind[] = Object.freeze(["SPAN_KIND_SERVER", "SPAN_KIND_CONSUMER"]);
|
|
78
|
+
|
|
46
79
|
export type RedDashboardMembers = { dashboard: DashboardEntity };
|
|
47
80
|
|
|
48
81
|
/** What `RedDashboard(...)` returns: its dashboard, as `dashboard`. */
|
|
@@ -50,28 +83,65 @@ export type RedDashboardInstance = CompositeInstance<RedDashboardMembers> & RedD
|
|
|
50
83
|
|
|
51
84
|
const DEFAULT_QUANTILES = [0.5, 0.95, 0.99];
|
|
52
85
|
|
|
86
|
+
function cap(s: string): string {
|
|
87
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
53
90
|
function isNames(x: unknown): x is SpanMetricsNames {
|
|
54
91
|
return typeof x === "object" && x !== null && "calls" in x && "labels" in x && "errorStatus" in x;
|
|
55
92
|
}
|
|
56
93
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
94
|
+
/**
|
|
95
|
+
* The span-kind matcher for `kinds`, or none: none for `[]`, and none for
|
|
96
|
+
* the default kinds when the connector excludes `span.kind`.
|
|
97
|
+
*/
|
|
98
|
+
function spanKindMatchers(names: SpanMetricsNames, kinds: readonly SpanKind[] | undefined): Matcher[] {
|
|
99
|
+
const label = names.labels.spanKind;
|
|
100
|
+
if (kinds === undefined) return label ? [[label, "=~", RED_DEFAULT_SPAN_KINDS.join("|")]] : [];
|
|
101
|
+
for (const k of kinds) {
|
|
102
|
+
if (!SPAN_KINDS.includes(k)) throw new Error(`RedDashboard: unknown span kind ${JSON.stringify(k)}; use one of ${SPAN_KINDS.join(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
if (kinds.length === 0) return [];
|
|
105
|
+
if (!label) throw new Error("RedDashboard: the connector excludes span.kind, so the metrics can't be filtered by spanKinds");
|
|
106
|
+
return [[label, "=~", [...new Set(kinds)].join("|")]];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The PromQL the RED dashboard runs, from the connector's names. Exposed for
|
|
111
|
+
* tests and for panels of your own. `spanKinds` defaults to server and
|
|
112
|
+
* consumer spans; `[]` counts every kind.
|
|
113
|
+
*/
|
|
114
|
+
export function redQueries(
|
|
115
|
+
names: SpanMetricsNames,
|
|
116
|
+
quantiles: number[] = DEFAULT_QUANTILES,
|
|
117
|
+
serviceFilter = "$service",
|
|
118
|
+
spanKinds?: readonly SpanKind[],
|
|
119
|
+
) {
|
|
59
120
|
const svc = names.labels.service;
|
|
60
121
|
const status = names.labels.statusCode;
|
|
61
122
|
if (!svc) throw new Error("RedDashboard: the connector excludes service.name, so there is no service to break the metrics down by");
|
|
62
123
|
if (!status) throw new Error("RedDashboard: the connector excludes status.code, so errors can't be told from successes");
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
124
|
+
const kind = spanKindMatchers(names, spanKinds);
|
|
125
|
+
const scope: Matcher[] = [[svc, "=~", serviceFilter], ...kind];
|
|
126
|
+
const calls = selector(names.calls.prometheus, scope);
|
|
127
|
+
const errors = selector(names.calls.prometheus, [...scope, [status, "=", names.errorStatus]]);
|
|
128
|
+
const buckets = names.duration ? selector(`${names.duration.prometheus}_bucket`, scope) : undefined;
|
|
67
129
|
return {
|
|
68
130
|
rate: sumRate(calls, [svc]),
|
|
69
|
-
errorRatio:
|
|
131
|
+
errorRatio: errorRatio(errors, calls, [svc]),
|
|
70
132
|
duration: buckets ? quantiles.map((q) => ({ quantile: q, expr: quantile(q, buckets, [svc]) })) : [],
|
|
71
|
-
services: `label_values(${names.calls.prometheus}, ${svc})`,
|
|
133
|
+
services: `label_values(${selector(names.calls.prometheus, kind)}, ${svc})`,
|
|
72
134
|
};
|
|
73
135
|
}
|
|
74
136
|
|
|
137
|
+
/** "server and consumer spans", for panel descriptions. */
|
|
138
|
+
function kindsText(names: SpanMetricsNames, kinds: readonly SpanKind[] | undefined): string {
|
|
139
|
+
const counted = kinds ?? (names.labels.spanKind ? RED_DEFAULT_SPAN_KINDS : []);
|
|
140
|
+
if (counted.length === 0) return "spans";
|
|
141
|
+
const words = [...new Set(counted)].map((k) => k.replace(/^SPAN_KIND_/, "").toLowerCase());
|
|
142
|
+
return `${words.length > 1 ? `${words.slice(0, -1).join(", ")} and ${words[words.length - 1]}` : words[0]} spans`;
|
|
143
|
+
}
|
|
144
|
+
|
|
75
145
|
/**
|
|
76
146
|
* A RED dashboard (rate, errors, duration) per service, built from a
|
|
77
147
|
* `spanmetrics` connector declaration.
|
|
@@ -94,7 +164,8 @@ export const RedDashboard = Composite<RedDashboardProps, RedDashboardMembers>((p
|
|
|
94
164
|
for (const q of quantiles) {
|
|
95
165
|
if (!(typeof q === "number" && q > 0 && q < 1)) throw new Error(`RedDashboard: a quantile must be between 0 and 1, got ${String(q)}`);
|
|
96
166
|
}
|
|
97
|
-
const q = redQueries(names, quantiles);
|
|
167
|
+
const q = redQueries(names, quantiles, "$service", props.spanKinds);
|
|
168
|
+
const counted = kindsText(names, props.spanKinds);
|
|
98
169
|
const svc = names.labels.service!;
|
|
99
170
|
const ds = props.datasource;
|
|
100
171
|
|
|
@@ -112,7 +183,7 @@ export const RedDashboard = Composite<RedDashboardProps, RedDashboardMembers>((p
|
|
|
112
183
|
|
|
113
184
|
const rate = new TimeSeriesPanel({
|
|
114
185
|
title: "Rate",
|
|
115
|
-
description:
|
|
186
|
+
description: `${cap(counted)} per second by service, from ${names.calls.prometheus}.`,
|
|
116
187
|
datasource: ds,
|
|
117
188
|
gridPos: { w: 12, h: 8 },
|
|
118
189
|
targets: [new PromQuery({ expr: q.rate, legendFormat: legend(svc) })],
|
|
@@ -120,7 +191,7 @@ export const RedDashboard = Composite<RedDashboardProps, RedDashboardMembers>((p
|
|
|
120
191
|
});
|
|
121
192
|
const errors = new TimeSeriesPanel({
|
|
122
193
|
title: "Errors",
|
|
123
|
-
description: `Share of
|
|
194
|
+
description: `Share of ${counted} with ${names.labels.statusCode}="${names.errorStatus}", by service.`,
|
|
124
195
|
datasource: ds,
|
|
125
196
|
gridPos: { w: 12, h: 8 },
|
|
126
197
|
targets: [new PromQuery({ expr: q.errorRatio, legendFormat: legend(svc) })],
|
|
@@ -135,7 +206,7 @@ export const RedDashboard = Composite<RedDashboardProps, RedDashboardMembers>((p
|
|
|
135
206
|
(d) =>
|
|
136
207
|
new TimeSeriesPanel({
|
|
137
208
|
title: `Duration ${quantileName(d.quantile)}`,
|
|
138
|
-
description: `${quantileName(d.quantile)}
|
|
209
|
+
description: `${quantileName(d.quantile)} duration of ${counted} by service, from ${names.duration!.prometheus}.`,
|
|
139
210
|
datasource: ds,
|
|
140
211
|
gridPos: { w: width, h: 8 },
|
|
141
212
|
targets: [new PromQuery({ expr: d.expr, legendFormat: legend(svc) })],
|
package/src/composites/shared.ts
CHANGED
|
@@ -39,6 +39,17 @@ export function sumRate(sel: string, by: string[], range = "$__rate_interval"):
|
|
|
39
39
|
return `sum${by.length ? ` by (${by.join(", ")})` : ""} (rate(${sel}[${range}]))`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* `errors / all` as rates summed by `by`, with the numerator padded to zero:
|
|
44
|
+
* `(errors or all * 0) / all`. A group with calls but no error series gets
|
|
45
|
+
* 0 instead of dropping out of the result, so a panel shows 0% rather than
|
|
46
|
+
* "No data" for a service that has had no errors.
|
|
47
|
+
*/
|
|
48
|
+
export function errorRatio(errorSel: string, allSel: string, by: string[]): string {
|
|
49
|
+
const all = sumRate(allSel, by);
|
|
50
|
+
return `(\n${sumRate(errorSel, by)}\nor\n${all} * 0\n)\n/\n${all}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
/** `histogram_quantile(q, sum by (le, labels) (rate(bucket[range])))`. */
|
|
43
54
|
export function quantile(q: number, bucketSel: string, by: string[]): string {
|
|
44
55
|
return `histogram_quantile(${num(q)}, ${sumRate(bucketSel, ["le", ...by])})`;
|
|
@@ -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
|
@@ -40,6 +40,13 @@ export interface DashboardProps {
|
|
|
40
40
|
graphTooltip?: "default" | "sharedCrosshair" | "sharedTooltip";
|
|
41
41
|
/** Whether users may edit it in the UI. Defaults to true, as in Grafana. */
|
|
42
42
|
editable?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The dashboard schema version the JSON is written at. Leave it out for
|
|
45
|
+
* the pinned one (`DASHBOARD_SCHEMA_VERSION`). `chant import` sets it for
|
|
46
|
+
* a dashboard saved by an older Grafana, so Grafana still runs its
|
|
47
|
+
* migrations when it loads the rebuilt JSON.
|
|
48
|
+
*/
|
|
49
|
+
schemaVersion?: number;
|
|
43
50
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
51
|
variables?: VariableEntity<any>[];
|
|
45
52
|
/** Panels and rows, top to bottom. A row's own `panels` follow it. */
|
|
@@ -49,7 +56,10 @@ export interface DashboardProps {
|
|
|
49
56
|
/**
|
|
50
57
|
* The Grafana folder to provision it into. Written as a subdirectory of
|
|
51
58
|
* `dashboards/`, which the provider maps to folders
|
|
52
|
-
* (`foldersFromFilesStructure`).
|
|
59
|
+
* (`foldersFromFilesStructure`). `"Platform/Kubernetes"` nests one
|
|
60
|
+
* directory in another, which Grafana 13.1 and later make a Kubernetes
|
|
61
|
+
* folder inside Platform; earlier versions use only the last level
|
|
62
|
+
* (GRAF109 warns). Leave it out for the General folder.
|
|
53
63
|
*/
|
|
54
64
|
folder?: string;
|
|
55
65
|
}
|
|
@@ -84,7 +94,11 @@ export interface DashboardProviderProps {
|
|
|
84
94
|
/** Provider name, unique in the provisioning file. */
|
|
85
95
|
name: string;
|
|
86
96
|
orgId?: number;
|
|
87
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* Put every dashboard this provider loads in this folder. Setting it (or
|
|
99
|
+
* `folderUid`) turns `foldersFromFilesStructure` off by default, so each
|
|
100
|
+
* dashboard's own `folder` is ignored (GRAF109 warns).
|
|
101
|
+
*/
|
|
88
102
|
folder?: string;
|
|
89
103
|
folderUid?: string;
|
|
90
104
|
/** Where the `dashboards/` output is mounted in the Grafana container. Defaults to `/var/lib/grafana/dashboards`. */
|