From 61eb837dd6d386ea1cb45cc794ff47f785105b42 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Wed, 5 Mar 2025 14:55:36 +0100 Subject: [PATCH 01/11] Chore: Bump Go to 1.23.7 (#101576) --- .drone.yml | 206 +++++++++--------- .github/workflows/pr-backend-code-checks.yml | 2 +- .github/workflows/pr-test-backend.yml | 2 +- Dockerfile | 2 +- Makefile | 2 +- apps/advisor/go.mod | 2 +- apps/alerting/notifications/go.mod | 2 +- apps/investigations/go.mod | 2 +- apps/playlist/go.mod | 2 +- .../docker/blocks/prometheus_high_card/go.mod | 2 +- devenv/docker/blocks/prometheus_utf8/go.mod | 2 +- .../docker/blocks/stateful_webhook/Dockerfile | 2 +- go.mod | 2 +- go.work | 2 +- hack/go.mod | 2 +- pkg/aggregator/go.mod | 4 +- pkg/apimachinery/go.mod | 2 +- pkg/apiserver/go.mod | 4 +- pkg/build/go.mod | 2 +- pkg/build/wire/go.mod | 2 +- pkg/codegen/go.mod | 2 +- pkg/plugins/codegen/go.mod | 2 +- pkg/promlib/go.mod | 4 +- pkg/semconv/go.mod | 2 +- pkg/storage/unified/apistore/go.mod | 2 +- pkg/storage/unified/resource/go.mod | 2 +- pkg/util/xorm/go.mod | 4 +- scripts/build/ci-windows-test/Dockerfile | 2 +- scripts/drone/variables.star | 2 +- scripts/go-workspace/go.mod | 2 +- 30 files changed, 132 insertions(+), 140 deletions(-) diff --git a/.drone.yml b/.drone.yml index 491c607a83b..dbcc1662f84 100644 --- a/.drone.yml +++ b/.drone.yml @@ -25,7 +25,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - ./bin/build verify-drone @@ -75,7 +75,7 @@ steps: - go install github.com/bazelbuild/buildtools/buildifier@latest - buildifier --lint=warn -mode=check -r . depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: lint-starlark trigger: event: @@ -437,7 +437,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -446,21 +446,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m depends_on: - wire-install - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: test-backend - commands: - apk add --update build-base @@ -469,7 +469,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: test-backend-integration trigger: event: @@ -524,7 +524,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - echo $(/usr/bin/github-app-external-token) > /github-app/token @@ -569,16 +569,16 @@ steps: - apk add --update make - make gen-go depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: validate-openapi-spec trigger: event: @@ -655,7 +655,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -665,7 +665,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -674,7 +674,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-jsonnet - commands: - yarn install --immutable || yarn install --immutable @@ -712,7 +712,7 @@ steps: - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 -a targz:grafana:linux/arm/v7 -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 - -a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.5 --yarn-cache=$$YARN_CACHE_FOLDER + -a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.7 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.21.3 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' --verify='false' --grafana-dir=$$PWD @@ -1110,7 +1110,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -1124,7 +1124,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1133,14 +1133,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -1161,7 +1161,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -1182,7 +1182,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -1198,7 +1198,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -1214,7 +1214,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -1230,7 +1230,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: remote-alertmanager-integration-tests trigger: event: @@ -1312,7 +1312,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue trigger: event: @@ -1433,7 +1433,7 @@ steps: && return 1; fi depends_on: - clone-enterprise - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: swagger-gen trigger: event: @@ -1538,7 +1538,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1549,7 +1549,7 @@ steps: - CODEGEN_VERIFY=1 make gen-cue depends_on: - clone-enterprise - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1559,14 +1559,14 @@ steps: - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: - clone-enterprise - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - apk add --update build-base @@ -1574,7 +1574,7 @@ steps: - go test -v -run=^$ -benchmem -timeout=1h -count=8 -bench=. ${GO_PACKAGES} depends_on: - wire-install - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: sqlite-benchmark-integration-tests - commands: - apk add --update build-base @@ -1586,7 +1586,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: postgres-benchmark-integration-tests - commands: - apk add --update build-base @@ -1597,7 +1597,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: mysql-8.0-benchmark-integration-tests trigger: event: @@ -1669,7 +1669,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue trigger: branch: main @@ -1852,7 +1852,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -1861,21 +1861,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m depends_on: - wire-install - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: test-backend - commands: - apk add --update build-base @@ -1884,7 +1884,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: test-backend-integration trigger: branch: main @@ -1929,22 +1929,22 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - apk add --update make - make gen-go depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: validate-openapi-spec - commands: - ./bin/build verify-drone @@ -2076,7 +2076,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -2086,7 +2086,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2095,7 +2095,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-jsonnet - commands: - yarn install --immutable || yarn install --immutable @@ -2132,7 +2132,7 @@ steps: - /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64 -a targz:grafana:linux/arm/v7 -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 - -a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.5 --yarn-cache=$$YARN_CACHE_FOLDER + -a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.7 --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.21.3 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' --verify='false' --grafana-dir=$$PWD @@ -2607,7 +2607,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -2621,7 +2621,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -2630,14 +2630,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -2658,7 +2658,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -2679,7 +2679,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -2695,7 +2695,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -2711,7 +2711,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -2727,7 +2727,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: remote-alertmanager-integration-tests trigger: branch: main @@ -2996,7 +2996,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3005,21 +3005,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m depends_on: - wire-install - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: test-backend - commands: - apk add --update build-base @@ -3028,7 +3028,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: test-backend-integration trigger: branch: @@ -3071,22 +3071,22 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - apk add --update make - make gen-go depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - go run scripts/modowners/modowners.go check go.mod - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: validate-modfile - commands: - apk add --update make - make swagger-validate - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: validate-openapi-spec trigger: branch: @@ -3165,7 +3165,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME @@ -3179,7 +3179,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -3188,14 +3188,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -3216,7 +3216,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -3237,7 +3237,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -3253,7 +3253,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -3269,7 +3269,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -3285,7 +3285,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: remote-alertmanager-integration-tests trigger: branch: @@ -3385,7 +3385,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - ./bin/build artifacts docker fetch --edition oss @@ -3517,7 +3517,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - ./bin/build artifacts docker fetch --edition oss @@ -3658,7 +3658,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - ./bin/build artifacts packages --artifacts-editions=oss --tag $${DRONE_TAG} --src-bucket @@ -3750,7 +3750,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - yarn install --immutable || yarn install --immutable @@ -3850,7 +3850,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - depends_on: - compile-build-cmd @@ -3947,7 +3947,7 @@ steps: depends_on: [] environment: CGO_ENABLED: 0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: compile-build-cmd - commands: - ./bin/build publish grafana-com --edition oss ${DRONE_TAG} @@ -4009,7 +4009,7 @@ steps: from_secret: grafana_api_key GCP_KEY_BASE64: from_secret: gcp_key_base64 - GO_VERSION: 1.23.5 + GO_VERSION: 1.23.7 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -4084,7 +4084,7 @@ steps: from_secret: grafana_api_key GCP_KEY_BASE64: from_secret: gcp_key_base64 - GO_VERSION: 1.23.5 + GO_VERSION: 1.23.7 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -4201,7 +4201,7 @@ steps: from_secret: grafana_api_key GCP_KEY_BASE64: from_secret: gcp_key_base64 - GO_VERSION: 1.23.5 + GO_VERSION: 1.23.7 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -4352,7 +4352,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -4361,21 +4361,21 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - apk add --update build-base shared-mime-info shared-mime-info-lang - go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m depends_on: - wire-install - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: test-backend - commands: - apk add --update build-base @@ -4384,7 +4384,7 @@ steps: | grep -o '\(.*\)/' | sort -u) depends_on: - wire-install - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: test-backend-integration trigger: cron: @@ -4438,7 +4438,7 @@ steps: from_secret: grafana_api_key GCP_KEY_BASE64: from_secret: gcp_key_base64 - GO_VERSION: 1.23.5 + GO_VERSION: 1.23.7 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -4582,7 +4582,7 @@ steps: from_secret: grafana_api_key GCP_KEY_BASE64: from_secret: gcp_key_base64 - GO_VERSION: 1.23.5 + GO_VERSION: 1.23.7 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -4689,7 +4689,7 @@ steps: - export GITHUB_TOKEN=$(cat /github-app/token) - dagger run --silent /src/grafana-build artifacts -a $${ARTIFACTS} --grafana-ref=$${GRAFANA_REF} --enterprise-ref=$${ENTERPRISE_REF} --grafana-repo=$${GRAFANA_REPO} --version=$${VERSION} - --go-version=1.23.5 + --go-version=1.23.7 depends_on: - github-app-generate-token environment: @@ -4710,7 +4710,7 @@ steps: from_secret: grafana_api_key GCP_KEY_BASE64: from_secret: gcp_key_base64 - GO_VERSION: 1.23.5 + GO_VERSION: 1.23.7 GPG_PASSPHRASE: from_secret: packages_gpg_passphrase GPG_PRIVATE_KEY: @@ -4848,7 +4848,7 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-cue depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-cue - commands: - '# It is required that generated jsonnet is committed and in sync with its inputs.' @@ -4857,14 +4857,14 @@ steps: - apk add --update make - CODEGEN_VERIFY=1 make gen-jsonnet depends_on: [] - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: verify-gen-jsonnet - commands: - apk add --update make - make gen-go depends_on: - verify-gen-cue - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: wire-install - commands: - dockerize -wait tcp://postgres:5432 -timeout 120s @@ -4885,7 +4885,7 @@ steps: GRAFANA_TEST_DB: postgres PGPASSWORD: grafanatest POSTGRES_HOST: postgres - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: postgres-integration-tests - commands: - dockerize -wait tcp://mysql80:3306 -timeout 120s @@ -4906,7 +4906,7 @@ steps: environment: GRAFANA_TEST_DB: mysql MYSQL_HOST: mysql80 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: mysql-8.0-integration-tests - commands: - dockerize -wait tcp://redis:6379 -timeout 120s @@ -4922,7 +4922,7 @@ steps: - wait-for-redis environment: REDIS_URL: redis://redis:6379/0 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: redis-integration-tests - commands: - dockerize -wait tcp://memcached:11211 -timeout 120s @@ -4938,7 +4938,7 @@ steps: - wait-for-memcached environment: MEMCACHED_HOSTS: memcached:11211 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: memcached-integration-tests - commands: - dockerize -wait tcp://mimir_backend:8080 -timeout 120s @@ -4954,7 +4954,7 @@ steps: environment: AM_TENANT_ID: test AM_URL: http://mimir_backend:8080 - image: golang:1.23.5-alpine + image: golang:1.23.7-alpine name: remote-alertmanager-integration-tests trigger: event: @@ -5257,7 +5257,7 @@ steps: - commands: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM docker:27-cli - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine/git:2.40.1 - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.23.5-alpine + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.23.7-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:22.11.0-alpine - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:22-bookworm - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 @@ -5295,7 +5295,7 @@ steps: - commands: - trivy --exit-code 1 --severity HIGH,CRITICAL docker:27-cli - trivy --exit-code 1 --severity HIGH,CRITICAL alpine/git:2.40.1 - - trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.23.5-alpine + - trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.23.7-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL node:22.11.0-alpine - trivy --exit-code 1 --severity HIGH,CRITICAL node:22-bookworm - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 @@ -5564,6 +5564,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: f16a4715c7a4e6a4ffb1fe041b42fb966310fd5da455239614e9a239493aff82 +hmac: 33f2e5615dfd7889899f9f8f16f7716190fa637fe98f1efd7e29607f8946be7d ... diff --git a/.github/workflows/pr-backend-code-checks.yml b/.github/workflows/pr-backend-code-checks.yml index bd26963ad39..daf48f4c391 100644 --- a/.github/workflows/pr-backend-code-checks.yml +++ b/.github/workflows/pr-backend-code-checks.yml @@ -21,7 +21,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.23.5' + go-version: '1.23.7' cache: true - name: Validate go.mod diff --git a/.github/workflows/pr-test-backend.yml b/.github/workflows/pr-test-backend.yml index da7e3450a54..6246fe5837d 100644 --- a/.github/workflows/pr-test-backend.yml +++ b/.github/workflows/pr-test-backend.yml @@ -33,7 +33,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.23.5' + go-version: '1.23.7' cache: true - name: Install dependencies diff --git a/Dockerfile b/Dockerfile index 09cba46da26..1ba856b9220 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ARG BASE_IMAGE=alpine:3.21 ARG JS_IMAGE=node:22-alpine ARG JS_PLATFORM=linux/amd64 -ARG GO_IMAGE=golang:1.23.5-alpine +ARG GO_IMAGE=golang:1.23.7-alpine # Default to building locally ARG GO_SRC=go-builder diff --git a/Makefile b/Makefile index bfc5491e782..f2f37e34812 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ WIRE_TAGS = "oss" include .bingo/Variables.mk GO = go -GO_VERSION = 1.23.5 +GO_VERSION = 1.23.7 GO_LINT_FILES ?= $(shell ./scripts/go-workspace/golangci-lint-includes.sh) GO_TEST_FILES ?= $(shell ./scripts/go-workspace/test-includes.sh) SH_FILES ?= $(shell find ./scripts -name *.sh) diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index df422dc8f68..0815c5ee5ae 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/apps/advisor -go 1.23.4 +go 1.23.7 require ( github.com/grafana/grafana-app-sdk v0.31.0 diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index ff1ac050736..748cef99505 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/apps/alerting/notifications -go 1.23.5 +go 1.23.7 replace github.com/grafana/grafana => ../../.. diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 7c8d9255258..bd1e9dd6f50 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/apps/investigations -go 1.23.4 +go 1.23.7 require ( github.com/grafana/grafana-app-sdk v0.31.0 diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index f42605d0aff..8e514c0674e 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/apps/playlist -go 1.23.4 +go 1.23.7 require ( github.com/grafana/grafana-app-sdk v0.31.0 diff --git a/devenv/docker/blocks/prometheus_high_card/go.mod b/devenv/docker/blocks/prometheus_high_card/go.mod index be940ff6862..53f02f9b01c 100644 --- a/devenv/docker/blocks/prometheus_high_card/go.mod +++ b/devenv/docker/blocks/prometheus_high_card/go.mod @@ -1,6 +1,6 @@ module high-card -go 1.22.4 +go 1.23.7 require ( github.com/prometheus/client_golang v1.20.2 diff --git a/devenv/docker/blocks/prometheus_utf8/go.mod b/devenv/docker/blocks/prometheus_utf8/go.mod index 706d2d12e81..baa4113ec94 100644 --- a/devenv/docker/blocks/prometheus_utf8/go.mod +++ b/devenv/docker/blocks/prometheus_utf8/go.mod @@ -1,6 +1,6 @@ module utf8-support -go 1.22.4 +go 1.23.7 require ( github.com/prometheus/client_golang v1.20.5 diff --git a/devenv/docker/blocks/stateful_webhook/Dockerfile b/devenv/docker/blocks/stateful_webhook/Dockerfile index 03b50db2135..4b671c20410 100644 --- a/devenv/docker/blocks/stateful_webhook/Dockerfile +++ b/devenv/docker/blocks/stateful_webhook/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23.5 +FROM golang:1.23.7 ADD main.go /go/src/webhook/main.go diff --git a/go.mod b/go.mod index 1c7630d7fbf..864cb848070 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana -go 1.23.5 +go 1.23.7 require ( buf.build/gen/go/parca-dev/parca/connectrpc/go v1.17.0-20240902100956-02fd72488966.1 // @grafana/observability-traces-and-profiling diff --git a/go.work b/go.work index 4403fc62dd9..2d53f72c4df 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.23.5 +go 1.23.7 // The `skip:golangci-lint` comment tag is used to exclude the package from the `golangci-lint` GitHub Action. // The module at the root of the repo (`.`) is excluded because ./pkg/... is included manually in the `golangci-lint` configuration. diff --git a/hack/go.mod b/hack/go.mod index 669cabb8401..ac55ce76add 100644 --- a/hack/go.mod +++ b/hack/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/hack -go 1.23.1 +go 1.23.7 require k8s.io/code-generator v0.32.0 diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index 4d707fc4210..60752a5eaf1 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -1,8 +1,6 @@ module github.com/grafana/grafana/pkg/aggregator -go 1.23.5 - -toolchain go1.23.6 +go 1.23.7 require ( github.com/emicklei/go-restful/v3 v3.11.0 diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index a01ad0639a7..b0d88688d0b 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/apimachinery -go 1.23.1 +go 1.23.7 require ( github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 // @grafana/identity-access-team diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index de4e560a09e..b096ad30caa 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -1,8 +1,6 @@ module github.com/grafana/grafana/pkg/apiserver -go 1.23.4 - -toolchain go1.23.6 +go 1.23.7 require ( github.com/google/go-cmp v0.7.0 diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 325e87190ea..a2a9d74e185 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/build -go 1.23.1 +go 1.23.7 // Override docker/docker to avoid: // go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires diff --git a/pkg/build/wire/go.mod b/pkg/build/wire/go.mod index 61a2bb2b9db..32bfb68353e 100644 --- a/pkg/build/wire/go.mod +++ b/pkg/build/wire/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/build/wire -go 1.23.1 +go 1.23.7 require ( github.com/google/go-cmp v0.7.0 diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index f6f900fc6b8..ba0e8352134 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/codegen -go 1.23.1 +go 1.23.7 require ( cuelang.org/go v0.11.1 diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index 8205acbb824..7939dc3de06 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/plugins/codegen -go 1.23.1 +go 1.23.7 replace github.com/grafana/grafana/pkg/codegen => ../../codegen diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 9195b58d961..dbe61b901bf 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -1,8 +1,6 @@ module github.com/grafana/grafana/pkg/promlib -go 1.23.5 - -toolchain go1.23.6 +go 1.23.7 require ( github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 diff --git a/pkg/semconv/go.mod b/pkg/semconv/go.mod index d015899f50c..34662abfac7 100644 --- a/pkg/semconv/go.mod +++ b/pkg/semconv/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/semconv -go 1.23.1 +go 1.23.7 require go.opentelemetry.io/otel v1.34.0 diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index e695a670216..d61b0516d79 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/storage/unified/apistore -go 1.23.5 +go 1.23.7 replace ( github.com/grafana/grafana => ../../../.. diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 3cc30478e1f..7f5e3f06001 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -1,6 +1,6 @@ module github.com/grafana/grafana/pkg/storage/unified/resource -go 1.23.5 +go 1.23.7 replace ( github.com/grafana/grafana => ../../../.. diff --git a/pkg/util/xorm/go.mod b/pkg/util/xorm/go.mod index dbe7c972121..dbf7d9a0684 100644 --- a/pkg/util/xorm/go.mod +++ b/pkg/util/xorm/go.mod @@ -1,8 +1,6 @@ module github.com/grafana/grafana/pkg/util/xorm -go 1.23.0 - -toolchain go1.24.0 +go 1.23.7 require ( github.com/googleapis/go-sql-spanner v1.11.1 diff --git a/scripts/build/ci-windows-test/Dockerfile b/scripts/build/ci-windows-test/Dockerfile index 86dadef8e4c..150a4b1e3d6 100644 --- a/scripts/build/ci-windows-test/Dockerfile +++ b/scripts/build/ci-windows-test/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20.3-windowsservercore-1809 +FROM golang:1.23.7-windowsservercore-1809 SHELL ["powershell", "-command"] diff --git a/scripts/drone/variables.star b/scripts/drone/variables.star index 6eeb6cfa03c..c737ef65d61 100644 --- a/scripts/drone/variables.star +++ b/scripts/drone/variables.star @@ -3,7 +3,7 @@ global variables """ grabpl_version = "v3.1.2" -golang_version = "1.23.5" +golang_version = "1.23.7" # nodejs_version should match what's in ".nvmrc", but without the v prefix. nodejs_version = "22.11.0" diff --git a/scripts/go-workspace/go.mod b/scripts/go-workspace/go.mod index 3dd4675a42d..c8134977d9f 100644 --- a/scripts/go-workspace/go.mod +++ b/scripts/go-workspace/go.mod @@ -1,5 +1,5 @@ module github.com/grafana/grafana/scripts/go-workspace -go 1.23.1 +go 1.23.7 require golang.org/x/mod v0.20.0 From da2c382d80052dacabeeca9c82f052dab779703b Mon Sep 17 00:00:00 2001 From: colin-stuart Date: Wed, 5 Mar 2025 09:02:01 -0500 Subject: [PATCH 02/11] Tests: Add SearchOrgUsersFn to FakeOrgService (#101589) add SearchOrgUsersFn to FakeOrgService --- pkg/services/org/orgtest/fake.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/services/org/orgtest/fake.go b/pkg/services/org/orgtest/fake.go index 0ec1c6e848c..6c0c08e579e 100644 --- a/pkg/services/org/orgtest/fake.go +++ b/pkg/services/org/orgtest/fake.go @@ -20,6 +20,7 @@ type FakeOrgService struct { ExpectedOrgUsers []*org.OrgUserDTO ExpectedSearchOrgUsersResult *org.SearchOrgUsersQueryResult ExpectedOrgListResponse OrgListResponse + SearchOrgUsersFn func(context.Context, *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) } func NewOrgServiceFake() *FakeOrgService { @@ -98,6 +99,9 @@ func (f *FakeOrgService) RemoveOrgUser(ctx context.Context, cmd *org.RemoveOrgUs } func (f *FakeOrgService) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) { + if f.SearchOrgUsersFn != nil { + return f.SearchOrgUsersFn(ctx, query) + } return f.ExpectedSearchOrgUsersResult, f.ExpectedError } From 374380d1f6ab05e98bc00b0b0fdaa076d1d30ffa Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 5 Mar 2025 09:15:26 -0500 Subject: [PATCH 03/11] Alerting: Keep the latest version of deleted rule in version table (#101481) * add feature toggle alertRuleRestore * Update delete rule to require UserUID, remove all versions and create "delete" version that holds information about who and when deleted the rule --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 ++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 16 ++++ .../ngalert/api/api_provisioning_test.go | 5 +- pkg/services/ngalert/api/api_ruler.go | 4 +- pkg/services/ngalert/api/api_ruler_test.go | 2 +- pkg/services/ngalert/api/persist.go | 2 +- .../ngalert/provisioning/alert_rules.go | 10 +-- .../ngalert/provisioning/alert_rules_test.go | 29 +++++-- pkg/services/ngalert/provisioning/persist.go | 2 +- pkg/services/ngalert/store/alert_rule.go | 81 ++++++++++++++++-- pkg/services/ngalert/store/alert_rule_test.go | 85 +++++++++++++++++-- pkg/services/ngalert/tests/fakes/rules.go | 4 +- 16 files changed, 215 insertions(+), 39 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 7ceb0dc3460..6850b23da88 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -117,6 +117,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `improvedExternalSessionHandling` | Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves. | | `elasticsearchCrossClusterSearch` | Enables cross cluster search in the Elasticsearch datasource | | `improvedExternalSessionHandlingSAML` | Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly. | +| `alertRuleRestore` | Enables the alert rule restore feature | ## Experimental feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 7e3ac0d6dba..adc5f4641e8 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -255,4 +255,5 @@ export interface FeatureToggles { newShareReportDrawer?: boolean; rendererDisableAppPluginsPreload?: boolean; assetSriChecks?: boolean; + alertRuleRestore?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 27da9533003..89efcfe082e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1782,6 +1782,13 @@ var ( Owner: grafanaFrontendOpsWG, FrontendOnly: true, }, + { + Name: "alertRuleRestore", + Description: "Enables the alert rule restore feature", + Stage: FeatureStagePublicPreview, + Owner: grafanaAlertingSquad, + Expression: "true", // enabled by default + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index ace09d69f56..5c19a1a1211 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -236,3 +236,4 @@ alertingRuleVersionHistoryRestore,GA,@grafana/alerting-squad,false,false,true newShareReportDrawer,experimental,@grafana/sharing-squad,false,false,false rendererDisableAppPluginsPreload,experimental,@grafana/sharing-squad,false,false,true assetSriChecks,experimental,@grafana/frontend-ops,false,false,true +alertRuleRestore,preview,@grafana/alerting-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index d651c7290f6..fde91f9bc4e 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -954,4 +954,8 @@ const ( // FlagAssetSriChecks // Enables SRI checks for Grafana JavaScript assets FlagAssetSriChecks = "assetSriChecks" + + // FlagAlertRuleRestore + // Enables the alert rule restore feature + FlagAlertRuleRestore = "alertRuleRestore" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index b093cb5dd21..9cb6e99b789 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -108,6 +108,22 @@ "frontend": true } }, + { + "metadata": { + "name": "alertRuleRestore", + "resourceVersion": "1741127758142", + "creationTimestamp": "2025-03-04T22:29:36Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-03-04 22:35:58.1421143 +0000 UTC" + } + }, + "spec": { + "description": "Enables the alert rule restore feature", + "stage": "preview", + "codeowner": "@grafana/alerting-squad", + "expression": "true" + } + }, { "metadata": { "name": "alertStateHistoryLokiOnly", diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 297a85ad488..14e1e571f12 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -1930,8 +1930,9 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { Cfg: setting.UnifiedAlertingSettings{ BaseInterval: time.Second * 10, }, - FolderService: folderService, - Bus: bus.ProvideBus(tracing.InitializeTracerForTest()), + FolderService: folderService, + Bus: bus.ProvideBus(tracing.InitializeTracerForTest()), + FeatureToggles: featuremgmt.WithFeatures(), } user := &user.SignedInUser{ OrgID: 1, diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 359b3c1a5b7..2d88ae849e1 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -157,7 +157,7 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceU rulesToDelete = append(rulesToDelete, uid...) } if len(rulesToDelete) > 0 { - err := srv.store.DeleteAlertRulesByUID(ctx, c.SignedInUser.GetOrgID(), rulesToDelete...) + err := srv.store.DeleteAlertRulesByUID(ctx, c.SignedInUser.GetOrgID(), ngmodels.NewUserUID(c.SignedInUser), rulesToDelete...) if err != nil { return err } @@ -461,7 +461,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey UIDs = append(UIDs, rule.UID) } - if err = srv.store.DeleteAlertRulesByUID(tranCtx, c.SignedInUser.GetOrgID(), UIDs...); err != nil { + if err = srv.store.DeleteAlertRulesByUID(tranCtx, c.SignedInUser.GetOrgID(), ngmodels.NewUserUID(c.SignedInUser), UIDs...); err != nil { return fmt.Errorf("failed to delete rules: %w", err) } } diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index 17b0d8334ab..55481e4faf3 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -60,7 +60,7 @@ func TestRouteDeleteAlertRules(t *testing.T) { deleteCommands := getRecordedCommand(ruleStore) require.Len(t, deleteCommands, 1) cmd := deleteCommands[0] - actualUIDs := cmd.Params[1].([]string) + actualUIDs := cmd.Params[2].([]string) require.Len(t, actualUIDs, len(expectedRules)) for _, rule := range expectedRules { require.Containsf(t, actualUIDs, rule.UID, "Rule %s was expected to be deleted but it wasn't", rule.UID) diff --git a/pkg/services/ngalert/api/persist.go b/pkg/services/ngalert/api/persist.go index d2e81d833e2..b169be59c07 100644 --- a/pkg/services/ngalert/api/persist.go +++ b/pkg/services/ngalert/api/persist.go @@ -28,7 +28,7 @@ type RuleStore interface { // and return the map of uuid to id. InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.AlertRule) ([]ngmodels.AlertRuleKeyWithId, error) UpdateAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.UpdateRule) error - DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error + DeleteAlertRulesByUID(ctx context.Context, orgID int64, user *ngmodels.UserUID, ruleUID ...string) error // IncreaseVersionForAllRulesInNamespaces Increases version for all rules that have specified namespace uids IncreaseVersionForAllRulesInNamespaces(ctx context.Context, orgID int64, namespaceUIDs []string) ([]ngmodels.AlertRuleKeyWithVersion, error) diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index 17b95ef314d..c178c5683c9 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -556,7 +556,7 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity }) } } - if err := service.deleteRules(ctx, user.GetOrgID(), delta.Delete...); err != nil { + if err := service.deleteRules(ctx, user, delta.Delete...); err != nil { return err } } @@ -749,7 +749,7 @@ func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, user ident // This is different from deleting groups. We delete the rules directly rather than persisting a delta here to keep the semantics the same. // TODO: Either persist a delta here as a breaking change, or deprecate this endpoint in favor of the group endpoint. return service.xact.InTransaction(ctx, func(ctx context.Context) error { - return service.deleteRules(ctx, user.GetOrgID(), rule) + return service.deleteRules(ctx, user, rule) }) } @@ -775,18 +775,18 @@ func (service *AlertRuleService) checkLimitsTransactionCtx(ctx context.Context, } // deleteRules deletes a set of target rules and associated data, while checking for database consistency. -func (service *AlertRuleService) deleteRules(ctx context.Context, orgID int64, targets ...*models.AlertRule) error { +func (service *AlertRuleService) deleteRules(ctx context.Context, user identity.Requester, targets ...*models.AlertRule) error { uids := make([]string, 0, len(targets)) for _, tgt := range targets { if tgt != nil { uids = append(uids, tgt.UID) } } - if err := service.ruleStore.DeleteAlertRulesByUID(ctx, orgID, uids...); err != nil { + if err := service.ruleStore.DeleteAlertRulesByUID(ctx, user.GetOrgID(), models.NewUserUID(user), uids...); err != nil { return err } for _, uid := range uids { - if err := service.provenanceStore.DeleteProvenance(ctx, &models.AlertRule{UID: uid}, orgID); err != nil { + if err := service.provenanceStore.DeleteProvenance(ctx, &models.AlertRule{UID: uid}, user.GetOrgID()); err != nil { // We failed to clean up the record, but this doesn't break things. Log it and move on. service.log.Warn("Failed to delete provenance record for rule: %w", err) } diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index 491d084c096..c98c293fd34 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -1726,7 +1726,7 @@ func TestDeleteRuleGroup(t *testing.T) { func TestDeleteRuleGroups(t *testing.T) { orgID1 := rand.Int63() orgID2 := rand.Int63() - u := &user.SignedInUser{OrgID: orgID1} + u := &user.SignedInUser{OrgID: orgID1, UserUID: "test-test"} // Create groups across different orgs and namespaces groupKey1 := models.AlertRuleGroupKey{ @@ -1805,6 +1805,7 @@ func TestDeleteRuleGroups(t *testing.T) { // Verify only rules from group1 in org1 were deleted deletes := getDeletedRules(t, ruleStore) require.Len(t, deletes, 1) + require.Equal(t, "test-test", deletes[0].userID) require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids) }) @@ -2045,8 +2046,9 @@ func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery { } type deleteRuleOperation struct { - orgID int64 - uids []string + orgID int64 + userID string + uids []string } func getDeletedRules(t *testing.T, ruleStore *fakes.RuleStore) []deleteRuleOperation { @@ -2058,12 +2060,20 @@ func getDeletedRules(t *testing.T, ruleStore *fakes.RuleStore) []deleteRuleOpera orgID, ok := q.Params[0].(int64) require.True(t, ok, "orgID parameter should be int64") - uids, ok := q.Params[1].([]string) + uid := "" + userUID, ok := q.Params[1].(*models.UserUID) + require.True(t, ok, "parameter should be UserUID") + if userUID != nil { + uid = string(*userUID) + } + + uids, ok := q.Params[2].([]string) require.True(t, ok, "uids parameter should be []string") operations = append(operations, deleteRuleOperation{ - orgID: orgID, - uids: uids, + orgID: orgID, + userID: uid, + uids: uids, }) } return operations @@ -2077,9 +2087,10 @@ func createAlertRuleService(t *testing.T, folderService folder.Service) AlertRul Cfg: setting.UnifiedAlertingSettings{ BaseInterval: time.Second * 10, }, - Logger: log.NewNopLogger(), - FolderService: folderService, - Bus: bus.ProvideBus(tracing.InitializeTracerForTest()), + Logger: log.NewNopLogger(), + FolderService: folderService, + Bus: bus.ProvideBus(tracing.InitializeTracerForTest()), + FeatureToggles: featuremgmt.WithFeatures(), } // store := fakes.NewRuleStore(t) quotas := MockQuotaChecker{} diff --git a/pkg/services/ngalert/provisioning/persist.go b/pkg/services/ngalert/provisioning/persist.go index f41ea602927..bd85502d2c2 100644 --- a/pkg/services/ngalert/provisioning/persist.go +++ b/pkg/services/ngalert/provisioning/persist.go @@ -35,7 +35,7 @@ type RuleStore interface { GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error) InsertAlertRules(ctx context.Context, user *models.UserUID, rule []models.AlertRule) ([]models.AlertRuleKeyWithId, error) UpdateAlertRules(ctx context.Context, user *models.UserUID, rule []models.UpdateRule) error - DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error + DeleteAlertRulesByUID(ctx context.Context, orgID int64, user *models.UserUID, ruleUID ...string) error GetAlertRulesGroupByRuleUID(ctx context.Context, query *models.GetAlertRulesGroupByRuleUIDQuery) ([]*models.AlertRule, error) } diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 089543d51b2..47ab409c63a 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -39,7 +39,10 @@ var ( ) // DeleteAlertRulesByUID is a handler for deleting an alert rule. -func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error { +func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, user *ngmodels.UserUID, ruleUID ...string) error { + if len(ruleUID) == 0 { + return nil + } logger := st.Logger.New("org_id", orgID, "rule_uids", ruleUID) return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { rows, err := sess.Table(alertRule{}).Where("org_id = ?", orgID).In("uid", ruleUID).Delete(alertRule{}) @@ -57,12 +60,6 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI }) } - rows, err = sess.Table(alertRuleVersion{}).Where("rule_org_id = ?", orgID).In("rule_uid", ruleUID).Delete(alertRule{}) - if err != nil { - return err - } - logger.Debug("Deleted alert rule versions", "count", rows) - rows, err = sess.Table("alert_instance").Where("rule_org_id = ?", orgID).In("rule_uid", ruleUID).Delete(alertRule{}) if err != nil { return err @@ -75,10 +72,78 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI } logger.Debug("Deleted alert rule state", "count", rows) + var versions []alertRuleVersion + if st.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertRuleRestore) { + versions, err = st.getLatestVersionOfRulesByUID(ctx, orgID, ruleUID) + if err != nil { + logger.Error("Failed to get latest version of deleted alert rules. The recovery will not be possible", "error", err) + } + for idx := range versions { + version := &versions[idx] + version.ID = 0 + version.RuleUID = "" + version.Created = TimeNow() + version.CreatedBy = nil + if user != nil { + version.CreatedBy = util.Pointer(string(*user)) + } + } + } + + rows, err = sess.Table(alertRuleVersion{}).Where("rule_org_id = ?", orgID).In("rule_uid", ruleUID).Delete(alertRule{}) + if err != nil { + return err + } + logger.Debug("Deleted alert rule versions", "count", rows) + + if len(versions) > 0 { + _, err = sess.Insert(versions) + if err != nil { + return fmt.Errorf("failed to persist deleted rule for recovery: %w", err) + } + logger.Debug("Inserted alert rule versions for recovery", "count", len(versions)) + } return nil }) } +func (st DBstore) getLatestVersionOfRulesByUID(ctx context.Context, orgID int64, ruleUIDs []string) ([]alertRuleVersion, error) { + var result []alertRuleVersion + err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { + args, in := getINSubQueryArgs(ruleUIDs) + // take only the latest versions of each rule by GUID + rows, err := sess.SQL(fmt.Sprintf(` + SELECT v1.* FROM alert_rule_version AS v1 + INNER JOIN ( + SELECT rule_guid, MAX(id) AS id + FROM alert_rule_version + WHERE rule_org_id = ? + AND rule_uid IN (%s) + GROUP BY rule_guid + ) AS v2 ON v1.rule_guid = v2.rule_guid AND v1.id = v2.id + `, strings.Join(in, ",")), append([]any{orgID}, args...)...).Rows(new(alertRuleVersion)) + + if err != nil { + return err + } + result = make([]alertRuleVersion, 0, len(ruleUIDs)) + for rows.Next() { + rule := new(alertRuleVersion) + err = rows.Scan(rule) + if err != nil { + st.Logger.Error("Invalid rule version found in DB store, ignoring it", "func", "getLatestVersionOfRulesByUID", "error", err) + continue + } + result = append(result, *rule) + } + return nil + }) + if err != nil { + return nil, err + } + return result, nil +} + // IncreaseVersionForAllRulesInNamespaces Increases version for all rules that have specified namespace. Returns all rules that belong to the namespaces func (st DBstore) IncreaseVersionForAllRulesInNamespaces(ctx context.Context, orgID int64, namespaceUIDs []string) ([]ngmodels.AlertRuleKeyWithVersion, error) { var keys []ngmodels.AlertRuleKeyWithVersion @@ -820,7 +885,7 @@ func (st DBstore) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs [ } } - if err := st.DeleteAlertRulesByUID(ctx, orgID, uids...); err != nil { + if err := st.DeleteAlertRulesByUID(ctx, orgID, ngmodels.NewUserUID(user), uids...); err != nil { return err } } diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 1e60dfc9249..2b6d7d76971 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/ngalert/testutil" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/infra/db" @@ -685,7 +686,6 @@ func TestIntegration_DeleteInFolder(t *testing.T) { b := &fakeBus{} logger := log.New("test-dbstore") store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) - rule := createRule(t, store, nil) t.Run("should not be able to delete folder without permissions to delete rules", func(t *testing.T) { @@ -714,6 +714,9 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) { sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() + cfg.UnifiedAlerting.BaseInterval = 1 * time.Second + cfg.UnifiedAlerting.RuleVersionRecordLimit = -1 + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) logger := log.New("test-dbstore") store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, &fakeBus{}) @@ -742,7 +745,7 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) { called = true return nil } - err := store.DeleteAlertRulesByUID(context.Background(), rule.OrgID, rule.UID) + err := store.DeleteAlertRulesByUID(context.Background(), rule.OrgID, &models.AlertingUserUID, rule.UID) require.NoError(t, err) require.True(t, called) }) @@ -769,7 +772,7 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) { require.Len(t, savedInstances, 1) // Delete the rule - err = store.DeleteAlertRulesByUID(context.Background(), rule.OrgID, rule.UID) + err = store.DeleteAlertRulesByUID(context.Background(), rule.OrgID, &models.AlertingUserUID, rule.UID) require.NoError(t, err) // Now there should be no alert rule state @@ -780,6 +783,71 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) { require.NoError(t, err) require.Empty(t, savedInstances) }) + + t.Run("should remove all version and insert one with empty rule_uid", func(t *testing.T) { + orgID := int64(rand.Intn(1000)) + gen = gen.With(gen.WithOrgID(orgID)) + // Create a new store to pass the custom bus to check the signal + b := &fakeBus{} + logger := log.New("test-dbstore") + + store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b) + store.FeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagAlertRuleRestore) + + result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, gen.GenerateMany(3)) + uids := make([]string, 0, len(result)) + for _, rule := range result { + uids = append(uids, rule.UID) + } + require.NoError(t, err) + rules, err := store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{OrgID: orgID, RuleUIDs: uids}) + require.NoError(t, err) + + updates := make([]models.UpdateRule, 0, len(rules)) + for _, rule := range rules { + rule2 := models.CopyRule(rule, gen.WithTitle(util.GenerateShortUID())) + updates = append(updates, models.UpdateRule{ + Existing: rule, + New: *rule2, + }) + } + err = store.UpdateAlertRules(context.Background(), &models.AlertingUserUID, updates) + require.NoError(t, err) + + versions, err := store.GetAlertRuleVersions(context.Background(), orgID, rules[0].GUID) + require.NoError(t, err) + require.Len(t, versions, 2) + + err = store.DeleteAlertRulesByUID(context.Background(), orgID, util.Pointer(models.UserUID("test")), uids...) + require.NoError(t, err) + + guids := make([]string, 0, len(rules)) + for _, rule := range rules { + guids = append(guids, rule.GUID) + } + + _ = sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + var versions []alertRuleVersion + err = sess.Table(alertRuleVersion{}).Where(`rule_uid = ''`).In("rule_guid", guids).Find(&versions) + require.NoError(t, err) + require.Len(t, versions, len(rules)) // should be one version per GUID + + for _, version := range versions { + assert.Equal(t, "", version.RuleUID) + assert.Equal(t, "test", *version.CreatedBy) + // Remove the GUID from guids + for i, guid := range guids { + if guid == version.RuleGUID { + guids = append(guids[:i], guids[i+1:]...) + break + } + } + } + // Ensure that guids is empty + assert.Empty(t, guids, "Some rules are left unrecoverable") + return nil + }) + }) } func TestIntegrationInsertAlertRules(t *testing.T) { @@ -1894,11 +1962,12 @@ func createTestStore( bus bus.Bus, ) *DBstore { return &DBstore{ - SQLStore: sqlStore, - FolderService: folderService, - Logger: logger, - Cfg: cfg, - Bus: bus, + SQLStore: sqlStore, + FolderService: folderService, + Logger: logger, + Cfg: cfg, + Bus: bus, + FeatureToggles: featuremgmt.WithFeatures(), } } diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index 993cdcf2049..10bc282e8a8 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -102,10 +102,10 @@ func (f *RuleStore) GetRecordedCommands(predicate func(cmd any) (any, bool)) []a return result } -func (f *RuleStore) DeleteAlertRulesByUID(_ context.Context, orgID int64, UIDs ...string) error { +func (f *RuleStore) DeleteAlertRulesByUID(_ context.Context, orgID int64, user *models.UserUID, UIDs ...string) error { f.RecordedOps = append(f.RecordedOps, GenericRecordedQuery{ Name: "DeleteAlertRulesByUID", - Params: []any{orgID, UIDs}, + Params: []any{orgID, user, UIDs}, }) rules := f.Rules[orgID] From c23bb3695688a98102a213b5b3dc63b022a8c7d8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 5 Mar 2025 15:24:58 +0100 Subject: [PATCH 04/11] Zanzana: Fix health check endpoint (#101612) --- pkg/services/authz/zanzana/server/server.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pkg/services/authz/zanzana/server/server.go b/pkg/services/authz/zanzana/server/server.go index 9ec9a79c735..a0ae53af4d2 100644 --- a/pkg/services/authz/zanzana/server/server.go +++ b/pkg/services/authz/zanzana/server/server.go @@ -9,7 +9,6 @@ import ( "github.com/fullstorydev/grpchan/inprocgrpc" authzv1 "github.com/grafana/authlib/authz/proto/v1" openfgav1 "github.com/openfga/api/proto/openfga/v1" - "google.golang.org/protobuf/types/known/wrapperspb" dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1" "github.com/grafana/grafana/pkg/infra/localcache" @@ -71,12 +70,7 @@ func NewServer(cfg setting.ZanzanaServerSettings, openfga OpenFGAServer, logger } func (s *Server) IsHealthy(ctx context.Context) (bool, error) { - // FIXME: get back to openfga.IsReady() when issue is fixed - // https://github.com/openfga/openfga/issues/2251 - _, err := s.openfga.ListStores(ctx, &openfgav1.ListStoresRequest{ - PageSize: wrapperspb.Int32(1), - }) - return err == nil, nil + return s.openfga.IsReady(ctx) } func (s *Server) getContextuals(subject string) (*openfgav1.ContextualTupleKeys, error) { From 31e92ba7209d7bb9f0ec6f38cb5d4e22c532f6af Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:47:44 +0000 Subject: [PATCH 05/11] Trace View: Add link from the Trace View to the Profiles Drilldown (#101422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add link from the Trace View to the Profiles App via Profiles for this span button dropdown * Fix tests * Lint * Fix test * Lint * Fallback if plugin extension does not exist * Move span detail link buttons to their own file * Add tests * Update type name * Update naming * Prettier * Move button * Remove fallback * Use proper time range * Betterer * Update test * Use CoreApp * Betterer * Simplify context * Update tests --------- Co-authored-by: Piotr Jamróz --- .../src/types/pluginExtensions.ts | 1 + public/app/features/explore/Explore.tsx | 1 + .../explore/TraceView/TraceView.test.tsx | 10 +- .../features/explore/TraceView/TraceView.tsx | 4 + .../TraceView/TraceViewContainer.test.tsx | 5 +- .../explore/TraceView/TraceViewContainer.tsx | 6 +- .../SpanDetail/SpanDetailLinkButtons.test.tsx | 230 ++++++++++++++++++ .../SpanDetail/SpanDetailLinkButtons.tsx | 176 ++++++++++++++ .../SpanDetail/index.test.tsx | 13 +- .../TraceTimelineViewer/SpanDetail/index.tsx | 80 ++---- .../SpanDetailRow.test.tsx | 14 ++ .../TraceTimelineViewer/SpanDetailRow.tsx | 8 +- .../VirtualizedTraceView.tsx | 8 +- .../components/TraceTimelineViewer/index.tsx | 4 +- .../TraceView/components/types/links.ts | 1 + .../app/plugins/panel/traces/TracesPanel.tsx | 1 + 16 files changed, 490 insertions(+), 72 deletions(-) create mode 100644 public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.test.tsx create mode 100644 public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 4bd286d4e3b..318a66f145b 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -185,6 +185,7 @@ export enum PluginExtensionPoints { DataSourceConfig = 'grafana/datasources/config', ExploreToolbarAction = 'grafana/explore/toolbar/action', UserProfileTab = 'grafana/user/profile/tab', + TraceViewDetails = 'grafana/traceview/details', } export type PluginExtensionPanelContext = { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 7229276849e..47c5928ff1c 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -514,6 +514,7 @@ export class Explore extends PureComponent { dataFrames={dataFrames} splitOpenFn={this.onSplitOpen('traceView')} scrollElement={this.scrollElement} + timeRange={queryResponse.timeRange} /> ) diff --git a/public/app/features/explore/TraceView/TraceView.test.tsx b/public/app/features/explore/TraceView/TraceView.test.tsx index 0f5cac923a1..3f8d46cc5a5 100644 --- a/public/app/features/explore/TraceView/TraceView.test.tsx +++ b/public/app/features/explore/TraceView/TraceView.test.tsx @@ -3,8 +3,8 @@ import userEvent from '@testing-library/user-event'; import { createRef } from 'react'; import { Provider } from 'react-redux'; -import { DataFrame, MutableDataFrame } from '@grafana/data'; -import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; +import { DataFrame, MutableDataFrame, TimeRange } from '@grafana/data'; +import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime'; import { configureStore } from '../../../store/configureStore'; @@ -25,6 +25,7 @@ function getTraceView(frames: DataFrame[]) { traceProp={transformDataFrames(frames[0])!} datasource={undefined} topOfViewRef={topOfViewRef} + timeRange={{} as TimeRange} /> ); @@ -47,6 +48,11 @@ function renderTraceViewNew() { describe('TraceView', () => { beforeAll(() => { + setPluginLinksHook(() => ({ + isLoading: false, + links: [], + })); + setDataSourceSrv({ getInstanceSettings() { return undefined; diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index 0366c74ba20..3bd40c6e45f 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -13,6 +13,7 @@ import { LinkModel, mapInternalLinkToExplore, SplitOpen, + TimeRange, } from '@grafana/data'; import { getTraceToLogsOptions, TraceToMetricsData, TraceToProfilesData } from '@grafana/o11y-ds-frontend'; import { getTemplateSrv } from '@grafana/runtime'; @@ -67,6 +68,7 @@ type Props = { focusedSpanId?: string; createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel; spanFilters?: SearchProps; + timeRange: TimeRange; }; export function TraceView(props: Props) { @@ -238,6 +240,8 @@ export function TraceView(props: Props) { setTraceFlameGraphs={setTraceFlameGraphs} redrawListView={redrawListView} setRedrawListView={setRedrawListView} + timeRange={props.timeRange} + app={exploreId ? CoreApp.Explore : CoreApp.Unknown} /> ) : ( diff --git a/public/app/features/explore/TraceView/TraceViewContainer.test.tsx b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx index ca88947687e..0d1bf7c5c66 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.test.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx @@ -2,6 +2,8 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; +import { TimeRange } from '@grafana/data'; + import { configureStore } from '../../../store/configureStore'; import { frameOld } from './TraceView.test'; @@ -11,6 +13,7 @@ jest.mock('@grafana/runtime', () => { return { ...jest.requireActual('@grafana/runtime'), reportInteraction: jest.fn(), + usePluginLinks: jest.fn().mockReturnValue({ isLoading: false, links: [] }), }; }); @@ -19,7 +22,7 @@ function renderTraceViewContainer(frames = [frameOld]) { const { container, baseElement } = render( - {}} /> + {}} timeRange={{} as TimeRange} /> ); return { diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx index 68bb08dcad5..4d0e114c865 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import { DataFrame, SplitOpen } from '@grafana/data'; +import { DataFrame, SplitOpen, TimeRange } from '@grafana/data'; import { PanelChrome } from '@grafana/ui'; import { StoreState, useSelector } from 'app/types'; @@ -12,12 +12,13 @@ interface Props { splitOpenFn: SplitOpen; exploreId: string; scrollElement?: Element; + timeRange: TimeRange; } export function TraceViewContainer(props: Props) { // At this point we only show single trace const frame = props.dataFrames[0]; - const { dataFrames, splitOpenFn, exploreId, scrollElement } = props; + const { dataFrames, splitOpenFn, exploreId, scrollElement, timeRange } = props; const traceProp = useMemo(() => transformDataFrames(frame), [frame]); const datasource = useSelector( (state: StoreState) => state.explore.panes[props.exploreId]?.datasourceInstance ?? undefined @@ -36,6 +37,7 @@ export function TraceViewContainer(props: Props) { scrollElement={scrollElement} traceProp={traceProp} datasource={datasource} + timeRange={timeRange} /> ); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.test.tsx new file mode 100644 index 00000000000..07cfff3ff66 --- /dev/null +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.test.tsx @@ -0,0 +1,230 @@ +import { CoreApp, TimeRange } from '@grafana/data'; +import { usePluginLinks } from '@grafana/runtime'; +import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer'; + +import { SpanLinkType } from '../../types/links'; +import { TraceSpan } from '../../types/trace'; + +import { getSpanDetailLinkButtons, getProfileLinkButtonsContext } from './SpanDetailLinkButtons'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + usePluginLinks: jest.fn().mockReturnValue({ isLoading: false, links: [] }), +})); + +const span = { + process: { + serviceName: 'test-service', + }, + tags: [{ key: 'pyroscope.profile.id', value: 'test-profile' }], +} as TraceSpan; + +const createSpanLink = jest.fn(); +const timeRange = { + from: new Date(0), + to: new Date(1000), +} as unknown as TimeRange; + +describe('getSpanDetailLinkButtons', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return empty buttons when createSpanLink is not provided', () => { + const result = getSpanDetailLinkButtons({ + span, + createSpanLink: undefined, + datasourceType: 'test', + traceToProfilesOptions: undefined, + timeRange, + app: CoreApp.Explore, + }); + + expect(result.logLinkButton).toBeNull(); + expect(result.profileLinkButtons).toBeNull(); + expect(result.sessionLinkButton).toBeNull(); + }); + + it('should create log link button when logs link exists', () => { + createSpanLink.mockReturnValue([{ type: SpanLinkType.Logs, href: '/logs', title: 'Logs' }]); + + const result = getSpanDetailLinkButtons({ + span, + createSpanLink, + datasourceType: 'test', + traceToProfilesOptions: undefined, + timeRange, + app: CoreApp.Explore, + }); + + expect(result.logLinkButton).toBeDefined(); + expect(result.profileLinkButtons).toBeNull(); + expect(result.sessionLinkButton).toBeNull(); + }); + + it('should create profile link button when profiles link exists', () => { + createSpanLink.mockReturnValue([{ type: SpanLinkType.Profiles, href: '/profiles', title: RelatedProfilesTitle }]); + + const result = getSpanDetailLinkButtons({ + span, + createSpanLink, + datasourceType: 'test', + traceToProfilesOptions: { + datasourceUid: 'test-uid', + profileTypeId: 'test-type', + customQuery: false, + }, + timeRange, + app: CoreApp.Explore, + }); + + expect(result.logLinkButton).toBeNull(); + expect(result.profileLinkButtons).toBeDefined(); + expect(result.sessionLinkButton).toBeNull(); + }); + + it('should create session link button when session link exists', () => { + createSpanLink.mockReturnValue([{ type: SpanLinkType.Session, href: '/session', title: 'Session' }]); + + const result = getSpanDetailLinkButtons({ + span, + createSpanLink, + datasourceType: 'test', + traceToProfilesOptions: undefined, + timeRange, + app: CoreApp.Explore, + }); + + expect(result.logLinkButton).toBeNull(); + expect(result.profileLinkButtons).toBeNull(); + expect(result.sessionLinkButton).toBeDefined(); + }); + + it('should create profile drilldown button when plugin link exists', () => { + createSpanLink.mockReturnValue([{ type: SpanLinkType.Profiles, href: '/profiles', title: RelatedProfilesTitle }]); + (usePluginLinks as jest.Mock).mockReturnValue({ + isLoading: false, + links: [ + { + pluginId: 'grafana-pyroscope-app', + title: 'Open in Profiles Drilldown', + onClick: jest.fn(), + }, + ], + }); + + const result = getSpanDetailLinkButtons({ + span, + createSpanLink, + datasourceType: 'test', + traceToProfilesOptions: { + datasourceUid: 'test-uid', + profileTypeId: 'test-type', + customQuery: false, + }, + timeRange, + app: CoreApp.Explore, + }); + + expect(result.profileLinkButtons).toBeDefined(); + // Should render both the original profile link and the drilldown button + expect(result.profileLinkButtons?.props.children).toHaveLength(2); + }); + + it('should not create profile drilldown button when not in Explore', () => { + createSpanLink.mockReturnValue([{ type: SpanLinkType.Profiles, href: '/profiles', title: RelatedProfilesTitle }]); + (usePluginLinks as jest.Mock).mockReturnValue({ + isLoading: false, + links: [ + { + pluginId: 'grafana-pyroscope-app', + title: 'Open in Profiles Drilldown', + onClick: jest.fn(), + }, + ], + }); + + const result = getSpanDetailLinkButtons({ + span, + createSpanLink, + datasourceType: 'test', + traceToProfilesOptions: { + datasourceUid: 'test-uid', + profileTypeId: 'test-type', + customQuery: false, + }, + timeRange, + app: CoreApp.Dashboard, + }); + + expect(result.profileLinkButtons).toBeDefined(); + // Should only render the original profile link + expect(result.profileLinkButtons?.props.children).toBeFalsy(); + }); +}); + +describe('getProfileLinkButtonsContext', () => { + const traceToProfilesOptions = { + datasourceUid: 'test-uid', + profileTypeId: 'test-type', + customQuery: false, + }; + + it('should create context with all properties', () => { + const context = getProfileLinkButtonsContext(span, traceToProfilesOptions, timeRange); + + expect(context).toEqual({ + serviceName: 'test-service', + profileTypeId: 'test-type', + spanSelector: 'test-profile', + explorationType: 'flame-graph', + timeRange: { + from: new Date(0).toISOString(), + to: new Date(1000).toISOString(), + }, + datasource: { + uid: 'test-uid', + }, + }); + }); + + it('should handle missing traceToProfilesOptions', () => { + const context = getProfileLinkButtonsContext(span, undefined, timeRange); + + expect(context).toEqual({ + serviceName: 'test-service', + profileTypeId: '', + spanSelector: 'test-profile', + explorationType: 'flame-graph', + timeRange: { + from: new Date(0).toISOString(), + to: new Date(1000).toISOString(), + }, + datasource: { + uid: undefined, + }, + }); + }); + + it('should handle missing service name', () => { + const spanWithoutService = { + process: {}, + tags: [{ key: 'pyroscope.profile.id', value: 'test-profile' }], + } as TraceSpan; + + const context = getProfileLinkButtonsContext(spanWithoutService, traceToProfilesOptions, timeRange); + + expect(context.serviceName).toBe(''); + }); + + it('should handle missing profile ID tag', () => { + const spanWithoutProfileId = { + process: { serviceName: 'test-service' }, + tags: [], + } as unknown as TraceSpan; + + const context = getProfileLinkButtonsContext(spanWithoutProfileId, traceToProfilesOptions, timeRange); + + expect(context.spanSelector).toBe(''); + }); +}); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx new file mode 100644 index 00000000000..a8084cd4fec --- /dev/null +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx @@ -0,0 +1,176 @@ +import * as React from 'react'; + +import { CoreApp, IconName, PluginExtensionPoints, RawTimeRange, TimeRange } from '@grafana/data'; +import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; +import { config, locationService, reportInteraction, usePluginLinks } from '@grafana/runtime'; +import { DataSourceRef } from '@grafana/schema'; +import { Button, DataLinkButton } from '@grafana/ui'; +import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer'; + +import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; +import { SpanLinkFunc } from '../../types'; +import { SpanLinkDef, SpanLinkType } from '../../types/links'; +import { TraceSpan } from '../../types/trace'; + +export type ProfilesButtonContext = { + serviceName: string; + profileTypeId: string; + spanSelector: string; + explorationType: string; + timeRange: RawTimeRange; + datasource: DataSourceRef; +}; + +export type Props = { + span: TraceSpan; + traceToProfilesOptions?: TraceToProfilesOptions; + datasourceType: string; + timeRange: TimeRange; + createSpanLink?: SpanLinkFunc; + app: CoreApp; +}; + +export const getSpanDetailLinkButtons = (props: Props) => { + const { span, createSpanLink, traceToProfilesOptions, timeRange, datasourceType, app } = props; + + let logLinkButton: JSX.Element | null = null; + let profileLinkButton: JSX.Element | null = null; + let sessionLinkButton: JSX.Element | null = null; + if (createSpanLink) { + const links = createSpanLink(span); + const logsLink = links?.filter((link) => link.type === SpanLinkType.Logs); + if (links && logsLink && logsLink.length > 0) { + logLinkButton = createLinkButton(logsLink[0], SpanLinkType.Logs, 'Logs for this span', 'gf-logs', datasourceType); + } + const profilesLink = links?.filter( + (link) => link.type === SpanLinkType.Profiles && link.title === RelatedProfilesTitle + ); + if (links && profilesLink && profilesLink.length > 0) { + profileLinkButton = createLinkButton( + profilesLink[0], + SpanLinkType.Profiles, + 'Profiles for this span', + 'link', + datasourceType + ); + } + const sessionLink = links?.filter((link) => link.type === SpanLinkType.Session); + if (links && sessionLink && sessionLink.length > 0) { + sessionLinkButton = createLinkButton( + sessionLink[0], + SpanLinkType.Session, + 'Session for this span', + 'frontend-observability', + datasourceType + ); + } + } + + let profileLinkButtons = profileLinkButton; + if (profileLinkButton) { + // ensure we have a profile link + const profilesDrilldownPluginId = 'grafana-pyroscope-app'; + const context = getProfileLinkButtonsContext(span, traceToProfilesOptions, timeRange); + + // if in explore, use the plugin extension point to get the link + // note: plugin extension point links are not currently supported in panel plugins + if (app === CoreApp.Explore) { + const extensionPointId = PluginExtensionPoints.TraceViewDetails; + const { links } = usePluginLinks({ extensionPointId, context, limitPerPlugin: 1 }); + const link = links && links.length > 0 ? links.find((link) => link.pluginId === profilesDrilldownPluginId) : null; + const label = 'Open in Profiles Drilldown'; + + // if we have a plugin link, add a button to open in Grafana Profiles Drilldown + if (link) { + const profileDrilldownLinkButton = ( + + ); + + profileLinkButtons = ( + <> + {profileLinkButton} + {profileDrilldownLinkButton} + + ); + } + } + } + + return { profileLinkButtons, logLinkButton, sessionLinkButton }; +}; + +export const getProfileLinkButtonsContext = ( + span: TraceSpan, + traceToProfilesOptions: TraceToProfilesOptions | undefined, + timeRange: TimeRange +) => { + const spanSelector = span.tags.filter((tag) => tag.key === pyroscopeProfileIdTagKey); + const context: ProfilesButtonContext = { + serviceName: span.process.serviceName ?? '', + profileTypeId: traceToProfilesOptions?.profileTypeId ?? '', + spanSelector: spanSelector.length === 1 && spanSelector[0].value ? spanSelector[0].value : '', + explorationType: 'flame-graph', + timeRange: { + from: timeRange.from.toISOString(), + to: timeRange.to.toISOString(), + }, + datasource: { uid: traceToProfilesOptions?.datasourceUid }, + }; + return context; +}; + +const createLinkButton = ( + link: SpanLinkDef, + type: SpanLinkType, + title: string, + icon: IconName, + datasourceType: string, + className?: string +) => { + return ( + { + // DataLinkButton assumes if you provide an onClick event you would want to prevent default behavior like navigation + // In this case, if an onClick is not defined, restore navigation to the provided href while keeping the tracking + // this interaction will not be tracked with link right clicks + reportInteraction('grafana_traces_trace_view_span_link_clicked', { + datasourceType, + grafana_version: config.buildInfo.version, + type, + location: 'spanDetails', + }); + + if (link.onClick) { + link.onClick?.(event); + } else { + locationService.push(link.href); + } + }, + }} + buttonProps={{ icon, className }} + /> + ); +}; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx index d9b3c14f52c..1ad71c04cbd 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx @@ -19,7 +19,7 @@ import userEvent from '@testing-library/user-event'; import { createDataFrame, DataSourceInstanceSettings } from '@grafana/data'; import { data } from '@grafana/flamegraph'; -import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; +import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime'; import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; import traceGenerator from '../../demo/trace-generators'; @@ -70,6 +70,12 @@ describe('', () => { createFocusSpanLink: jest.fn().mockReturnValue({}), traceFlameGraphs: { [span.spanID]: createDataFrame(data) }, setRedrawListView: jest.fn(), + timeRange: { + raw: { + from: 0, + to: 1000000000000, + }, + }, }; span.tags = [ @@ -156,6 +162,11 @@ describe('', () => { props.logsToggle.mockReset(); props.logItemToggle.mockReset(); + setPluginLinksHook(() => ({ + isLoading: false, + links: [], + })); + setDataSourceSrv({ getList() { return [pyroSettings]; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index 9d6c42fe92c..7a6ec6dbc17 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -15,29 +15,26 @@ import { css } from '@emotion/css'; import { SpanStatusCode } from '@opentelemetry/api'; import cx from 'classnames'; -import * as React from 'react'; import { + CoreApp, DataFrame, dateTimeFormat, GrafanaTheme2, - IconName, LinkModel, + TimeRange, TraceKeyValuePair, TraceLog, } from '@grafana/data'; import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; -import { config, locationService, reportInteraction } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; -import { DataLinkButton, Divider, Icon, TextArea, useStyles2 } from '@grafana/ui'; -import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer'; +import { Divider, Icon, TextArea, useStyles2 } from '@grafana/ui'; import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; import { autoColor } from '../../Theme'; import LabeledList from '../../common/LabeledList'; import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE } from '../../constants/span'; import { SpanLinkFunc, TNil } from '../../types'; -import { SpanLinkDef, SpanLinkType } from '../../types/links'; import { TraceLink, TraceSpan, TraceSpanReference } from '../../types/trace'; import { formatDuration } from '../utils'; @@ -46,6 +43,7 @@ import AccordianLogs from './AccordianLogs'; import AccordianReferences from './AccordianReferences'; import AccordianText from './AccordianText'; import DetailState from './DetailState'; +import { getSpanDetailLinkButtons } from './SpanDetailLinkButtons'; import SpanFlameGraph from './SpanFlameGraph'; const getStyles = (theme: GrafanaTheme2) => { @@ -168,6 +166,8 @@ export type SpanDetailProps = { traceFlameGraphs: TraceFlameGraphs; setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void; setRedrawListView: (redraw: {}) => void; + timeRange: TimeRange; + app: CoreApp; }; export default function SpanDetail(props: SpanDetailProps) { @@ -193,6 +193,8 @@ export default function SpanDetail(props: SpanDetailProps) { setTraceFlameGraphs, traceToProfilesOptions, setRedrawListView, + timeRange, + app, } = props; const { isTagsOpen, @@ -289,62 +291,14 @@ export default function SpanDetail(props: SpanDetailProps) { }); } - const createLinkButton = (link: SpanLinkDef, type: SpanLinkType, title: string, icon: IconName) => { - return ( - { - // DataLinkButton assumes if you provide an onClick event you would want to prevent default behavior like navigation - // In this case, if an onClick is not defined, restore navigation to the provided href while keeping the tracking - // this interaction will not be tracked with link right clicks - reportInteraction('grafana_traces_trace_view_span_link_clicked', { - datasourceType: datasourceType, - grafana_version: config.buildInfo.version, - type, - location: 'spanDetails', - }); - - if (link.onClick) { - link.onClick?.(event); - } else { - locationService.push(link.href); - } - }, - }} - buttonProps={{ icon }} - /> - ); - }; - - let logLinkButton: JSX.Element | null = null; - let profileLinkButton: JSX.Element | null = null; - let sessionLinkButton: JSX.Element | null = null; - if (createSpanLink) { - const links = createSpanLink(span); - const logsLink = links?.filter((link) => link.type === SpanLinkType.Logs); - if (links && logsLink && logsLink.length > 0) { - logLinkButton = createLinkButton(logsLink[0], SpanLinkType.Logs, 'Logs for this span', 'gf-logs'); - } - const profilesLink = links?.filter( - (link) => link.type === SpanLinkType.Profiles && link.title === RelatedProfilesTitle - ); - if (links && profilesLink && profilesLink.length > 0) { - profileLinkButton = createLinkButton(profilesLink[0], SpanLinkType.Profiles, 'Profiles for this span', 'link'); - } - const sessionLink = links?.filter((link) => link.type === SpanLinkType.Session); - if (links && sessionLink && sessionLink.length > 0) { - sessionLinkButton = createLinkButton( - sessionLink[0], - SpanLinkType.Session, - 'Session for this span', - 'frontend-observability' - ); - } - } + const { profileLinkButtons, logLinkButton, sessionLinkButton } = getSpanDetailLinkButtons({ + span, + createSpanLink, + datasourceType, + traceToProfilesOptions, + timeRange, + app, + }); const focusSpanLink = createFocusSpanLink(traceID, spanID); return ( @@ -359,7 +313,7 @@ export default function SpanDetail(props: SpanDetailProps) {
{logLinkButton} - {profileLinkButton} + {profileLinkButtons} {sessionLinkButton}
diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx index 90223e8ed2b..6ac58bf1b1f 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx @@ -16,6 +16,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { createTheme } from '@grafana/data'; +import { setPluginLinksHook } from '@grafana/runtime'; import DetailState from './SpanDetail/DetailState'; import { UnthemedSpanDetailRow, SpanDetailRowProps } from './SpanDetailRow'; @@ -47,12 +48,25 @@ const setup = (propOverrides?: SpanDetailRowProps) => { traceStartTime: 1000, theme: createTheme(), traceFlameGraphs: {}, + timeRange: { + raw: { + from: 0, + to: 1000000000000, + }, + }, ...propOverrides, }; return render(); }; describe('SpanDetailRow tests', () => { + beforeEach(() => { + setPluginLinksHook(() => ({ + isLoading: false, + links: [], + })); + }); + it('renders without exploding', () => { expect(() => setup()).not.toThrow(); }); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx index 799e8cc739b..e8b8c3b4000 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx @@ -16,7 +16,7 @@ import { css } from '@emotion/css'; import classNames from 'classnames'; import { PureComponent } from 'react'; -import { GrafanaTheme2, LinkModel, TraceKeyValuePair, TraceLog } from '@grafana/data'; +import { CoreApp, GrafanaTheme2, LinkModel, TimeRange, TraceKeyValuePair, TraceLog } from '@grafana/data'; import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; import { TimeZone } from '@grafana/schema'; import { Button, clearButtonStyles, stylesFactory, withTheme2 } from '@grafana/ui'; @@ -103,6 +103,8 @@ export type SpanDetailRowProps = { traceFlameGraphs: TraceFlameGraphs; setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void; setRedrawListView: (redraw: {}) => void; + timeRange: TimeRange; + app: CoreApp; }; export class UnthemedSpanDetailRow extends PureComponent { @@ -146,6 +148,8 @@ export class UnthemedSpanDetailRow extends PureComponent { traceFlameGraphs, setTraceFlameGraphs, setRedrawListView, + timeRange, + app, } = this.props; const styles = getStyles(theme); return ( @@ -193,6 +197,8 @@ export class UnthemedSpanDetailRow extends PureComponent { traceFlameGraphs={traceFlameGraphs} setTraceFlameGraphs={setTraceFlameGraphs} setRedrawListView={setRedrawListView} + timeRange={timeRange} + app={app} /> diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx index c769f585b77..d84d9e984c5 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -18,7 +18,7 @@ import memoizeOne from 'memoize-one'; import * as React from 'react'; import { RefObject } from 'react'; -import { GrafanaTheme2, LinkModel, TraceKeyValuePair, TraceLog } from '@grafana/data'; +import { CoreApp, GrafanaTheme2, LinkModel, TimeRange, TraceKeyValuePair, TraceLog } from '@grafana/data'; import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; import { config, reportInteraction } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; @@ -109,6 +109,8 @@ type TVirtualizedTraceViewOwnProps = { setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void; redrawListView: {}; setRedrawListView: (redraw: {}) => void; + timeRange: TimeRange; + app: CoreApp; }; export type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TTraceTimeline; @@ -557,6 +559,8 @@ export class UnthemedVirtualizedTraceView extends React.Component ); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx index 0857ad6f588..25885c1b0f7 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx @@ -15,7 +15,7 @@ import { css } from '@emotion/css'; import { PureComponent, RefObject } from 'react'; -import { GrafanaTheme2, LinkModel, TraceKeyValuePair, TraceLog } from '@grafana/data'; +import { CoreApp, GrafanaTheme2, LinkModel, TimeRange, TraceKeyValuePair, TraceLog } from '@grafana/data'; import { SpanBarOptions, TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; import { config, reportInteraction } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; @@ -112,6 +112,8 @@ export type TProps = { setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void; redrawListView: {}; setRedrawListView: (redraw: {}) => void; + timeRange: TimeRange; + app: CoreApp; }; type State = { diff --git a/public/app/features/explore/TraceView/components/types/links.ts b/public/app/features/explore/TraceView/components/types/links.ts index 00f8f74b6ba..2610e9511cf 100644 --- a/public/app/features/explore/TraceView/components/types/links.ts +++ b/public/app/features/explore/TraceView/components/types/links.ts @@ -9,6 +9,7 @@ export enum SpanLinkType { Traces = 'trace', Metrics = 'metric', Profiles = 'profile', + ProfilesDrilldown = 'profile-drilldown', Session = 'session', Unknown = 'unknown', } diff --git a/public/app/plugins/panel/traces/TracesPanel.tsx b/public/app/plugins/panel/traces/TracesPanel.tsx index b83e64af263..cf6fe0fae97 100644 --- a/public/app/plugins/panel/traces/TracesPanel.tsx +++ b/public/app/plugins/panel/traces/TracesPanel.tsx @@ -52,6 +52,7 @@ export const TracesPanel = ({ data, options, replaceVariables }: PanelProps ); From 3bdc9d1e19da047daf054da912f795cc5f5a272d Mon Sep 17 00:00:00 2001 From: Andrew Hackmann <5140848+bossinc@users.noreply.github.com> Date: Wed, 5 Mar 2025 09:32:58 -0600 Subject: [PATCH 06/11] Graphite: Compare query builder query to raw query (#101104) * compare queries to insure query isnt changed * comment * removed calls to getTemplateSrv because it makes things impossible to test. added a check for empty raw queries * prettier * Update public/app/plugins/datasource/graphite/graphite_query.ts Co-authored-by: Adam Yeats <16296989+adamyeats@users.noreply.github.com> --------- Co-authored-by: Adam Yeats <16296989+adamyeats@users.noreply.github.com> --- .../components/GraphiteQueryEditor.tsx | 1 + .../datasource/graphite/datasource.test.ts | 3 +++ .../plugins/datasource/graphite/datasource.ts | 2 +- .../datasource/graphite/graphite_query.ts | 24 ++++++++++++++++--- .../graphite/specs/graphite_query.test.ts | 14 +++++++++++ .../datasource/graphite/state/store.ts | 3 +-- 6 files changed, 41 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/graphite/components/GraphiteQueryEditor.tsx b/public/app/plugins/datasource/graphite/components/GraphiteQueryEditor.tsx index 3d5413717a3..cfa65a29086 100644 --- a/public/app/plugins/datasource/graphite/components/GraphiteQueryEditor.tsx +++ b/public/app/plugins/datasource/graphite/components/GraphiteQueryEditor.tsx @@ -53,6 +53,7 @@ function GraphiteQueryEditorContent() { icon="pen" variant="secondary" aria-label="Toggle editor mode" + tooltip={state?.queryModel?.error} onClick={() => { dispatch(actions.toggleEditorMode()); }} diff --git a/public/app/plugins/datasource/graphite/datasource.test.ts b/public/app/plugins/datasource/graphite/datasource.test.ts index de993ff6102..548797d7342 100644 --- a/public/app/plugins/datasource/graphite/datasource.test.ts +++ b/public/app/plugins/datasource/graphite/datasource.test.ts @@ -744,6 +744,9 @@ describe('graphiteDatasource', () => { params: [{ multiple: true }], }, updateText: () => {}, + render: () => { + return ''; + }, })); }); diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 9998fe8980e..e8a6487a275 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -143,7 +143,7 @@ export class GraphiteDatasource target: query.target || '', textEditor: false, }, - getTemplateSrv() + this.templateSrv ); graphiteQuery.parseTarget(); diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index d3009ecb71f..5c23e83f695 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -77,6 +77,21 @@ export default class GraphiteQuery { try { this.parseTargetRecursive(astNode, null); + if (this.target.target) { + const oldQuery = this.target.target; + const newQuery = this.generateQueryString(); + + // Spaces, quotes, and commas are used when rendering the AST back into a string. + // We are removing these for less false positives of query changes. + const sanitizeQuery = (o: string): string => o.replace(/\s|'|"|,/g, ''); + const oldSanitized = sanitizeQuery(oldQuery); + const newSanitized = sanitizeQuery(newQuery); + if (oldSanitized && newSanitized && oldSanitized !== newSanitized) { + throw new Error( + `Failed to make a visual query builder query that is equivalent to the query.\nOriginal query: ${oldQuery}\nQuery builder query: ${newQuery}` + ); + } + } } catch (err) { if (err instanceof Error) { console.error('error parsing target:', err.message); @@ -181,16 +196,19 @@ export default class GraphiteQuery { arrayMove(this.functions, index, index + offset); } - updateModelTarget(targets: any) { + generateQueryString(): string { const wrapFunction = (target: string, func: FuncInstance) => { return func.render(target, (value: string) => { return this.templateSrv ? this.templateSrv.replace(value, this.scopedVars) : value; }); }; + const metricPath = this.getSegmentPathUpTo(this.segments.length).replace(/\.?select metric$/, ''); + return reduce(this.functions, wrapFunction, metricPath); + } + updateModelTarget(targets: any) { if (!this.target.textEditor) { - const metricPath = this.getSegmentPathUpTo(this.segments.length).replace(/\.?select metric$/, ''); - this.target.target = reduce(this.functions, wrapFunction, metricPath); + this.target.target = this.generateQueryString(); } this.updateRenderedTarget(this.target, targets); diff --git a/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts b/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts index 5d5035c5dd6..e6e048e45de 100644 --- a/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts +++ b/public/app/plugins/datasource/graphite/specs/graphite_query.test.ts @@ -258,6 +258,20 @@ describe('Graphite query model', () => { ctx.queryModel.updateModelTarget(targets); expect(ctx.queryModel.target.target).toContain(nestedFunctionAsParam); }); + + //This is not preferred behavior. The query builder cannot parse `maxSeries(sum(testSeries1), sum(testSeries2))` and when it can, remove this test + it('should return an error when visual query builder query does not match raw query', () => { + jest.spyOn(console, 'error').mockImplementation(); + ctx.target = { + refId: 'A', + target: 'maxSeries(sum(testSeries1), sum(testSeries2))', + }; + ctx.targets = [ctx.target]; + ctx.queryModel = new GraphiteQuery(ctx.datasource, ctx.target, ctx.templateSrv); + expect(ctx.queryModel.error).toBe( + 'Failed to make a visual query builder query that is equivalent to the query.\nOriginal query: maxSeries(sum(testSeries1), sum(testSeries2))\nQuery builder query: maxSeries(sumSeries(sumSeries(testSeries1), testSeries2))' + ); + }); }); }); }); diff --git a/public/app/plugins/datasource/graphite/state/store.ts b/public/app/plugins/datasource/graphite/state/store.ts index 0081c2bd0ea..6df365d4eee 100644 --- a/public/app/plugins/datasource/graphite/state/store.ts +++ b/public/app/plugins/datasource/graphite/state/store.ts @@ -2,7 +2,6 @@ import { AnyAction } from '@reduxjs/toolkit'; import { Action, Dispatch } from 'redux'; import { DataQuery, TimeRange } from '@grafana/data'; -import { getTemplateSrv } from '@grafana/runtime'; import { TemplateSrv } from '../../../../features/templating/template_srv'; import { GraphiteDatasource } from '../datasource'; @@ -57,7 +56,7 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise state = { ...state, ...deps, - queryModel: new GraphiteQuery(deps.datasource, deps.target, getTemplateSrv()), + queryModel: new GraphiteQuery(deps.datasource, deps.target, state.templateSrv), supportsTags: deps.datasource.supportsTags, paused: false, removeTagValue: '-- remove tag --', From 47f82a0c16e06b182693754bd85edcef3178f5f2 Mon Sep 17 00:00:00 2001 From: Sam Jewell <2903904+samjewell@users.noreply.github.com> Date: Wed, 5 Mar 2025 15:52:07 +0000 Subject: [PATCH 07/11] SQL Expressions: Resizable code-editor (#101407) * Resizeable SQL expressions text-area Generated by an LLM for me - we'll see. It expands quite large on first page-load * Switch to useLayoutEffect to avoid visual flashing * Get the LLM to rewrite the approach, inspired by InfluxDB The Influx DB text-area is also resizable vertically, but that one isn't a Monaco editor (we need to tell Monaco to update its own size when the outer div is resized), so this is necessarily a little more complex than Influx. But still this approach looks simpler: The Javascript here is shorter * Start at 240px, to match the current default size Question: Is there a better approach to achieve this? * Don't clip the bottom border of the Monaco editor * Fix linting errors --- .../expressions/components/SqlExpr.tsx | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/public/app/features/expressions/components/SqlExpr.tsx b/public/app/features/expressions/components/SqlExpr.tsx index 5b8d6c0a424..93fad1699b7 100644 --- a/public/app/features/expressions/components/SqlExpr.tsx +++ b/public/app/features/expressions/components/SqlExpr.tsx @@ -1,10 +1,15 @@ -import { useMemo } from 'react'; +import { css } from '@emotion/css'; +import { useMemo, useRef, useEffect, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { SQLEditor } from '@grafana/plugin-ui'; +import { useStyles2 } from '@grafana/ui'; import { ExpressionQuery } from '../types'; +// Account for Monaco editor's border to prevent clipping +const EDITOR_BORDER_ADJUSTMENT = 2; // 1px border on top and bottom + interface Props { refIds: Array>; query: ExpressionQuery; @@ -13,8 +18,10 @@ interface Props { export const SqlExpr = ({ onChange, refIds, query }: Props) => { const vars = useMemo(() => refIds.map((v) => v.value!), [refIds]); - const initialQuery = `select * from ${vars[0]} limit 1`; + const styles = useStyles2(getStyles); + const containerRef = useRef(null); + const [dimensions, setDimensions] = useState({ height: 0 }); const onEditorChange = (expression: string) => { onChange({ @@ -23,5 +30,37 @@ export const SqlExpr = ({ onChange, refIds, query }: Props) => { }); }; - return ; + // Set up resize observer to handle container resizing + useEffect(() => { + if (!containerRef.current) { + return; + } + + const resizeObserver = new ResizeObserver((entries) => { + const { height } = entries[0].contentRect; + setDimensions({ height }); + }); + + resizeObserver.observe(containerRef.current); + return () => resizeObserver.disconnect(); + }, []); + + return ( +
+ +
+ ); }; + +const getStyles = () => ({ + editorContainer: css({ + height: '240px', + resize: 'vertical', + overflow: 'auto', + minHeight: '100px', + }), +}); From 962496c50d188e8d35467643cb504203d1e21bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 5 Mar 2025 17:01:53 +0100 Subject: [PATCH 08/11] DashboardLayouts: Remove unused functions (#101602) --- .../DefaultGridLayoutManager.tsx | 28 ------------- .../ResponsiveGridLayoutManager.tsx | 41 +------------------ .../scene/layout-rows/RowsLayoutManager.tsx | 10 ----- .../scene/layout-tabs/TabsLayoutManager.tsx | 10 ----- .../scene/types/DashboardLayoutManager.ts | 5 --- 5 files changed, 1 insertion(+), 93 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index bad747f55de..22452f73c61 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -26,7 +26,6 @@ import { getGridItemKeyForPanelId, getDashboardSceneFor, } from '../../utils/utils'; -import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -180,22 +179,6 @@ export class DefaultGridLayoutManager return panels; } - public hasVizPanels(): boolean { - for (const child of this.state.grid.state.children) { - if (child instanceof DashboardGridItem) { - return true; - } else if (child instanceof SceneGridRow) { - for (const rowChild of child.state.children) { - if (rowChild instanceof DashboardGridItem) { - return true; - } - } - } - } - - return false; - } - public addNewRow(): SceneGridRow { const id = dashboardSceneGraph.getNextPanelId(this); @@ -224,17 +207,6 @@ export class DefaultGridLayoutManager return row; } - public addNewTab() { - const shouldAddTab = this.hasVizPanels(); - const tabsLayout = TabsLayoutManager.createFromLayout(this); - - if (shouldAddTab) { - tabsLayout.addNewTab(); - } - - getDashboardSceneFor(this).switchLayout(tabsLayout); - } - public editModeChanged(isEditing: boolean) { const updateResizeAndDragging = () => { this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing }); diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index 8c3505038c2..0823498bccf 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -4,14 +4,7 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { joinCloneKeys } from '../../utils/clone'; import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; -import { - getDashboardSceneFor, - getGridItemKeyForPanelId, - getPanelIdForVizPanel, - getVizPanelKeyForPanelId, -} from '../../utils/utils'; -import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; -import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; +import { getGridItemKeyForPanelId, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -111,16 +104,6 @@ export class ResponsiveGridLayoutManager return panels; } - public hasVizPanels(): boolean { - for (const child of this.state.layout.state.children) { - if (child instanceof ResponsiveGridItem) { - return true; - } - } - - return false; - } - public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { return this.clone({ layout: this.state.layout.clone({ @@ -143,28 +126,6 @@ export class ResponsiveGridLayoutManager }); } - public addNewRow() { - const shouldAddRow = this.hasVizPanels(); - const rowsLayout = RowsLayoutManager.createFromLayout(this); - - if (shouldAddRow) { - rowsLayout.addNewRow(); - } - - getDashboardSceneFor(this).switchLayout(rowsLayout); - } - - public addNewTab() { - const shouldAddTab = this.hasVizPanels(); - const tabsLayout = TabsLayoutManager.createFromLayout(this); - - if (shouldAddTab) { - tabsLayout.addNewTab(); - } - - getDashboardSceneFor(this).switchLayout(tabsLayout); - } - public getOptions(): OptionsPaneItemDescriptor[] { return getEditOptions(this); } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index a4959f810ec..5acf9063531 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -65,16 +65,6 @@ export class RowsLayoutManager extends SceneObjectBase i return panels; } - public hasVizPanels(): boolean { - for (const row of this.state.rows) { - if (row.getLayout().hasVizPanels()) { - return true; - } - } - - return false; - } - public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { throw new Error('Method not implemented.'); } diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx index c30491cc6fc..410fe06d781 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -85,16 +85,6 @@ export class TabsLayoutManager extends SceneObjectBase i throw new Error('Method not implemented.'); } - public hasVizPanels(): boolean { - for (const tab of this.state.tabs) { - if (tab.getLayout().hasVizPanels()) { - return true; - } - } - - return false; - } - public addNewTab() { const currentTab = new TabItem(); this.setState({ tabs: [...this.state.tabs, currentTab], currentTabIndex: this.state.tabs.length }); diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts index 53862b2578d..190a6806467 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts @@ -39,11 +39,6 @@ export interface DashboardLayoutManager extends SceneObject { */ getVizPanels(): VizPanel[]; - /** - * Check if the layout has viz panels - */ - hasVizPanels(): boolean; - /** * Notify the layout manager that the edit mode has changed * @param isEditing From c1c9ea6964897d74ca62b1a5bd2e4fd7f6b9bb75 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:11:44 +0100 Subject: [PATCH 09/11] SCIM: Assign requester org to new provisioned users (#101548) Assign requester org to new provisioned users Co-authored-by: Mihai Doarna --- pkg/services/user/userimpl/user.go | 33 +++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index 68e229d2140..55b3b809d23 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -10,6 +10,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/tracing" @@ -100,16 +101,28 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use return nil, user.ErrEmptyUsernameAndEmail.Errorf("user cannot be created with empty username and email") } - cmdOrg := org.GetOrgIDForNewUserCommand{ - Email: cmd.Email, - Login: cmd.Login, - OrgID: cmd.OrgID, - OrgName: cmd.OrgName, - SkipOrgSetup: cmd.SkipOrgSetup, - } - orgID, err := s.orgService.GetIDForNewUser(ctx, cmdOrg) - if err != nil { - return nil, err + // if the user is provisioned, use the org ID from the requester + var orgID int64 + var err error + if cmd.IsProvisioned { + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + + orgID = requester.GetOrgID() + } else { + cmdOrg := org.GetOrgIDForNewUserCommand{ + Email: cmd.Email, + Login: cmd.Login, + OrgID: cmd.OrgID, + OrgName: cmd.OrgName, + SkipOrgSetup: cmd.SkipOrgSetup, + } + orgID, err = s.orgService.GetIDForNewUser(ctx, cmdOrg) + if err != nil { + return nil, err + } } if cmd.Email == "" { cmd.Email = cmd.Login From 627e8995c0bd5c6aa302c7fb37821ba80707b655 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Wed, 5 Mar 2025 17:04:06 +0000 Subject: [PATCH 10/11] LBAC for datasources: Adds feature availability (#101604) * adds feature availability * spellling * table format enough --- .../data-source-management/teamlbac/_index.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/sources/administration/data-source-management/teamlbac/_index.md b/docs/sources/administration/data-source-management/teamlbac/_index.md index 74470c792bf..e254a81cfca 100644 --- a/docs/sources/administration/data-source-management/teamlbac/_index.md +++ b/docs/sources/administration/data-source-management/teamlbac/_index.md @@ -20,8 +20,19 @@ Label-Based Access Control (LBAC) allows fine-grained access control to data sou ## Supported Data Sources +### Feature availability + LBAC for data sources is currently generally available for `Loki` and in **experimental** for `Prometheus`. Support for additional data sources may be added in future updates. +| Data source | [Grafana Cloud](/docs/grafana-cloud) | [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}}) | +| ----------- | ------------------------------------ | ----------------------------------------------------------------------------- | +| Loki | GA | GA | +| Prometheus | PrivatePreview | PrivatePreview | + +{{% admonition type="note" %}} +For enterprise this feature requires Grafana Enterprise Metrics (GEM) or Grafana Enterprise Logs (GEL) to function. +{{% /admonition %}} + **LBAC for data sources offers:** - Team-based access control using `LogQL` rules. From 5980c8efbb7d4c1af1c497741d5e48f2b4e67091 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 5 Mar 2025 20:08:20 +0300 Subject: [PATCH 11/11] Dashboards: Show provisioned badge from annotations (#101625) --- public/app/features/apiserver/types.ts | 26 ++++++++++------ .../dashboard-scene/scene/DashboardScene.tsx | 30 +++++++++++++++++++ .../scene/ManagedDashboardNavBarBadge.tsx | 23 ++++++++++++++ .../scene/NavToolbarActions.tsx | 16 ++++++++-- 4 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx diff --git a/public/app/features/apiserver/types.ts b/public/app/features/apiserver/types.ts index 89f15d33a80..6df2e903ca0 100644 --- a/public/app/features/apiserver/types.ts +++ b/public/app/features/apiserver/types.ts @@ -43,11 +43,18 @@ export const AnnoKeyFolderUrl = 'grafana.app/folderUrl'; export const AnnoKeyMessage = 'grafana.app/message'; export const AnnoKeySlug = 'grafana.app/slug'; -// Identify where values came from -export const AnnoKeyRepoName = 'grafana.app/repoName'; -export const AnnoKeyRepoPath = 'grafana.app/repoPath'; -export const AnnoKeyRepoHash = 'grafana.app/repoHash'; -export const AnnoKeyRepoTimestamp = 'grafana.app/repoTimestamp'; +export enum ManagerKind { + Repo = 'repo', + Terraform = 'terraform', + Kubectl = 'kubectl', + Plugin = 'plugin', +} + +export const AnnoKeyManagerKind = 'grafana.app/managedBy'; +export const AnnoKeyManagerIdentity = 'grafana.app/managerId'; +export const AnnoKeySourcePath = 'grafana.app/sourcePath'; +export const AnnoKeySourceChecksum = 'grafana.app/sourceChecksum'; +export const AnnoKeySourceTimestamp = 'grafana.app/sourceTimestamp'; export const AnnoKeySavedFromUI = 'grafana.app/saved-from-ui'; export const AnnoKeyDashboardNotFound = 'grafana.app/dashboard-not-found'; @@ -66,10 +73,11 @@ type GrafanaAnnotations = { [AnnoKeyFolder]?: string; [AnnoKeySlug]?: string; - [AnnoKeyRepoName]?: string; - [AnnoKeyRepoPath]?: string; - [AnnoKeyRepoHash]?: string; - [AnnoKeyRepoTimestamp]?: string; + [AnnoKeyManagerKind]?: ManagerKind; + [AnnoKeyManagerIdentity]?: string; + [AnnoKeySourcePath]?: string; + [AnnoKeySourceChecksum]?: string; + [AnnoKeySourceTimestamp]?: string; }; // Annotations provided by the front-end client diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 9978a002fe7..81c0acacd8f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -42,6 +42,7 @@ import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; +import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, AnnoKeySourcePath, ManagerKind } from '../../apiserver/types'; import { DashboardEditPane } from '../edit-pane/DashboardEditPane'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; @@ -741,6 +742,35 @@ export class DashboardScene extends SceneObjectBase impleme getDashboardChanges(saveTimeRange?: boolean, saveVariables?: boolean, saveRefresh?: boolean): DashboardChangeInfo { return this._serializer.getDashboardChangesFromScene(this, { saveTimeRange, saveVariables, saveRefresh }); } + + getManagerKind(): ManagerKind | undefined { + return this.state.meta.k8s?.annotations?.[AnnoKeyManagerKind]; + } + + isManaged() { + return Boolean(this.getManagerKind()); + } + + isManagedRepository() { + return Boolean(this.getManagerKind() === ManagerKind.Repo); + } + + getPath() { + return this.state.meta.k8s?.annotations?.[AnnoKeySourcePath]; + } + + setManager(kind: ManagerKind, id: string) { + this.setState({ + meta: { + k8s: { + annotations: { + [AnnoKeyManagerKind]: kind, + [AnnoKeyManagerIdentity]: id, + }, + }, + }, + }); + } } export class DashboardVariableDependency implements SceneVariableDependencyConfigLike { diff --git a/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx b/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx new file mode 100644 index 00000000000..9594e1c1eb3 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx @@ -0,0 +1,23 @@ +import { Badge } from '@grafana/ui'; +import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; +import { DashboardMeta } from 'app/types'; + +export default function ManagedDashboardNavBarBadge({ meta }: { meta: DashboardMeta }) { + const obj = meta.k8s; + if (!obj?.annotations) { + return; + } + + let text = 'Provisioned'; + const kind = obj.annotations?.[AnnoKeyManagerKind]; + const id = obj.annotations?.[AnnoKeyManagerIdentity]; + switch (kind) { + case ManagerKind.Terraform: + text = 'Terraform'; + case ManagerKind.Kubectl: + text = 'Kubectl'; + case ManagerKind.Plugin: + text = `Plugin: ${id}`; + } + return ; +} diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index d5564aa2814..65eed8a797d 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -37,6 +37,7 @@ import { isLibraryPanel } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton'; +import ManagedDashboardNavBarBadge from './ManagedDashboardNavBarBadge'; interface Props { dashboard: DashboardScene; @@ -69,7 +70,7 @@ export function ToolbarActions({ dashboard }: Props) { const isViewingPanel = Boolean(viewPanelScene); const isEditedPanelDirty = usePanelEditDirty(editPanel); const isEditingLibraryPanel = editPanel && isLibraryPanel(editPanel.state.panelRef.resolve()); - const isNew = !Boolean(uid); + const isNew = !Boolean(uid || dashboard.isManaged()); const hasCopiedPanel = store.exists(LS_PANEL_COPY_KEY); // Means we are not in settings view, fullscreen panel or edit panel @@ -77,6 +78,7 @@ export function ToolbarActions({ dashboard }: Props) { const isEditingAndShowingDashboard = isEditing && isShowingDashboard; const showScopesSelector = config.featureToggles.scopeFilters && !isEditing; const dashboardNewLayouts = config.featureToggles.dashboardNewLayouts; + const isManaged = Boolean(dashboard.isManaged()); if (!isEditingPanel) { // This adds the presence indicators in enterprise @@ -125,6 +127,16 @@ export function ToolbarActions({ dashboard }: Props) { }); } + if (isManaged && meta.canEdit) { + toolbarActions.push({ + group: 'icon-actions', + condition: true, + render: () => { + return ; + }, + }); + } + const isDevEnv = config.buildInfo.env === 'development'; toolbarActions.push({ @@ -548,7 +560,7 @@ export function ToolbarActions({ dashboard }: Props) { } // If we only can save as copy - if (canSaveAs && !meta.canSave && !meta.canMakeEditable) { + if (canSaveAs && !meta.canSave && !meta.canMakeEditable && !isManaged) { return (