Compare commits

...

21 Commits

Author SHA1 Message Date
Serge Zaitsev 3e28c884d3 try annotations with unified storage 2026-01-12 15:34:17 +01:00
Yunwen Zheng 0c60d356d1 RecentlyViewedDashboards: Hide entire section when there is no recently view item (#115905)
* RecentlyViewedDashboards: Hide entire section when there is no recently view item
2026-01-07 13:31:48 -05:00
Ezequiel Victorero 41d7213d7e Docs: Update dualwrite ini config (#115934) 2026-01-07 17:58:58 +01:00
Todd Treece efad6c7be0 Chore: Update enterprise imports (#115947) 2026-01-07 16:55:59 +00:00
Paulo Dias e116254f32 Alerting: Update createdBy field when silence is being Recreated (#115543) 2026-01-07 16:05:53 +00:00
Matheus Macabu 2efcc88e62 FeatureToggles: Remove unused kubernetesFeatureToggles (#115933) 2026-01-07 15:53:58 +01:00
Galen Kistler 6fea614106 LogsTable: Inspect button fix (#115912)
* fix: inspect button

* chore: memoize component
2026-01-07 14:31:04 +00:00
antonio c0c05a65fd docs/alerting: add video to tutorial (#115675) 2026-01-07 15:11:41 +01:00
Alexander Akhmetov 41ed2aeb23 Alerting: Display change message next to the rule version when exists (#115664)
* Alerting: Display change message next to the rule version when exists

* Alerting: Update version history tests for message field

Updates test mocks and assertions to include message fields in version
history data. Adds three message examples to the mock handler and updates
test expectations to verify the Notes column displays correctly when
messages are present or absent.

---------

Co-authored-by: Konrad Lalik <konradlalik@gmail.com>
2026-01-07 15:06:41 +01:00
Johnny Kartheiser 9e9233051e alerting docs: saved searches (#115524)
* alerting docs: saved searches

adds paragraph about saved searches functionality

* typo and explainer

details on default search option

* image update
2026-01-07 08:03:38 -06:00
Ricardo Galeno a5faedbe68 Explore: escape character of break-line in Traceql in Search tab fixing an issue when filtering by a multi line span tag value (#114672)
* Explore: escape character of break-line in Traceql in Search tab

* Explore: fix test for escape character of break-line in Traceql in Search tab
2026-01-07 13:31:29 +01:00
Alberto 6fee200327 Pyroscope: Exemplar support for series queries (#113926)
* feat(pyroscope): Exemplar support for series queries

use enum flag, add exemplar flag to explore

disable exemplars on explore as well

tests

feature toggle

fixing tests

* resolve conflicts

* lint
2026-01-07 13:25:42 +01:00
Alexander Zobnin 0b9046be15 Zanzana: Add more openfga settings for fine tuning (#115928)
* Zanzana: Add more openfga settings for fine tuning

* neat

* refactor
2026-01-07 13:07:13 +01:00
Hugo Häggmark 20eeff3e7b Plugins: add missing addedFunctions property in extensions (#115926) 2026-01-07 13:05:38 +01:00
Will Browne ec55871b9b Plugins: Remove pkg/tsdb/* as dependency (#115886)
* remove deps

* singular

* add rest

* make update-workspace

* undo go.mod changes
2026-01-07 11:28:25 +00:00
Pepe Cano 0bfcc55411 docs(alerting): new best practices guide (#115687)
* Split best practices section

* Write Examples and Guides docs

* Move recording rule recommendations

* docs(alerting): new best practices guide

* fix vale errors

* Detail meaning of alert `escalation`

* Include the recovery threshold option

* Include lower severity channels for infrastructure alerts

* Remove timing options

* minor intro edits

* Rename heading to avoid gerunds
2026-01-07 12:05:58 +01:00
Tania 016301c304 OpenFeature: Rename provider from goff to features-service (#115895)
* OpenFeature: Rename provider

* Update other references to goff

* Update pkg/services/featuremgmt/openfeature.go

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Update pkg/services/featuremgmt/openfeature.go

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Update pkg/services/featuremgmt/openfeature.go

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Update pkg/services/featuremgmt/openfeature.go

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

---------

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>
2026-01-07 12:04:30 +01:00
grafana-pr-automation[bot] 5e05289bc8 I18n: Download translations from Crowdin (#115913)
New Crowdin translations by GitHub Action

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-07 10:32:13 +00:00
Marc M. be734e970e CustomVariable: support values with multiple properties (json values format) (#113844)
* update Scenes libraries

---------

Co-authored-by: idastambuk <ida.stambuk@grafana.com>
2026-01-07 11:01:03 +01:00
Ida Štambuk 05681efee3 Dynamic Dashboards: Show hidden variables greyed out (#115723) 2026-01-07 09:48:50 +01:00
Stephanie Hingtgen 844a7332b9 Zanzana: Add orphan cleanup to reconciler (#115775) 2026-01-06 23:19:25 +00:00
175 changed files with 4661 additions and 748 deletions
+9
View File
@@ -0,0 +1,9 @@
include ../sdk.mk
.PHONY: generate # Run Grafana App SDK code generation
generate: install-app-sdk update-app-sdk
@$(APP_SDK_BIN) generate \
--source=./kinds/ \
--gogenpath=./pkg/apis \
--grouping=group \
--defencoding=none
+102
View File
@@ -0,0 +1,102 @@
module github.com/grafana/grafana/apps/anno
go 1.25.5
require (
github.com/grafana/grafana-app-sdk v0.48.7
github.com/grafana/grafana-app-sdk/logging v0.48.7
k8s.io/apimachinery v0.34.3
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-test/deep v1.1.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/onsi/gomega v1.36.2 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.3 // indirect
k8s.io/apiextensions-apiserver v0.34.3 // indirect
k8s.io/client-go v0.34.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
+264
View File
@@ -0,0 +1,264 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I=
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo=
github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k=
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU=
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g=
k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0=
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+23
View File
@@ -0,0 +1,23 @@
package kinds
annov0alpha1: {
kind: "Annotation"
pluralName: "Annotations"
schema: {
spec: {
text: string
time: int64
timeEnd?: int64
dashboardUID?: string
panelID?: int64
tags?: [...string]
}
}
selectableFields: [
"spec.time",
"spec.timeEnd",
"spec.dashboardUID",
"spec.panelID",
]
}
+2
View File
@@ -0,0 +1,2 @@
module: "anno.grafana.app/kinds"
language: version: "v0.8.2"
+35
View File
@@ -0,0 +1,35 @@
package kinds
manifest: {
appName: "anno"
groupOverride: "anno.grafana.app"
versions: {
"v0alpha1": v0alpha1
}
}
v0alpha1: {
kinds: [annov0alpha1]
routes: {
namespaced: {
"/tags": {
"GET": {
response: {
tags: [...{
tag: string
count: number
}]
}
}
}
}
}
codegen: {
ts: {
enabled: true
}
go: {
enabled: true
}
}
}
@@ -0,0 +1,99 @@
package v0alpha1
import (
"context"
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type AnnotationClient struct {
client *resource.TypedClient[*Annotation, *AnnotationList]
}
func NewAnnotationClient(client resource.Client) *AnnotationClient {
return &AnnotationClient{
client: resource.NewTypedClient[*Annotation, *AnnotationList](client, AnnotationKind()),
}
}
func NewAnnotationClientFromGenerator(generator resource.ClientGenerator) (*AnnotationClient, error) {
c, err := generator.ClientFor(AnnotationKind())
if err != nil {
return nil, err
}
return NewAnnotationClient(c), nil
}
func (c *AnnotationClient) Get(ctx context.Context, identifier resource.Identifier) (*Annotation, error) {
return c.client.Get(ctx, identifier)
}
func (c *AnnotationClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*AnnotationList, error) {
return c.client.List(ctx, namespace, opts)
}
func (c *AnnotationClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*AnnotationList, error) {
resp, err := c.client.List(ctx, namespace, resource.ListOptions{
ResourceVersion: opts.ResourceVersion,
Limit: opts.Limit,
LabelFilters: opts.LabelFilters,
FieldSelectors: opts.FieldSelectors,
})
if err != nil {
return nil, err
}
for resp.GetContinue() != "" {
page, err := c.client.List(ctx, namespace, resource.ListOptions{
Continue: resp.GetContinue(),
ResourceVersion: opts.ResourceVersion,
Limit: opts.Limit,
LabelFilters: opts.LabelFilters,
FieldSelectors: opts.FieldSelectors,
})
if err != nil {
return nil, err
}
resp.SetContinue(page.GetContinue())
resp.SetResourceVersion(page.GetResourceVersion())
resp.SetItems(append(resp.GetItems(), page.GetItems()...))
}
return resp, nil
}
func (c *AnnotationClient) Create(ctx context.Context, obj *Annotation, opts resource.CreateOptions) (*Annotation, error) {
// Make sure apiVersion and kind are set
obj.APIVersion = GroupVersion.Identifier()
obj.Kind = AnnotationKind().Kind()
return c.client.Create(ctx, obj, opts)
}
func (c *AnnotationClient) Update(ctx context.Context, obj *Annotation, opts resource.UpdateOptions) (*Annotation, error) {
return c.client.Update(ctx, obj, opts)
}
func (c *AnnotationClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Annotation, error) {
return c.client.Patch(ctx, identifier, req, opts)
}
func (c *AnnotationClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus AnnotationStatus, opts resource.UpdateOptions) (*Annotation, error) {
return c.client.Update(ctx, &Annotation{
TypeMeta: metav1.TypeMeta{
Kind: AnnotationKind().Kind(),
APIVersion: GroupVersion.Identifier(),
},
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: opts.ResourceVersion,
Namespace: identifier.Namespace,
Name: identifier.Name,
},
Status: newStatus,
}, resource.UpdateOptions{
Subresource: "status",
ResourceVersion: opts.ResourceVersion,
})
}
func (c *AnnotationClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
return c.client.Delete(ctx, identifier, opts)
}
+28
View File
@@ -0,0 +1,28 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha1
import (
"encoding/json"
"io"
"github.com/grafana/grafana-app-sdk/resource"
)
// AnnotationJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
type AnnotationJSONCodec struct{}
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
func (*AnnotationJSONCodec) Read(reader io.Reader, into resource.Object) error {
return json.NewDecoder(reader).Decode(into)
}
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
func (*AnnotationJSONCodec) Write(writer io.Writer, from resource.Object) error {
return json.NewEncoder(writer).Encode(from)
}
// Interface compliance checks
var _ resource.Codec = &AnnotationJSONCodec{}
@@ -0,0 +1,31 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
import (
time "time"
)
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
type AnnotationMetadata struct {
UpdateTimestamp time.Time `json:"updateTimestamp"`
CreatedBy string `json:"createdBy"`
Uid string `json:"uid"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Finalizers []string `json:"finalizers"`
ResourceVersion string `json:"resourceVersion"`
Generation int64 `json:"generation"`
UpdatedBy string `json:"updatedBy"`
Labels map[string]string `json:"labels"`
}
// NewAnnotationMetadata creates a new AnnotationMetadata object.
func NewAnnotationMetadata() *AnnotationMetadata {
return &AnnotationMetadata{
Finalizers: []string{},
Labels: map[string]string{},
}
}
+326
View File
@@ -0,0 +1,326 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha1
import (
"fmt"
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"time"
)
// +k8s:openapi-gen=true
type Annotation struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
// Spec is the spec of the Annotation
Spec AnnotationSpec `json:"spec" yaml:"spec"`
Status AnnotationStatus `json:"status" yaml:"status"`
}
func NewAnnotation() *Annotation {
return &Annotation{
Spec: *NewAnnotationSpec(),
Status: *NewAnnotationStatus(),
}
}
func (o *Annotation) GetSpec() any {
return o.Spec
}
func (o *Annotation) SetSpec(spec any) error {
cast, ok := spec.(AnnotationSpec)
if !ok {
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
}
o.Spec = cast
return nil
}
func (o *Annotation) GetSubresources() map[string]any {
return map[string]any{
"status": o.Status,
}
}
func (o *Annotation) GetSubresource(name string) (any, bool) {
switch name {
case "status":
return o.Status, true
default:
return nil, false
}
}
func (o *Annotation) SetSubresource(name string, value any) error {
switch name {
case "status":
cast, ok := value.(AnnotationStatus)
if !ok {
return fmt.Errorf("cannot set status type %#v, not of type AnnotationStatus", value)
}
o.Status = cast
return nil
default:
return fmt.Errorf("subresource '%s' does not exist", name)
}
}
func (o *Annotation) GetStaticMetadata() resource.StaticMetadata {
gvk := o.GroupVersionKind()
return resource.StaticMetadata{
Name: o.ObjectMeta.Name,
Namespace: o.ObjectMeta.Namespace,
Group: gvk.Group,
Version: gvk.Version,
Kind: gvk.Kind,
}
}
func (o *Annotation) SetStaticMetadata(metadata resource.StaticMetadata) {
o.Name = metadata.Name
o.Namespace = metadata.Namespace
o.SetGroupVersionKind(schema.GroupVersionKind{
Group: metadata.Group,
Version: metadata.Version,
Kind: metadata.Kind,
})
}
func (o *Annotation) GetCommonMetadata() resource.CommonMetadata {
dt := o.DeletionTimestamp
var deletionTimestamp *time.Time
if dt != nil {
deletionTimestamp = &dt.Time
}
// Legacy ExtraFields support
extraFields := make(map[string]any)
if o.Annotations != nil {
extraFields["annotations"] = o.Annotations
}
if o.ManagedFields != nil {
extraFields["managedFields"] = o.ManagedFields
}
if o.OwnerReferences != nil {
extraFields["ownerReferences"] = o.OwnerReferences
}
return resource.CommonMetadata{
UID: string(o.UID),
ResourceVersion: o.ResourceVersion,
Generation: o.Generation,
Labels: o.Labels,
CreationTimestamp: o.CreationTimestamp.Time,
DeletionTimestamp: deletionTimestamp,
Finalizers: o.Finalizers,
UpdateTimestamp: o.GetUpdateTimestamp(),
CreatedBy: o.GetCreatedBy(),
UpdatedBy: o.GetUpdatedBy(),
ExtraFields: extraFields,
}
}
func (o *Annotation) SetCommonMetadata(metadata resource.CommonMetadata) {
o.UID = types.UID(metadata.UID)
o.ResourceVersion = metadata.ResourceVersion
o.Generation = metadata.Generation
o.Labels = metadata.Labels
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
if metadata.DeletionTimestamp != nil {
dt := metav1.NewTime(*metadata.DeletionTimestamp)
o.DeletionTimestamp = &dt
} else {
o.DeletionTimestamp = nil
}
o.Finalizers = metadata.Finalizers
if o.Annotations == nil {
o.Annotations = make(map[string]string)
}
if !metadata.UpdateTimestamp.IsZero() {
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
}
if metadata.CreatedBy != "" {
o.SetCreatedBy(metadata.CreatedBy)
}
if metadata.UpdatedBy != "" {
o.SetUpdatedBy(metadata.UpdatedBy)
}
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
if metadata.ExtraFields != nil {
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
if cast, ok := annotations.(map[string]string); ok {
o.Annotations = cast
}
}
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
o.ManagedFields = cast
}
}
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
o.OwnerReferences = cast
}
}
}
}
func (o *Annotation) GetCreatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
}
func (o *Annotation) SetCreatedBy(createdBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
}
func (o *Annotation) GetUpdateTimestamp() time.Time {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
return parsed
}
func (o *Annotation) SetUpdateTimestamp(updateTimestamp time.Time) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
}
func (o *Annotation) GetUpdatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
}
func (o *Annotation) SetUpdatedBy(updatedBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
}
func (o *Annotation) Copy() resource.Object {
return resource.CopyObject(o)
}
func (o *Annotation) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *Annotation) DeepCopy() *Annotation {
cpy := &Annotation{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *Annotation) DeepCopyInto(dst *Annotation) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
o.Spec.DeepCopyInto(&dst.Spec)
o.Status.DeepCopyInto(&dst.Status)
}
// Interface compliance compile-time check
var _ resource.Object = &Annotation{}
// +k8s:openapi-gen=true
type AnnotationList struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ListMeta `json:"metadata" yaml:"metadata"`
Items []Annotation `json:"items" yaml:"items"`
}
func (o *AnnotationList) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *AnnotationList) Copy() resource.ListObject {
cpy := &AnnotationList{
TypeMeta: o.TypeMeta,
Items: make([]Annotation, len(o.Items)),
}
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
for i := 0; i < len(o.Items); i++ {
if item, ok := o.Items[i].Copy().(*Annotation); ok {
cpy.Items[i] = *item
}
}
return cpy
}
func (o *AnnotationList) GetItems() []resource.Object {
items := make([]resource.Object, len(o.Items))
for i := 0; i < len(o.Items); i++ {
items[i] = &o.Items[i]
}
return items
}
func (o *AnnotationList) SetItems(items []resource.Object) {
o.Items = make([]Annotation, len(items))
for i := 0; i < len(items); i++ {
o.Items[i] = *items[i].(*Annotation)
}
}
func (o *AnnotationList) DeepCopy() *AnnotationList {
cpy := &AnnotationList{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *AnnotationList) DeepCopyInto(dst *AnnotationList) {
resource.CopyObjectInto(dst, o)
}
// Interface compliance compile-time check
var _ resource.ListObject = &AnnotationList{}
// Copy methods for all subresource types
// DeepCopy creates a full deep copy of Spec
func (s *AnnotationSpec) DeepCopy() *AnnotationSpec {
cpy := &AnnotationSpec{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Spec into another Spec object
func (s *AnnotationSpec) DeepCopyInto(dst *AnnotationSpec) {
resource.CopyObjectInto(dst, s)
}
// DeepCopy creates a full deep copy of AnnotationStatus
func (s *AnnotationStatus) DeepCopy() *AnnotationStatus {
cpy := &AnnotationStatus{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies AnnotationStatus into another AnnotationStatus object
func (s *AnnotationStatus) DeepCopyInto(dst *AnnotationStatus) {
resource.CopyObjectInto(dst, s)
}
@@ -0,0 +1,90 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha1
import (
"errors"
"fmt"
"github.com/grafana/grafana-app-sdk/resource"
)
// schema is unexported to prevent accidental overwrites
var (
schemaAnnotation = resource.NewSimpleSchema("anno.grafana.app", "v0alpha1", NewAnnotation(), &AnnotationList{}, resource.WithKind("Annotation"),
resource.WithPlural("annotations"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{
FieldSelector: "spec.time",
FieldValueFunc: func(o resource.Object) (string, error) {
cast, ok := o.(*Annotation)
if !ok {
return "", errors.New("provided object must be of type *Annotation")
}
return fmt.Sprintf("%d", cast.Spec.Time), nil
},
},
resource.SelectableField{
FieldSelector: "spec.timeEnd",
FieldValueFunc: func(o resource.Object) (string, error) {
cast, ok := o.(*Annotation)
if !ok {
return "", errors.New("provided object must be of type *Annotation")
}
if cast.Spec.TimeEnd == nil {
return "", nil
}
return fmt.Sprintf("%d", *cast.Spec.TimeEnd), nil
},
},
resource.SelectableField{
FieldSelector: "spec.dashboardUID",
FieldValueFunc: func(o resource.Object) (string, error) {
cast, ok := o.(*Annotation)
if !ok {
return "", errors.New("provided object must be of type *Annotation")
}
if cast.Spec.DashboardUID == nil {
return "", nil
}
return *cast.Spec.DashboardUID, nil
},
},
resource.SelectableField{
FieldSelector: "spec.panelID",
FieldValueFunc: func(o resource.Object) (string, error) {
cast, ok := o.(*Annotation)
if !ok {
return "", errors.New("provided object must be of type *Annotation")
}
if cast.Spec.PanelID == nil {
return "", nil
}
return fmt.Sprintf("%d", *cast.Spec.PanelID), nil
},
},
}))
kindAnnotation = resource.Kind{
Schema: schemaAnnotation,
Codecs: map[resource.KindEncoding]resource.Codec{
resource.KindEncodingJSON: &AnnotationJSONCodec{},
},
}
)
// Kind returns a resource.Kind for this Schema with a JSON codec
func AnnotationKind() resource.Kind {
return kindAnnotation
}
// Schema returns a resource.SimpleSchema representation of Annotation
func AnnotationSchema() *resource.SimpleSchema {
return schemaAnnotation
}
// Interface compliance checks
var _ resource.Schema = kindAnnotation
+18
View File
@@ -0,0 +1,18 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
// +k8s:openapi-gen=true
type AnnotationSpec struct {
Text string `json:"text"`
Time int64 `json:"time"`
TimeEnd *int64 `json:"timeEnd,omitempty"`
DashboardUID *string `json:"dashboardUID,omitempty"`
PanelID *int64 `json:"panelID,omitempty"`
Tags []string `json:"tags,omitempty"`
}
// NewAnnotationSpec creates a new AnnotationSpec object.
func NewAnnotationSpec() *AnnotationSpec {
return &AnnotationSpec{}
}
@@ -0,0 +1,44 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
// +k8s:openapi-gen=true
type AnnotationstatusOperatorState struct {
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State AnnotationStatusOperatorStateState `json:"state"`
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]interface{} `json:"details,omitempty"`
}
// NewAnnotationstatusOperatorState creates a new AnnotationstatusOperatorState object.
func NewAnnotationstatusOperatorState() *AnnotationstatusOperatorState {
return &AnnotationstatusOperatorState{}
}
// +k8s:openapi-gen=true
type AnnotationStatus struct {
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]AnnotationstatusOperatorState `json:"operatorStates,omitempty"`
// additionalFields is reserved for future use
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
}
// NewAnnotationStatus creates a new AnnotationStatus object.
func NewAnnotationStatus() *AnnotationStatus {
return &AnnotationStatus{}
}
// +k8s:openapi-gen=true
type AnnotationStatusOperatorStateState string
const (
AnnotationStatusOperatorStateStateSuccess AnnotationStatusOperatorStateState = "success"
AnnotationStatusOperatorStateStateInProgress AnnotationStatusOperatorStateState = "in_progress"
AnnotationStatusOperatorStateStateFailed AnnotationStatusOperatorStateState = "failed"
)
@@ -0,0 +1,18 @@
package v0alpha1
import "k8s.io/apimachinery/pkg/runtime/schema"
const (
// APIGroup is the API group used by all kinds in this package
APIGroup = "anno.grafana.app"
// APIVersion is the API version used by all kinds in this package
APIVersion = "v0alpha1"
)
var (
// GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package
GroupVersion = schema.GroupVersion{
Group: APIGroup,
Version: APIVersion,
}
)
@@ -0,0 +1,26 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
// +k8s:openapi-gen=true
type GetTagsBody struct {
Tags []V0alpha1GetTagsBodyTags `json:"tags"`
}
// NewGetTagsBody creates a new GetTagsBody object.
func NewGetTagsBody() *GetTagsBody {
return &GetTagsBody{
Tags: []V0alpha1GetTagsBodyTags{},
}
}
// +k8s:openapi-gen=true
type V0alpha1GetTagsBodyTags struct {
Tag string `json:"tag"`
Count float64 `json:"count"`
}
// NewV0alpha1GetTagsBodyTags creates a new V0alpha1GetTagsBodyTags object.
func NewV0alpha1GetTagsBodyTags() *V0alpha1GetTagsBodyTags {
return &V0alpha1GetTagsBodyTags{}
}
@@ -0,0 +1,37 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
import (
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// +k8s:openapi-gen=true
type GetTags struct {
metav1.TypeMeta `json:",inline"`
GetTagsBody `json:",inline"`
}
func NewGetTags() *GetTags {
return &GetTags{}
}
func (t *GetTagsBody) DeepCopyInto(dst *GetTagsBody) {
_ = resource.CopyObjectInto(dst, t)
}
func (o *GetTags) DeepCopyObject() runtime.Object {
dst := NewGetTags()
o.DeepCopyInto(dst)
return dst
}
func (o *GetTags) DeepCopyInto(dst *GetTags) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.GetTagsBody.DeepCopyInto(&dst.GetTagsBody)
}
var _ runtime.Object = NewGetTags()
@@ -0,0 +1,205 @@
//
// This file is generated by grafana-app-sdk
// DO NOT EDIT
//
package manifestdata
import (
"encoding/json"
"fmt"
"strings"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/resource"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
v0alpha1 "github.com/grafana/grafana/apps/anno/pkg/apis/anno/v0alpha1"
)
var (
rawSchemaAnnotationv0alpha1 = []byte(`{"Annotation":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"dashboardUID":{"type":"string"},"panelID":{"type":"integer"},"tags":{"items":{"type":"string"},"type":"array"},"text":{"type":"string"},"time":{"type":"integer"},"timeEnd":{"type":"integer"}},"required":["text","time"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`)
versionSchemaAnnotationv0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaAnnotationv0alpha1, &versionSchemaAnnotationv0alpha1)
)
var appManifestData = app.ManifestData{
AppName: "anno",
Group: "anno.grafana.app",
PreferredVersion: "v0alpha1",
Versions: []app.ManifestVersion{
{
Name: "v0alpha1",
Served: true,
Kinds: []app.ManifestVersionKind{
{
Kind: "Annotation",
Plural: "Annotations",
Scope: "Namespaced",
Conversion: false,
Schema: &versionSchemaAnnotationv0alpha1,
SelectableFields: []string{
"spec.time",
"spec.timeEnd",
"spec.dashboardUID",
"spec.panelID",
},
},
},
Routes: app.ManifestVersionRoutes{
Namespaced: map[string]spec3.PathProps{
"/tags": {
Get: &spec3.Operation{
OperationProps: spec3.OperationProps{
OperationId: "getTags",
Responses: &spec3.Responses{
ResponsesProps: spec3.ResponsesProps{
Default: &spec3.Response{
ResponseProps: spec3.ResponseProps{
Description: "Default OK response",
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"apiVersion": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
},
},
"kind": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
},
},
"tags": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"count": {
SchemaProps: spec.SchemaProps{
Type: []string{"number"},
},
},
"tag": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
},
Required: []string{
"tag",
"count",
},
}},
},
},
},
},
Required: []string{
"tags",
"apiVersion",
"kind",
},
}},
}},
},
},
},
}},
},
},
},
},
Cluster: map[string]spec3.PathProps{},
Schemas: map[string]spec.Schema{},
},
},
},
}
func LocalManifest() app.Manifest {
return app.NewEmbeddedManifest(appManifestData)
}
func RemoteManifest() app.Manifest {
return app.NewAPIServerManifest("anno")
}
var kindVersionToGoType = map[string]resource.Kind{
"Annotation/v0alpha1": v0alpha1.AnnotationKind(),
}
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
// If there is no association for the provided Kind and Version, exists will return false.
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
return goType, exists
}
var customRouteToGoResponseType = map[string]any{
"v0alpha1||<namespace>/tags|GET": v0alpha1.GetTags{},
}
// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists.
// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths.
// If there is no association for the provided kind, version, custom route path, and method, exists will return false.
// Resource routes (those without a kind) should prefix their route with "<namespace>/" if the route is namespaced (otherwise the route is assumed to be cluster-scope)
func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
var customRouteToGoParamsType = map[string]runtime.Object{}
func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
var customRouteToGoRequestBodyType = map[string]any{}
func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
type GoTypeAssociator struct{}
func NewGoTypeAssociator() *GoTypeAssociator {
return &GoTypeAssociator{}
}
func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) {
return ManifestGoTypeAssociator(kind, version)
}
func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) {
return ManifestCustomRouteResponsesAssociator(kind, version, path, verb)
}
func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) {
return ManifestCustomRouteQueryAssociator(kind, version, path, verb)
}
func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) {
return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb)
}
+54
View File
@@ -0,0 +1,54 @@
package app
import (
"context"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana-app-sdk/operator"
"github.com/grafana/grafana-app-sdk/resource"
"github.com/grafana/grafana-app-sdk/simple"
annov0alpha1 "github.com/grafana/grafana/apps/anno/pkg/apis/anno/v0alpha1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func New(cfg app.Config) (app.App, error) {
simpleConfig := simple.AppConfig{
Name: "anno",
KubeConfig: cfg.KubeConfig,
InformerConfig: simple.AppInformerConfig{
InformerOptions: operator.InformerOptions{
ErrorHandler: func(ctx context.Context, err error) {
logging.FromContext(ctx).Error("Informer processing error", "error", err)
},
},
},
ManagedKinds: []simple.AppManagedKind{{
Kind: annov0alpha1.AnnotationKind(),
},
},
}
a, err := simple.NewApp(simpleConfig)
if err != nil {
return nil, err
}
err = a.ValidateManifest(cfg.ManifestData)
if err != nil {
return nil, err
}
return a, nil
}
func GetKinds() map[schema.GroupVersion][]resource.Kind {
gv := schema.GroupVersion{
Group: annov0alpha1.AnnotationKind().Group(),
Version: annov0alpha1.AnnotationKind().Version(),
}
return map[schema.GroupVersion][]resource.Kind{
gv: {annov0alpha1.AnnotationKind()},
}
}
@@ -0,0 +1,49 @@
/*
* This file was generated by grafana-app-sdk. DO NOT EDIT.
*/
import { Spec } from './types.spec.gen';
import { Status } from './types.status.gen';
export interface Metadata {
name: string;
namespace: string;
generateName?: string;
selfLink?: string;
uid?: string;
resourceVersion?: string;
generation?: number;
creationTimestamp?: string;
deletionTimestamp?: string;
deletionGracePeriodSeconds?: number;
labels?: Record<string, string>;
annotations?: Record<string, string>;
ownerReferences?: OwnerReference[];
finalizers?: string[];
managedFields?: ManagedFieldsEntry[];
}
export interface OwnerReference {
apiVersion: string;
kind: string;
name: string;
uid: string;
controller?: boolean;
blockOwnerDeletion?: boolean;
}
export interface ManagedFieldsEntry {
manager?: string;
operation?: string;
apiVersion?: string;
time?: string;
fieldsType?: string;
subresource?: string;
}
export interface Annotation {
kind: string;
apiVersion: string;
metadata: Metadata;
spec: Spec;
status: Status;
}
@@ -0,0 +1,30 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
export interface Metadata {
updateTimestamp: string;
createdBy: string;
uid: string;
creationTimestamp: string;
deletionTimestamp?: string;
finalizers: string[];
resourceVersion: string;
generation: number;
updatedBy: string;
labels: Record<string, string>;
}
export const defaultMetadata = (): Metadata => ({
updateTimestamp: "",
createdBy: "",
uid: "",
creationTimestamp: "",
finalizers: [],
resourceVersion: "",
generation: 0,
updatedBy: "",
labels: {},
});
@@ -0,0 +1,16 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
export interface Spec {
text: string;
time: number;
timeEnd?: number;
dashboardUID?: string;
panelID?: number;
tags?: string[];
}
export const defaultSpec = (): Spec => ({
text: "",
time: 0,
});
@@ -0,0 +1,30 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
export interface OperatorState {
// lastEvaluation is the ResourceVersion last evaluated
lastEvaluation: string;
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
state: "success" | "in_progress" | "failed";
// descriptiveState is an optional more descriptive state field which has no requirements on format
descriptiveState?: string;
// details contains any extra information that is operator-specific
details?: Record<string, any>;
}
export const defaultOperatorState = (): OperatorState => ({
lastEvaluation: "",
state: "success",
});
export interface Status {
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
operatorStates?: Record<string, OperatorState>;
// additionalFields is reserved for future use
additionalFields?: Record<string, any>;
}
export const defaultStatus = (): Status => ({
});
@@ -129,7 +129,7 @@ DashboardLink: {
placement?: DashboardLinkPlacement
}
// Dashboard Link placement. Defines where the link should be displayed.
// Dashboard Link placement. Defines where the link should be displayed.
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
DashboardLinkPlacement: "inControlsMenu"
@@ -932,6 +932,7 @@ CustomVariableSpec: {
skipUrlSync: bool | *false
description?: string
allowCustomValue: bool | *true
valuesFormat?: "csv" | "json"
}
// Custom variable kind
@@ -935,6 +935,7 @@ CustomVariableSpec: {
skipUrlSync: bool | *false
description?: string
allowCustomValue: bool | *true
valuesFormat?: "csv" | "json"
}
// Custom variable kind
@@ -222,8 +222,10 @@ lineage: schemas: [{
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
regex?: string
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Optional, indicates whether a custom type variable uses CSV or JSON to define its values
valuesFormat?: "csv" | "json" | *"csv"
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Additional static options for query variable
staticOptions?: [...#VariableOption]
// Ordering of static options in relation to options returned from data source for query variable
@@ -222,8 +222,10 @@ lineage: schemas: [{
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
regex?: string
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Optional, indicates whether a custom type variable uses CSV or JSON to define its values
valuesFormat?: "csv" | "json" | *"csv"
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Additional static options for query variable
staticOptions?: [...#VariableOption]
// Ordering of static options in relation to options returned from data source for query variable
@@ -133,7 +133,7 @@ DashboardLink: {
placement?: DashboardLinkPlacement
}
// Dashboard Link placement. Defines where the link should be displayed.
// Dashboard Link placement. Defines where the link should be displayed.
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
DashboardLinkPlacement: "inControlsMenu"
@@ -936,6 +936,7 @@ CustomVariableSpec: {
skipUrlSync: bool | *false
description?: string
allowCustomValue: bool | *true
valuesFormat?: "csv" | "json"
}
// Custom variable kind
@@ -1703,18 +1703,19 @@ func NewDashboardCustomVariableKind() *DashboardCustomVariableKind {
// Custom variable specification
// +k8s:openapi-gen=true
type DashboardCustomVariableSpec struct {
Name string `json:"name"`
Query string `json:"query"`
Current DashboardVariableOption `json:"current"`
Options []DashboardVariableOption `json:"options"`
Multi bool `json:"multi"`
IncludeAll bool `json:"includeAll"`
AllValue *string `json:"allValue,omitempty"`
Label *string `json:"label,omitempty"`
Hide DashboardVariableHide `json:"hide"`
SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"`
AllowCustomValue bool `json:"allowCustomValue"`
Name string `json:"name"`
Query string `json:"query"`
Current DashboardVariableOption `json:"current"`
Options []DashboardVariableOption `json:"options"`
Multi bool `json:"multi"`
IncludeAll bool `json:"includeAll"`
AllValue *string `json:"allValue,omitempty"`
Label *string `json:"label,omitempty"`
Hide DashboardVariableHide `json:"hide"`
SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"`
AllowCustomValue bool `json:"allowCustomValue"`
ValuesFormat *DashboardCustomVariableSpecValuesFormat `json:"valuesFormat,omitempty"`
}
// NewDashboardCustomVariableSpec creates a new DashboardCustomVariableSpec object.
@@ -2098,6 +2099,14 @@ const (
DashboardQueryVariableSpecStaticOptionsOrderSorted DashboardQueryVariableSpecStaticOptionsOrder = "sorted"
)
// +k8s:openapi-gen=true
type DashboardCustomVariableSpecValuesFormat string
const (
DashboardCustomVariableSpecValuesFormatCsv DashboardCustomVariableSpecValuesFormat = "csv"
DashboardCustomVariableSpecValuesFormatJson DashboardCustomVariableSpecValuesFormat = "json"
)
// +k8s:openapi-gen=true
type DashboardPanelKindOrLibraryPanelKind struct {
PanelKind *DashboardPanelKind `json:"PanelKind,omitempty"`
@@ -1548,6 +1548,12 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableSpec(ref common.R
Format: "",
},
},
"valuesFormat": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"name", "query", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"},
},
@@ -939,6 +939,7 @@ CustomVariableSpec: {
skipUrlSync: bool | *false
description?: string
allowCustomValue: bool | *true
valuesFormat?: "csv" | "json"
}
// Custom variable kind
@@ -1707,18 +1707,19 @@ func NewDashboardCustomVariableKind() *DashboardCustomVariableKind {
// Custom variable specification
// +k8s:openapi-gen=true
type DashboardCustomVariableSpec struct {
Name string `json:"name"`
Query string `json:"query"`
Current DashboardVariableOption `json:"current"`
Options []DashboardVariableOption `json:"options"`
Multi bool `json:"multi"`
IncludeAll bool `json:"includeAll"`
AllValue *string `json:"allValue,omitempty"`
Label *string `json:"label,omitempty"`
Hide DashboardVariableHide `json:"hide"`
SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"`
AllowCustomValue bool `json:"allowCustomValue"`
Name string `json:"name"`
Query string `json:"query"`
Current DashboardVariableOption `json:"current"`
Options []DashboardVariableOption `json:"options"`
Multi bool `json:"multi"`
IncludeAll bool `json:"includeAll"`
AllValue *string `json:"allValue,omitempty"`
Label *string `json:"label,omitempty"`
Hide DashboardVariableHide `json:"hide"`
SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"`
AllowCustomValue bool `json:"allowCustomValue"`
ValuesFormat *DashboardCustomVariableSpecValuesFormat `json:"valuesFormat,omitempty"`
}
// NewDashboardCustomVariableSpec creates a new DashboardCustomVariableSpec object.
@@ -2133,6 +2134,14 @@ const (
DashboardQueryVariableSpecStaticOptionsOrderSorted DashboardQueryVariableSpecStaticOptionsOrder = "sorted"
)
// +k8s:openapi-gen=true
type DashboardCustomVariableSpecValuesFormat string
const (
DashboardCustomVariableSpecValuesFormatCsv DashboardCustomVariableSpecValuesFormat = "csv"
DashboardCustomVariableSpecValuesFormatJson DashboardCustomVariableSpecValuesFormat = "json"
)
// +k8s:openapi-gen=true
type DashboardPanelKindOrLibraryPanelKind struct {
PanelKind *DashboardPanelKind `json:"PanelKind,omitempty"`
@@ -1560,6 +1560,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableSpec(ref common.Re
Format: "",
},
},
"valuesFormat": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"name", "query", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"},
},
File diff suppressed because one or more lines are too long
@@ -1336,6 +1336,17 @@ func buildCustomVariable(varMap map[string]interface{}, commonProps CommonVariab
customVar.Spec.AllValue = &allValue
}
if valuesFormat := schemaversion.GetStringValue(varMap, "valuesFormat"); valuesFormat != "" {
switch valuesFormat {
case string(dashv2alpha1.DashboardCustomVariableSpecValuesFormatJson):
format := dashv2alpha1.DashboardCustomVariableSpecValuesFormatJson
customVar.Spec.ValuesFormat = &format
case string(dashv2alpha1.DashboardCustomVariableSpecValuesFormatCsv):
format := dashv2alpha1.DashboardCustomVariableSpecValuesFormatCsv
customVar.Spec.ValuesFormat = &format
}
}
return dashv2alpha1.DashboardVariableKind{
CustomVariableKind: customVar,
}, nil
@@ -685,6 +685,7 @@ func convertVariable_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableKind,
SkipUrlSync: in.CustomVariableKind.Spec.SkipUrlSync,
Description: in.CustomVariableKind.Spec.Description,
AllowCustomValue: in.CustomVariableKind.Spec.AllowCustomValue,
ValuesFormat: convertCustomValuesFormat_V2alpha1_to_V2beta1(in.CustomVariableKind.Spec.ValuesFormat),
},
}
}
@@ -758,6 +759,23 @@ func convertVariable_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableKind,
return nil
}
func convertCustomValuesFormat_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardCustomVariableSpecValuesFormat) *dashv2beta1.DashboardCustomVariableSpecValuesFormat {
if in == nil {
return nil
}
switch *in {
case dashv2alpha1.DashboardCustomVariableSpecValuesFormatJson:
v := dashv2beta1.DashboardCustomVariableSpecValuesFormatJson
return &v
case dashv2alpha1.DashboardCustomVariableSpecValuesFormatCsv:
v := dashv2beta1.DashboardCustomVariableSpecValuesFormatCsv
return &v
default:
return nil
}
}
func convertQueryVariableSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardQueryVariableSpec, out *dashv2beta1.DashboardQueryVariableSpec, scope conversion.Scope) error {
out.Name = in.Name
out.Current = convertVariableOption_V2alpha1_to_V2beta1(in.Current)
+6 -4
View File
@@ -82,8 +82,8 @@ cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2zn
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 h1:Zh+Ur3OsoWpvALHPLT45nOekHkgOt+IOfutBbPqM17I=
cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8=
cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0=
@@ -749,6 +749,8 @@ github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/gnostic v0.7.1 h1:t5Kc7j/8kYr8t2u11rykRrPPovlEMG4+xdc/SpekATs=
github.com/google/gnostic v0.7.1/go.mod h1:KSw6sxnxEBFM8jLPfJd46xZP+yQcfE8XkiqfZx5zR28=
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -887,8 +889,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae h1:35W3Wjp9KWnSoV/DuymmyIj5aHE0CYlDQ5m2KeXUPAc=
github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae/go.mod h1:6CJ1uXmLZ13ufpO9xE4pST+DyaBt0uszzrV0YnoaVLQ=
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f h1:fTlIj5n4x5dU63XHItug7GLjtnaeJdPqBlqg4zlABq0=
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f/go.mod h1:VBNcIhunCZsJ3/mcYx+j7uFf0P/108eiWa+8+Z9ll3o=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/grafana/sqlds/v5 v5.0.3 h1:+yUMUxfa0WANQsmS9xtTFSRX1Q55Iv1B9EjlrW4VlBU=
+7
View File
@@ -217,6 +217,13 @@ metaV0Alpha1: {
title: string
description?: string
}]
// +listType=atomic
addedFunctions?: [...{
// +listType=set
targets: [...string]
title: string
description?: string
}]
// +listType=set
// +listMapKey=id
exposedComponents?: [...{
+17
View File
@@ -193,6 +193,8 @@ type MetaExtensions struct {
AddedComponents []MetaV0alpha1ExtensionsAddedComponents `json:"addedComponents,omitempty"`
// +listType=atomic
AddedLinks []MetaV0alpha1ExtensionsAddedLinks `json:"addedLinks,omitempty"`
// +listType=atomic
AddedFunctions []MetaV0alpha1ExtensionsAddedFunctions `json:"addedFunctions,omitempty"`
// +listType=set
// +listMapKey=id
ExposedComponents []MetaV0alpha1ExtensionsExposedComponents `json:"exposedComponents,omitempty"`
@@ -396,6 +398,21 @@ func NewMetaV0alpha1ExtensionsAddedLinks() *MetaV0alpha1ExtensionsAddedLinks {
}
}
// +k8s:openapi-gen=true
type MetaV0alpha1ExtensionsAddedFunctions struct {
// +listType=set
Targets []string `json:"targets"`
Title string `json:"title"`
Description *string `json:"description,omitempty"`
}
// NewMetaV0alpha1ExtensionsAddedFunctions creates a new MetaV0alpha1ExtensionsAddedFunctions object.
func NewMetaV0alpha1ExtensionsAddedFunctions() *MetaV0alpha1ExtensionsAddedFunctions {
return &MetaV0alpha1ExtensionsAddedFunctions{
Targets: []string{},
}
}
// +k8s:openapi-gen=true
type MetaV0alpha1ExtensionsExposedComponents struct {
Id string `json:"id"`
File diff suppressed because one or more lines are too long
+16 -1
View File
@@ -367,7 +367,8 @@ func jsonDataToMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.MetaJSOND
// Map Extensions
if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 ||
len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 {
len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 ||
len(jsonData.Extensions.AddedFunctions) > 0 {
extensions := &pluginsv0alpha1.MetaExtensions{}
if len(jsonData.Extensions.AddedLinks) > 0 {
@@ -398,6 +399,20 @@ func jsonDataToMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.MetaJSOND
}
}
if len(jsonData.Extensions.AddedFunctions) > 0 {
extensions.AddedFunctions = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsAddedFunctions, 0, len(jsonData.Extensions.AddedFunctions))
for _, comp := range jsonData.Extensions.AddedFunctions {
v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedFunctions{
Targets: comp.Targets,
Title: comp.Title,
}
if comp.Description != "" {
v0Comp.Description = &comp.Description
}
extensions.AddedFunctions = append(extensions.AddedFunctions, v0Comp)
}
}
if len(jsonData.Extensions.ExposedComponents) > 0 {
extensions.ExposedComponents = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents))
for _, comp := range jsonData.Extensions.ExposedComponents {
@@ -48,6 +48,14 @@ Recording rules can be helpful in various scenarios, such as:
The evaluation group of the recording rule determines how often the metric is pre-computed.
## Recommendations
- **Use frequent evaluation intervals**. Set frequent evaluation intervals for recording rules. Long intervals, such as an hour, can cause the recorded metric to be stale and lead to misaligned alert rule evaluations, especially when combined with a long pending period.
- **Align alert evaluation with recording frequency**. The evaluation interval of an alert rule that depends on a recorded metric should be aligned with the recording rule's interval. If a recording rule runs every 3 minutes, the alert rule should also be evaluated at a similar frequency to ensure it acts on fresh data.
- **Use `_over_time` functions for instant queries**. Since all alert rules are ultimately executed as an instant query, you can use functions like `max_over_time(my_metric[5m])` as an instant query. This allows you to get an aggregated value over a period without using a range query and a reduce expression.
## Types of recording rules
Similar to alert rules, Grafana supports two types of recording rules:
1. [Grafana-managed recording rules](ref:grafana-managed-recording-rules), which can query any Grafana data source supported by alerting. It's the recommended option.
@@ -1,57 +0,0 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/best-practices/
description: This section provides a set of guides for useful alerting practices and recommendations
keywords:
- grafana
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Best practices
title: Grafana Alerting best practices
weight: 170
---
# Grafana Alerting best practices
This section provides a set of guides and examples of best practices for Grafana Alerting. Here you can learn more about how to handle common alert management problems and you can see examples of more advanced usage of Grafana Alerting.
{{< section >}}
Designing and configuring an alert management set up that works takes time. Here are some additional tips on how to create an effective alert management set up:
{{< shared id="alert-planning-fundamentals" >}}
**Which are the key metrics for your business that you want to monitor and alert on?**
- Find events that are important to know about and not so trivial or frequent that recipients ignore them.
- Alerts should only be created for big events that require immediate attention or intervention.
- Consider quality over quantity.
**How do you want to organize your alerts and notifications?**
- Be selective about who you set to receive alerts. Consider sending them to the right teams, whoever is on call, and the specific channels.
- Think carefully about priority and severity levels.
- Automate as far as possible provisioning Alerting resources with the API or Terraform.
**Which information should you include in notifications?**
- Consider who the alert receivers and responders are.
- Share information that helps responders identify and address potential issues.
- Link alerts to dashboards to guide responders on which data to investigate.
**How can you reduce alert fatigue?**
- Avoid noisy, unnecessary alerts by using silences, mute timings, or pausing alert rule evaluation.
- Continually tune your alert rules to review effectiveness. Remove alert rules to avoid duplication or ineffective alerts.
- Continually review your thresholds and evaluation rules.
**How should you configure recording rules?**
- Use frequent evaluation intervals. It is recommended to set a frequent evaluation interval for recording rules. Long intervals, such as an hour, can cause the recorded metric to be stale and lead to misaligned alert rule evaluations, especially when combined with a long pending period.
- Understand query types. Grafana Alerting uses both **Instant** and **Range** queries. Instant queries fetch a single data point, while Range queries fetch a series of data points over time. When using a Range query in an alert condition, you must use a Reduce expression to aggregate the series into a single value.
- Align alert evaluation with recording frequency. The evaluation interval of an alert rule that depends on a recorded metric should be aligned with the recording rule's interval. If a recording rule runs every 3 minutes, the alert rule should also be evaluated at a similar frequency to ensure it acts on fresh data.
- Use `_over_time` functions for instant queries. Since all alert rules are ultimately executed as an instant query, you can use functions like `max_over_time(my_metric[1h])` as an instant query. This allows you to get an aggregated value over a period without using a range query and a reduce expression.
{{< /shared >}}
+22
View File
@@ -0,0 +1,22 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/examples/
description: This section provides a set of guides for useful alerting practices and recommendations
keywords:
- grafana
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Examples
title: Examples
weight: 180
---
# Examples
This section provides practical examples that show how to work with different types of alerting data, apply alert design patterns, reuse alert logic, and take advantage of specific Grafana Alerting features.
This section includes:
{{< section >}}
@@ -1,5 +1,7 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/best-practices/dynamic-labels
aliases:
- ../best-practices/dynamic-labels/ # /docs/grafana/<GRAFANA_VERSION>/alerting/best-practices/dynamic-labels/
canonical: https://grafana.com/docs/grafana/latest/alerting/examples/dynamic-labels
description: This example shows how to define dynamic labels based on query values, along with important behavior to keep in mind when using them.
keywords:
- grafana
@@ -10,7 +12,7 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Examples of dynamic labels
menuTitle: Dynamic labels
title: Example of dynamic labels in alert instances
weight: 1104
refs:
@@ -1,5 +1,7 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/best-practices/dynamic-thresholds
aliases:
- ../best-practices/dynamic-thresholds/ # /docs/grafana/<GRAFANA_VERSION>/alerting/best-practices/dynamic-thresholds/
canonical: https://grafana.com/docs/grafana/latest/alerting/examples/dynamic-thresholds
description: This example shows how to use a distinct threshold value per dimension using multi-dimensional alerts and a Math expression.
keywords:
- grafana
@@ -10,7 +12,7 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Examples of dynamic thresholds
menuTitle: Dynamic thresholds
title: Example of dynamic thresholds per dimension
weight: 1105
refs:
@@ -1,5 +1,7 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/best-practices/high-cardinality-alerts/
aliases:
- ../best-practices/high-cardinality-alerts/ # /docs/grafana/<GRAFANA_VERSION>/alerting/best-practices/high-cardinality-alerts/
canonical: https://grafana.com/docs/grafana/latest/alerting/examples/high-cardinality-alerts/
description: Learn how to detect and alert on high-cardinality metrics that can overload your metrics backend and increase observability costs.
keywords:
- grafana
@@ -8,7 +10,7 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Examples of high-cardinality alerts
menuTitle: High-cardinality alerts
title: Examples of high-cardinality alerts
weight: 1105
refs:
@@ -1,5 +1,7 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/best-practices/multi-dimensional-alerts/
aliases:
- ../best-practices/multi-dimensional-alerts/ # /docs/grafana/<GRAFANA_VERSION>/alerting/best-practices/multi-dimensional-alerts/
canonical: https://grafana.com/docs/grafana/latest/alerting/examples/multi-dimensional-alerts/
description: This example shows how a single alert rule can generate multiple alert instances using time series data.
keywords:
- grafana
@@ -8,7 +10,7 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Examples of multi-dimensional alerts
menuTitle: Multi-dimensional alerts
title: Example of multi-dimensional alerts on time series data
weight: 1101
refs:
@@ -1,5 +1,7 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/best-practices/table-data
aliases:
- ../best-practices/table-data/ # /docs/grafana/<GRAFANA_VERSION>/alerting/best-practices/table-data/
canonical: https://grafana.com/docs/grafana/latest/alerting/examples/table-data
description: This example shows how to create an alert rule using table data.
keywords:
- grafana
@@ -8,7 +10,7 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Examples of table data
menuTitle: Table data
title: Example of alerting on tabular data
weight: 1102
refs:
@@ -1,5 +1,7 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/best-practices/trace-based-alerts/
aliases:
- ../best-practices/trace-based-alerts/ # /docs/grafana/<GRAFANA_VERSION>/alerting/best-practices/trace-based-alerts/
canonical: https://grafana.com/docs/grafana/latest/alerting/examples/trace-based-alerts/
description: This guide provides introductory examples and distinct approaches for setting up trace-based alerts in Grafana.
keywords:
- grafana
@@ -8,7 +10,7 @@ labels:
- cloud
- enterprise
- oss
title: Examples of trace-based alerts
title: Trace-based alerts
weight: 1103
refs:
testdata-data-source:
@@ -1,5 +1,7 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/best-practices/tutorials/
aliases:
- ../best-practices/tutorials/ # /docs/grafana/<GRAFANA_VERSION>/alerting/best-practices/tutorials/
canonical: https://grafana.com/docs/grafana/latest/alerting/examples/tutorials/
description: This section provides a set of step-by-step tutorials guides to get started with Grafana Aletings.
keywords:
- grafana
+35
View File
@@ -0,0 +1,35 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/guides/
description: This section provides a set of guides for useful alerting practices and recommendations
keywords:
- grafana
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Guides
title: Guides
weight: 170
refs:
examples:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/examples/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/examples/
tutorials:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/examples/tutorials/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/examples/tutorials/
---
# Guides
Guides in the Grafana Alerting documentation provide best practices and practical recommendations to help you move from a basic alerting setup to real-world use cases.
These guides cover topics such as:
{{< section >}}
For more hands-on examples, refer to [Examples](ref:examples) and [Tutorials](ref:tutorials).
@@ -0,0 +1,201 @@
---
aliases:
- ../best-practices/ # /docs/grafana/<GRAFANA_VERSION>/alerting/best-practices/
canonical: https://grafana.com/docs/grafana/latest/alerting/guides/best-practices/
description: Designing and configuring an effective alerting system takes time. This guide focuses on building alerting systems that scale with real-world operations.
keywords:
- grafana
- alerting
- guide
labels:
products:
- cloud
- enterprise
- oss
menuTitle: Best practices
title: Best practices
weight: 1010
refs:
recovery-threshold:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/queries-conditions/#recovery-threshold
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/fundamentals/alert-rules/queries-conditions/#recovery-threshold
keep-firing-for:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rule-evaluation/#keep-firing-for
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/fundamentals/alert-rule-evaluation/#keep-firing-for
pending-period:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rule-evaluation/#pending-period
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/fundamentals/alert-rule-evaluation/#pending-period
silences:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/configure-notifications/create-silence/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/create-silence/
timing-options:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/notifications/group-alert-notifications/#timing-options
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notifications/group-alert-notifications/#timing-options
group-alert-notifications:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/notifications/group-alert-notifications/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notifications/group-alert-notifications/
notification-policies:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/notifications/notification-policies/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notifications/notification-policies/
annotations:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/annotation-label/#annotations
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/annotation-label/#annotations
multi-dimensional-alerts:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/examples/multi-dimensional-alerts/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/alerting-and-irm/alerting/examples/multi-dimensional-alerts/
---
# Alerting best practices
Designing and configuring an effective alerting system takes time. This guide focuses on building alerting systems that scale with real-world operations.
The practices described here are intentionally high-level and apply regardless of tooling. Whether you use Prometheus, Grafana Alerting, or another stack, the same constraints apply: complex systems, imperfect signals, and humans on call.
Alerting is never finished. It evolves with incidents, organizational changes, and the systems its meant to protect.
{{< shared id="alert-planning-fundamentals" >}}
## Prioritize symptoms, but dont ignore infrastructure signals
Alerts should primarily detect user-facing failures, not internal component behavior. Users don't care that a pod restarted; they care when the application is slow or failing. Symptom-based alerts tie directly to user impact.
Reliability metrics that impact users—latency, errors, availability—are better paging signals than infrastructure events or internal errors.
That said, infrastructure signals still matter. They can act as early warning indicators and are often useful when alerting maturity is low. A sustained spike in CPU or memory usage might not justify a page, but it can help explain or anticipate symptom-based failures.
Infrastructure alerts tend to be noisy and are often ignored when treated like paging signals. They are usually better suited for lower-severity channels such as dashboards, alert lists, or non-paging destinations like a dedicated Slack channel, where they can be monitored without interrupting on-call.
The key is balance as your alerting matures. Use infrastructure alerts to support diagnosis and prevention, not as a replacement for symptom-based alerts.
## Escalate priority based on confidence
Alert priority is often tied to user impact and the urgency to respond, but confidence should determine when escalation is necessary.
In this context, escalation defines how responders are notified as confidence grows. This can include increasing alert priority, widening notification, paging additional responders, or opening an incident once intervention is clearly required.
Early signals are often ambiguous, and confidence in a non-transient failure is usually low. Paging too early creates noise; paging too late means users are impacted for longer before anyone acts. A small or sudden increase in latency may not justify immediate action, but it can indicate a failure in progress.
Confidence increases as signals become stronger or begin to correlate.
Escalation is justified when issues are sustained or reinforced by multiple signals. For example, high latency combined with a rising error rate, or the same event firing over a sustained period. These patterns reduce the chance of transient noise and increase the likelihood of real impact.
Use confidence in user impact to drive escalation and avoid unnecessary pages.
## Scope alerts for scalability and actionability
In distributed systems, avoid creating separate alert rules for every host, service, or endpoint. Instead, define alert rules that scale automatically using [multi-dimensional alert rules](ref:multi-dimensional-alerts). This reduces rule duplication and allows alerting to scale as the system grows.
Start simple. Default to a single dimension such as `service` or `endpoint` to keep alerts manageable. Add dimensions only when they improve actionability. For example, when missing a dimension like `region` hides failures or doesn't provide enough information to act quickly.
Additional dimensions like `region` or `instance` can help identify the root cause, but more isn't always better.
## Design alerts for first responders and clear actions
Alerts should be designed for the first responder, not the person who created the alert. Anyone on call should be able to understand what's wrong and what to do next without deep knowledge of the system or alert configuration.
Avoid vague alerts that force responders to spend time figuring out context. Every alert should clearly explain why it exists, what triggered it, and how to investigate. Use [annotations](ref:annotations) to link to relevant dashboards and runbooks, which are essential for faster resolution.
Alerts should indicate a real problem and be actionable, even if the impact is low. Informational alerts add noise without improving reliability.
If no action is possible, it shouldn't be an alert—consider using a dashboard instead. Over time, alerts behave like technical debt: easy to create, costly to maintain, and hard to remove.
Review alerts often and remove those that dont lead to action.
## Alerts should have an owner and system scope
Alerts without ownership are often ignored. Every alert must have an owner: a team responsible for maintaining the alert and responding when it fires.
Alerts must also define a system scope, such as a service or infrastructure component. Scope provides organizational context and connects alerts with ownership. Defining clear scopes is easier when services are treated as first-class entities, and organizations are built around service ownership.
> [Service Center in Grafana Cloud](/docs/grafana-cloud/alerting-and-irm/service-center/) can help operate a service-oriented view of your system and align alert scope with ownership.
After scope, ownership, and alert priority are defined, routing determines where alerts go and how they escalate. **Notification routing is as important as the alerts**.
Alerts should be delivered to the right team and channel based on priority, ownership, and team workflows. Use [notification policies](ref:notification-policies) to define a routing tree that matches the context of your service or scope:
- Define a parent policy for default routing within the scope.
- Define nested policies for specific cases or higher-priority issues.
## Prevent notification overload with alert grouping
Without alert grouping, responders can receive many notifications for the same underlying problem.
For example, a database failure can trigger several alerts at the same time like increased latency, higher error rates, and internal errors. Paging separately for each symptom quickly turns into notification spam, even though there is a single root cause.
[Notification grouping](ref:group-alert-notifications) consolidates related alerts into a single notification. Instead of receiving multiple pages for the same issue, responders get one alert that represents the incident and includes all related firing alerts.
Grouping should follow operational boundaries such as service or owner, as defined by notification policies. Downstream or cascading failures should be grouped together so they surface as one issue rather than many.
## Mitigate flapping alerts
Short-lived failure spikes often trigger alerts that auto-resolve quickly. Alerting on transient failures creates noise and leads responders to ignore them.
Require issues to persist before alerting. Set a [pending period](ref:pending-period) to define how long a condition must remain true before firing. For example, instead of alerting immediately on high error rate, require it to stay above the threshold for some minutes.
Also, stabilize alerts by tuning query ranges and aggregations. Using raw data makes alerts sensitive to noise. Instead, evaluate over a time window and aggregate the data to smooth short spikes.
```promql
# Reacts to transient spikes. Avoid this.
cpu_usage > 90
# Smooth fluctuations.
avg_over_time(cpu_usage[5m]) > 90
```
For latency and error-based alerts, percentiles are often more useful than averages:
```promql
quantile_over_time(0.95, http_duration_seconds[5m]) > 3
```
Finally, avoid rapid resolve-and-fire notifications by using [`keep_firing_for`](ref:keep-firing-for) or [recovery thresholds](ref:recovery-threshold) to keep alerts active briefly during recovery. Both options reduce flapping and unnecessary notifications.
## Graduate symptom-based alerts into SLOs
When a symptom-based alert fires frequently, it usually indicates a reliability concern that should be measured and managed more deliberately. This is often a sign that the alert could evolve into an [SLO](/docs/grafana-cloud/alerting-and-irm/slo/).
Traditional alerts create pressure to react immediately, while error budgets introduce a buffer of time to act, changing how urgency is handled. Alerts can then be defined in terms of error budget burn rate rather than reacting to every minor deviation.
SLOs also align distinct teams around common reliability goals by providing a shared definition of what "good" looks like. They help consolidate multiple symptom alerts into a single user-facing objective.
For example, instead of several teams alerting on high latency, a single SLO can be used across teams to capture overall API performance.
## Integrate alerting into incident post-mortems
Every incident is an opportunity to improve alerting. After each incident, evaluate whether alerts helped responders act quickly or added unnecessary noise.
Assess which alerts fired, and how they influenced incident response. Review whether alerts triggered too late, too early, or without enough context, and adjust thresholds, priority, or escalation based on what actually happened.
Use [silences](ref:silences) during active incidents to reduce repeated notifications, but scope them carefully to avoid silencing unrelated alerts.
Post-mortems should evaluate alerts with root causes and lessons learned. If responders lacked key information during the incident, enrich alerts with additional context, dashboards, or better guidance.
## Alerts should be continuously improved
Alerting is an iterative process. Alerts that arent reviewed and refined lose effectiveness as systems and traffic patterns change.
Schedule regular reviews of existing alerts. Remove alerts that dont lead to action, and tune alerts or thresholds that fire too often without providing useful signal. Reduce false positives to combat alert fatigue.
Prioritize clarity and simplicity in alert design. Simpler alerts are easier to understand, maintain, and trust under pressure. Favor fewer high-quality, actionable alerts over a large number of low-value ones.
Use dashboards and observability tools for investigation, not alerts.
{{< /shared >}}
@@ -1,5 +1,7 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/best-practices/connectivity-errors/
aliases:
- ../best-practices/connectivity-errors/ # /docs/grafana/<GRAFANA_VERSION>/alerting/best-practices/connectivity-errors/
canonical: https://grafana.com/docs/grafana/latest/alerting/guides/connectivity-errors/
description: Learn how to detect and handle connectivity issues in alerts using Prometheus, Grafana Alerting, or both.
keywords:
- grafana
@@ -14,7 +16,7 @@ labels:
- oss
menuTitle: Handle connectivity errors
title: Handle connectivity errors in alerts
weight: 1010
weight: 1020
refs:
pending-period:
- pattern: /docs/grafana/
@@ -1,5 +1,7 @@
---
canonical: https://grafana.com/docs/grafana/latest/alerting/best-practices/missing-data/
aliases:
- ../best-practices/missing-data/ # /docs/grafana/<GRAFANA_VERSION>/alerting/best-practices/missing-data/
canonical: https://grafana.com/docs/grafana/latest/alerting/guides/missing-data/
description: Learn how to detect missing metrics and design alerts that handle gaps in data in Prometheus and Grafana Alerting.
keywords:
- grafana
@@ -14,7 +16,7 @@ labels:
- oss
menuTitle: Handle missing data
title: Handle missing data in Grafana Alerting
weight: 1020
weight: 1030
refs:
connectivity-errors-guide:
- pattern: /docs/grafana/
@@ -41,9 +41,13 @@ Select a group to expand it and view the list of alert rules within that group.
The list view includes a number of filters to simplify managing large volumes of alerts.
## Filter and save searches
Click the **Filter** button to open the filter popup. You can filter by name, label, folder/namespace, evaluation group, data source, contact point, rule source, rule state, rule type, and the health of the alert rule from the popup menu. Click **Apply** at the bottom of the filter popup to enact the filters as you search.
{{< figure src="/media/docs/alerting/alerting-list-view-filter.png" max-width="750px" alt="Alert rule filter options" >}}
Click the **Saved searches** button to open the list of previously saved searches, or click **+ Save current search** to add your current search to the saved searches list. You can also rename a saved search or set it as a default search. When you set a saved search as the default search, the Alert rules page opens with the search applied.
{{< figure src="/media/docs/alerting/alerting-saved-searches.png" max-width="750px" alt="Alert rule filter options" >}}
## Change alert rules list view
@@ -23,6 +23,8 @@ killercoda:
This tutorial is a continuation of the [Get started with Grafana Alerting - Route alerts using dynamic labels](http://www.grafana.com/tutorials/alerting-get-started-pt5/) tutorial.
{{< youtube id="mqj_hN24zLU" >}}
<!-- USE CASE -->
In this tutorial you will learn how to:
@@ -1,6 +1,6 @@
import { test, expect } from '@grafana/plugin-e2e';
import { flows, type Variable } from './utils';
import { flows, saveDashboard, type Variable } from './utils';
test.use({
featureToggles: {
@@ -64,20 +64,7 @@ test.describe(
label: 'VariableUnderTest',
};
// common steps to add a new variable
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// set the textbox variable value
const type = 'variable-type Value';
const fieldLabel = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel(type)
);
await expect(fieldLabel).toBeVisible();
const inputField = fieldLabel.locator('input');
await expect(inputField).toBeVisible();
await inputField.fill(variable.value);
await inputField.blur();
await flows.addNewTextBoxVariable(dashboardPage, variable);
// select the variable in the dashboard and confirm the variable value is set
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click();
@@ -140,5 +127,94 @@ test.describe(
await expect(panelContent).toBeVisible();
await expect(markdownContent).toContainText('VariableUnderTest: 10m');
});
test('can hide a variable', async ({ dashboardPage, selectors, page }) => {
const variable: Variable = {
type: 'textbox',
name: 'VariableUnderTest',
value: 'foo',
label: 'VariableUnderTest',
};
await saveDashboard(dashboardPage, page, selectors, 'can hide a variable');
await flows.addNewTextBoxVariable(dashboardPage, variable);
// check the variable is visible in the dashboard
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label)
);
await expect(variableLabel).toBeVisible();
// hide the variable
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.generalDisplaySelect)
.click();
await page.getByText('Hidden', { exact: true }).click();
// check that the variable is still visible
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!))
).toBeVisible();
// save dashboard and exit edit mode and check variable is not visible
await saveDashboard(dashboardPage, page, selectors);
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!))
).toBeHidden();
// refresh and check that variable isn't visible
await page.reload();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!))
).toBeHidden();
// check that the variable is visible in edit mode
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!))
).toBeVisible();
});
test('can hide variable under the controls menu', async ({ dashboardPage, selectors, page }) => {
const variable: Variable = {
type: 'textbox',
name: 'VariableUnderTest',
value: 'foo',
label: 'VariableUnderTest',
};
await saveDashboard(dashboardPage, page, selectors, 'can hide a variable in controls menu');
await flows.addNewTextBoxVariable(dashboardPage, variable);
// check the variable is visible in the dashboard
const variableLabel = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label)
);
await expect(variableLabel).toBeVisible();
// hide the variable
await dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.generalDisplaySelect)
.click();
await page.getByText('Controls menu', { exact: true }).click();
// check that the variable is hidden under the controls menu
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!))
).toBeHidden();
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.ControlsButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!))
).toBeVisible();
// save dashboard and refresh
await saveDashboard(dashboardPage, page, selectors);
await page.reload();
//check that the variable is hidden under the controls menu
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!))
).toBeHidden();
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.ControlsButton).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!))
).toBeVisible();
});
}
);
+23 -1
View File
@@ -79,6 +79,20 @@ export const flows = {
await variableLabelInput.blur();
}
},
async addNewTextBoxVariable(dashboardPage: DashboardPage, variable: Variable) {
await flows.newEditPaneVariableClick(dashboardPage, selectors);
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
// set the textbox variable value
const type = 'variable-type Value';
const fieldLabel = dashboardPage.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel(type)
);
await expect(fieldLabel).toBeVisible();
const inputField = fieldLabel.locator('input');
await expect(inputField).toBeVisible();
await inputField.fill(variable.value);
await inputField.blur();
},
};
export type Variable = {
@@ -89,8 +103,16 @@ export type Variable = {
value: string;
};
export async function saveDashboard(dashboardPage: DashboardPage, page: Page, selectors: E2ESelectorGroups) {
export async function saveDashboard(
dashboardPage: DashboardPage,
page: Page,
selectors: E2ESelectorGroups,
title?: string
) {
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click();
if (title) {
await page.getByTestId(selectors.components.Drawer.DashboardSaveDrawer.saveAsTitleInput).fill(title);
}
await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click();
await expect(page.getByText('Dashboard saved')).toBeVisible();
}
+5 -4
View File
@@ -7,7 +7,7 @@ require (
buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.36.2-20250703125925-3f0fcf4bff96.1 // @grafana/observability-traces-and-profiling
cloud.google.com/go/kms v1.22.0 // @grafana/grafana-backend-group
cloud.google.com/go/storage v1.55.0 // @grafana/grafana-backend-group
connectrpc.com/connect v1.18.1 // @grafana/observability-traces-and-profiling
connectrpc.com/connect v1.19.1 // @grafana/observability-traces-and-profiling
cuelang.org/go v0.11.1 // @grafana/grafana-as-code
dario.cat/mergo v1.0.2 // @grafana/grafana-app-platform-squad
filippo.io/age v1.2.1 // @grafana/identity-access-team
@@ -33,12 +33,14 @@ require (
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // @grafana/grafana-operator-experience-squad
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // @grafana/grafana-operator-experience-squad
github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
@@ -111,7 +113,7 @@ require (
github.com/grafana/nanogit v0.3.0 // indirect; @grafana/grafana-git-ui-sync-team
github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // @grafana/observability-traces-and-profiling
github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae // @grafana/observability-traces-and-profiling
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f // @grafana/observability-traces-and-profiling
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/grafana-search-and-storage
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // @grafana/plugins-platform-backend
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // @grafana/grafana-backend-group
@@ -343,7 +345,6 @@ require (
github.com/at-wat/mqtt-go v0.19.6 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
@@ -358,7 +359,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
@@ -681,6 +681,7 @@ require (
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/google/gnostic v0.7.1 // indirect
github.com/gophercloud/gophercloud/v2 v2.9.0 // indirect
github.com/grafana/sqlds/v5 v5.0.3 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
+6 -4
View File
@@ -627,8 +627,8 @@ cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoIS
cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M=
cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA=
cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw=
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeSEBLPA9YJp5bjrofdU3pIXs=
cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 h1:Zh+Ur3OsoWpvALHPLT45nOekHkgOt+IOfutBbPqM17I=
cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8=
@@ -1503,6 +1503,8 @@ github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PU
github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/gnostic v0.7.1 h1:t5Kc7j/8kYr8t2u11rykRrPPovlEMG4+xdc/SpekATs=
github.com/google/gnostic v0.7.1/go.mod h1:KSw6sxnxEBFM8jLPfJd46xZP+yQcfE8XkiqfZx5zR28=
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -1685,8 +1687,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae h1:35W3Wjp9KWnSoV/DuymmyIj5aHE0CYlDQ5m2KeXUPAc=
github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae/go.mod h1:6CJ1uXmLZ13ufpO9xE4pST+DyaBt0uszzrV0YnoaVLQ=
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f h1:fTlIj5n4x5dU63XHItug7GLjtnaeJdPqBlqg4zlABq0=
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f/go.mod h1:VBNcIhunCZsJ3/mcYx+j7uFf0P/108eiWa+8+Z9ll3o=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56 h1:SDGrP81Vcd102L3UJEryRd1eestRw73wt+b8vnVEFe0=
+1
View File
@@ -10,6 +10,7 @@ use (
./apps/alerting/historian
./apps/alerting/notifications
./apps/alerting/rules
./apps/anno
./apps/annotation
./apps/collections
./apps/correlations
+2
View File
@@ -755,6 +755,8 @@ github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZP
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
github.com/flowstack/go-jsonschema v0.1.1 h1:dCrjGJRXIlbDsLAgTJZTjhwUJnnxVWl1OgNyYh5nyDc=
github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c h1:yKN46XJHYC/gvgH2UsisJ31+n4K3S7QYZSfU2uAWjuI=
github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c/go.mod h1:L92h+dgwElEyUuShEwjbiHjseW410WIcNz+Bjutc8YQ=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
+4 -2
View File
@@ -218,8 +218,10 @@ lineage: schemas: [{
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
regex?: string
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Optional, indicates whether a custom type variable uses CSV or JSON to define its values
valuesFormat?: "csv" | "json" | *"csv"
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Additional static options for query variable
staticOptions?: [...#VariableOption]
// Ordering of static options in relation to options returned from data source for query variable
+2 -2
View File
@@ -295,8 +295,8 @@
"@grafana/plugin-ui": "^0.11.1",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "6.52.0",
"@grafana/scenes-react": "6.52.0",
"@grafana/scenes": "v6.52.1",
"@grafana/scenes-react": "v6.52.1",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
+4 -4
View File
@@ -400,10 +400,6 @@ export interface FeatureToggles {
*/
tableSharedCrosshair?: boolean;
/**
* Use the kubernetes API for feature toggle management in the frontend
*/
kubernetesFeatureToggles?: boolean;
/**
* Enabled grafana cloud specific RBAC roles
*/
cloudRBACRoles?: boolean;
@@ -1263,4 +1259,8 @@ export interface FeatureToggles {
* Enables the creation of keepers that manage secrets stored on AWS secrets manager
*/
secretsManagementAppPlatformAwsKeeper?: boolean;
/**
* Enables profiles exemplars support in profiles drilldown
*/
profilesExemplars?: boolean;
}
@@ -103,6 +103,7 @@ export interface IntervalVariableModel extends VariableWithOptions {
export interface CustomVariableModel extends VariableWithMultiSupport {
type: 'custom';
valuesFormat?: 'csv' | 'json';
}
export interface DataSourceVariableModel extends VariableWithMultiSupport {
@@ -266,6 +266,9 @@ export const versionedPages = {
Controls: {
'11.1.0': 'data-testid dashboard controls',
},
ControlsButton: {
'12.3.0': 'data-testid dashboard controls button',
},
SubMenu: {
submenu: {
[MIN_GRAFANA_VERSION]: 'Dashboard submenu',
@@ -25,6 +25,10 @@ export interface GrafanaPyroscopeDataQuery extends common.DataQuery {
* Allows to group the results.
*/
groupBy: Array<string>;
/**
* If set to true, exemplars will be requested
*/
includeExemplars: boolean;
/**
* Specifies the query label selectors.
*/
@@ -49,6 +53,7 @@ export interface GrafanaPyroscopeDataQuery extends common.DataQuery {
export const defaultGrafanaPyroscopeDataQuery: Partial<GrafanaPyroscopeDataQuery> = {
groupBy: [],
includeExemplars: false,
labelSelector: '{}',
spanSelector: [],
};
@@ -211,6 +211,10 @@ export interface VariableModel {
* Type of variable
*/
type: VariableType;
/**
* Optional, indicates whether a custom type variable uses CSV or JSON to define its values
*/
valuesFormat?: ('csv' | 'json');
}
export const defaultVariableModel: Partial<VariableModel> = {
@@ -220,6 +224,7 @@ export const defaultVariableModel: Partial<VariableModel> = {
options: [],
skipUrlSync: false,
staticOptions: [],
valuesFormat: 'csv',
};
/**
@@ -317,6 +317,7 @@ export const handyTestingSchema: Spec = {
query: 'option1, option2',
skipUrlSync: false,
allowCustomValue: true,
valuesFormat: 'csv',
},
},
{
@@ -300,7 +300,7 @@ export interface FieldConfig {
description?: string;
// An explicit path to the field in the datasource. When the frame meta includes a path,
// This will default to `${frame.meta.path}/${field.name}
//
//
// When defined, this value can be used as an identifier within the datasource scope, and
// may be used to update the results
path?: string;
@@ -1353,6 +1353,7 @@ export interface CustomVariableSpec {
skipUrlSync: boolean;
description?: string;
allowCustomValue: boolean;
valuesFormat?: "csv" | "json";
}
export const defaultCustomVariableSpec = (): CustomVariableSpec => ({
@@ -1365,6 +1366,7 @@ export const defaultCustomVariableSpec = (): CustomVariableSpec => ({
hide: "dontHide",
skipUrlSync: false,
allowCustomValue: true,
valuesFormat: undefined,
});
// Group variable kind
@@ -1549,4 +1551,3 @@ export const defaultSpec = (): Spec => ({
title: "",
variables: [],
});
@@ -1359,6 +1359,7 @@ export interface CustomVariableSpec {
skipUrlSync: boolean;
description?: string;
allowCustomValue: boolean;
valuesFormat?: "csv" | "json";
}
export const defaultCustomVariableSpec = (): CustomVariableSpec => ({
+1 -1
View File
@@ -20,7 +20,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/caching"
@@ -28,6 +27,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest"
"github.com/grafana/grafana/pkg/services/pluginsintegration"
"github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
+3 -1
View File
@@ -11,6 +11,9 @@ import (
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
_ "github.com/Azure/go-autorest/autorest"
_ "github.com/Azure/go-autorest/autorest/adal"
_ "github.com/aws/aws-sdk-go-v2/credentials"
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
_ "github.com/aws/aws-sdk-go-v2/service/sts"
_ "github.com/beevik/etree"
_ "github.com/blugelabs/bluge"
_ "github.com/blugelabs/bluge_segment_api"
@@ -46,7 +49,6 @@ import (
_ "sigs.k8s.io/randfill"
_ "xorm.io/builder"
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
_ "github.com/grafana/authlib/authn"
_ "github.com/grafana/authlib/authz"
_ "github.com/grafana/authlib/cache"
+10
View File
@@ -837,6 +837,8 @@ type VariableModel struct {
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
Regex *string `json:"regex,omitempty"`
// Optional, indicates whether a custom type variable uses CSV or JSON to define its values
ValuesFormat *VariableModelValuesFormat `json:"valuesFormat,omitempty"`
// Determine whether regex applies to variable value or display text
RegexApplyTo *VariableRegexApplyTo `json:"regexApplyTo,omitempty"`
// Additional static options for query variable
@@ -852,6 +854,7 @@ func NewVariableModel() *VariableModel {
Multi: (func(input bool) *bool { return &input })(false),
AllowCustomValue: (func(input bool) *bool { return &input })(true),
IncludeAll: (func(input bool) *bool { return &input })(false),
ValuesFormat: (func(input VariableModelValuesFormat) *VariableModelValuesFormat { return &input })(VariableModelValuesFormatCsv),
}
}
@@ -1191,6 +1194,13 @@ const (
DataTransformerConfigTopicAlertStates DataTransformerConfigTopic = "alertStates"
)
type VariableModelValuesFormat string
const (
VariableModelValuesFormatCsv VariableModelValuesFormat = "csv"
VariableModelValuesFormatJson VariableModelValuesFormat = "json"
)
type VariableModelStaticOptionsOrder string
const (
@@ -5,7 +5,6 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/grpcplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
"github.com/grafana/grafana/pkg/plugins/log"
@@ -27,10 +26,6 @@ func New(providers ...PluginBackendProvider) *Service {
}
}
func ProvideService(coreRegistry *coreplugin.Registry) *Service {
return New(coreRegistry.BackendFactoryProvider(), DefaultProvider)
}
func (s *Service) BackendFactory(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc {
for _, provider := range s.providerChain {
if factory := provider(ctx, p); factory != nil {
+2 -2
View File
@@ -276,7 +276,7 @@ func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) {
return
}
if b.providerType == setting.GOFFProviderType || b.providerType == setting.OFREPProviderType {
if b.providerType == setting.FeaturesServiceProviderType || b.providerType == setting.OFREPProviderType {
b.proxyFlagReq(ctx, flagKey, isAuthedReq, w, r)
return
}
@@ -304,7 +304,7 @@ func (b *APIBuilder) allFlagsHandler(w http.ResponseWriter, r *http.Request) {
isAuthedReq := b.isAuthenticatedRequest(r)
span.SetAttributes(attribute.Bool("authenticated", isAuthedReq))
if b.providerType == setting.GOFFProviderType || b.providerType == setting.OFREPProviderType {
if b.providerType == setting.FeaturesServiceProviderType || b.providerType == setting.OFREPProviderType {
b.proxyAllFlagReq(ctx, isAuthedReq, w, r)
return
}
+55
View File
@@ -0,0 +1,55 @@
package anno
import (
"context"
"k8s.io/apiserver/pkg/authorization/authorizer"
restclient "k8s.io/client-go/rest"
"github.com/grafana/grafana-app-sdk/app"
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
"github.com/grafana/grafana-app-sdk/simple"
apis "github.com/grafana/grafana/apps/anno/pkg/apis/manifestdata"
annoapp "github.com/grafana/grafana/apps/anno/pkg/app"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
var (
_ appsdkapiserver.AppInstaller = (*AnnotationAppInstaller)(nil)
)
type AnnotationAppInstaller struct {
appsdkapiserver.AppInstaller
cfg *setting.Cfg
}
func RegisterAppInstaller(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
) (*AnnotationAppInstaller, error) {
installer := &AnnotationAppInstaller{
cfg: cfg,
}
provider := simple.NewAppProvider(apis.LocalManifest(), nil, annoapp.New)
appConfig := app.Config{
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
ManifestData: *apis.LocalManifest().ManifestData,
}
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, apis.NewGoTypeAssociator())
if err != nil {
return nil, err
}
installer.AppInstaller = i
return installer, nil
}
func (a *AnnotationAppInstaller) GetAuthorizer() authorizer.Authorizer {
return authorizer.AuthorizerFunc(
func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
return authorizer.DecisionAllow, "", nil
},
)
}
+4
View File
@@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apps/alerting/historian"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apps/alerting/rules"
"github.com/grafana/grafana/pkg/registry/apps/anno"
"github.com/grafana/grafana/pkg/registry/apps/annotation"
"github.com/grafana/grafana/pkg/registry/apps/correlations"
"github.com/grafana/grafana/pkg/registry/apps/example"
@@ -43,6 +44,7 @@ func ProvideAppInstallers(
alertingNotificationAppInstaller *notifications.AlertingNotificationsAppInstaller,
logsdrilldownAppInstaller *logsdrilldown.LogsDrilldownAppInstaller,
annotationAppInstaller *annotation.AnnotationAppInstaller,
annoAppInstaller *anno.AnnotationAppInstaller,
exampleAppInstaller *example.ExampleAppInstaller,
advisorAppInstaller *advisor.AdvisorAppInstaller,
alertingHistorianAppInstaller *historian.AlertingHistorianAppInstaller,
@@ -89,6 +91,8 @@ func ProvideAppInstallers(
installers = append(installers, alertingHistorianAppInstaller)
}
installers = append(installers, annoAppInstaller)
return installers
}
+2
View File
@@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apps/alerting/historian"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apps/alerting/rules"
"github.com/grafana/grafana/pkg/registry/apps/anno"
"github.com/grafana/grafana/pkg/registry/apps/annotation"
"github.com/grafana/grafana/pkg/registry/apps/correlations"
"github.com/grafana/grafana/pkg/registry/apps/example"
@@ -30,6 +31,7 @@ var WireSet = wire.NewSet(
historian.RegisterAppInstaller,
logsdrilldown.RegisterAppInstaller,
annotation.RegisterAppInstaller,
anno.RegisterAppInstaller,
quotas.RegisterAppInstaller,
example.RegisterAppInstaller,
)
+16 -8
View File
@@ -37,8 +37,6 @@ import (
"github.com/grafana/grafana/pkg/login/social/socialimpl"
"github.com/grafana/grafana/pkg/middleware/csrf"
"github.com/grafana/grafana/pkg/middleware/loggermw"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
provider2 "github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
manager4 "github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/plugins/manager/filestore"
"github.com/grafana/grafana/pkg/plugins/manager/process"
@@ -83,6 +81,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apps/alerting/historian"
notifications2 "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apps/alerting/rules"
"github.com/grafana/grafana/pkg/registry/apps/anno"
"github.com/grafana/grafana/pkg/registry/apps/annotation"
correlations2 "github.com/grafana/grafana/pkg/registry/apps/correlations"
"github.com/grafana/grafana/pkg/registry/apps/example"
@@ -178,6 +177,7 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/angulardetectorsprovider"
"github.com/grafana/grafana/pkg/services/pluginsintegration/angularinspector"
"github.com/grafana/grafana/pkg/services/pluginsintegration/angularpatternsstore"
"github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin"
"github.com/grafana/grafana/pkg/services/pluginsintegration/dashboards"
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync"
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever"
@@ -557,7 +557,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
zipkinService := zipkin.ProvideService(httpclientProvider)
jaegerService := jaeger.ProvideService(httpclientProvider)
corepluginRegistry := coreplugin.ProvideCoreRegistry(tracer, azuremonitorService, cloudwatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService)
providerService := provider2.ProvideService(corepluginRegistry)
backendFactoryProvider := coreplugin.ProvideCoreProvider(corepluginRegistry)
processService := process.ProvideService()
retrieverService := retriever.ProvideService(sqlStore, apikeyService, kvStore, userService, orgService)
serviceAccountPermissionsService, err := ossaccesscontrol.ProvideServiceAccountPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, retrieverService, acimplService, teamService, userService, actionSetService)
@@ -573,7 +573,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
service13 := service6.ProvideService(sqlStore, secretsService)
serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service13)
noop := provisionedplugins.NewNoop()
initialize := pipeline.ProvideInitializationStage(pluginManagementCfg, inMemory, providerService, processService, serviceregistrationService, acimplService, actionSetService, envVarsProvider, tracingService, noop)
initialize := pipeline.ProvideInitializationStage(pluginManagementCfg, inMemory, backendFactoryProvider, processService, serviceregistrationService, acimplService, actionSetService, envVarsProvider, tracingService, noop)
terminate, err := pipeline.ProvideTerminationStage(pluginManagementCfg, inMemory, processService)
if err != nil {
return nil, err
@@ -814,6 +814,10 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
annoAnnotationAppInstaller, err := anno.RegisterAppInstaller(cfg, featureToggles)
if err != nil {
return nil, err
}
exampleAppInstaller, err := example.RegisterAppInstaller(cfg, featureToggles)
if err != nil {
return nil, err
@@ -831,7 +835,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller)
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, annoAnnotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller)
builderMetrics := builder.ProvideBuilderMetrics(registerer)
backend := auditing.ProvideNoopBackend()
policyRuleProvider := auditing.ProvideNoopPolicyRuleProvider()
@@ -1217,7 +1221,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
zipkinService := zipkin.ProvideService(httpclientProvider)
jaegerService := jaeger.ProvideService(httpclientProvider)
corepluginRegistry := coreplugin.ProvideCoreRegistry(tracer, azuremonitorService, cloudwatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService)
providerService := provider2.ProvideService(corepluginRegistry)
backendFactoryProvider := coreplugin.ProvideCoreProvider(corepluginRegistry)
processService := process.ProvideService()
retrieverService := retriever.ProvideService(sqlStore, apikeyService, kvStore, userService, orgService)
serviceAccountPermissionsService, err := ossaccesscontrol.ProvideServiceAccountPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, retrieverService, acimplService, teamService, userService, actionSetService)
@@ -1233,7 +1237,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
service13 := service6.ProvideService(sqlStore, secretsService)
serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service13)
noop := provisionedplugins.NewNoop()
initialize := pipeline.ProvideInitializationStage(pluginManagementCfg, inMemory, providerService, processService, serviceregistrationService, acimplService, actionSetService, envVarsProvider, tracingService, noop)
initialize := pipeline.ProvideInitializationStage(pluginManagementCfg, inMemory, backendFactoryProvider, processService, serviceregistrationService, acimplService, actionSetService, envVarsProvider, tracingService, noop)
terminate, err := pipeline.ProvideTerminationStage(pluginManagementCfg, inMemory, processService)
if err != nil {
return nil, err
@@ -1476,6 +1480,10 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
annoAnnotationAppInstaller, err := anno.RegisterAppInstaller(cfg, featureToggles)
if err != nil {
return nil, err
}
exampleAppInstaller, err := example.RegisterAppInstaller(cfg, featureToggles)
if err != nil {
return nil, err
@@ -1493,7 +1501,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller)
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, annoAnnotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller)
builderMetrics := builder.ProvideBuilderMetrics(registerer)
backend := auditing.ProvideNoopBackend()
policyRuleProvider := auditing.ProvideNoopPolicyRuleProvider()
@@ -3,11 +3,13 @@ package dualwrite
import (
"context"
"fmt"
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
claims "github.com/grafana/authlib/types"
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
)
@@ -19,14 +21,30 @@ type legacyTupleCollector func(ctx context.Context, orgID int64) (map[string]map
type zanzanaTupleCollector func(ctx context.Context, client zanzana.Client, object string, namespace string) (map[string]*openfgav1.TupleKey, error)
type resourceReconciler struct {
name string
legacy legacyTupleCollector
zanzana zanzanaTupleCollector
client zanzana.Client
name string
legacy legacyTupleCollector
zanzana zanzanaTupleCollector
client zanzana.Client
orphanObjectPrefix string
orphanRelations []string
}
func newResourceReconciler(name string, legacy legacyTupleCollector, zanzana zanzanaTupleCollector, client zanzana.Client) resourceReconciler {
return resourceReconciler{name, legacy, zanzana, client}
func newResourceReconciler(name string, legacy legacyTupleCollector, zanzanaCollector zanzanaTupleCollector, client zanzana.Client) resourceReconciler {
r := resourceReconciler{name: name, legacy: legacy, zanzana: zanzanaCollector, client: client}
// we only need to worry about orphaned tuples for reconcilers that use the managed permissions collector (i.e. dashboards & folders)
switch name {
case "managed folder permissions":
// prefix for folders is `folder:`
r.orphanObjectPrefix = zanzana.NewObjectEntry(zanzana.TypeFolder, "", "", "", "")
r.orphanRelations = append([]string{}, zanzana.RelationsFolder...)
case "managed dashboard permissions":
// prefix for dashboards will be `resource:dashboard.grafana.app/dashboards/`
r.orphanObjectPrefix = fmt.Sprintf("%s/", zanzana.NewObjectEntry(zanzana.TypeResource, dashboardV1.APIGroup, dashboardV1.DASHBOARD_RESOURCE, "", ""))
r.orphanRelations = append([]string{}, zanzana.RelationsResouce...)
}
return r
}
func (r resourceReconciler) reconcile(ctx context.Context, namespace string) error {
@@ -35,6 +53,15 @@ func (r resourceReconciler) reconcile(ctx context.Context, namespace string) err
return err
}
// 0. Fetch all tuples currently stored in Zanzana. This will be used later on
// to cleanup orphaned tuples.
// This order needs to be kept (fetching from Zanzana first) to avoid accidentally
// cleaning up new tuples that were added after the legacy tuples were fetched.
allTuplesInZanzana, err := r.readAllTuples(ctx, namespace)
if err != nil {
return fmt.Errorf("failed to read all tuples from zanzana for %s: %w", r.name, err)
}
// 1. Fetch grafana resources stored in grafana db.
res, err := r.legacy(ctx, info.OrgID)
if err != nil {
@@ -87,6 +114,14 @@ func (r resourceReconciler) reconcile(ctx context.Context, namespace string) err
}
}
// when the last managed permission for a resource is removed, the legacy results will no
// longer contain any tuples for that resource. this process cleans it up when applicable.
orphans, err := r.collectOrphanDeletes(ctx, namespace, allTuplesInZanzana, res)
if err != nil {
return fmt.Errorf("failed to collect orphan deletes (%s): %w", r.name, err)
}
deletes = append(deletes, orphans...)
if len(writes) == 0 && len(deletes) == 0 {
return nil
}
@@ -119,3 +154,79 @@ func (r resourceReconciler) reconcile(ctx context.Context, namespace string) err
return nil
}
// collectOrphanDeletes collects tuples that are no longer present in the legacy results
// but still are present in zanzana. when that is the case, we need to delete the tuple from
// zanzana. this will happen when the last managed permission for a resource is removed.
// this is only used for dashboards and folders, as those are the only resources that use the managed permissions collector.
func (r resourceReconciler) collectOrphanDeletes(
ctx context.Context,
namespace string,
allTuplesInZanzana []*authzextv1.Tuple,
legacyReturnedTuples map[string]map[string]*openfgav1.TupleKey,
) ([]*openfgav1.TupleKeyWithoutCondition, error) {
if r.orphanObjectPrefix == "" || len(r.orphanRelations) == 0 {
return []*openfgav1.TupleKeyWithoutCondition{}, nil
}
seen := map[string]struct{}{}
out := []*openfgav1.TupleKeyWithoutCondition{}
// what relation types we are interested in cleaning up
relationsToCleanup := map[string]struct{}{}
for _, rel := range r.orphanRelations {
relationsToCleanup[rel] = struct{}{}
}
for _, tuple := range allTuplesInZanzana {
if tuple == nil || tuple.Key == nil {
continue
}
// only cleanup the particular relation types we are interested in
if _, ok := relationsToCleanup[tuple.Key.Relation]; !ok {
continue
}
// only cleanup the particular object types we are interested in (either dashboards or folders)
if !strings.HasPrefix(tuple.Key.Object, r.orphanObjectPrefix) {
continue
}
// if legacy returned this object, it's not orphaned
if _, ok := legacyReturnedTuples[tuple.Key.Object]; ok {
continue
}
// keep track of the tuples we have already seen and marked for deletion
key := fmt.Sprintf("%s|%s|%s", tuple.Key.User, tuple.Key.Relation, tuple.Key.Object)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, &openfgav1.TupleKeyWithoutCondition{
User: tuple.Key.User,
Relation: tuple.Key.Relation,
Object: tuple.Key.Object,
})
}
return out, nil
}
func (r resourceReconciler) readAllTuples(ctx context.Context, namespace string) ([]*authzextv1.Tuple, error) {
var (
out []*authzextv1.Tuple
continueToken string
)
for {
res, err := r.client.Read(ctx, &authzextv1.ReadRequest{
Namespace: namespace,
ContinuationToken: continueToken,
})
if err != nil {
return nil, err
}
out = append(out, res.Tuples...)
continueToken = res.ContinuationToken
if continueToken == "" {
return out, nil
}
}
}
@@ -0,0 +1,110 @@
package dualwrite
import (
"context"
"testing"
authlib "github.com/grafana/authlib/types"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/stretchr/testify/require"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
)
type fakeZanzanaClient struct {
readTuples []*authzextv1.Tuple
writeReqs []*authzextv1.WriteRequest
}
func (f *fakeZanzanaClient) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error) {
return &authzextv1.ReadResponse{
Tuples: f.readTuples,
ContinuationToken: "",
}, nil
}
func (f *fakeZanzanaClient) Write(ctx context.Context, req *authzextv1.WriteRequest) error {
f.writeReqs = append(f.writeReqs, req)
return nil
}
func (f *fakeZanzanaClient) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
return &authzextv1.BatchCheckResponse{}, nil
}
func (f *fakeZanzanaClient) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error {
return nil
}
func (f *fakeZanzanaClient) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) {
return &authzextv1.QueryResponse{}, nil
}
func (f *fakeZanzanaClient) Check(ctx context.Context, info authlib.AuthInfo, req authlib.CheckRequest, folder string) (authlib.CheckResponse, error) {
return authlib.CheckResponse{Allowed: true}, nil
}
func (f *fakeZanzanaClient) Compile(ctx context.Context, info authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, authlib.Zookie, error) {
return func(name, folder string) bool { return true }, authlib.NoopZookie{}, nil
}
func TestResourceReconciler_OrphanedManagedDashboardTuplesAreDeleted(t *testing.T) {
legacy := func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) {
return map[string]map[string]*openfgav1.TupleKey{}, nil
}
zCollector := func(ctx context.Context, client zanzana.Client, object string, namespace string) (map[string]*openfgav1.TupleKey, error) {
return map[string]*openfgav1.TupleKey{}, nil
}
fake := &fakeZanzanaClient{}
r := newResourceReconciler("managed dashboard permissions", legacy, zCollector, fake)
require.NotEmpty(t, r.orphanObjectPrefix)
require.NotEmpty(t, r.orphanRelations)
relAllowed := r.orphanRelations[0]
objAllowed := r.orphanObjectPrefix + "dash-uid-1"
fake.readTuples = []*authzextv1.Tuple{
// should be removed
{
Key: &authzextv1.TupleKey{
User: "user:1",
Relation: relAllowed,
Object: objAllowed,
},
},
// same relation but different object type/prefix - should stay
{
Key: &authzextv1.TupleKey{
User: "user:1",
Relation: relAllowed,
Object: "folder:some-folder",
},
},
// same prefix but different relation - should stay
{
Key: &authzextv1.TupleKey{
User: "user:1",
Relation: zanzana.RelationParent,
Object: objAllowed,
},
},
}
err := r.reconcile(context.Background(), authlib.OrgNamespaceFormatter(1))
require.NoError(t, err)
require.Len(t, fake.writeReqs, 1)
wr := fake.writeReqs[0]
require.NotNil(t, wr.Deletes)
require.Nil(t, wr.Writes)
require.Len(t, wr.Deletes.TupleKeys, 1)
del := wr.Deletes.TupleKeys[0]
require.Equal(t, "user:1", del.User)
require.Equal(t, relAllowed, del.Relation)
require.Equal(t, objAllowed, del.Object)
}
@@ -32,6 +32,8 @@ func NewOpenFGAServer(cfg setting.ZanzanaServerSettings, store storage.OpenFGADa
opts := []server.OpenFGAServiceV1Option{
server.WithDatastore(store),
server.WithLogger(zlogger.New(logger)),
// Cache settings
server.WithCheckCacheLimit(cfg.CacheSettings.CheckCacheLimit),
server.WithCacheControllerEnabled(cfg.CacheSettings.CacheControllerEnabled),
server.WithCacheControllerTTL(cfg.CacheSettings.CacheControllerTTL),
@@ -40,16 +42,25 @@ func NewOpenFGAServer(cfg setting.ZanzanaServerSettings, store storage.OpenFGADa
server.WithCheckIteratorCacheEnabled(cfg.CacheSettings.CheckIteratorCacheEnabled),
server.WithCheckIteratorCacheMaxResults(cfg.CacheSettings.CheckIteratorCacheMaxResults),
server.WithCheckIteratorCacheTTL(cfg.CacheSettings.CheckIteratorCacheTTL),
// ListObjects settings
server.WithListObjectsMaxResults(cfg.ListObjectsMaxResults),
server.WithListObjectsIteratorCacheEnabled(cfg.CacheSettings.ListObjectsIteratorCacheEnabled),
server.WithListObjectsIteratorCacheMaxResults(cfg.CacheSettings.ListObjectsIteratorCacheMaxResults),
server.WithListObjectsIteratorCacheTTL(cfg.CacheSettings.ListObjectsIteratorCacheTTL),
server.WithListObjectsDeadline(cfg.ListObjectsDeadline),
// Shared iterator settings
server.WithSharedIteratorEnabled(cfg.CacheSettings.SharedIteratorEnabled),
server.WithSharedIteratorLimit(cfg.CacheSettings.SharedIteratorLimit),
server.WithSharedIteratorTTL(cfg.CacheSettings.SharedIteratorTTL),
server.WithListObjectsDeadline(cfg.ListObjectsDeadline),
server.WithContextPropagationToDatastore(true),
}
openfgaOpts := withOpenFGAOptions(cfg)
opts = append(opts, openfgaOpts...)
srv, err := server.NewServerWithOpts(opts...)
if err != nil {
return nil, err
@@ -58,6 +69,129 @@ func NewOpenFGAServer(cfg setting.ZanzanaServerSettings, store storage.OpenFGADa
return srv, nil
}
func withOpenFGAOptions(cfg setting.ZanzanaServerSettings) []server.OpenFGAServiceV1Option {
opts := make([]server.OpenFGAServiceV1Option, 0)
listOpts := withListOptions(cfg)
opts = append(opts, listOpts...)
// Check settings
if cfg.OpenFgaServerSettings.MaxConcurrentReadsForCheck != 0 {
opts = append(opts, server.WithMaxConcurrentReadsForCheck(cfg.OpenFgaServerSettings.MaxConcurrentReadsForCheck))
}
if cfg.OpenFgaServerSettings.CheckDatabaseThrottleThreshold != 0 || cfg.OpenFgaServerSettings.CheckDatabaseThrottleDuration != 0 {
opts = append(opts, server.WithCheckDatabaseThrottle(cfg.OpenFgaServerSettings.CheckDatabaseThrottleThreshold, cfg.OpenFgaServerSettings.CheckDatabaseThrottleDuration))
}
// Batch check settings
if cfg.OpenFgaServerSettings.MaxConcurrentChecksPerBatchCheck != 0 {
opts = append(opts, server.WithMaxConcurrentChecksPerBatchCheck(cfg.OpenFgaServerSettings.MaxConcurrentChecksPerBatchCheck))
}
if cfg.OpenFgaServerSettings.MaxChecksPerBatchCheck != 0 {
opts = append(opts, server.WithMaxChecksPerBatchCheck(cfg.OpenFgaServerSettings.MaxChecksPerBatchCheck))
}
// Resolve node settings
if cfg.OpenFgaServerSettings.ResolveNodeLimit != 0 {
opts = append(opts, server.WithResolveNodeLimit(cfg.OpenFgaServerSettings.ResolveNodeLimit))
}
if cfg.OpenFgaServerSettings.ResolveNodeBreadthLimit != 0 {
opts = append(opts, server.WithResolveNodeBreadthLimit(cfg.OpenFgaServerSettings.ResolveNodeBreadthLimit))
}
// Dispatch throttling settings
if cfg.OpenFgaServerSettings.DispatchThrottlingCheckResolverEnabled {
opts = append(opts, server.WithDispatchThrottlingCheckResolverEnabled(cfg.OpenFgaServerSettings.DispatchThrottlingCheckResolverEnabled))
}
if cfg.OpenFgaServerSettings.DispatchThrottlingCheckResolverFrequency != 0 {
opts = append(opts, server.WithDispatchThrottlingCheckResolverFrequency(cfg.OpenFgaServerSettings.DispatchThrottlingCheckResolverFrequency))
}
if cfg.OpenFgaServerSettings.DispatchThrottlingCheckResolverThreshold != 0 {
opts = append(opts, server.WithDispatchThrottlingCheckResolverThreshold(cfg.OpenFgaServerSettings.DispatchThrottlingCheckResolverThreshold))
}
if cfg.OpenFgaServerSettings.DispatchThrottlingCheckResolverMaxThreshold != 0 {
opts = append(opts, server.WithDispatchThrottlingCheckResolverMaxThreshold(cfg.OpenFgaServerSettings.DispatchThrottlingCheckResolverMaxThreshold))
}
// Shadow check/query settings
if cfg.OpenFgaServerSettings.ShadowCheckResolverTimeout != 0 {
opts = append(opts, server.WithShadowCheckResolverTimeout(cfg.OpenFgaServerSettings.ShadowCheckResolverTimeout))
}
if cfg.OpenFgaServerSettings.ShadowListObjectsQueryTimeout != 0 {
opts = append(opts, server.WithShadowListObjectsQueryTimeout(cfg.OpenFgaServerSettings.ShadowListObjectsQueryTimeout))
}
if cfg.OpenFgaServerSettings.ShadowListObjectsQueryMaxDeltaItems != 0 {
opts = append(opts, server.WithShadowListObjectsQueryMaxDeltaItems(cfg.OpenFgaServerSettings.ShadowListObjectsQueryMaxDeltaItems))
}
if cfg.OpenFgaServerSettings.RequestTimeout != 0 {
opts = append(opts, server.WithRequestTimeout(cfg.OpenFgaServerSettings.RequestTimeout))
}
if cfg.OpenFgaServerSettings.MaxAuthorizationModelSizeInBytes != 0 {
opts = append(opts, server.WithMaxAuthorizationModelSizeInBytes(cfg.OpenFgaServerSettings.MaxAuthorizationModelSizeInBytes))
}
if cfg.OpenFgaServerSettings.AuthorizationModelCacheSize != 0 {
opts = append(opts, server.WithAuthorizationModelCacheSize(cfg.OpenFgaServerSettings.AuthorizationModelCacheSize))
}
if cfg.OpenFgaServerSettings.ChangelogHorizonOffset != 0 {
opts = append(opts, server.WithChangelogHorizonOffset(cfg.OpenFgaServerSettings.ChangelogHorizonOffset))
}
return opts
}
func withListOptions(cfg setting.ZanzanaServerSettings) []server.OpenFGAServiceV1Option {
opts := make([]server.OpenFGAServiceV1Option, 0)
// ListObjects settings
if cfg.OpenFgaServerSettings.MaxConcurrentReadsForListObjects != 0 {
opts = append(opts, server.WithMaxConcurrentReadsForListObjects(cfg.OpenFgaServerSettings.MaxConcurrentReadsForListObjects))
}
if cfg.OpenFgaServerSettings.ListObjectsDispatchThrottlingEnabled {
opts = append(opts, server.WithListObjectsDispatchThrottlingEnabled(cfg.OpenFgaServerSettings.ListObjectsDispatchThrottlingEnabled))
}
if cfg.OpenFgaServerSettings.ListObjectsDispatchThrottlingFrequency != 0 {
opts = append(opts, server.WithListObjectsDispatchThrottlingFrequency(cfg.OpenFgaServerSettings.ListObjectsDispatchThrottlingFrequency))
}
if cfg.OpenFgaServerSettings.ListObjectsDispatchThrottlingThreshold != 0 {
opts = append(opts, server.WithListObjectsDispatchThrottlingThreshold(cfg.OpenFgaServerSettings.ListObjectsDispatchThrottlingThreshold))
}
if cfg.OpenFgaServerSettings.ListObjectsDispatchThrottlingMaxThreshold != 0 {
opts = append(opts, server.WithListObjectsDispatchThrottlingMaxThreshold(cfg.OpenFgaServerSettings.ListObjectsDispatchThrottlingMaxThreshold))
}
if cfg.OpenFgaServerSettings.ListObjectsDatabaseThrottleThreshold != 0 || cfg.OpenFgaServerSettings.ListObjectsDatabaseThrottleDuration != 0 {
opts = append(opts, server.WithListObjectsDatabaseThrottle(cfg.OpenFgaServerSettings.ListObjectsDatabaseThrottleThreshold, cfg.OpenFgaServerSettings.ListObjectsDatabaseThrottleDuration))
}
// ListUsers settings
if cfg.OpenFgaServerSettings.ListUsersDeadline != 0 {
opts = append(opts, server.WithListUsersDeadline(cfg.OpenFgaServerSettings.ListUsersDeadline))
}
if cfg.OpenFgaServerSettings.ListUsersMaxResults != 0 {
opts = append(opts, server.WithListUsersMaxResults(cfg.OpenFgaServerSettings.ListUsersMaxResults))
}
if cfg.OpenFgaServerSettings.MaxConcurrentReadsForListUsers != 0 {
opts = append(opts, server.WithMaxConcurrentReadsForListUsers(cfg.OpenFgaServerSettings.MaxConcurrentReadsForListUsers))
}
if cfg.OpenFgaServerSettings.ListUsersDispatchThrottlingEnabled {
opts = append(opts, server.WithListUsersDispatchThrottlingEnabled(cfg.OpenFgaServerSettings.ListUsersDispatchThrottlingEnabled))
}
if cfg.OpenFgaServerSettings.ListUsersDispatchThrottlingFrequency != 0 {
opts = append(opts, server.WithListUsersDispatchThrottlingFrequency(cfg.OpenFgaServerSettings.ListUsersDispatchThrottlingFrequency))
}
if cfg.OpenFgaServerSettings.ListUsersDispatchThrottlingThreshold != 0 {
opts = append(opts, server.WithListUsersDispatchThrottlingThreshold(cfg.OpenFgaServerSettings.ListUsersDispatchThrottlingThreshold))
}
if cfg.OpenFgaServerSettings.ListUsersDispatchThrottlingMaxThreshold != 0 {
opts = append(opts, server.WithListUsersDispatchThrottlingMaxThreshold(cfg.OpenFgaServerSettings.ListUsersDispatchThrottlingMaxThreshold))
}
if cfg.OpenFgaServerSettings.ListUsersDatabaseThrottleThreshold != 0 || cfg.OpenFgaServerSettings.ListUsersDatabaseThrottleDuration != 0 {
opts = append(opts, server.WithListUsersDatabaseThrottle(cfg.OpenFgaServerSettings.ListUsersDatabaseThrottleThreshold, cfg.OpenFgaServerSettings.ListUsersDatabaseThrottleDuration))
}
return opts
}
func NewOpenFGAHttpServer(cfg setting.ZanzanaServerSettings, srv grpcserver.Provider) (*http.Server, error) {
dialOpts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
@@ -7,7 +7,7 @@ import (
"github.com/open-feature/go-sdk/openfeature"
)
func newGOFFProvider(url string, client *http.Client) (openfeature.FeatureProvider, error) {
func newFeaturesServiceProvider(url string, client *http.Client) (openfeature.FeatureProvider, error) {
options := gofeatureflag.ProviderOptions{
Endpoint: url,
// consider using github.com/grafana/grafana/pkg/infra/httpclient/provider.go
+12 -12
View File
@@ -19,11 +19,11 @@ const (
// OpenFeatureConfig holds configuration for initializing OpenFeature
type OpenFeatureConfig struct {
// ProviderType is either "static", "goff", or "ofrep"
// ProviderType is either "static", "features-service", or "ofrep"
ProviderType string
// URL is the GOFF or OFREP service URL (required for GOFF + OFREP providers)
// URL is the remote provider's URL (required for features-service + OFREP providers)
URL *url.URL
// HTTPClient is a pre-configured HTTP client (optional, used for GOFF + OFREP providers)
// HTTPClient is a pre-configured HTTP client (optional, used by features-service + OFREP providers)
HTTPClient *http.Client
// StaticFlags are the feature flags to use with static provider
StaticFlags map[string]bool
@@ -35,9 +35,9 @@ type OpenFeatureConfig struct {
// InitOpenFeature initializes OpenFeature with the provided configuration
func InitOpenFeature(config OpenFeatureConfig) error {
// For GOFF + OFREP providers, ensure we have a URL
if (config.ProviderType == setting.GOFFProviderType || config.ProviderType == setting.OFREPProviderType) && (config.URL == nil || config.URL.String() == "") {
return fmt.Errorf("URL is required for GOFF + OFREP providers")
// For remote providers, ensure we have a URL
if (config.ProviderType == setting.FeaturesServiceProviderType || config.ProviderType == setting.OFREPProviderType) && (config.URL == nil || config.URL.String() == "") {
return fmt.Errorf("URL is required for remote providers")
}
p, err := createProvider(config.ProviderType, config.URL, config.StaticFlags, config.HTTPClient)
@@ -66,10 +66,10 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
}
var httpcli *http.Client
if cfg.OpenFeature.ProviderType == setting.GOFFProviderType || cfg.OpenFeature.ProviderType == setting.OFREPProviderType {
if cfg.OpenFeature.ProviderType == setting.FeaturesServiceProviderType || cfg.OpenFeature.ProviderType == setting.OFREPProviderType {
var m *clientauthmiddleware.TokenExchangeMiddleware
if cfg.OpenFeature.ProviderType == setting.GOFFProviderType {
if cfg.OpenFeature.ProviderType == setting.FeaturesServiceProviderType {
m, err = clientauthmiddleware.NewTokenExchangeMiddleware(cfg)
if err != nil {
return fmt.Errorf("failed to create token exchange middleware: %w", err)
@@ -103,13 +103,13 @@ func createProvider(
staticFlags map[string]bool,
httpClient *http.Client,
) (openfeature.FeatureProvider, error) {
if providerType == setting.GOFFProviderType || providerType == setting.OFREPProviderType {
if providerType == setting.FeaturesServiceProviderType || providerType == setting.OFREPProviderType {
if u == nil || u.String() == "" {
return nil, fmt.Errorf("feature provider url is required for GOFFProviderType + OFREPProviderType")
return nil, fmt.Errorf("feature provider url is required for FeaturesServiceProviderType + OFREPProviderType")
}
if providerType == setting.GOFFProviderType {
return newGOFFProvider(u.String(), httpClient)
if providerType == setting.FeaturesServiceProviderType {
return newFeaturesServiceProvider(u.String(), httpClient)
}
if providerType == setting.OFREPProviderType {
+10 -10
View File
@@ -35,9 +35,9 @@ func TestCreateProvider(t *testing.T) {
expectedProvider: setting.StaticProviderType,
},
{
name: "goff provider",
name: "features-service provider",
cfg: setting.OpenFeatureSettings{
ProviderType: setting.GOFFProviderType,
ProviderType: setting.FeaturesServiceProviderType,
URL: u,
TargetingKey: "grafana",
},
@@ -45,12 +45,12 @@ func TestCreateProvider(t *testing.T) {
Namespace: "*",
Audiences: []string{"features.grafana.app"},
},
expectedProvider: setting.GOFFProviderType,
expectedProvider: setting.FeaturesServiceProviderType,
},
{
name: "goff provider with failing token exchange",
name: "features-service provider with failing token exchange",
cfg: setting.OpenFeatureSettings{
ProviderType: setting.GOFFProviderType,
ProviderType: setting.FeaturesServiceProviderType,
URL: u,
TargetingKey: "grafana",
},
@@ -58,7 +58,7 @@ func TestCreateProvider(t *testing.T) {
Namespace: "*",
Audiences: []string{"features.grafana.app"},
},
expectedProvider: setting.GOFFProviderType,
expectedProvider: setting.FeaturesServiceProviderType,
failSigning: true,
},
{
@@ -107,7 +107,7 @@ func TestCreateProvider(t *testing.T) {
tokenExchangeMiddleware := middleware.TestingTokenExchangeMiddleware(tokenExchangeClient)
httpClient, err := createHTTPClient(tokenExchangeMiddleware)
require.NoError(t, err, "failed to create goff http client")
require.NoError(t, err, "failed to create features-service http client")
provider, err := createProvider(tc.cfg.ProviderType, tc.cfg.URL, nil, httpClient)
require.NoError(t, err)
@@ -115,7 +115,7 @@ func TestCreateProvider(t *testing.T) {
require.NoError(t, err, "failed to set provider")
switch tc.expectedProvider {
case setting.GOFFProviderType:
case setting.FeaturesServiceProviderType:
_, ok := provider.(*gofeatureflag.Provider)
assert.True(t, ok, "expected provider to be of type goff.Provider")
@@ -141,10 +141,10 @@ func testGoFFProvider(t *testing.T, failSigning bool) {
_, err := openfeature.NewDefaultClient().BooleanValueDetails(ctx, "test", false, openfeature.NewEvaluationContext("test", map[string]interface{}{"test": "test"}))
// Error related to the token exchange should be returned if signing fails
// otherwise, it should return a connection refused error since the goff URL is not set
// otherwise, it should return a connection refused error since the features-service URL is not set
if failSigning {
assert.ErrorContains(t, err, "failed to exchange token: error signing token", "should return an error when signing fails")
} else {
assert.ErrorContains(t, err, "connect: connection refused", "should return an error when goff url is not set")
assert.ErrorContains(t, err, "connect: connection refused", "should return an error when features-service url is not set")
}
}
+7 -7
View File
@@ -650,13 +650,6 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaDatavizSquad,
},
{
Name: "kubernetesFeatureToggles",
Description: "Use the kubernetes API for feature toggle management in the frontend",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaOperatorExperienceSquad,
},
{
Name: "cloudRBACRoles",
Description: "Enabled grafana cloud specific RBAC roles",
@@ -2090,6 +2083,13 @@ var (
FrontendOnly: false,
Owner: grafanaOperatorExperienceSquad,
},
{
Name: "profilesExemplars",
Description: "Enables profiles exemplars support in profiles drilldown",
Stage: FeatureStageExperimental,
Owner: grafanaObservabilityTracesAndProfilingSquad,
FrontendOnly: false,
},
}
)
+1 -1
View File
@@ -90,7 +90,6 @@ pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false
canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true
timeComparison,experimental,@grafana/dataviz-squad,false,false,true
tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true
kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true
cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false
alertingQueryOptimization,GA,@grafana/alerting-squad,false,false,false
jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false
@@ -283,3 +282,4 @@ useMTPlugins,experimental,@grafana/plugins-platform-backend,false,false,true
multiPropsVariables,experimental,@grafana/dashboards-squad,false,false,true
smoothingTransformation,experimental,@grafana/datapro,false,false,true
secretsManagementAppPlatformAwsKeeper,experimental,@grafana/grafana-operator-experience-squad,false,false,false
profilesExemplars,experimental,@grafana/observability-traces-and-profiling,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
90 canvasPanelPanZoom preview @grafana/dataviz-squad false false true
91 timeComparison experimental @grafana/dataviz-squad false false true
92 tableSharedCrosshair experimental @grafana/dataviz-squad false false true
kubernetesFeatureToggles experimental @grafana/grafana-operator-experience-squad false false true
93 cloudRBACRoles preview @grafana/identity-access-team false true false
94 alertingQueryOptimization GA @grafana/alerting-squad false false false
95 jitterAlertRulesWithinGroups preview @grafana/alerting-squad false true false
282 multiPropsVariables experimental @grafana/dashboards-squad false false true
283 smoothingTransformation experimental @grafana/datapro false false true
284 secretsManagementAppPlatformAwsKeeper experimental @grafana/grafana-operator-experience-squad false false false
285 profilesExemplars experimental @grafana/observability-traces-and-profiling false false false
+4
View File
@@ -785,4 +785,8 @@ const (
// FlagSecretsManagementAppPlatformAwsKeeper
// Enables the creation of keepers that manage secrets stored on AWS secrets manager
FlagSecretsManagementAppPlatformAwsKeeper = "secretsManagementAppPlatformAwsKeeper"
// FlagProfilesExemplars
// Enables profiles exemplars support in profiles drilldown
FlagProfilesExemplars = "profilesExemplars"
)
+14 -1
View File
@@ -2044,7 +2044,8 @@
"metadata": {
"name": "kubernetesFeatureToggles",
"resourceVersion": "1764664939750",
"creationTimestamp": "2024-01-18T05:32:44Z"
"creationTimestamp": "2024-01-18T05:32:44Z",
"deletionTimestamp": "2026-01-07T12:02:51Z"
},
"spec": {
"description": "Use the kubernetes API for feature toggle management in the frontend",
@@ -2866,6 +2867,18 @@
"expression": "true"
}
},
{
"metadata": {
"name": "profilesExemplars",
"resourceVersion": "1767777507980",
"creationTimestamp": "2026-01-07T09:18:27Z"
},
"spec": {
"description": "Enables profiles exemplars support in profiles drilldown",
"stage": "experimental",
"codeowner": "@grafana/observability-traces-and-profiling"
}
},
{
"metadata": {
"name": "prometheusAzureOverrideAudience",
@@ -14,6 +14,8 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
cloudmonitoring "github.com/grafana/grafana/pkg/tsdb/cloud-monitoring"
@@ -92,6 +94,10 @@ func NewRegistry(store map[string]backendplugin.PluginFactoryFunc) *Registry {
}
}
func ProvideCoreProvider(coreRegistry *Registry) plugins.BackendFactoryProvider {
return provider.New(coreRegistry.BackendFactoryProvider(), provider.DefaultProvider)
}
func ProvideCoreRegistry(tracer trace.Tracer, am *azuremonitor.Service, cw *cloudwatch.Service, cm *cloudmonitoring.Service,
es *elasticsearch.Service, grap *graphite.Service, idb *influxdb.Service, lk *loki.Service, otsdb *opentsdb.Service,
pr *prometheus.Service, t *tempo.Service, td *testdatasource.Service, pg *postgres.Service, my *mysql.Service,
@@ -156,7 +162,7 @@ func asBackendPlugin(svc any) backendplugin.PluginFactoryFunc {
if opts.QueryDataHandler != nil || opts.CallResourceHandler != nil ||
opts.CheckHealthHandler != nil || opts.StreamHandler != nil {
return New(opts)
return coreplugin.New(opts)
}
return nil
@@ -6,7 +6,6 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/auth"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/envvars"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
@@ -19,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins"
)
@@ -10,8 +10,6 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/auth"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
"github.com/grafana/grafana/pkg/plugins/envvars"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/client"
@@ -39,6 +37,7 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/angularinspector"
"github.com/grafana/grafana/pkg/services/pluginsintegration/angularpatternsstore"
"github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware"
"github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin"
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync"
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever"
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic"
@@ -146,8 +145,7 @@ var WireSet = wire.NewSet(
// WireExtensionSet provides a wire.ProviderSet of plugin providers that can be
// extended.
var WireExtensionSet = wire.NewSet(
provider.ProvideService,
wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)),
coreplugin.ProvideCoreProvider,
signature.ProvideOSSAuthorizer,
wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)),
ProvideClientWithMiddlewares,
@@ -19,10 +19,10 @@ import (
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration"
"github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/services/sqlstore"
@@ -8,8 +8,6 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
pluginsCfg "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/client"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
@@ -27,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/pluginassets"
"github.com/grafana/grafana/pkg/plugins/pluginerrs"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsources"
@@ -52,7 +51,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core
disc := pipeline.ProvideDiscoveryStage(pCfg, reg)
boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), pluginassets.NewLocalProvider())
valid := pipeline.ProvideValidationStage(pCfg, signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg)), angularInspector)
init := pipeline.ProvideInitializationStage(pCfg, reg, provider.ProvideService(coreRegistry), proc, &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), nil, tracing.InitializeTracerForTest(), provisionedplugins.NewNoop())
init := pipeline.ProvideInitializationStage(pCfg, reg, coreplugin.ProvideCoreProvider(coreRegistry), proc, &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), nil, tracing.InitializeTracerForTest(), provisionedplugins.NewNoop())
term, err := pipeline.ProvideTerminationStage(pCfg, reg, proc)
require.NoError(t, err)
@@ -98,7 +97,7 @@ func CreateTestLoader(t *testing.T, cfg *pluginsCfg.PluginManagementCfg, opts Lo
if opts.Initializer == nil {
reg := registry.ProvideService()
coreRegistry := coreplugin.NewRegistry(make(map[string]backendplugin.PluginFactoryFunc))
opts.Initializer = pipeline.ProvideInitializationStage(cfg, reg, provider.ProvideService(coreRegistry), process.ProvideService(), &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), nil, tracing.InitializeTracerForTest(), provisionedplugins.NewNoop())
opts.Initializer = pipeline.ProvideInitializationStage(cfg, reg, coreplugin.ProvideCoreProvider(coreRegistry), process.ProvideService(), &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), nil, tracing.InitializeTracerForTest(), provisionedplugins.NewNoop())
}
if opts.Terminator == nil {
+4 -4
View File
@@ -6,9 +6,9 @@ import (
)
const (
StaticProviderType = "static"
GOFFProviderType = "goff"
OFREPProviderType = "ofrep"
StaticProviderType = "static"
FeaturesServiceProviderType = "features-service"
OFREPProviderType = "ofrep"
)
type OpenFeatureSettings struct {
@@ -34,7 +34,7 @@ func (cfg *Cfg) readOpenFeatureSettings() error {
cfg.OpenFeature.TargetingKey = config.Key("targetingKey").MustString(defaultTargetingKey)
if strURL != "" && (cfg.OpenFeature.ProviderType == GOFFProviderType || cfg.OpenFeature.ProviderType == OFREPProviderType) {
if strURL != "" && (cfg.OpenFeature.ProviderType == FeaturesServiceProviderType || cfg.OpenFeature.ProviderType == OFREPProviderType) {
u, err := url.Parse(strURL)
if err != nil {
return fmt.Errorf("invalid feature provider url: %w", err)
+139
View File
@@ -37,6 +37,8 @@ type ZanzanaServerSettings struct {
OpenFGAHttpAddr string
// Cache settings
CacheSettings OpenFgaCacheSettings
// OpenFGA server settings
OpenFgaServerSettings OpenFgaServerSettings
// Max number of results returned by ListObjects() query. Default is 1000.
ListObjectsMaxResults uint32
// Deadline for the ListObjects() query. Default is 3 seconds.
@@ -50,6 +52,92 @@ type ZanzanaServerSettings struct {
AllowInsecure bool
}
type OpenFgaServerSettings struct {
// ListObjects settings
// Max number of concurrent datastore reads for ListObjects queries
MaxConcurrentReadsForListObjects uint32
// Enable dispatch throttling for ListObjects queries
ListObjectsDispatchThrottlingEnabled bool
// Frequency for dispatch throttling in ListObjects queries
ListObjectsDispatchThrottlingFrequency time.Duration
// Threshold for dispatch throttling in ListObjects queries
ListObjectsDispatchThrottlingThreshold uint32
// Max threshold for dispatch throttling in ListObjects queries
ListObjectsDispatchThrottlingMaxThreshold uint32
// Database throttle threshold for ListObjects queries
ListObjectsDatabaseThrottleThreshold int
// Database throttle duration for ListObjects queries
ListObjectsDatabaseThrottleDuration time.Duration
// ListUsers settings
// Deadline for ListUsers queries
ListUsersDeadline time.Duration
// Max number of results returned by ListUsers queries
ListUsersMaxResults uint32
// Max number of concurrent datastore reads for ListUsers queries
MaxConcurrentReadsForListUsers uint32
// Enable dispatch throttling for ListUsers queries
ListUsersDispatchThrottlingEnabled bool
// Frequency for dispatch throttling in ListUsers queries
ListUsersDispatchThrottlingFrequency time.Duration
// Threshold for dispatch throttling in ListUsers queries
ListUsersDispatchThrottlingThreshold uint32
// Max threshold for dispatch throttling in ListUsers queries
ListUsersDispatchThrottlingMaxThreshold uint32
// Database throttle threshold for ListUsers queries
ListUsersDatabaseThrottleThreshold int
// Database throttle duration for ListUsers queries
ListUsersDatabaseThrottleDuration time.Duration
// Check settings
// Max number of concurrent datastore reads for Check queries
MaxConcurrentReadsForCheck uint32
// Database throttle threshold for Check queries
CheckDatabaseThrottleThreshold int
// Database throttle duration for Check queries
CheckDatabaseThrottleDuration time.Duration
// Batch check settings
// Max number of concurrent checks per batch check request
MaxConcurrentChecksPerBatchCheck uint32
// Max number of checks per batch check request
MaxChecksPerBatchCheck uint32
// Resolve node settings
// Max number of nodes that can be resolved in a single query
ResolveNodeLimit uint32
// Max breadth of nodes that can be resolved in a single query
ResolveNodeBreadthLimit uint32
// Dispatch throttling settings for Check resolver
// Enable dispatch throttling for Check resolver
DispatchThrottlingCheckResolverEnabled bool
// Frequency for dispatch throttling in Check resolver
DispatchThrottlingCheckResolverFrequency time.Duration
// Threshold for dispatch throttling in Check resolver
DispatchThrottlingCheckResolverThreshold uint32
// Max threshold for dispatch throttling in Check resolver
DispatchThrottlingCheckResolverMaxThreshold uint32
// Shadow check/query settings
// Timeout for shadow check resolver
ShadowCheckResolverTimeout time.Duration
// Timeout for shadow ListObjects query
ShadowListObjectsQueryTimeout time.Duration
// Max delta items for shadow ListObjects query
ShadowListObjectsQueryMaxDeltaItems int
// Request settings
// Global request timeout
RequestTimeout time.Duration
// Max size in bytes for authorization model
MaxAuthorizationModelSizeInBytes int
// Size of the authorization model cache
AuthorizationModelCacheSize int
// Offset for changelog horizon
ChangelogHorizonOffset int
}
// Parameters to configure OpenFGA cache.
type OpenFgaCacheSettings struct {
// Number of items that will be kept in the in-memory cache used to resolve Check queries.
@@ -156,5 +244,56 @@ func (cfg *Cfg) readZanzanaSettings() {
zs.CacheSettings.SharedIteratorLimit = uint32(serverSec.Key("shared_iterator_limit").MustUint(1000))
zs.CacheSettings.SharedIteratorTTL = serverSec.Key("shared_iterator_ttl").MustDuration(10 * time.Second)
openfgaSec := cfg.SectionWithEnvOverrides("openfga")
// ListObjects settings
zs.OpenFgaServerSettings.MaxConcurrentReadsForListObjects = uint32(openfgaSec.Key("max_concurrent_reads_for_list_objects").MustUint(0))
zs.OpenFgaServerSettings.ListObjectsDispatchThrottlingEnabled = openfgaSec.Key("list_objects_dispatch_throttling_enabled").MustBool(false)
zs.OpenFgaServerSettings.ListObjectsDispatchThrottlingFrequency = openfgaSec.Key("list_objects_dispatch_throttling_frequency").MustDuration(0)
zs.OpenFgaServerSettings.ListObjectsDispatchThrottlingThreshold = uint32(openfgaSec.Key("list_objects_dispatch_throttling_threshold").MustUint(0))
zs.OpenFgaServerSettings.ListObjectsDispatchThrottlingMaxThreshold = uint32(openfgaSec.Key("list_objects_dispatch_throttling_max_threshold").MustUint(0))
zs.OpenFgaServerSettings.ListObjectsDatabaseThrottleThreshold = openfgaSec.Key("list_objects_database_throttle_threshold").MustInt(0)
zs.OpenFgaServerSettings.ListObjectsDatabaseThrottleDuration = openfgaSec.Key("list_objects_database_throttle_duration").MustDuration(0)
// ListUsers settings
zs.OpenFgaServerSettings.ListUsersDeadline = openfgaSec.Key("list_users_deadline").MustDuration(0)
zs.OpenFgaServerSettings.ListUsersMaxResults = uint32(openfgaSec.Key("list_users_max_results").MustUint(0))
zs.OpenFgaServerSettings.MaxConcurrentReadsForListUsers = uint32(openfgaSec.Key("max_concurrent_reads_for_list_users").MustUint(0))
zs.OpenFgaServerSettings.ListUsersDispatchThrottlingEnabled = openfgaSec.Key("list_users_dispatch_throttling_enabled").MustBool(false)
zs.OpenFgaServerSettings.ListUsersDispatchThrottlingFrequency = openfgaSec.Key("list_users_dispatch_throttling_frequency").MustDuration(0)
zs.OpenFgaServerSettings.ListUsersDispatchThrottlingThreshold = uint32(openfgaSec.Key("list_users_dispatch_throttling_threshold").MustUint(0))
zs.OpenFgaServerSettings.ListUsersDispatchThrottlingMaxThreshold = uint32(openfgaSec.Key("list_users_dispatch_throttling_max_threshold").MustUint(0))
zs.OpenFgaServerSettings.ListUsersDatabaseThrottleThreshold = openfgaSec.Key("list_users_database_throttle_threshold").MustInt(0)
zs.OpenFgaServerSettings.ListUsersDatabaseThrottleDuration = openfgaSec.Key("list_users_database_throttle_duration").MustDuration(0)
// Check settings
zs.OpenFgaServerSettings.MaxConcurrentReadsForCheck = uint32(openfgaSec.Key("max_concurrent_reads_for_check").MustUint(0))
zs.OpenFgaServerSettings.CheckDatabaseThrottleThreshold = openfgaSec.Key("check_database_throttle_threshold").MustInt(0)
zs.OpenFgaServerSettings.CheckDatabaseThrottleDuration = openfgaSec.Key("check_database_throttle_duration").MustDuration(0)
// Batch check settings
zs.OpenFgaServerSettings.MaxConcurrentChecksPerBatchCheck = uint32(openfgaSec.Key("max_concurrent_checks_per_batch_check").MustUint(0))
zs.OpenFgaServerSettings.MaxChecksPerBatchCheck = uint32(openfgaSec.Key("max_checks_per_batch_check").MustUint(0))
// Resolve node settings
zs.OpenFgaServerSettings.ResolveNodeLimit = uint32(openfgaSec.Key("resolve_node_limit").MustUint(0))
zs.OpenFgaServerSettings.ResolveNodeBreadthLimit = uint32(openfgaSec.Key("resolve_node_breadth_limit").MustUint(0))
// Dispatch throttling settings for Check resolver
zs.OpenFgaServerSettings.DispatchThrottlingCheckResolverEnabled = openfgaSec.Key("dispatch_throttling_check_resolver_enabled").MustBool(false)
zs.OpenFgaServerSettings.DispatchThrottlingCheckResolverFrequency = openfgaSec.Key("dispatch_throttling_check_resolver_frequency").MustDuration(0)
zs.OpenFgaServerSettings.DispatchThrottlingCheckResolverThreshold = uint32(openfgaSec.Key("dispatch_throttling_check_resolver_threshold").MustUint(0))
zs.OpenFgaServerSettings.DispatchThrottlingCheckResolverMaxThreshold = uint32(openfgaSec.Key("dispatch_throttling_check_resolver_max_threshold").MustUint(0))
// Shadow check/query settings
zs.OpenFgaServerSettings.ShadowCheckResolverTimeout = openfgaSec.Key("shadow_check_resolver_timeout").MustDuration(0)
zs.OpenFgaServerSettings.ShadowListObjectsQueryTimeout = openfgaSec.Key("shadow_list_objects_query_timeout").MustDuration(0)
zs.OpenFgaServerSettings.ShadowListObjectsQueryMaxDeltaItems = openfgaSec.Key("shadow_list_objects_query_max_delta_items").MustInt(0)
zs.OpenFgaServerSettings.RequestTimeout = openfgaSec.Key("request_timeout").MustDuration(0)
zs.OpenFgaServerSettings.MaxAuthorizationModelSizeInBytes = openfgaSec.Key("max_authorization_model_size_in_bytes").MustInt(0)
zs.OpenFgaServerSettings.AuthorizationModelCacheSize = openfgaSec.Key("authorization_model_cache_size").MustInt(0)
zs.OpenFgaServerSettings.ChangelogHorizonOffset = openfgaSec.Key("changelog_horizon_offset").MustInt(0)
cfg.ZanzanaServer = zs
}
+4 -3
View File
@@ -806,8 +806,10 @@ flowchart TD
#### Setting Dual Writer Mode
```ini
[unified_storage.{resource}.{kind}.{group}]
dualWriterMode = {0-5}
; [unified_storage.{resource}.{group}]
[unified_storage.dashboards.dashboard.grafana.app]
; modes {0-5}
dualWriterMode = 0
```
#### Background Sync Configuration
@@ -1376,4 +1378,3 @@ disable_data_migrations = false
### Documentation
For detailed information about migration architecture, validators, and troubleshooting, refer to [migrations/README.md](./migrations/README.md).

Some files were not shown because too many files have changed in this diff Show More