merge with main
This commit is contained in:
+8
-4
@@ -4687,9 +4687,9 @@ steps:
|
||||
path: /github-app
|
||||
- commands:
|
||||
- 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
|
||||
- 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
|
||||
depends_on:
|
||||
- github-app-generate-token
|
||||
environment:
|
||||
@@ -4734,6 +4734,8 @@ steps:
|
||||
- printenv GCP_KEY_BASE64 | base64 -d > /tmp/key.json
|
||||
- gcloud auth activate-service-account --key-file=/tmp/key.json
|
||||
- gcloud storage cp -r dist/* $${UPLOAD_TO}
|
||||
depends_on:
|
||||
- rgm-build
|
||||
environment:
|
||||
_EXPERIMENTAL_DAGGER_CLOUD_TOKEN:
|
||||
from_secret: dagger_token
|
||||
@@ -4774,6 +4776,8 @@ volumes:
|
||||
name: docker
|
||||
- name: github-app
|
||||
path: /github-app
|
||||
- name: github-app
|
||||
temp: {}
|
||||
---
|
||||
clone:
|
||||
retries: 3
|
||||
@@ -5560,6 +5564,6 @@ kind: secret
|
||||
name: gcr_credentials
|
||||
---
|
||||
kind: signature
|
||||
hmac: 6e01278547a6f0803b7723e5f1e73bd94c572fa4805517232d3897717cee30f6
|
||||
hmac: 558d477c002eb799c23f6631aafc7df933518e445e59f34ceb989e73f4dc60bc
|
||||
|
||||
...
|
||||
|
||||
@@ -11,14 +11,21 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy-pr-preview:
|
||||
if: ${{ ! github.event.pull_request.head.repo.fork }}
|
||||
if: "!github.event.pull_request.head.repo.fork"
|
||||
uses: grafana/writers-toolkit/.github/workflows/deploy-preview.yml@main
|
||||
with:
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
branch: ${{ github.head_ref }}
|
||||
event_number: ${{ github.event.number }}
|
||||
title: ${{ github.event.pull_request.title }}
|
||||
repo: grafana
|
||||
website_directory: content/docs/grafana/latest
|
||||
relative_prefix: /docs/grafana/latest/
|
||||
index_file: true
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
sources: |
|
||||
[
|
||||
{
|
||||
"index_file": "content/docs/grafana/_index.md",
|
||||
"relative_prefix": "/docs/grafana/latest/",
|
||||
"repo": "grafana",
|
||||
"source_directory": "docs/sources",
|
||||
"website_directory": "content/docs/grafana/latest"
|
||||
}
|
||||
]
|
||||
title: ${{ github.event.pull_request.title }}
|
||||
|
||||
@@ -22,7 +22,7 @@ func GetAuthorizer() authorizer.Authorizer {
|
||||
}
|
||||
|
||||
// check if is admin
|
||||
if u.GetIsGrafanaAdmin() {
|
||||
if u.HasRole(identity.RoleAdmin) {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
|
||||
@@ -75,4 +75,8 @@ func (m *mockUser) GetIsGrafanaAdmin() bool {
|
||||
return m.isGrafanaAdmin
|
||||
}
|
||||
|
||||
func (m *mockUser) HasRole(role identity.RoleType) bool {
|
||||
return role == identity.RoleAdmin && m.isGrafanaAdmin
|
||||
}
|
||||
|
||||
// Implement other methods of identity.Requester as needed
|
||||
|
||||
@@ -62,4 +62,5 @@ func (s *Service) Checks() []checks.Check {
|
||||
type AdvisorAppConfig struct {
|
||||
CheckRegistry CheckService
|
||||
PluginConfig map[string]string
|
||||
StackID string
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@ package datasourcecheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type check struct {
|
||||
@@ -20,6 +21,7 @@ type check struct {
|
||||
PluginStore pluginstore.Store
|
||||
PluginContextProvider pluginContextProvider
|
||||
PluginClient plugins.Client
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -33,6 +35,7 @@ func New(
|
||||
PluginStore: pluginStore,
|
||||
PluginContextProvider: pluginContextProvider,
|
||||
PluginClient: pluginClient,
|
||||
log: log.New("advisor.datasourcecheck"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +61,7 @@ func (c *check) Steps() []checks.Step {
|
||||
&healthCheckStep{
|
||||
PluginContextProvider: c.PluginContextProvider,
|
||||
PluginClient: c.PluginClient,
|
||||
log: c.log,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -102,6 +106,7 @@ func (s *uidValidationStep) Run(ctx context.Context, obj *advisor.CheckSpec, i a
|
||||
type healthCheckStep struct {
|
||||
PluginContextProvider pluginContextProvider
|
||||
PluginClient plugins.Client
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (s *healthCheckStep) Title() string {
|
||||
@@ -134,7 +139,7 @@ func (s *healthCheckStep) Run(ctx context.Context, obj *advisor.CheckSpec, i any
|
||||
pCtx, err := s.PluginContextProvider.GetWithDataSource(ctx, ds.Type, requester, ds)
|
||||
if err != nil {
|
||||
// Unable to check health check
|
||||
klog.Error("Failed to get plugin context", "datasource_uid", ds.UID, "error", err)
|
||||
s.log.Error("Failed to get plugin context", "datasource_uid", ds.UID, "error", err)
|
||||
return nil, nil
|
||||
}
|
||||
req := &backend.CheckHealthRequest{
|
||||
@@ -143,6 +148,15 @@ func (s *healthCheckStep) Run(ctx context.Context, obj *advisor.CheckSpec, i any
|
||||
}
|
||||
resp, err := s.PluginClient.CheckHealth(ctx, req)
|
||||
if err != nil || resp.Status != backend.HealthStatusOk {
|
||||
if err != nil {
|
||||
s.log.Debug("Failed to check health", "datasource_uid", ds.UID, "error", err)
|
||||
if errors.Is(err, plugins.ErrMethodNotImplemented) || errors.Is(err, plugins.ErrPluginUnavailable) {
|
||||
// The plugin does not support backend health checks
|
||||
return nil, nil
|
||||
}
|
||||
} else {
|
||||
s.log.Debug("Failed to check health", "datasource_uid", ds.UID, "status", resp.Status, "message", resp.Message)
|
||||
}
|
||||
return checks.NewCheckReportFailure(
|
||||
advisor.CheckReportFailureSeverityHigh,
|
||||
s.ID(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -28,6 +29,7 @@ func TestCheck_Run(t *testing.T) {
|
||||
DatasourceSvc: mockDatasourceSvc,
|
||||
PluginContextProvider: mockPluginContextProvider,
|
||||
PluginClient: mockPluginClient,
|
||||
log: log.New("advisor.datasourcecheck"),
|
||||
}
|
||||
|
||||
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{})
|
||||
@@ -62,6 +64,7 @@ func TestCheck_Run(t *testing.T) {
|
||||
DatasourceSvc: mockDatasourceSvc,
|
||||
PluginContextProvider: mockPluginContextProvider,
|
||||
PluginClient: mockPluginClient,
|
||||
log: log.New("advisor.datasourcecheck"),
|
||||
}
|
||||
|
||||
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{})
|
||||
@@ -97,6 +100,7 @@ func TestCheck_Run(t *testing.T) {
|
||||
DatasourceSvc: mockDatasourceSvc,
|
||||
PluginContextProvider: mockPluginContextProvider,
|
||||
PluginClient: mockPluginClient,
|
||||
log: log.New("advisor.datasourcecheck"),
|
||||
}
|
||||
|
||||
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{})
|
||||
@@ -118,6 +122,40 @@ func TestCheck_Run(t *testing.T) {
|
||||
assert.Len(t, failures, 1)
|
||||
assert.Equal(t, "health-check", failures[0].StepID)
|
||||
})
|
||||
|
||||
t.Run("should skip health check when plugin does not support backend health checks", func(t *testing.T) {
|
||||
datasources := []*datasources.DataSource{
|
||||
{UID: "valid-uid-1", Type: "prometheus", Name: "Prometheus"},
|
||||
}
|
||||
mockDatasourceSvc := &MockDatasourceSvc{dss: datasources}
|
||||
mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}}
|
||||
mockPluginClient := &MockPluginClient{err: plugins.ErrMethodNotImplemented}
|
||||
|
||||
check := &check{
|
||||
DatasourceSvc: mockDatasourceSvc,
|
||||
PluginContextProvider: mockPluginContextProvider,
|
||||
PluginClient: mockPluginClient,
|
||||
log: log.New("advisor.datasourcecheck"),
|
||||
}
|
||||
|
||||
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{})
|
||||
items, err := check.Items(ctx)
|
||||
assert.NoError(t, err)
|
||||
failures := []advisor.CheckReportFailure{}
|
||||
for _, step := range check.Steps() {
|
||||
for _, item := range items {
|
||||
stepFailures, err := step.Run(ctx, &advisor.CheckSpec{}, item)
|
||||
assert.NoError(t, err)
|
||||
if stepFailures != nil {
|
||||
failures = append(failures, *stepFailures)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(items))
|
||||
assert.Len(t, failures, 0)
|
||||
})
|
||||
}
|
||||
|
||||
type MockDatasourceSvc struct {
|
||||
@@ -142,8 +180,9 @@ type MockPluginClient struct {
|
||||
plugins.Client
|
||||
|
||||
res *backend.CheckHealthResult
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *MockPluginClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
return m.res, nil
|
||||
return m.res, m.err
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,3 +27,14 @@ func NewCheckReportFailure(
|
||||
Links: links,
|
||||
}
|
||||
}
|
||||
|
||||
func GetNamespace(stackID string) (string, error) {
|
||||
if stackID == "" {
|
||||
return metav1.NamespaceDefault, nil
|
||||
}
|
||||
stackId, err := strconv.ParseInt(stackID, 10, 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid stack id: %s", stackID)
|
||||
}
|
||||
return types.CloudNamespaceFormatter(stackId), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package checks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestGetNamespace(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "empty stack ID",
|
||||
input: "",
|
||||
expected: metav1.NamespaceDefault,
|
||||
},
|
||||
{
|
||||
name: "valid stack ID",
|
||||
input: "1234567890",
|
||||
expected: "stacks-1234567890",
|
||||
},
|
||||
{
|
||||
name: "invalid stack ID",
|
||||
input: "invalid",
|
||||
expected: "",
|
||||
expectedErr: "invalid stack id: invalid",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := GetNamespace(tt.input)
|
||||
if tt.expectedErr != "" {
|
||||
assert.EqualError(t, err, tt.expectedErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ type Runner struct {
|
||||
client resource.Client
|
||||
evaluationInterval time.Duration
|
||||
maxHistory int
|
||||
namespace string
|
||||
}
|
||||
|
||||
// NewRunner creates a new Runner.
|
||||
@@ -47,6 +48,10 @@ func New(cfg app.Config) (app.Runnable, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namespace, err := checks.GetNamespace(specificConfig.StackID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prepare storage client
|
||||
clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{})
|
||||
@@ -60,6 +65,7 @@ func New(cfg app.Config) (app.Runnable, error) {
|
||||
client: client,
|
||||
evaluationInterval: evalInterval,
|
||||
maxHistory: maxHistory,
|
||||
namespace: namespace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -114,7 +120,7 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
// regardless of its ID. This assumes that the checks are created in batches
|
||||
// so a batch will have a similar creation time.
|
||||
func (r *Runner) checkLastCreated(ctx context.Context) (time.Time, error) {
|
||||
list, err := r.client.List(ctx, metav1.NamespaceDefault, resource.ListOptions{})
|
||||
list, err := r.client.List(ctx, r.namespace, resource.ListOptions{})
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
@@ -134,7 +140,7 @@ func (r *Runner) createChecks(ctx context.Context) error {
|
||||
obj := &advisorv0alpha1.Check{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "check-",
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
Namespace: r.namespace,
|
||||
Labels: map[string]string{
|
||||
checks.TypeLabel: check.ID(),
|
||||
},
|
||||
@@ -152,7 +158,7 @@ func (r *Runner) createChecks(ctx context.Context) error {
|
||||
|
||||
// cleanupChecks deletes the olders checks if the number of checks exceeds the limit.
|
||||
func (r *Runner) cleanupChecks(ctx context.Context) error {
|
||||
list, err := r.client.List(ctx, metav1.NamespaceDefault, resource.ListOptions{Limit: -1})
|
||||
list, err := r.client.List(ctx, r.namespace, resource.ListOptions{Limit: -1})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
type Runner struct {
|
||||
checkRegistry checkregistry.CheckService
|
||||
client resource.Client
|
||||
namespace string
|
||||
}
|
||||
|
||||
// NewRunner creates a new Runner.
|
||||
@@ -29,6 +31,10 @@ func New(cfg app.Config) (app.Runnable, error) {
|
||||
return nil, fmt.Errorf("invalid config type")
|
||||
}
|
||||
checkRegistry := specificConfig.CheckRegistry
|
||||
namespace, err := checks.GetNamespace(specificConfig.StackID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prepare storage client
|
||||
clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{})
|
||||
@@ -40,6 +46,7 @@ func New(cfg app.Config) (app.Runnable, error) {
|
||||
return &Runner{
|
||||
checkRegistry: checkRegistry,
|
||||
client: client,
|
||||
namespace: namespace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -58,7 +65,7 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
obj := &advisorv0alpha1.CheckType{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: t.ID(),
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
Namespace: r.namespace,
|
||||
},
|
||||
Spec: advisorv0alpha1.CheckTypeSpec{
|
||||
Name: t.ID(),
|
||||
|
||||
@@ -3,6 +3,7 @@ package checktyperegisterer
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
@@ -88,6 +89,24 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
},
|
||||
expectedErr: errors.New("update error"),
|
||||
},
|
||||
{
|
||||
name: "custom namespace",
|
||||
checks: []checks.Check{
|
||||
&mockCheck{
|
||||
id: "check1",
|
||||
steps: []checks.Step{
|
||||
&mockStep{id: "step1", title: "Step 1", description: "Description 1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
createFunc: func(ctx context.Context, id resource.Identifier, obj resource.Object, opts resource.CreateOptions) (resource.Object, error) {
|
||||
if obj.GetNamespace() != "custom-namespace" {
|
||||
return nil, fmt.Errorf("expected namespace %s, got %s", "custom-namespace", obj.GetNamespace())
|
||||
}
|
||||
return obj, nil
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -98,6 +117,7 @@ func TestCheckTypesRegisterer_Run(t *testing.T) {
|
||||
createFunc: tt.createFunc,
|
||||
updateFunc: tt.updateFunc,
|
||||
},
|
||||
namespace: "custom-namespace",
|
||||
}
|
||||
err := r.Run(context.Background())
|
||||
if err != nil {
|
||||
|
||||
@@ -72,13 +72,13 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
|
||||
@@ -207,8 +207,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -223,11 +223,11 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -239,12 +239,12 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -62,12 +62,12 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
|
||||
@@ -155,10 +155,10 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -169,12 +169,12 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -63,12 +63,12 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
|
||||
@@ -155,10 +155,10 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -169,12 +169,12 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -44,6 +44,10 @@ Grafana allows you to link an alert rule to a dashboard panel. This can help you
|
||||
|
||||
An alert rule is linked to a panel by setting the [`dashboardUId` and `panelId` annotations](ref:annotations). Both annotations must be set together.
|
||||
|
||||
{{% admonition type="tutorial" %}}
|
||||
For a hands-on example of integrating alert rules with dashboards, check out [Part 5 of our Get Started with Grafana Alerting tutorial](http://www.grafana.com/tutorials/alerting-get-started-pt5/).
|
||||
{{% /admonition %}}
|
||||
|
||||
## Link alert rules to panels
|
||||
|
||||
When configuring the alert rule, you can set the dashboard and panel annotations as shown in this [video](https://youtu.be/ClLp-iSoaSY?si=qKWnvSVaQuvYcuw9&t=170).
|
||||
|
||||
@@ -109,8 +109,14 @@ The threshold expression allows the comparison between two single values. Availa
|
||||
|
||||
- **Is above**: `$A > 5`
|
||||
- **Is below**: `$B < 3`
|
||||
- **Is equal to**: `$A == 2`
|
||||
- **Is not equal to**: `$B =! 4`
|
||||
- **Is above or equal to**: `$A >= 8`
|
||||
- **Is below or equal to**: `$B <= 16`
|
||||
- **Is within range**: `$A > 0 AND $A < 10`
|
||||
- **Is outside range**: `$B < 0 OR $B > 100`
|
||||
- **Is within range included**: `$A >= 0 AND $A <= 10`
|
||||
- **Is outside range included**: `$B <= 0 OR $B >= 100`
|
||||
|
||||
A threshold returns `0` when the condition is false and `1` when true.
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@ Content-Type: application/json
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** – Deleted
|
||||
- **200** – Restored
|
||||
- **401** – Unauthorized
|
||||
- **403** – Access denied
|
||||
- **404** – Not found
|
||||
|
||||
@@ -20,21 +20,31 @@ weight: 800
|
||||
|
||||
# Introduction to exemplars
|
||||
|
||||
An exemplar is a specific trace representative of measurement taken in a given time interval. While metrics excel at giving you an aggregated view of your system, traces give you a fine grained view of a single request; exemplars are a way to link the two.
|
||||
An exemplar is a specific trace representative of measurement taken in a given time interval.
|
||||
While metrics excel at giving you an aggregated view of your system, traces give you a fine grained view of a single request; exemplars are a way to link the two.
|
||||
|
||||
Suppose your company website is experiencing a surge in traffic volumes. While more than eighty percent of the users are able to access the website in under two seconds, some users are experiencing a higher than normal response time resulting in bad user experience.
|
||||
Suppose your company website is experiencing a surge in traffic volumes.
|
||||
While more than eighty percent of the users are able to access the website in under two seconds, some users are experiencing a higher than normal response time resulting in bad user experience.
|
||||
|
||||
To identify the factors that are contributing to the latency, you must compare a trace for a fast response against a trace for a slow response. Given the vast amount of data in a typical production environment, it will be extremely laborious and time-consuming effort.
|
||||
To identify the factors that are contributing to the latency, you must compare a trace for a fast response against a trace for a slow response.
|
||||
Given the vast amount of data in a typical production environment, it's an extremely laborious and time-consuming effort.
|
||||
|
||||
Use exemplars to help isolate problems within your data distribution by pinpointing query traces exhibiting high latency within a time interval. Once you localize the latency problem to a few exemplar traces, you can combine it with additional system based information or location properties to perform a root cause analysis faster, leading to quick resolutions to performance issues.
|
||||
Use exemplars to help isolate problems within your data distribution by pinpointing query traces exhibiting high latency within a time interval.
|
||||
After you localize the latency problem to a few exemplar traces, you can combine it with additional system based information or location properties to perform a root cause analysis faster, leading to quick resolutions to performance issues.
|
||||
|
||||
Support for exemplars is available for the Prometheus data source only. Once you enable the functionality, exemplar data is available by default. For more information on exemplar configuration and how to enable exemplars, refer to [configuring exemplars in the Prometheus data source]({{< relref "../../datasources/prometheus/configure-prometheus-data-source#exemplars" >}}).
|
||||
Support for exemplars is available for the Prometheus data source only.
|
||||
After you enable the functionality, exemplar data is available by default.
|
||||
For more information on exemplar configuration and how to enable exemplars, refer to [configuring exemplars in the Prometheus data source](../../datasources/prometheus/configure-prometheus-data-source/#exemplars).
|
||||
|
||||
Grafana shows exemplars alongside a metric in the Explore view and in dashboards. Each exemplar displays as a highlighted star. You can hover your cursor over an exemplar to view the unique trace ID, which is a combination of a key value pair. To investigate further, click the blue button next to the `traceID` property.
|
||||
Grafana shows exemplars alongside a metric in the Explore view and in dashboards.
|
||||
Each exemplar displays as a highlighted star.
|
||||
You can hover your cursor over an exemplar to view the unique trace ID, which is a combination of a key value pair.
|
||||
To investigate further, click the blue button next to the `traceID` property.
|
||||
|
||||
{{< figure src="/media/docs/grafana/exemplars/screenshot-exemplars.png" class="docs-image--no-shadow" max-width= "750px" caption="Screenshot showing the detail window of an exemplar" >}}
|
||||
|
||||
Refer to [View exemplar data]({{< relref "#view-exemplar-data" >}}) for instructions on how to drill down and view exemplar trace details from metrics and logs. To know more about exemplars, refer to the blogpost [Intro to exemplars, which enable Grafana Tempo’s distributed tracing at massive scale](/blog/2021/03/31/intro-to-exemplars-which-enable-grafana-tempos-distributed-tracing-at-massive-scale/).
|
||||
Refer to [View exemplar data](#view-exemplar-data) for instructions on how to drill down and view exemplar trace details from metrics and logs.
|
||||
To know more about exemplars, refer to the blog post [Intro to exemplars, which enable Grafana Tempo’s distributed tracing at massive scale](/blog/2021/03/31/intro-to-exemplars-which-enable-grafana-tempos-distributed-tracing-at-massive-scale/).
|
||||
|
||||
## View exemplar data
|
||||
|
||||
@@ -42,15 +52,19 @@ When support for exemplar support is enabled for a Prometheus data source, you c
|
||||
|
||||
### In Explore
|
||||
|
||||
Explore visualizes exemplar traces as highlighted stars alongside metrics data. For more information on how Explore visualizes trace data, refer to [Tracing in Explore]({{< relref "../../explore/trace-integration" >}}).
|
||||
Explore visualizes exemplar traces as highlighted stars alongside metrics data.
|
||||
For more information on how Explore visualizes trace data, refer to [Tracing in Explore](../../explore/trace-integration/).
|
||||
|
||||
To examine the details of an exemplar trace:
|
||||
|
||||
1. Place your cursor over an exemplar (highlighted star). Depending on the trace data source you are using, you will see a blue button with the label `Query with <data source name>`. In the following example, the tracing data source is Tempo.
|
||||
1. Place your cursor over an exemplar (highlighted star).
|
||||
Depending on the trace data source you are using, you may see a blue button with the label `Query with <DATA SOURCE NAME>`.
|
||||
In the following example, the tracing data source is Tempo.
|
||||
|
||||
{{< figure src="/media/docs/grafana/exemplars/screenshot-exemplar-details.png" class="docs-image--no-shadow" max-width= "350px" caption="Screenshot showing exemplar details" >}}
|
||||
|
||||
1. Click the **Query with Tempo** option next to the `traceID` property. The trace details, including the spans within the trace are listed in a separate panel on the right.
|
||||
1. Click the **Query with Tempo** option next to the `traceID` property.
|
||||
The trace details, including the spans within the trace are listed in a separate panel on the right.
|
||||
|
||||
{{< figure src="/media/docs/grafana/exemplars/screenshot-exemplar-explore-view.png" class="docs-image--no-shadow" max-width= "900px" caption="Explorer view with panel showing trace details" >}}
|
||||
|
||||
@@ -58,13 +72,20 @@ For more information on how to drill down and analyze the trace and span details
|
||||
|
||||
### In logs
|
||||
|
||||
You can also view exemplar trace details from the Loki logs in Explore. Use regex within the Derived fields links for Loki to extract the `traceID` information. Now when you expand Loki logs, you can see a `traceID` property under the **Detected fields** section. To learn more about how to extract a part of a log message into an internal or external link, refer to [using derived fields in Loki]({{< relref "../../explore/logs-integration" >}}).
|
||||
You can also view exemplar trace details from the Loki logs in Explore.
|
||||
Use regular expressions within the Derived fields links for Loki to extract the `traceID` information.
|
||||
Now when you expand Loki logs, you can see a `traceID` property under the **Detected fields** section.
|
||||
To learn more about how to extract a part of a log message into an internal or external link, refer to [using derived fields in Loki](../../explore/logs-integration/).
|
||||
|
||||
To view the details of an exemplar trace:
|
||||
|
||||
1. Expand a log line and scroll down to the `Fields` section. Depending on your backend trace data source, you will see a blue button with the label `<data source name>`.
|
||||
1. Expand a log line and scroll down to the `Fields` section.
|
||||
Depending on your backend trace data source, you may see a blue button with the label `<DATA SOURCE NAME>`.
|
||||
|
||||
1. Click the blue button next to the `traceID` property. Typically, it will have the name of the backend data source. In the following example, the tracing data source is Tempo. The trace details, including the spans within the trace are listed in a separate panel on the right.
|
||||
1. Click the blue button next to the `traceID` property.
|
||||
Typically, it has the name of the backend data source.
|
||||
In the following example, the tracing data source is Tempo.
|
||||
The trace details, including the spans within the trace are listed in a separate panel on the right.
|
||||
|
||||
{{< figure src="/media/docs/grafana/exemplars/screenshot-exemplar-loki-logs.png" class="docs-image--no-shadow" max-width= "750px" caption="Explorer view with panel showing trace details" >}}
|
||||
|
||||
@@ -78,16 +99,20 @@ This panel shows the details of the trace in different segments.
|
||||
|
||||
You can add more traces to the results using the `Add query` button.
|
||||
|
||||
- The next segment shows the entire span for the specific trace as a narrow strip. All levels of the trace from the client all the way down to database query is displayed, which provides a bird's eye view of the time distribution across all layers over which the HTTP request was processed.
|
||||
- The next segment shows the entire span for the specific trace as a narrow strip.
|
||||
All levels of the trace from the client all the way down to database query is displayed, which provides a bird's eye view of the time distribution across all layers over which the HTTP request was processed.
|
||||
|
||||
1. You can click within this strip view to display a magnified view of a smaller time segment within the span. This magnified view shows up in the bottom segment of the panel.
|
||||
|
||||
1. In the magnified view, you can expand or collapse the various levels of the trace to drill down to the specific span of interest.
|
||||
|
||||
For example, if the strip view shows that most of the latency was within the app layer, you can expand the trace down the app layer to investigate the problem further. To expand a particular layer of span, click the left icon. The same button can collapse an expanded span.
|
||||
For example, if the strip view shows that most of the latency was within the app layer, you can expand the trace down the app layer to investigate the problem further.
|
||||
To expand a particular layer of span, click the left icon.
|
||||
The same button can collapse an expanded span.
|
||||
|
||||
- To see the details of the span at any level, click the span itself.
|
||||
|
||||
This displays additional metadata associated with the span. The metadata itself is initially shown in a narrow strip but you can see more details by clicking the metadata strip.
|
||||
This displays additional metadata associated with the span.
|
||||
The metadata itself is initially shown in a narrow strip but you can see more details by clicking the metadata strip.
|
||||
|
||||
{{< figure src="/media/docs/grafana/exemplars/screenshot-exemplar-span-details.png" class="docs-image--no-shadow" max-width= "600px" caption="Span details" >}}
|
||||
|
||||
@@ -32,7 +32,7 @@ Grafana relies on other open source software to operate. For a list of open sour
|
||||
Grafana supports the following operating systems:
|
||||
|
||||
- [Debian or Ubuntu]({{< relref "./debian" >}})
|
||||
- [Red Hat, RHEL, or Fedora]({{< relref "./redhat-rhel-fedora" >}})
|
||||
- [RHEL or Fedora]({{< relref "./rhel-fedora" >}})
|
||||
- [SUSE or openSUSE]({{< relref "./suse-opensuse" >}})
|
||||
- [macOS]({{< relref "./mac" >}})
|
||||
- [Windows]({{< relref "./windows" >}})
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('Import Dashboards Test', () => {
|
||||
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
|
||||
});
|
||||
|
||||
it('Ensure you can import a number of json test dashboards from a specific test directory', () => {
|
||||
it.skip('Ensure you can import a number of json test dashboards from a specific test directory', () => {
|
||||
e2e.flows.importDashboard(testDashboard, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20250221202230-9d7e00921e44 // @grafana/alerting-backend
|
||||
github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642 // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 // @grafana/identity-access-team
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
@@ -167,13 +167,13 @@ require (
|
||||
go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage
|
||||
go.uber.org/zap v1.27.0 // @grafana/identity-access-team
|
||||
gocloud.dev v0.40.0 // @grafana/grafana-app-platform-squad
|
||||
golang.org/x/crypto v0.32.0 // @grafana/grafana-backend-group
|
||||
golang.org/x/crypto v0.35.0 // @grafana/grafana-backend-group
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // @grafana/alerting-backend
|
||||
golang.org/x/mod v0.22.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/net v0.34.0 // @grafana/oss-big-tent @grafana/partner-datasources
|
||||
golang.org/x/oauth2 v0.26.0 // @grafana/identity-access-team
|
||||
golang.org/x/net v0.35.0 // @grafana/oss-big-tent @grafana/partner-datasources
|
||||
golang.org/x/oauth2 v0.27.0 // @grafana/identity-access-team
|
||||
golang.org/x/sync v0.11.0 // @grafana/alerting-backend
|
||||
golang.org/x/text v0.21.0 // @grafana/grafana-backend-group
|
||||
golang.org/x/text v0.22.0 // @grafana/grafana-backend-group
|
||||
golang.org/x/time v0.9.0 // @grafana/grafana-backend-group
|
||||
golang.org/x/tools v0.29.0 // indirect; @grafana/grafana-as-code
|
||||
gonum.org/v1/gonum v0.15.1 // @grafana/oss-big-tent
|
||||
@@ -520,7 +520,7 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
|
||||
|
||||
@@ -1511,8 +1511,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250221202230-9d7e00921e44 h1:vboqvbAO0s0CTALHnqfmNvhCP1ziBcZNpYDbORqvOgg=
|
||||
github.com/grafana/alerting v0.0.0-20250221202230-9d7e00921e44/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642 h1:iQ0h/h+QoguSZDF+ZpPxcM/C+m1kjh+aXjMpxywowPA=
|
||||
github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
|
||||
@@ -2552,8 +2552,8 @@ golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -2699,8 +2699,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -2736,8 +2736,8 @@ golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4
|
||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
|
||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -2904,8 +2904,8 @@ golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -2925,8 +2925,8 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -619,6 +619,7 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
|
||||
github.com/grafana/alerting v0.0.0-20250221202230-9d7e00921e44/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
@@ -1112,6 +1113,8 @@ golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5D
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
@@ -1137,6 +1140,7 @@ golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
+1
-1
@@ -376,7 +376,7 @@
|
||||
"react-highlight-words": "0.21.0",
|
||||
"react-hook-form": "^7.49.2",
|
||||
"react-i18next": "^15.0.0",
|
||||
"react-inlinesvg": "4.1.5",
|
||||
"react-inlinesvg": "4.2.0",
|
||||
"react-loading-skeleton": "3.5.0",
|
||||
"react-moveable": "0.56.0",
|
||||
"react-redux": "9.2.0",
|
||||
|
||||
@@ -1,53 +1,15 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'path';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import { nodeExternals } from 'rollup-plugin-node-externals';
|
||||
|
||||
import { entryPoint, plugins, esmOutput, cjsOutput, tsDeclarationOutput } from '../rollup.config.parts';
|
||||
|
||||
const rq = createRequire(import.meta.url);
|
||||
const pkg = rq('./package.json');
|
||||
|
||||
const legacyOutputDefaults = {
|
||||
esModule: true,
|
||||
interop: 'compat',
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
plugins: [
|
||||
nodeExternals({ deps: true, packagePath: './package.json' }),
|
||||
resolve(),
|
||||
esbuild({
|
||||
target: 'es2018',
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
}),
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.main),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
{
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.module),
|
||||
preserveModules: true,
|
||||
// @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string)
|
||||
preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-data/src`),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
input: './compiled/index.d.ts',
|
||||
plugins: [dts()],
|
||||
output: {
|
||||
file: pkg.publishConfig.types,
|
||||
format: 'es',
|
||||
},
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-data')],
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
];
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface StandardEditorContext<TOptions, TState = any> {
|
||||
options?: TOptions;
|
||||
instanceState?: TState;
|
||||
isOverride?: boolean;
|
||||
annotations?: DataFrame[];
|
||||
}
|
||||
|
||||
export interface StandardEditorProps<TValue = any, TSettings = any, TOptions = any, TState = any> {
|
||||
|
||||
@@ -179,4 +179,6 @@ export enum FeatureState {
|
||||
privatePreview = 'private preview',
|
||||
/** used to mark features that are in public preview with low/medium risk, or as a shared badge for public and private previews */
|
||||
preview = 'preview',
|
||||
/** used to mark new GA features */
|
||||
new = 'new',
|
||||
}
|
||||
|
||||
@@ -51,5 +51,5 @@ export interface OptionEditorConfig<TOptions, TSettings = any, TValue = any> {
|
||||
/**
|
||||
* Function that enables configuration of when option editor should be shown based on current panel option properties.
|
||||
*/
|
||||
showIf?: (currentOptions: TOptions, data?: DataFrame[]) => boolean | undefined;
|
||||
showIf?: (currentOptions: TOptions, data?: DataFrame[], annotations?: DataFrame[]) => boolean | undefined;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export function getDefaultTimeRange(): TimeRange {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default realtive time range.
|
||||
* Returns the default relative time range.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -1,53 +1,15 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'path';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import { nodeExternals } from 'rollup-plugin-node-externals';
|
||||
|
||||
import { cjsOutput, entryPoint, esmOutput, plugins, tsDeclarationOutput } from '../rollup.config.parts';
|
||||
|
||||
const rq = createRequire(import.meta.url);
|
||||
const pkg = rq('./package.json');
|
||||
|
||||
const legacyOutputDefaults = {
|
||||
esModule: true,
|
||||
interop: 'compat',
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
plugins: [
|
||||
nodeExternals({ deps: true, packagePath: './package.json' }),
|
||||
resolve(),
|
||||
esbuild({
|
||||
target: 'es2018',
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
}),
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.main),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
{
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.module),
|
||||
preserveModules: true,
|
||||
// @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string)
|
||||
preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-e2e-selectors/src`),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
input: './compiled/index.d.ts',
|
||||
plugins: [dts()],
|
||||
output: {
|
||||
file: pkg.publishConfig.types,
|
||||
format: 'es',
|
||||
},
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-e2e-selectors')],
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
];
|
||||
|
||||
@@ -1,53 +1,15 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'path';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import { nodeExternals } from 'rollup-plugin-node-externals';
|
||||
|
||||
import { cjsOutput, entryPoint, esmOutput, plugins, tsDeclarationOutput } from '../rollup.config.parts';
|
||||
|
||||
const rq = createRequire(import.meta.url);
|
||||
const pkg = rq('./package.json');
|
||||
|
||||
const legacyOutputDefaults = {
|
||||
esModule: true,
|
||||
interop: 'compat',
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
plugins: [
|
||||
nodeExternals({ deps: true, packagePath: './package.json' }),
|
||||
resolve(),
|
||||
esbuild({
|
||||
target: 'es2018',
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
}),
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.main),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
{
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.module),
|
||||
preserveModules: true,
|
||||
// @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string)
|
||||
preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-ui/src`),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
input: './compiled/index.d.ts',
|
||||
plugins: [dts()],
|
||||
output: {
|
||||
file: pkg.publishConfig.types,
|
||||
format: 'es',
|
||||
},
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-flamegraph')],
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
];
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,45 +1,15 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'path';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import { nodeExternals } from 'rollup-plugin-node-externals';
|
||||
|
||||
import { entryPoint, esmOutput, plugins, tsDeclarationOutput } from '../rollup.config.parts';
|
||||
|
||||
const rq = createRequire(import.meta.url);
|
||||
const pkg = rq('./package.json');
|
||||
|
||||
const legacyOutputDefaults = {
|
||||
esModule: true,
|
||||
interop: 'compat',
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
plugins: [
|
||||
nodeExternals({ deps: true, packagePath: './package.json' }),
|
||||
resolve(),
|
||||
esbuild({
|
||||
target: 'es2018',
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
}),
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.main),
|
||||
preserveModules: true,
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
plugins: [dts()],
|
||||
output: {
|
||||
file: pkg.publishConfig.types,
|
||||
format: 'es',
|
||||
},
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: esmOutput(pkg, 'grafana-icons'),
|
||||
},
|
||||
tsDeclarationOutput(pkg, { input: 'src/index.ts' }),
|
||||
];
|
||||
|
||||
@@ -1,55 +1,16 @@
|
||||
import image from '@rollup/plugin-image';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'path';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import { nodeExternals } from 'rollup-plugin-node-externals';
|
||||
|
||||
import { cjsOutput, entryPoint, esmOutput, plugins, tsDeclarationOutput } from '../rollup.config.parts';
|
||||
|
||||
const rq = createRequire(import.meta.url);
|
||||
const pkg = rq('./package.json');
|
||||
|
||||
const legacyOutputDefaults = {
|
||||
esModule: true,
|
||||
interop: 'compat',
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
plugins: [
|
||||
nodeExternals({ deps: true, packagePath: './package.json' }),
|
||||
resolve(),
|
||||
esbuild({
|
||||
target: 'es2018',
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
}),
|
||||
image(),
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.main),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
{
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.module),
|
||||
preserveModules: true,
|
||||
// @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string)
|
||||
preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-prometheus/src`),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
input: './compiled/index.d.ts',
|
||||
plugins: [dts()],
|
||||
output: {
|
||||
file: pkg.publishConfig.types,
|
||||
format: 'es',
|
||||
},
|
||||
input: entryPoint,
|
||||
plugins: [...plugins, image()],
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-prometheus')],
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
];
|
||||
|
||||
@@ -1,53 +1,15 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'path';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import { nodeExternals } from 'rollup-plugin-node-externals';
|
||||
|
||||
import { cjsOutput, entryPoint, esmOutput, plugins, tsDeclarationOutput } from '../rollup.config.parts';
|
||||
|
||||
const rq = createRequire(import.meta.url);
|
||||
const pkg = rq('./package.json');
|
||||
|
||||
const legacyOutputDefaults = {
|
||||
esModule: true,
|
||||
interop: 'compat',
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
plugins: [
|
||||
nodeExternals({ deps: true, packagePath: './package.json' }),
|
||||
resolve(),
|
||||
esbuild({
|
||||
target: 'es2018',
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
}),
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.main),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
{
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.module),
|
||||
preserveModules: true,
|
||||
// @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string)
|
||||
preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-runtime/src`),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
input: './compiled/index.d.ts',
|
||||
plugins: [dts()],
|
||||
output: {
|
||||
file: pkg.publishConfig.types,
|
||||
format: 'es',
|
||||
},
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-runtime')],
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
];
|
||||
|
||||
@@ -1,57 +1,22 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { glob } from 'glob';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'path';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import { nodeExternals } from 'rollup-plugin-node-externals';
|
||||
|
||||
import { cjsOutput, entryPoint, esmOutput, plugins, tsDeclarationOutput } from '../rollup.config.parts';
|
||||
|
||||
const rq = createRequire(import.meta.url);
|
||||
const pkg = rq('./package.json');
|
||||
|
||||
const legacyOutputDefaults = {
|
||||
esModule: true,
|
||||
interop: 'compat',
|
||||
};
|
||||
const [_, noderesolve, esbuild] = plugins;
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
plugins: [
|
||||
nodeExternals({ deps: true, packagePath: './package.json' }),
|
||||
resolve(),
|
||||
esbuild({
|
||||
target: 'es2018',
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
}),
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.main),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
{
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.module),
|
||||
preserveModules: true,
|
||||
// @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string)
|
||||
preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-schema/src`),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
input: './dist/esm/index.d.ts',
|
||||
plugins: [dts()],
|
||||
output: {
|
||||
file: pkg.publishConfig.types,
|
||||
format: 'es',
|
||||
},
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-schema')],
|
||||
},
|
||||
tsDeclarationOutput(pkg, { input: './dist/esm/index.d.ts' }),
|
||||
{
|
||||
input: Object.fromEntries(
|
||||
glob
|
||||
@@ -61,13 +26,7 @@ export default [
|
||||
fileURLToPath(new URL(file, import.meta.url)),
|
||||
])
|
||||
),
|
||||
plugins: [
|
||||
resolve(),
|
||||
esbuild({
|
||||
target: 'es2018',
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
}),
|
||||
],
|
||||
plugins: [noderesolve, esbuild],
|
||||
output: {
|
||||
format: 'esm',
|
||||
dir: path.dirname(pkg.publishConfig.module),
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"react-highlight-words": "0.21.0",
|
||||
"react-hook-form": "^7.49.2",
|
||||
"react-i18next": "^15.0.0",
|
||||
"react-inlinesvg": "4.1.5",
|
||||
"react-inlinesvg": "4.2.0",
|
||||
"react-loading-skeleton": "3.5.0",
|
||||
"react-router-dom": "5.3.4",
|
||||
"react-router-dom-v5-compat": "^6.26.1",
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'path';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import { nodeExternals } from 'rollup-plugin-node-externals';
|
||||
import svg from 'rollup-plugin-svg-import';
|
||||
|
||||
import { cjsOutput, entryPoint, esmOutput, plugins, tsDeclarationOutput } from '../rollup.config.parts';
|
||||
|
||||
const rq = createRequire(import.meta.url);
|
||||
const icons = rq('../../public/app/core/icons/cached.json');
|
||||
const pkg = rq('./package.json');
|
||||
@@ -15,51 +12,18 @@ const iconSrcPaths = icons.map((iconSubPath) => {
|
||||
return `../../public/img/icons/${iconSubPath}.svg`;
|
||||
});
|
||||
|
||||
const legacyOutputDefaults = {
|
||||
esModule: true,
|
||||
interop: 'compat',
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
input: entryPoint,
|
||||
plugins: [
|
||||
nodeExternals({ deps: true, packagePath: './package.json' }),
|
||||
...plugins,
|
||||
svg({ stringify: true }),
|
||||
resolve(),
|
||||
copy({
|
||||
targets: [{ src: iconSrcPaths, dest: './dist/public/' }],
|
||||
flatten: false,
|
||||
}),
|
||||
esbuild({
|
||||
target: 'es2018',
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
}),
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.main),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
{
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
dir: path.dirname(pkg.publishConfig.module),
|
||||
preserveModules: true,
|
||||
// @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string)
|
||||
preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-ui/src`),
|
||||
...legacyOutputDefaults,
|
||||
},
|
||||
],
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-ui')],
|
||||
},
|
||||
{
|
||||
input: './compiled/index.d.ts',
|
||||
plugins: [dts()],
|
||||
output: {
|
||||
file: pkg.publishConfig.types,
|
||||
format: 'es',
|
||||
},
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
];
|
||||
|
||||
@@ -79,7 +79,7 @@ const getStyles = (theme: GrafanaTheme2, color: BadgeColor) => {
|
||||
border: `1px solid ${borderColor}`,
|
||||
color: textColor,
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
gap: '2px',
|
||||
gap: theme.spacing(0.5),
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
lineHeight: theme.typography.bodySmall.lineHeight,
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FeatureState } from '@grafana/data';
|
||||
|
||||
import { t } from '../../utils/i18n';
|
||||
import { Badge, BadgeProps } from '../Badge/Badge';
|
||||
|
||||
export interface FeatureBadgeProps {
|
||||
@@ -30,21 +31,28 @@ function getPanelStateBadgeDisplayModel(featureState: FeatureState): BadgeProps
|
||||
|
||||
case FeatureState.experimental:
|
||||
return {
|
||||
text: 'Experimental',
|
||||
text: t('grafana-ui.feature-badge.experimental', 'Experimental'),
|
||||
icon: 'exclamation-triangle',
|
||||
color: 'orange',
|
||||
};
|
||||
|
||||
case FeatureState.preview:
|
||||
return {
|
||||
text: 'Preview',
|
||||
text: t('grafana-ui.feature-badge.preview', 'Preview'),
|
||||
icon: 'rocket',
|
||||
color: 'blue',
|
||||
};
|
||||
|
||||
case FeatureState.privatePreview:
|
||||
return {
|
||||
text: 'Private preview',
|
||||
text: t('grafana-ui.feature-badge.private-preview', 'Private preview'),
|
||||
icon: 'rocket',
|
||||
color: 'blue',
|
||||
};
|
||||
|
||||
case FeatureState.new:
|
||||
return {
|
||||
text: t('grafana-ui.feature-badge.new', 'New!'),
|
||||
icon: 'rocket',
|
||||
color: 'blue',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PanelOptionsEditorBuilder } from '@grafana/data';
|
||||
import { DataFrame, PanelOptionsEditorBuilder } from '@grafana/data';
|
||||
import { OptionsWithTooltip, TooltipDisplayMode, SortOrder } from '@grafana/schema';
|
||||
|
||||
/** @internal */
|
||||
@@ -94,6 +94,13 @@ export function addTooltipOptions<T extends OptionsWithTooltip>(
|
||||
settings: {
|
||||
integer: true,
|
||||
},
|
||||
showIf: (options: T) => options.tooltip?.mode === TooltipDisplayMode.Multi,
|
||||
showIf: (options: T, data: DataFrame[] | undefined, annotations: DataFrame[] | undefined) => {
|
||||
return (
|
||||
options.tooltip?.mode === TooltipDisplayMode.Multi ||
|
||||
annotations?.some((df) => {
|
||||
return df.meta?.custom?.resultType === 'exemplar';
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// This file contains the common parts of the rollup configuration that are shared across multiple packages.
|
||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import { nodeExternals } from 'rollup-plugin-node-externals';
|
||||
|
||||
// This is the path to the root of the grafana project
|
||||
// Prefer PROJECT_CWD env var set by yarn berry
|
||||
const projectCwd = process.env.PROJECT_CWD ?? '../../';
|
||||
|
||||
export const entryPoint = 'src/index.ts';
|
||||
|
||||
// Plugins that are shared across all rollup configurations. Their order can affect build output.
|
||||
// Externalising and resolving modules should happen before transformation.
|
||||
export const plugins = [
|
||||
nodeExternals({ deps: true, packagePath: './package.json' }),
|
||||
nodeResolve(),
|
||||
esbuild({
|
||||
target: 'es2018',
|
||||
tsconfig: 'tsconfig.build.json',
|
||||
}),
|
||||
];
|
||||
|
||||
// Generates a rollup configuration for commonjs output.
|
||||
export function cjsOutput(pkg) {
|
||||
return {
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
dir: dirname(pkg.publishConfig.main),
|
||||
esModule: true,
|
||||
interop: 'compat',
|
||||
};
|
||||
}
|
||||
|
||||
// Generate a rollup configuration for es module output.
|
||||
export function esmOutput(pkg, pkgName) {
|
||||
return {
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
dir: dirname(pkg.publishConfig.module),
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: resolve(projectCwd, `packages/${pkgName}/src`),
|
||||
};
|
||||
}
|
||||
|
||||
// Generate a rollup configuration for rolling up typescript declaration files into a single file.
|
||||
export function tsDeclarationOutput(pkg, overrides = {}) {
|
||||
return {
|
||||
input: './compiled/index.d.ts',
|
||||
plugins: [dts()],
|
||||
output: {
|
||||
file: pkg.publishConfig.types,
|
||||
format: 'es',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -134,15 +134,15 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
|
||||
+10
-10
@@ -396,8 +396,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
@@ -418,11 +418,11 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -447,12 +447,12 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -36,11 +36,11 @@ require (
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
google.golang.org/grpc v1.70.0 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
|
||||
@@ -89,8 +89,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -103,8 +103,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -135,8 +135,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
|
||||
@@ -81,11 +81,12 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
|
||||
|
||||
+10
-10
@@ -233,8 +233,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@@ -251,11 +251,11 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -269,12 +269,12 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
+4
-4
@@ -26,12 +26,12 @@ require (
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect; @grafana/grafana-backend-group
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect; @grafana/grafana-backend-group
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/crypto v0.32.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/crypto v0.35.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/mod v0.22.0 // @grafana/grafana-backend-group
|
||||
golang.org/x/net v0.34.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources
|
||||
golang.org/x/oauth2 v0.26.0 // @grafana/identity-access-team
|
||||
golang.org/x/net v0.35.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources
|
||||
golang.org/x/oauth2 v0.27.0 // @grafana/identity-access-team
|
||||
golang.org/x/sync v0.11.0 // indirect; @grafana/alerting-backend
|
||||
golang.org/x/text v0.21.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/text v0.22.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/time v0.9.0 // indirect; @grafana/grafana-backend-group
|
||||
google.golang.org/api v0.216.0 // @grafana/grafana-backend-group
|
||||
google.golang.org/grpc v1.70.0 // indirect; @grafana/plugins-platform-backend
|
||||
|
||||
+8
-8
@@ -267,8 +267,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
@@ -288,11 +288,11 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -315,8 +315,8 @@ golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
|
||||
+2
-2
@@ -44,9 +44,9 @@ require (
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yalue/merged_fs v1.3.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
+4
-4
@@ -100,12 +100,12 @@ github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY
|
||||
github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -43,10 +43,10 @@ require (
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yalue/merged_fs v1.3.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -94,16 +94,16 @@ github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY
|
||||
github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
+4
-2
@@ -107,12 +107,14 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.216.0 // indirect
|
||||
|
||||
+8
-8
@@ -329,8 +329,8 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -342,10 +342,10 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -367,8 +367,8 @@ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -142,7 +142,8 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge
|
||||
if errors.Is(err, dashboards.ErrFolderNotFound) || err == nil {
|
||||
err = resourceInfo.NewNotFound(name)
|
||||
}
|
||||
return nil, err
|
||||
statusErr := apierrors.ToFolderStatusError(err)
|
||||
return nil, &statusErr
|
||||
}
|
||||
|
||||
r, err := convertToK8sResource(dto, s.namespacer)
|
||||
|
||||
@@ -24,6 +24,7 @@ func RegisterApp(
|
||||
specificConfig := checkregistry.AdvisorAppConfig{
|
||||
CheckRegistry: checkRegistry,
|
||||
PluginConfig: pluginConfig,
|
||||
StackID: cfg.StackID,
|
||||
}
|
||||
appCfg := &runner.AppBuilderConfig{
|
||||
OpenAPIDefGetter: advisorv0alpha1.GetOpenAPIDefinitions,
|
||||
|
||||
@@ -187,8 +187,8 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
|
||||
}), m)
|
||||
|
||||
if api.FeatureManager.IsEnabledGlobally(featuremgmt.FlagAlertingConversionAPI) {
|
||||
api.RegisterConvertPrometheusApiEndpoints(NewConvertPrometheusApi(&ConvertPrometheusSrv{
|
||||
logger: logger,
|
||||
}), m)
|
||||
api.RegisterConvertPrometheusApiEndpoints(NewConvertPrometheusApi(
|
||||
NewConvertPrometheusSrv(&api.Cfg.UnifiedAlerting, logger, api.RuleStore, api.DatasourceCache, api.AlertRules),
|
||||
), m)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,60 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/prom"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
datasourceUIDHeader = "X-Grafana-Alerting-Datasource-UID"
|
||||
recordingRulesPausedHeader = "X-Grafana-Alerting-Recording-Rules-Paused"
|
||||
alertRulesPausedHeader = "X-Grafana-Alerting-Alert-Rules-Paused"
|
||||
)
|
||||
|
||||
var (
|
||||
errDatasourceUIDHeaderMissing = errutil.ValidationFailed(
|
||||
"alerting.datasourceUIDHeaderMissing",
|
||||
errutil.WithPublicMessage(fmt.Sprintf("Missing datasource UID header: %s", datasourceUIDHeader)),
|
||||
).Errorf("missing datasource UID header")
|
||||
|
||||
errInvalidHeaderValueMsg = "Invalid value for header {{.Public.Header}}: must be 'true' or 'false'"
|
||||
errInvalidHeaderValueBase = errutil.ValidationFailed("aleting.invalidHeaderValue").MustTemplate(errInvalidHeaderValueMsg, errutil.WithPublic(errInvalidHeaderValueMsg))
|
||||
)
|
||||
|
||||
func errInvalidHeaderValue(header string) error {
|
||||
return errInvalidHeaderValueBase.Build(errutil.TemplateData{Public: map[string]any{"Header": header}})
|
||||
}
|
||||
|
||||
type ConvertPrometheusSrv struct {
|
||||
logger log.Logger
|
||||
cfg *setting.UnifiedAlertingSettings
|
||||
logger log.Logger
|
||||
ruleStore RuleStore
|
||||
datasourceCache datasources.CacheService
|
||||
alertRuleService *provisioning.AlertRuleService
|
||||
}
|
||||
|
||||
func NewConvertPrometheusSrv(cfg *setting.UnifiedAlertingSettings, logger log.Logger, ruleStore RuleStore, datasourceCache datasources.CacheService, alertRuleService *provisioning.AlertRuleService) *ConvertPrometheusSrv {
|
||||
return &ConvertPrometheusSrv{
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
ruleStore: ruleStore,
|
||||
datasourceCache: datasourceCache,
|
||||
alertRuleService: alertRuleService,
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.ReqContext) response.Response {
|
||||
@@ -28,9 +74,131 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmo
|
||||
}
|
||||
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
|
||||
return response.Error(501, "Not implemented", nil)
|
||||
// Just to make the mimirtool rules load work. It first checks if the group exists, and if the endpoint returns 501 it fails.
|
||||
return response.YAML(http.StatusOK, apimodels.PrometheusRuleGroup{})
|
||||
}
|
||||
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, prometheusGroup apimodels.PrometheusRuleGroup) response.Response {
|
||||
return response.Error(501, "Not implemented", nil)
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, promGroup apimodels.PrometheusRuleGroup) response.Response {
|
||||
logger := srv.logger.FromContext(c.Req.Context())
|
||||
logger = logger.New("folder_title", namespaceTitle, "group", promGroup.Name)
|
||||
|
||||
logger.Info("Converting Prometheus rule group", "rules", len(promGroup.Rules))
|
||||
|
||||
ns, errResp := srv.getOrCreateNamespace(c, namespaceTitle, logger)
|
||||
if errResp != nil {
|
||||
return errResp
|
||||
}
|
||||
|
||||
datasourceUID := strings.TrimSpace(c.Req.Header.Get(datasourceUIDHeader))
|
||||
if datasourceUID == "" {
|
||||
return response.Err(errDatasourceUIDHeaderMissing)
|
||||
}
|
||||
ds, err := srv.datasourceCache.GetDatasourceByUID(c.Req.Context(), datasourceUID, c.SignedInUser, c.SkipDSCache)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get datasource", "datasource_uid", datasourceUID, "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
group, err := srv.convertToGrafanaRuleGroup(c, ds, ns.UID, promGroup, logger)
|
||||
if err != nil {
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
err = srv.alertRuleService.ReplaceRuleGroup(c.Req.Context(), c.SignedInUser, *group, models.ProvenanceConvertedPrometheus)
|
||||
if err != nil {
|
||||
logger.Error("Failed to replace rule group", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusAccepted, map[string]string{"status": "success"})
|
||||
}
|
||||
|
||||
func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger) (*folder.Folder, response.Response) {
|
||||
logger.Debug("Getting or creating a new folder")
|
||||
|
||||
ns, err := srv.ruleStore.GetOrCreateNamespaceInRootByTitle(
|
||||
c.Req.Context(),
|
||||
title,
|
||||
c.SignedInUser.GetOrgID(),
|
||||
c.SignedInUser,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get or create a new folder", "error", err)
|
||||
return nil, toNamespaceErrorResponse(err)
|
||||
}
|
||||
|
||||
logger.Debug("Using folder for the converted rules", "folder_uid", ns.UID)
|
||||
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(c *contextmodel.ReqContext, ds *datasources.DataSource, namespaceUID string, promGroup apimodels.PrometheusRuleGroup, logger log.Logger) (*models.AlertRuleGroup, error) {
|
||||
logger.Info("Converting Prometheus rules to Grafana rules", "rules", len(promGroup.Rules), "folder_uid", namespaceUID, "datasource_uid", ds.UID, "datasource_type", ds.Type)
|
||||
|
||||
rules := make([]prom.PrometheusRule, len(promGroup.Rules))
|
||||
for i, r := range promGroup.Rules {
|
||||
rules[i] = prom.PrometheusRule{
|
||||
Alert: r.Alert,
|
||||
Expr: r.Expr,
|
||||
For: r.For,
|
||||
KeepFiringFor: r.KeepFiringFor,
|
||||
Labels: r.Labels,
|
||||
Annotations: r.Annotations,
|
||||
Record: r.Record,
|
||||
}
|
||||
}
|
||||
group := prom.PrometheusRuleGroup{
|
||||
Name: promGroup.Name,
|
||||
Interval: promGroup.Interval,
|
||||
Rules: rules,
|
||||
}
|
||||
|
||||
pauseRecordingRules, err := parseBooleanHeader(c.Req.Header.Get(recordingRulesPausedHeader), recordingRulesPausedHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pauseAlertRules, err := parseBooleanHeader(c.Req.Header.Get(alertRulesPausedHeader), alertRulesPausedHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
converter, err := prom.NewConverter(
|
||||
prom.Config{
|
||||
DatasourceUID: ds.UID,
|
||||
DatasourceType: ds.Type,
|
||||
DefaultInterval: srv.cfg.DefaultRuleEvaluationInterval,
|
||||
RecordingRules: prom.RulesConfig{
|
||||
IsPaused: pauseRecordingRules,
|
||||
},
|
||||
AlertRules: prom.RulesConfig{
|
||||
IsPaused: pauseAlertRules,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create Prometheus converter", "datasource_uid", ds.UID, "datasource_type", ds.Type, "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
grafanaGroup, err := converter.PrometheusRulesToGrafana(c.SignedInUser.GetOrgID(), namespaceUID, group)
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return grafanaGroup, nil
|
||||
}
|
||||
|
||||
// parseBooleanHeader parses a boolean header value, returning an error if the header
|
||||
// is present but invalid. If the header is not present, returns (false, nil).
|
||||
func parseBooleanHeader(header string, headerName string) (bool, error) {
|
||||
if header == "" {
|
||||
return false, nil
|
||||
}
|
||||
val, err := strconv.ParseBool(header)
|
||||
if err != nil {
|
||||
return false, errInvalidHeaderValue(headerName)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
prommodel "github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
dsfakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
const (
|
||||
existingDSUID = "test-ds"
|
||||
)
|
||||
|
||||
func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
simpleGroup := apimodels.PrometheusRuleGroup{
|
||||
Name: "Test Group",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "TestAlert",
|
||||
Expr: "up == 0",
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("without datasource UID header should return 400", func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
rc.Req.Header.Set(datasourceUIDHeader, "")
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", apimodels.PrometheusRuleGroup{})
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, response.Status())
|
||||
require.Contains(t, string(response.Body()), "Missing datasource UID header")
|
||||
})
|
||||
|
||||
t.Run("with invalid datasource should return error", func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
rc.Req.Header.Set(datasourceUIDHeader, "non-existing-ds")
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", apimodels.PrometheusRuleGroup{})
|
||||
|
||||
require.Equal(t, http.StatusNotFound, response.Status())
|
||||
})
|
||||
|
||||
t.Run("with rule group without evaluation interval should return 202", func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
})
|
||||
|
||||
t.Run("with valid pause header values should return 202", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
headerName string
|
||||
headerValue string
|
||||
}{
|
||||
{
|
||||
name: "true recording rules pause value",
|
||||
headerName: recordingRulesPausedHeader,
|
||||
headerValue: "true",
|
||||
},
|
||||
{
|
||||
name: "false recording rules pause value",
|
||||
headerName: recordingRulesPausedHeader,
|
||||
headerValue: "false",
|
||||
},
|
||||
{
|
||||
name: "true alert rules pause value",
|
||||
headerName: alertRulesPausedHeader,
|
||||
headerValue: "true",
|
||||
},
|
||||
{
|
||||
name: "false alert rules pause value",
|
||||
headerName: alertRulesPausedHeader,
|
||||
headerValue: "false",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
rc.Req.Header.Set(tc.headerName, tc.headerValue)
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid pause header values should return 400", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
headerName string
|
||||
headerValue string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "invalid recording rules pause value",
|
||||
headerName: recordingRulesPausedHeader,
|
||||
headerValue: "invalid",
|
||||
expectedError: "Invalid value for header X-Grafana-Alerting-Recording-Rules-Paused: must be 'true' or 'false'",
|
||||
},
|
||||
{
|
||||
name: "invalid alert rules pause value",
|
||||
headerName: alertRulesPausedHeader,
|
||||
headerValue: "invalid",
|
||||
expectedError: "Invalid value for header X-Grafana-Alerting-Alert-Rules-Paused: must be 'true' or 'false'",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
rc.Req.Header.Set(tc.headerName, tc.headerValue)
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
||||
require.Equal(t, http.StatusBadRequest, response.Status())
|
||||
require.Contains(t, string(response.Body()), tc.expectedError)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with valid request should return 202", func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
||||
require.Equal(t, http.StatusAccepted, response.Status())
|
||||
})
|
||||
}
|
||||
|
||||
func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasources.CacheService) {
|
||||
t.Helper()
|
||||
|
||||
ruleStore := fakes.NewRuleStore(t)
|
||||
folder := randFolder()
|
||||
ruleStore.Folders[1] = append(ruleStore.Folders[1], folder)
|
||||
|
||||
dsCache := &dsfakes.FakeCacheService{}
|
||||
ds := &datasources.DataSource{
|
||||
UID: existingDSUID,
|
||||
Type: datasources.DS_PROMETHEUS,
|
||||
}
|
||||
dsCache.DataSources = append(dsCache.DataSources, ds)
|
||||
|
||||
quotas := &provisioning.MockQuotaChecker{}
|
||||
quotas.EXPECT().LimitOK()
|
||||
|
||||
folderService := foldertest.NewFakeService()
|
||||
|
||||
alertRuleService := provisioning.NewAlertRuleService(
|
||||
ruleStore,
|
||||
fakes.NewFakeProvisioningStore(),
|
||||
folderService,
|
||||
quotas,
|
||||
&provisioning.NopTransactionManager{},
|
||||
60,
|
||||
10,
|
||||
100,
|
||||
log.New("test"),
|
||||
&provisioning.NotificationSettingsValidatorProviderFake{},
|
||||
&acfakes.FakeRuleService{},
|
||||
)
|
||||
|
||||
cfg := &setting.UnifiedAlertingSettings{
|
||||
DefaultRuleEvaluationInterval: 1 * time.Minute,
|
||||
}
|
||||
|
||||
srv := NewConvertPrometheusSrv(cfg, log.NewNopLogger(), ruleStore, dsCache, alertRuleService)
|
||||
|
||||
return srv, dsCache
|
||||
}
|
||||
|
||||
func createRequestCtx() *contextmodel.ReqContext {
|
||||
req := httptest.NewRequest("GET", "http://localhost", nil)
|
||||
req.Header.Set(datasourceUIDHeader, existingDSUID)
|
||||
|
||||
return &contextmodel.ReqContext{
|
||||
Context: &web.Context{
|
||||
Req: req,
|
||||
Resp: web.NewResponseWriter("GET", httptest.NewRecorder()),
|
||||
},
|
||||
SignedInUser: &user.SignedInUser{OrgID: 1},
|
||||
}
|
||||
}
|
||||
@@ -132,23 +132,26 @@ func (api *API) authorize(method, path string) web.Handler {
|
||||
)
|
||||
|
||||
case http.MethodGet + "/api/convert/prometheus/config/v1/rules":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
|
||||
eval = ac.EvalAll(
|
||||
ac.EvalPermission(ac.ActionAlertingRuleRead),
|
||||
ac.EvalPermission(dashboards.ActionFoldersRead),
|
||||
)
|
||||
|
||||
case http.MethodPost + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}":
|
||||
eval = ac.EvalAll(
|
||||
ac.EvalPermission(dashboards.ActionFoldersWrite),
|
||||
ac.EvalPermission(ac.ActionAlertingRuleRead),
|
||||
ac.EvalPermission(ac.ActionAlertingRuleUpdate),
|
||||
ac.EvalPermission(ac.ActionAlertingRuleCreate),
|
||||
ac.EvalPermission(ac.ActionAlertingRuleDelete),
|
||||
ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus),
|
||||
)
|
||||
|
||||
case http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}",
|
||||
http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}":
|
||||
eval = ac.EvalAll(
|
||||
ac.EvalPermission(ac.ActionAlertingRuleDelete),
|
||||
ac.EvalPermission(ac.ActionAlertingRuleRead),
|
||||
ac.EvalPermission(dashboards.ActionFoldersRead),
|
||||
eval = ac.EvalAny(
|
||||
ac.EvalAll(
|
||||
ac.EvalPermission(ac.ActionAlertingRuleRead),
|
||||
ac.EvalPermission(dashboards.ActionFoldersRead),
|
||||
ac.EvalPermission(ac.ActionAlertingRuleDelete),
|
||||
ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus),
|
||||
),
|
||||
)
|
||||
|
||||
// Alert Instances and Silences
|
||||
|
||||
@@ -15,6 +15,8 @@ type RuleStore interface {
|
||||
// by returning map[string]struct{} instead of map[string]*folder.Folder
|
||||
GetUserVisibleNamespaces(context.Context, int64, identity.Requester) (map[string]*folder.Folder, error)
|
||||
GetNamespaceByUID(ctx context.Context, uid string, orgID int64, user identity.Requester) (*folder.Folder, error)
|
||||
GetNamespaceInRootByTitle(ctx context.Context, fullpath string, orgID int64, user identity.Requester) (*folder.Folder, error)
|
||||
GetOrCreateNamespaceInRootByTitle(ctx context.Context, title string, orgID int64, user identity.Requester) (*folder.Folder, error)
|
||||
|
||||
GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) (*ngmodels.AlertRule, error)
|
||||
GetAlertRulesGroupByRuleUID(ctx context.Context, query *ngmodels.GetAlertRulesGroupByRuleUIDQuery) ([]*ngmodels.AlertRule, error)
|
||||
|
||||
@@ -4493,6 +4493,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"URL": {
|
||||
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nThe Host field contains the host and port subcomponents of the URL.\nWhen the port is present, it is separated from the host with a colon.\nWhen the host is an IPv6 address, it must be enclosed in square brackets:\n\"[fe80::1]:80\". The [net.JoinHostPort] function combines a host and port\ninto a string suitable for the Host field, adding square brackets to\nthe host when necessary.\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the [URL.EscapedPath] method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
|
||||
"properties": {
|
||||
"ForceQuery": {
|
||||
"type": "boolean"
|
||||
@@ -4528,7 +4529,7 @@
|
||||
"$ref": "#/definitions/Userinfo"
|
||||
}
|
||||
},
|
||||
"title": "URL is a custom URL type that allows validation at configuration load time.",
|
||||
"title": "A URL represents a parsed URL (technically, a URI reference).",
|
||||
"type": "object"
|
||||
},
|
||||
"UpdateRuleGroupResponse": {
|
||||
@@ -4931,7 +4932,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableAlert",
|
||||
"type": "object"
|
||||
@@ -5056,7 +5056,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableSilence",
|
||||
"type": "object"
|
||||
|
||||
@@ -313,15 +313,26 @@ type WebhookIntegration struct {
|
||||
|
||||
URL string `json:"url" yaml:"url" hcl:"url"`
|
||||
|
||||
HTTPMethod *string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty" hcl:"http_method"`
|
||||
MaxAlerts *int64 `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty" hcl:"max_alerts"`
|
||||
AuthorizationScheme *string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty" hcl:"authorization_scheme"`
|
||||
AuthorizationCredentials *Secret `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty" hcl:"authorization_credentials"`
|
||||
User *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"basic_auth_user"`
|
||||
Password *Secret `json:"password,omitempty" yaml:"password,omitempty" hcl:"basic_auth_password"`
|
||||
Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"`
|
||||
Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"`
|
||||
TLSConfig *TLSConfig `json:"tlsConfig,omitempty" yaml:"tlsConfig,omitempty" hcl:"tlsConfig,block"`
|
||||
HTTPMethod *string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty" hcl:"http_method"`
|
||||
MaxAlerts *int64 `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty" hcl:"max_alerts"`
|
||||
AuthorizationScheme *string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty" hcl:"authorization_scheme"`
|
||||
AuthorizationCredentials *Secret `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty" hcl:"authorization_credentials"`
|
||||
User *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"basic_auth_user"`
|
||||
Password *Secret `json:"password,omitempty" yaml:"password,omitempty" hcl:"basic_auth_password"`
|
||||
Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"`
|
||||
Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"`
|
||||
TLSConfig *TLSConfig `json:"tlsConfig,omitempty" yaml:"tlsConfig,omitempty" hcl:"tlsConfig,block"`
|
||||
HMACConfig *HMACConfig `json:"hmacConfig,omitempty" yaml:"hmacConfig,omitempty" hcl:"hmacConfig,block"`
|
||||
}
|
||||
|
||||
type HMACConfig struct {
|
||||
// Secret to use for HMAC signing.
|
||||
Secret *Secret `json:"secret,omitempty" yaml:"secret,omitempty" hcl:"secret"`
|
||||
// Header is the name of the header containing the HMAC signature.
|
||||
Header string `json:"header,omitempty" yaml:"header,omitempty" hcl:"header"`
|
||||
// TimestampHeader is the name of the header containing the timestamp
|
||||
// used to generate the HMAC signature. If empty, timestamp is not included.
|
||||
TimestampHeader string `yaml:"timestampHeader,omitempty" json:"timestampHeader,omitempty" hcl:"timestamp_header"`
|
||||
}
|
||||
|
||||
type WecomIntegration struct {
|
||||
|
||||
@@ -84,11 +84,11 @@ type RouteConvertPrometheusPostRuleGroupParams struct {
|
||||
// in: path
|
||||
NamespaceTitle string
|
||||
// in: header
|
||||
DatasourceUID string `json:"x-datasource-uid"`
|
||||
DatasourceUID string `json:"x-grafana-alerting-datasource-uid"`
|
||||
// in: header
|
||||
RecordingRulesPaused bool `json:"x-recording-rules-paused"`
|
||||
RecordingRulesPaused bool `json:"x-grafana-alerting-recording-rules-paused"`
|
||||
// in: header
|
||||
AlertRulesPaused bool `json:"x-alert-rules-paused"`
|
||||
AlertRulesPaused bool `json:"x-grafana-alerting-alert-rules-paused"`
|
||||
// in:body
|
||||
Body PrometheusRuleGroup
|
||||
}
|
||||
|
||||
@@ -4932,6 +4932,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableAlert",
|
||||
"type": "object"
|
||||
@@ -6515,17 +6516,17 @@
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "x-datasource-uid",
|
||||
"name": "x-grafana-alerting-datasource-uid",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "x-recording-rules-paused",
|
||||
"name": "x-grafana-alerting-recording-rules-paused",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "x-alert-rules-paused",
|
||||
"name": "x-grafana-alerting-alert-rules-paused",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1194,17 +1194,17 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "x-datasource-uid",
|
||||
"name": "x-grafana-alerting-datasource-uid",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "x-recording-rules-paused",
|
||||
"name": "x-grafana-alerting-recording-rules-paused",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "x-alert-rules-paused",
|
||||
"name": "x-grafana-alerting-alert-rules-paused",
|
||||
"in": "header"
|
||||
},
|
||||
{
|
||||
@@ -8872,6 +8872,7 @@
|
||||
}
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
|
||||
@@ -8,10 +8,12 @@ const (
|
||||
ProvenanceNone Provenance = ""
|
||||
ProvenanceAPI Provenance = "api"
|
||||
ProvenanceFile Provenance = "file"
|
||||
// ProvenanceConvertedPrometheus is used for objects converted from Prometheus definitions.
|
||||
ProvenanceConvertedPrometheus Provenance = "converted_prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
KnownProvenances = []Provenance{ProvenanceNone, ProvenanceAPI, ProvenanceFile}
|
||||
KnownProvenances = []Provenance{ProvenanceNone, ProvenanceAPI, ProvenanceFile, ProvenanceConvertedPrometheus}
|
||||
)
|
||||
|
||||
// Provisionable represents a resource that can be created through a provisioning mechanism, such as Terraform or config file.
|
||||
|
||||
@@ -1689,13 +1689,13 @@ func GetAvailableNotifiers() []*NotifierPlugin {
|
||||
Label: "Subject",
|
||||
Element: ElementTypeTextArea,
|
||||
InputType: InputTypeText,
|
||||
Description: "Optional subject. You can use templates to customize this field",
|
||||
Description: "Optional subject. By default, this field uses the default title template and can be customized with templates and custom messages. It cannot be an empty string",
|
||||
PropertyName: "subject",
|
||||
Placeholder: alertingTemplates.DefaultMessageTitleEmbed,
|
||||
},
|
||||
{
|
||||
Label: "Message",
|
||||
Description: "Optional message. You can use templates to customize this field. Using a custom message will replace the default message",
|
||||
Description: "Optional message. By default, this field uses the default message template and can be customized with templates and custom messages",
|
||||
Element: ElementTypeTextArea,
|
||||
PropertyName: "message",
|
||||
Placeholder: alertingTemplates.DefaultMessageEmbed,
|
||||
|
||||
@@ -27,8 +27,11 @@ const (
|
||||
|
||||
// Config defines the configuration options for the Prometheus to Grafana rules converter.
|
||||
type Config struct {
|
||||
DatasourceUID string
|
||||
DatasourceType string
|
||||
DatasourceUID string
|
||||
DatasourceType string
|
||||
// DefaultInterval is the default interval for rules in the groups that
|
||||
// don't have Interval set.
|
||||
DefaultInterval time.Duration
|
||||
FromTimeRange *time.Duration
|
||||
EvaluationOffset *time.Duration
|
||||
ExecErrState models.ExecutionErrorState
|
||||
@@ -68,6 +71,9 @@ func NewConverter(cfg Config) (*Converter, error) {
|
||||
if cfg.DatasourceType == "" {
|
||||
return nil, fmt.Errorf("datasource type is required")
|
||||
}
|
||||
if cfg.DefaultInterval == 0 {
|
||||
return nil, fmt.Errorf("default evaluation interval is required")
|
||||
}
|
||||
if cfg.FromTimeRange == nil {
|
||||
cfg.FromTimeRange = defaultConfig.FromTimeRange
|
||||
}
|
||||
@@ -93,9 +99,8 @@ func NewConverter(cfg Config) (*Converter, error) {
|
||||
// PrometheusRulesToGrafana converts a Prometheus rule group into Grafana Alerting rule group.
|
||||
func (p *Converter) PrometheusRulesToGrafana(orgID int64, namespaceUID string, group PrometheusRuleGroup) (*models.AlertRuleGroup, error) {
|
||||
for _, rule := range group.Rules {
|
||||
err := validatePrometheusRule(rule)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid Prometheus rule '%s': %w", rule.Alert, err)
|
||||
if err := rule.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,18 +112,15 @@ func (p *Converter) PrometheusRulesToGrafana(orgID int64, namespaceUID string, g
|
||||
return grafanaGroup, nil
|
||||
}
|
||||
|
||||
func validatePrometheusRule(rule PrometheusRule) error {
|
||||
if rule.KeepFiringFor != nil {
|
||||
return fmt.Errorf("keep_firing_for is not supported")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Converter) convertRuleGroup(orgID int64, namespaceUID string, promGroup PrometheusRuleGroup) (*models.AlertRuleGroup, error) {
|
||||
uniqueNames := map[string]int{}
|
||||
rules := make([]models.AlertRule, 0, len(promGroup.Rules))
|
||||
|
||||
interval := time.Duration(promGroup.Interval)
|
||||
if interval == 0 {
|
||||
interval = p.cfg.DefaultInterval
|
||||
}
|
||||
|
||||
for i, rule := range promGroup.Rules {
|
||||
gr, err := p.convertRule(orgID, namespaceUID, promGroup.Name, rule)
|
||||
if err != nil {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func TestPrometheusRulesToGrafana(t *testing.T) {
|
||||
fiveMin := prommodel.Duration(5 * time.Minute)
|
||||
defaultInterval := 2 * time.Minute
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -40,7 +40,7 @@ func TestPrometheusRulesToGrafana(t *testing.T) {
|
||||
{
|
||||
Alert: "alert-1",
|
||||
Expr: "cpu_usage > 80",
|
||||
For: &fiveMin,
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
@@ -63,14 +63,14 @@ func TestPrometheusRulesToGrafana(t *testing.T) {
|
||||
{
|
||||
Alert: "alert-1",
|
||||
Expr: "up == 0",
|
||||
KeepFiringFor: &fiveMin,
|
||||
KeepFiringFor: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "rule with empty interval",
|
||||
name: "rule group with empty interval",
|
||||
orgID: 1,
|
||||
namespace: "namespaceUID",
|
||||
promGroup: PrometheusRuleGroup{
|
||||
@@ -89,7 +89,8 @@ func TestPrometheusRulesToGrafana(t *testing.T) {
|
||||
orgID: 1,
|
||||
namespace: "namespaceUID",
|
||||
promGroup: PrometheusRuleGroup{
|
||||
Name: "test-group-1",
|
||||
Name: "test-group-1",
|
||||
Interval: prommodel.Duration(10 * time.Second),
|
||||
Rules: []PrometheusRule{
|
||||
{
|
||||
Record: "some_metric",
|
||||
@@ -105,6 +106,7 @@ func TestPrometheusRulesToGrafana(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.config.DatasourceUID = "datasource-uid"
|
||||
tc.config.DatasourceType = datasources.DS_PROMETHEUS
|
||||
tc.config.DefaultInterval = defaultInterval
|
||||
converter, err := NewConverter(tc.config)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -117,7 +119,11 @@ func TestPrometheusRulesToGrafana(t *testing.T) {
|
||||
require.NoError(t, err, tc.name)
|
||||
|
||||
require.Equal(t, tc.promGroup.Name, grafanaGroup.Title, tc.name)
|
||||
|
||||
expectedInterval := int64(time.Duration(tc.promGroup.Interval).Seconds())
|
||||
if expectedInterval == 0 {
|
||||
expectedInterval = int64(defaultInterval.Seconds())
|
||||
}
|
||||
require.Equal(t, expectedInterval, grafanaGroup.Interval, tc.name)
|
||||
|
||||
require.Equal(t, len(tc.promGroup.Rules), len(grafanaGroup.Rules), tc.name)
|
||||
@@ -164,8 +170,9 @@ func TestPrometheusRulesToGrafana(t *testing.T) {
|
||||
|
||||
func TestPrometheusRulesToGrafanaWithDuplicateRuleNames(t *testing.T) {
|
||||
cfg := Config{
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DefaultInterval: 2 * time.Minute,
|
||||
}
|
||||
converter, err := NewConverter(cfg)
|
||||
require.NoError(t, err)
|
||||
@@ -257,8 +264,9 @@ func TestCreateThresholdNode(t *testing.T) {
|
||||
|
||||
func TestPrometheusRulesToGrafana_NodesInRules(t *testing.T) {
|
||||
cfg := Config{
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DefaultInterval: 2 * time.Minute,
|
||||
}
|
||||
converter, err := NewConverter(cfg)
|
||||
require.NoError(t, err)
|
||||
@@ -344,8 +352,9 @@ func TestPrometheusRulesToGrafana_UID(t *testing.T) {
|
||||
}
|
||||
|
||||
converter, err := NewConverter(Config{
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DefaultInterval: 2 * time.Minute,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -372,8 +381,9 @@ func TestPrometheusRulesToGrafana_UID(t *testing.T) {
|
||||
namespace := "some-namespace"
|
||||
|
||||
converter, err := NewConverter(Config{
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DefaultInterval: 2 * time.Minute,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -390,8 +400,9 @@ func TestPrometheusRulesToGrafana_UID(t *testing.T) {
|
||||
namespace := "some-namespace"
|
||||
|
||||
converter, err := NewConverter(Config{
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DefaultInterval: 2 * time.Minute,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -408,8 +419,9 @@ func TestPrometheusRulesToGrafana_UID(t *testing.T) {
|
||||
namespace := "some-namespace"
|
||||
|
||||
converter, err := NewConverter(Config{
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DatasourceUID: "datasource-uid",
|
||||
DatasourceType: datasources.DS_PROMETHEUS,
|
||||
DefaultInterval: 2 * time.Minute,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@ package prom
|
||||
|
||||
import (
|
||||
prommodel "github.com/prometheus/common/model"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPrometheusRuleValidationFailed = errutil.ValidationFailed("alerting.prometheusRuleInvalid")
|
||||
)
|
||||
|
||||
type PrometheusRulesFile struct {
|
||||
@@ -23,3 +29,11 @@ type PrometheusRule struct {
|
||||
Annotations map[string]string `yaml:"annotations,omitempty"`
|
||||
Record string `yaml:"record,omitempty"`
|
||||
}
|
||||
|
||||
func (r *PrometheusRule) Validate() error {
|
||||
if r.KeepFiringFor != nil {
|
||||
return ErrPrometheusRuleValidationFailed.Errorf("keep_firing_for is not supported")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -648,36 +648,6 @@ func (st DBstore) GetRuleGroupInterval(ctx context.Context, orgID int64, namespa
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserVisibleNamespaces returns the folders that are visible to the user
|
||||
func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user identity.Requester) (map[string]*folder.Folder, error) {
|
||||
folders, err := st.FolderService.GetFolders(ctx, folder.GetFoldersQuery{
|
||||
OrgID: orgID,
|
||||
WithFullpath: true,
|
||||
SignedInUser: user,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
namespaceMap := make(map[string]*folder.Folder)
|
||||
for _, f := range folders {
|
||||
namespaceMap[f.UID] = f
|
||||
}
|
||||
return namespaceMap, nil
|
||||
}
|
||||
|
||||
// GetNamespaceByUID is a handler for retrieving a namespace by its UID. Alerting rules follow a Grafana folder-like structure which we call namespaces.
|
||||
func (st DBstore) GetNamespaceByUID(ctx context.Context, uid string, orgID int64, user identity.Requester) (*folder.Folder, error) {
|
||||
f, err := st.FolderService.GetFolders(ctx, folder.GetFoldersQuery{OrgID: orgID, UIDs: []string{uid}, WithFullpath: true, SignedInUser: user})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(f) == 0 {
|
||||
return nil, dashboards.ErrFolderAccessDenied
|
||||
}
|
||||
return f[0], nil
|
||||
}
|
||||
|
||||
func (st DBstore) GetAlertRulesKeysForScheduling(ctx context.Context) ([]ngmodels.AlertRuleKeyWithVersion, error) {
|
||||
var result []ngmodels.AlertRuleKeyWithVersion
|
||||
err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
|
||||
@@ -782,58 +782,6 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_GetNamespaceByUID(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
b := &fakeBus{}
|
||||
logger := log.New("test-dbstore")
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
|
||||
|
||||
u := &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleAdmin,
|
||||
IsGrafanaAdmin: true,
|
||||
}
|
||||
|
||||
uid := uuid.NewString()
|
||||
parentUid := uuid.NewString()
|
||||
title := "folder/title"
|
||||
parentTitle := "parent-title"
|
||||
createFolder(t, store, parentUid, parentTitle, 1, "")
|
||||
createFolder(t, store, uid, title, 1, parentUid)
|
||||
|
||||
actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, actual.Title)
|
||||
require.Equal(t, uid, actual.UID)
|
||||
require.Equal(t, title, actual.Fullpath)
|
||||
|
||||
t.Run("error when user does not have permissions", func(t *testing.T) {
|
||||
someUser := &user.SignedInUser{
|
||||
UserID: 2,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleViewer,
|
||||
}
|
||||
_, err = store.GetNamespaceByUID(context.Background(), uid, 1, someUser)
|
||||
require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("when nested folders are enabled full path should be populated with correct value", func(t *testing.T) {
|
||||
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
|
||||
actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, actual.Title)
|
||||
require.Equal(t, uid, actual.UID)
|
||||
require.Equal(t, "parent-title/folder\\/title", actual.Fullpath)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationInsertAlertRules(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
)
|
||||
|
||||
// GetUserVisibleNamespaces returns the folders that are visible to the user
|
||||
func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user identity.Requester) (map[string]*folder.Folder, error) {
|
||||
folders, err := st.FolderService.GetFolders(ctx, folder.GetFoldersQuery{
|
||||
OrgID: orgID,
|
||||
WithFullpath: true,
|
||||
SignedInUser: user,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
namespaceMap := make(map[string]*folder.Folder)
|
||||
for _, f := range folders {
|
||||
namespaceMap[f.UID] = f
|
||||
}
|
||||
return namespaceMap, nil
|
||||
}
|
||||
|
||||
// GetNamespaceByUID is a handler for retrieving a namespace by its UID. Alerting rules follow a Grafana folder-like structure which we call namespaces.
|
||||
func (st DBstore) GetNamespaceByUID(ctx context.Context, uid string, orgID int64, user identity.Requester) (*folder.Folder, error) {
|
||||
f, err := st.FolderService.GetFolders(ctx, folder.GetFoldersQuery{OrgID: orgID, UIDs: []string{uid}, WithFullpath: true, SignedInUser: user})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(f) == 0 {
|
||||
return nil, dashboards.ErrFolderAccessDenied
|
||||
}
|
||||
return f[0], nil
|
||||
}
|
||||
|
||||
// GetNamespaceInRootByTitle gets namespace by its title in the root folder.
|
||||
func (st DBstore) GetNamespaceInRootByTitle(ctx context.Context, title string, orgID int64, user identity.Requester) (*folder.Folder, error) {
|
||||
q := &folder.GetChildrenQuery{
|
||||
UID: folder.RootFolderUID,
|
||||
OrgID: orgID,
|
||||
SignedInUser: user,
|
||||
}
|
||||
folders, err := st.FolderService.GetChildren(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
foundByTitle := []*folder.Folder{}
|
||||
for _, f := range folders {
|
||||
if f.Title == title && f.ParentUID == folder.RootFolderUID {
|
||||
foundByTitle = append(foundByTitle, f)
|
||||
}
|
||||
}
|
||||
|
||||
if len(foundByTitle) == 0 {
|
||||
return nil, dashboards.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
// Sort by UID to return the first folder in case of multiple folders with the same title
|
||||
sort.Slice(foundByTitle, func(i, j int) bool {
|
||||
return foundByTitle[i].UID < foundByTitle[j].UID
|
||||
})
|
||||
|
||||
return foundByTitle[0], nil
|
||||
}
|
||||
|
||||
// GetOrCreateNamespaceInRootByTitle gets or creates a namespace by title in the _root_ folder.
|
||||
func (st DBstore) GetOrCreateNamespaceInRootByTitle(ctx context.Context, title string, orgID int64, user identity.Requester) (*folder.Folder, error) {
|
||||
var f *folder.Folder
|
||||
var err error
|
||||
|
||||
f, err = st.GetNamespaceInRootByTitle(ctx, title, orgID, user)
|
||||
if err != nil && !errors.Is(err, dashboards.ErrFolderAccessDenied) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
cmd := &folder.CreateFolderCommand{
|
||||
OrgID: orgID,
|
||||
Title: title,
|
||||
SignedInUser: user,
|
||||
}
|
||||
f, err = st.FolderService.Create(ctx, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestIntegration_GetUserVisibleNamespaces(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
b := &fakeBus{}
|
||||
logger := log.New("test-dbstore")
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
|
||||
|
||||
admin := &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleAdmin,
|
||||
IsGrafanaAdmin: true,
|
||||
}
|
||||
|
||||
folders := []struct {
|
||||
uid string
|
||||
title string
|
||||
parentUid string
|
||||
}{
|
||||
{uid: uuid.NewString(), title: "folder1", parentUid: ""},
|
||||
{uid: uuid.NewString(), title: "folder2", parentUid: ""},
|
||||
{uid: uuid.NewString(), title: "nested/folder", parentUid: ""},
|
||||
}
|
||||
|
||||
for _, f := range folders {
|
||||
createFolder(t, store, f.uid, f.title, 1, f.parentUid)
|
||||
}
|
||||
|
||||
t.Run("returns all folders", func(t *testing.T) {
|
||||
namespaces, err := store.GetUserVisibleNamespaces(context.Background(), 1, admin)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, namespaces, len(folders))
|
||||
})
|
||||
|
||||
t.Run("returns empty list for a non existing org", func(t *testing.T) {
|
||||
emptyOrgID := int64(999)
|
||||
namespaces, err := store.GetUserVisibleNamespaces(context.Background(), emptyOrgID, admin)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, namespaces)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_GetNamespaceByUID(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
b := &fakeBus{}
|
||||
logger := log.New("test-dbstore")
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
|
||||
|
||||
u := &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleAdmin,
|
||||
IsGrafanaAdmin: true,
|
||||
}
|
||||
|
||||
uid := uuid.NewString()
|
||||
parentUid := uuid.NewString()
|
||||
title := "folder/title"
|
||||
parentTitle := "parent-title"
|
||||
createFolder(t, store, parentUid, parentTitle, 1, "")
|
||||
createFolder(t, store, uid, title, 1, parentUid)
|
||||
|
||||
actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, actual.Title)
|
||||
require.Equal(t, uid, actual.UID)
|
||||
require.Equal(t, title, actual.Fullpath)
|
||||
|
||||
t.Run("error when user does not have permissions", func(t *testing.T) {
|
||||
someUser := &user.SignedInUser{
|
||||
UserID: 2,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleViewer,
|
||||
}
|
||||
_, err = store.GetNamespaceByUID(context.Background(), uid, 1, someUser)
|
||||
require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("error when folder does not exist", func(t *testing.T) {
|
||||
nonExistentUID := uuid.NewString()
|
||||
_, err := store.GetNamespaceByUID(context.Background(), nonExistentUID, 1, u)
|
||||
require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("when nested folders are enabled full path should be populated with correct value", func(t *testing.T) {
|
||||
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
|
||||
actual, err := store.GetNamespaceByUID(context.Background(), uid, 1, u)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, actual.Title)
|
||||
require.Equal(t, uid, actual.UID)
|
||||
require.Equal(t, "parent-title/folder\\/title", actual.Fullpath)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_GetNamespaceInRootByTitle(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
b := &fakeBus{}
|
||||
logger := log.New("test-dbstore")
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
|
||||
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
|
||||
|
||||
u := &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleAdmin,
|
||||
IsGrafanaAdmin: true,
|
||||
}
|
||||
|
||||
uid := uuid.NewString()
|
||||
title := "folder-title"
|
||||
createFolder(t, store, uid, title, 1, "")
|
||||
|
||||
actual, err := store.GetNamespaceInRootByTitle(context.Background(), title, 1, u)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, actual.Title)
|
||||
require.Equal(t, uid, actual.UID)
|
||||
}
|
||||
|
||||
func TestIntegration_GetOrCreateNamespaceInRootByTitle(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
u := &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: 1,
|
||||
OrgRole: org.RoleAdmin,
|
||||
IsGrafanaAdmin: true,
|
||||
}
|
||||
|
||||
setupStore := func(t *testing.T) *DBstore {
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
b := &fakeBus{}
|
||||
logger := log.New("test-dbstore")
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
|
||||
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
t.Run("should create folder when it does not exist", func(t *testing.T) {
|
||||
store := setupStore(t)
|
||||
|
||||
f, err := store.GetOrCreateNamespaceInRootByTitle(context.Background(), "new folder", 1, u)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "new folder", f.Title)
|
||||
require.NotEmpty(t, f.UID)
|
||||
|
||||
folders, err := store.FolderService.GetFolders(
|
||||
context.Background(),
|
||||
folder.GetFoldersQuery{
|
||||
OrgID: 1,
|
||||
WithFullpath: true,
|
||||
SignedInUser: u,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, folders, 1)
|
||||
})
|
||||
|
||||
t.Run("should return existing folder when it exists", func(t *testing.T) {
|
||||
store := setupStore(t)
|
||||
|
||||
title := "existing folder"
|
||||
createFolder(t, store, "", title, 1, "")
|
||||
f, err := store.GetOrCreateNamespaceInRootByTitle(context.Background(), title, 1, u)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, f.Title)
|
||||
|
||||
folders, err := store.FolderService.GetFolders(
|
||||
context.Background(),
|
||||
folder.GetFoldersQuery{
|
||||
OrgID: 1,
|
||||
WithFullpath: true,
|
||||
SignedInUser: u,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, folders, 1)
|
||||
})
|
||||
}
|
||||
@@ -258,6 +258,40 @@ func (f *RuleStore) GetNamespaceByUID(_ context.Context, uid string, orgID int64
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
func (f *RuleStore) GetOrCreateNamespaceInRootByTitle(ctx context.Context, title string, orgID int64, user identity.Requester) (*folder.Folder, error) {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
|
||||
for _, folder := range f.Folders[orgID] {
|
||||
if folder.Title == title {
|
||||
return folder, nil
|
||||
}
|
||||
}
|
||||
|
||||
newFolder := &folder.Folder{
|
||||
ID: rand.Int63(), // nolint:staticcheck
|
||||
UID: util.GenerateShortUID(),
|
||||
Title: title,
|
||||
Fullpath: "fullpath_" + title,
|
||||
}
|
||||
|
||||
f.Folders[orgID] = append(f.Folders[orgID], newFolder)
|
||||
return newFolder, nil
|
||||
}
|
||||
|
||||
func (f *RuleStore) GetNamespaceInRootByTitle(ctx context.Context, title string, orgID int64, user identity.Requester) (*folder.Folder, error) {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
|
||||
for _, folder := range f.Folders[orgID] {
|
||||
if folder.Title == title && folder.ParentUID == "" {
|
||||
return folder, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("namespace with title '%s' not found", title)
|
||||
}
|
||||
|
||||
func (f *RuleStore) UpdateAlertRules(_ context.Context, _ *models.UserUID, q []models.UpdateRule) error {
|
||||
f.mtx.Lock()
|
||||
defer f.mtx.Unlock()
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
alertingReceivers "github.com/grafana/alerting/receivers"
|
||||
alertingHTTP "github.com/grafana/alerting/http"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@@ -71,7 +71,7 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *
|
||||
request.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := alertingReceivers.NewTLSClient(webhook.TLSConfig).Do(request)
|
||||
resp, err := alertingHTTP.NewTLSClient(webhook.TLSConfig).Do(request)
|
||||
if err != nil {
|
||||
return redactURL(err)
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20250221202230-9d7e00921e44 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642 // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect
|
||||
@@ -366,14 +366,14 @@ require (
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
|
||||
@@ -566,8 +566,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250221202230-9d7e00921e44 h1:vboqvbAO0s0CTALHnqfmNvhCP1ziBcZNpYDbORqvOgg=
|
||||
github.com/grafana/alerting v0.0.0-20250221202230-9d7e00921e44/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642 h1:iQ0h/h+QoguSZDF+ZpPxcM/C+m1kjh+aXjMpxywowPA=
|
||||
github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
|
||||
@@ -1174,8 +1174,8 @@ golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1263,8 +1263,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1273,8 +1273,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -1370,8 +1370,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1385,8 +1385,8 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package sql
|
||||
package resource
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
@@ -0,0 +1,15 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestContinueToken(t *testing.T) {
|
||||
token := &ContinueToken{
|
||||
ResourceVersion: 100,
|
||||
StartOffset: 50,
|
||||
}
|
||||
assert.Equal(t, "eyJvIjo1MCwidiI6MTAwfQ==", token.String())
|
||||
}
|
||||
@@ -117,7 +117,7 @@ require (
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20250221202230-9d7e00921e44 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v0.31.5 // indirect
|
||||
@@ -217,14 +217,14 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
|
||||
@@ -397,8 +397,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
||||
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grafana/alerting v0.0.0-20250221202230-9d7e00921e44 h1:vboqvbAO0s0CTALHnqfmNvhCP1ziBcZNpYDbORqvOgg=
|
||||
github.com/grafana/alerting v0.0.0-20250221202230-9d7e00921e44/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642 h1:iQ0h/h+QoguSZDF+ZpPxcM/C+m1kjh+aXjMpxywowPA=
|
||||
github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
|
||||
@@ -824,8 +824,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
@@ -866,14 +866,14 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -931,8 +931,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
@@ -942,8 +942,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
|
||||
@@ -923,6 +923,10 @@ func (s *server) initWatcher() error {
|
||||
// pipe all events
|
||||
v := <-events
|
||||
|
||||
if v == nil {
|
||||
s.log.Error("received nil event")
|
||||
continue
|
||||
}
|
||||
// Skip events during batch updates
|
||||
if v.PreviousRV < 0 {
|
||||
continue
|
||||
|
||||
@@ -588,11 +588,11 @@ type listIter struct {
|
||||
|
||||
// ContinueToken implements resource.ListIterator.
|
||||
func (l *listIter) ContinueToken() string {
|
||||
return ContinueToken{ResourceVersion: l.listRV, StartOffset: l.offset}.String()
|
||||
return resource.ContinueToken{ResourceVersion: l.listRV, StartOffset: l.offset}.String()
|
||||
}
|
||||
|
||||
func (l *listIter) ContinueTokenWithCurrentRV() string {
|
||||
return ContinueToken{ResourceVersion: l.rv, StartOffset: l.offset}.String()
|
||||
return resource.ContinueToken{ResourceVersion: l.rv, StartOffset: l.offset}.String()
|
||||
}
|
||||
|
||||
func (l *listIter) Error() error {
|
||||
@@ -679,7 +679,7 @@ func (b *backend) listAtRevision(ctx context.Context, req *resource.ListRequest,
|
||||
// Get the RV
|
||||
iter := &listIter{listRV: req.ResourceVersion}
|
||||
if req.NextPageToken != "" {
|
||||
continueToken, err := GetContinueToken(req.NextPageToken)
|
||||
continueToken, err := resource.GetContinueToken(req.NextPageToken)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("get continue token: %w", err)
|
||||
}
|
||||
@@ -737,7 +737,7 @@ func (b *backend) getHistory(ctx context.Context, req *resource.ListRequest, cb
|
||||
|
||||
iter := &listIter{}
|
||||
if req.NextPageToken != "" {
|
||||
continueToken, err := GetContinueToken(req.NextPageToken)
|
||||
continueToken, err := resource.GetContinueToken(req.NextPageToken)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("get continue token: %w", err)
|
||||
}
|
||||
|
||||
@@ -45,14 +45,7 @@ func NewResourceServer(db infraDB.DB, cfg *setting.Cfg,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbCfg := cfg.SectionWithEnvOverrides("database")
|
||||
// Check in the config if HA is enabled by default we always assume a HA setup.
|
||||
isHA := dbCfg.Key("high_availability").MustBool(true)
|
||||
// SQLite is not possible to run in HA, so we set it to false.
|
||||
databaseType := dbCfg.Key("type").MustString(migrator.SQLite)
|
||||
if databaseType == migrator.SQLite {
|
||||
isHA = false
|
||||
}
|
||||
isHA := isHighAvailabilityEnabled(cfg.SectionWithEnvOverrides("database"))
|
||||
|
||||
store, err := NewBackend(BackendOptions{DBProvider: eDB, Tracer: tracer, IsHA: isHA})
|
||||
if err != nil {
|
||||
@@ -70,3 +63,19 @@ func NewResourceServer(db infraDB.DB, cfg *setting.Cfg,
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
// isHighAvailabilityEnabled determines if high availability mode should
|
||||
// be enabled based on database configuration. High availability is enabled
|
||||
// by default except for SQLite databases.
|
||||
func isHighAvailabilityEnabled(dbCfg *setting.DynamicSection) bool {
|
||||
// Check in the config if HA is enabled - by default we always assume a HA setup.
|
||||
isHA := dbCfg.Key("high_availability").MustBool(true)
|
||||
|
||||
// SQLite is not possible to run in HA, so we force it to false.
|
||||
databaseType := dbCfg.Key("type").String()
|
||||
if databaseType == migrator.SQLite {
|
||||
isHA = false
|
||||
}
|
||||
|
||||
return isHA
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsHighAvailabilityEnabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dbType string
|
||||
haConfigValue *bool
|
||||
isHA bool
|
||||
}{
|
||||
{
|
||||
name: "SQLite should never have HA enabled",
|
||||
dbType: migrator.SQLite,
|
||||
haConfigValue: boolPtr(true),
|
||||
isHA: false,
|
||||
},
|
||||
{
|
||||
name: "MySQL with HA enabled in config should default to true",
|
||||
dbType: migrator.MySQL,
|
||||
haConfigValue: boolPtr(true),
|
||||
isHA: true,
|
||||
},
|
||||
{
|
||||
name: "MySQL with HA disabled in config should default to false",
|
||||
dbType: migrator.MySQL,
|
||||
haConfigValue: boolPtr(false),
|
||||
isHA: false,
|
||||
},
|
||||
{
|
||||
name: "MySQL with no HA config should default to true",
|
||||
dbType: migrator.MySQL,
|
||||
haConfigValue: nil,
|
||||
isHA: true,
|
||||
},
|
||||
{
|
||||
name: "Postgres with HA enabled in config should default to true",
|
||||
dbType: migrator.Postgres,
|
||||
haConfigValue: boolPtr(true),
|
||||
isHA: true,
|
||||
},
|
||||
{
|
||||
name: "Postgres with HA disabled in config should default to false",
|
||||
dbType: migrator.Postgres,
|
||||
haConfigValue: boolPtr(false),
|
||||
isHA: false,
|
||||
},
|
||||
{
|
||||
name: "Postgres with no HA config should default to true",
|
||||
dbType: migrator.Postgres,
|
||||
haConfigValue: nil,
|
||||
isHA: true,
|
||||
},
|
||||
{
|
||||
name: "No database type set should default to true",
|
||||
dbType: "",
|
||||
haConfigValue: nil,
|
||||
isHA: true,
|
||||
},
|
||||
{
|
||||
name: "No database type set with HA enabled in config should default to true",
|
||||
dbType: "",
|
||||
haConfigValue: boolPtr(true),
|
||||
isHA: true,
|
||||
},
|
||||
{
|
||||
name: "No database type set with HA disabled in config should default to false",
|
||||
dbType: "",
|
||||
haConfigValue: boolPtr(false),
|
||||
isHA: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := setting.NewCfg().SectionWithEnvOverrides("database")
|
||||
if tt.dbType != "" {
|
||||
cfg.Key("type").SetValue(tt.dbType)
|
||||
}
|
||||
|
||||
if tt.haConfigValue != nil {
|
||||
cfg.Key("high_availability").SetValue(strconv.FormatBool(*tt.haConfigValue))
|
||||
}
|
||||
|
||||
result := isHighAvailabilityEnabled(cfg)
|
||||
require.Equal(t, tt.isHA, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
@@ -34,6 +33,7 @@ type NewBackendFunc func(ctx context.Context) resource.StorageBackend
|
||||
// TestOptions configures which tests to run
|
||||
type TestOptions struct {
|
||||
SkipTests map[string]bool // tests to skip
|
||||
|
||||
}
|
||||
|
||||
// RunStorageBackendTest runs the storage backend test suite
|
||||
@@ -294,21 +294,29 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend
|
||||
server := newServer(t, backend)
|
||||
|
||||
// Create a few resources before starting the watch
|
||||
rv1, _ := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED)
|
||||
rv1, err := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, rv1, int64(0))
|
||||
rv2, _ := writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED)
|
||||
rv2, err := writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, rv2, rv1)
|
||||
rv3, _ := writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED)
|
||||
rv3, err := writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, rv3, rv2)
|
||||
rv4, _ := writeEvent(ctx, backend, "item4", resource.WatchEvent_ADDED)
|
||||
rv4, err := writeEvent(ctx, backend, "item4", resource.WatchEvent_ADDED)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, rv4, rv3)
|
||||
rv5, _ := writeEvent(ctx, backend, "item5", resource.WatchEvent_ADDED)
|
||||
rv5, err := writeEvent(ctx, backend, "item5", resource.WatchEvent_ADDED)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, rv5, rv4)
|
||||
rv6, _ := writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED)
|
||||
rv6, err := writeEvent(ctx, backend, "item2", resource.WatchEvent_MODIFIED)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, rv6, rv5)
|
||||
rv7, _ := writeEvent(ctx, backend, "item3", resource.WatchEvent_DELETED)
|
||||
rv7, err := writeEvent(ctx, backend, "item3", resource.WatchEvent_DELETED)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, rv7, rv6)
|
||||
rv8, _ := writeEvent(ctx, backend, "item6", resource.WatchEvent_ADDED)
|
||||
rv8, err := writeEvent(ctx, backend, "item6", resource.WatchEvent_ADDED)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, rv8, rv7)
|
||||
|
||||
t.Run("fetch all latest", func(t *testing.T) {
|
||||
@@ -346,12 +354,12 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, res.Error)
|
||||
require.Len(t, res.Items, 3)
|
||||
continueToken, err := sql.GetContinueToken(res.NextPageToken)
|
||||
continueToken, err := resource.GetContinueToken(res.NextPageToken)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "item1 ADDED", string(res.Items[0].Value))
|
||||
require.Equal(t, "item2 MODIFIED", string(res.Items[1].Value))
|
||||
require.Equal(t, "item4 ADDED", string(res.Items[2].Value))
|
||||
require.Equal(t, rv8, continueToken.ResourceVersion)
|
||||
require.GreaterOrEqual(t, continueToken.ResourceVersion, rv8)
|
||||
})
|
||||
|
||||
t.Run("list at revision", func(t *testing.T) {
|
||||
@@ -394,13 +402,13 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend
|
||||
require.Equal(t, "item2 MODIFIED", string(res.Items[1].Value))
|
||||
require.Equal(t, "item4 ADDED", string(res.Items[2].Value))
|
||||
|
||||
continueToken, err := sql.GetContinueToken(res.NextPageToken)
|
||||
continueToken, err := resource.GetContinueToken(res.NextPageToken)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, rv7, continueToken.ResourceVersion)
|
||||
})
|
||||
|
||||
t.Run("fetch second page at revision", func(t *testing.T) {
|
||||
continueToken := &sql.ContinueToken{
|
||||
continueToken := &resource.ContinueToken{
|
||||
ResourceVersion: rv8,
|
||||
StartOffset: 2,
|
||||
}
|
||||
@@ -421,7 +429,7 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend
|
||||
require.Equal(t, "item4 ADDED", string(res.Items[0].Value))
|
||||
require.Equal(t, "item5 ADDED", string(res.Items[1].Value))
|
||||
|
||||
continueToken, err = sql.GetContinueToken(res.NextPageToken)
|
||||
continueToken, err = resource.GetContinueToken(res.NextPageToken)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, rv8, continueToken.ResourceVersion)
|
||||
require.Equal(t, int64(4), continueToken.StartOffset)
|
||||
@@ -478,14 +486,14 @@ func runTestIntegrationBackendListHistory(t *testing.T, backend resource.Storage
|
||||
require.Equal(t, "item1 MODIFIED", string(res.Items[2].Value))
|
||||
require.Equal(t, rvHistory3, res.Items[2].ResourceVersion)
|
||||
|
||||
continueToken, err := sql.GetContinueToken(res.NextPageToken)
|
||||
continueToken, err := resource.GetContinueToken(res.NextPageToken)
|
||||
require.NoError(t, err)
|
||||
// should return the furthest back RV as the next page token
|
||||
require.Equal(t, rvHistory3, continueToken.ResourceVersion)
|
||||
})
|
||||
|
||||
t.Run("fetch second page of history at revision", func(t *testing.T) {
|
||||
continueToken := &sql.ContinueToken{
|
||||
continueToken := &resource.ContinueToken{
|
||||
ResourceVersion: rvHistory3,
|
||||
StartOffset: 2,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
prommodel "github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
|
||||
testinfra.SQLiteIntegrationTest(t)
|
||||
|
||||
// Setup Grafana and its Database
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
EnableFeatureToggles: []string{"alertingConversionAPI"},
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
|
||||
// Create a user to make authenticated requests
|
||||
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleAdmin),
|
||||
Password: "password",
|
||||
Login: "admin",
|
||||
})
|
||||
|
||||
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
|
||||
namespace := "test-namespace"
|
||||
|
||||
promGroup1 := apimodels.PrometheusRuleGroup{
|
||||
Name: "test-group-1",
|
||||
Interval: prommodel.Duration(60 * time.Second),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
// Recording rule
|
||||
{
|
||||
Record: "test:requests:rate5m",
|
||||
Expr: "sum(rate(test_requests_total[5m])) by (job)",
|
||||
Labels: map[string]string{
|
||||
"env": "prod",
|
||||
"team": "infra",
|
||||
},
|
||||
},
|
||||
// Two alerting rules
|
||||
{
|
||||
Alert: "HighMemoryUsage",
|
||||
Expr: "process_memory_usage > 80",
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "warning",
|
||||
"team": "alerting",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"annotation-1": "value-1",
|
||||
"annotation-2": "value-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Alert: "ServiceDown",
|
||||
Expr: "up == 0",
|
||||
For: util.Pointer(prommodel.Duration(2 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"annotation-1": "value-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
promGroup2 := apimodels.PrometheusRuleGroup{
|
||||
Name: "test-group-2",
|
||||
Interval: prommodel.Duration(60 * time.Second),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "HighDiskUsage",
|
||||
Expr: "disk_usage > 80",
|
||||
For: util.Pointer(prommodel.Duration(1 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "low",
|
||||
"team": "alerting",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"annotation-5": "value-5",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
|
||||
|
||||
t.Run("create two rule groups and get them back", func(t *testing.T) {
|
||||
apiClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil)
|
||||
apiClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup2, nil)
|
||||
|
||||
ns, _, _ := apiClient.GetAllRulesWithStatus(t)
|
||||
|
||||
require.Len(t, ns[namespace], 2)
|
||||
|
||||
rulesByGroupName := map[string][]apimodels.GettableExtendedRuleNode{}
|
||||
for _, group := range ns[namespace] {
|
||||
rulesByGroupName[group.Name] = append(rulesByGroupName[group.Name], group.Rules...)
|
||||
}
|
||||
|
||||
require.Len(t, rulesByGroupName[promGroup1.Name], 3)
|
||||
require.Len(t, rulesByGroupName[promGroup2.Name], 1)
|
||||
})
|
||||
|
||||
t.Run("when pausing header is set, rules should be paused", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
recordingPaused bool
|
||||
alertPaused bool
|
||||
}{
|
||||
{
|
||||
name: "do not pause rules",
|
||||
recordingPaused: false,
|
||||
alertPaused: false,
|
||||
},
|
||||
{
|
||||
name: "pause recording rules",
|
||||
recordingPaused: true,
|
||||
alertPaused: false,
|
||||
},
|
||||
{
|
||||
name: "pause alert rules",
|
||||
recordingPaused: false,
|
||||
alertPaused: true,
|
||||
},
|
||||
{
|
||||
name: "pause both recording and alert rules",
|
||||
recordingPaused: true,
|
||||
alertPaused: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
headers := map[string]string{}
|
||||
if tc.recordingPaused {
|
||||
headers["X-Grafana-Alerting-Recording-Rules-Paused"] = "true"
|
||||
}
|
||||
if tc.alertPaused {
|
||||
headers["X-Grafana-Alerting-Alert-Rules-Paused"] = "true"
|
||||
}
|
||||
apiClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, headers)
|
||||
|
||||
ns, _, _ := apiClient.GetAllRulesWithStatus(t)
|
||||
|
||||
rulesByGroupName := map[string][]apimodels.GettableExtendedRuleNode{}
|
||||
for _, group := range ns[namespace] {
|
||||
rulesByGroupName[group.Name] = append(rulesByGroupName[group.Name], group.Rules...)
|
||||
}
|
||||
|
||||
require.Len(t, rulesByGroupName[promGroup1.Name], 3)
|
||||
|
||||
pausedRecordingRules := 0
|
||||
pausedAlertRules := 0
|
||||
|
||||
for _, rule := range rulesByGroupName[promGroup1.Name] {
|
||||
if rule.GrafanaManagedAlert.IsPaused {
|
||||
if rule.GrafanaManagedAlert.Record != nil {
|
||||
pausedRecordingRules++
|
||||
} else {
|
||||
pausedAlertRules++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tc.recordingPaused {
|
||||
require.Equal(t, 1, pausedRecordingRules)
|
||||
} else {
|
||||
require.Equal(t, 0, pausedRecordingRules)
|
||||
}
|
||||
|
||||
if tc.alertPaused {
|
||||
require.Equal(t, 2, pausedAlertRules)
|
||||
} else {
|
||||
require.Equal(t, 0, pausedAlertRules)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api"
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
@@ -752,7 +753,13 @@ func (a apiClient) SubmitRuleForTesting(t *testing.T, config apimodels.PostableE
|
||||
func (a apiClient) CreateTestDatasource(t *testing.T) (result api.CreateOrUpdateDatasourceResponse) {
|
||||
t.Helper()
|
||||
|
||||
payload := fmt.Sprintf(`{"name":"TestData-%s","type":"testdata","access":"proxy","isDefault":false}`, uuid.NewString())
|
||||
return a.CreateDatasource(t, "testdata")
|
||||
}
|
||||
|
||||
func (a apiClient) CreateDatasource(t *testing.T, dsType string) (result api.CreateOrUpdateDatasourceResponse) {
|
||||
t.Helper()
|
||||
|
||||
payload := fmt.Sprintf(`{"name":"TestDatasource-%s","type":"%s","access":"proxy","isDefault":false}`, uuid.NewString(), dsType)
|
||||
buf := bytes.Buffer{}
|
||||
buf.Write([]byte(payload))
|
||||
|
||||
@@ -1094,6 +1101,25 @@ func (a apiClient) GetRuleByUID(t *testing.T, ruleUID string) apimodels.Gettable
|
||||
return rule
|
||||
}
|
||||
|
||||
func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle, datasourceUID string, promGroup apimodels.PrometheusRuleGroup, headers map[string]string) {
|
||||
t.Helper()
|
||||
|
||||
data, err := yaml.Marshal(promGroup)
|
||||
require.NoError(t, err)
|
||||
buf := bytes.NewReader(data)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), buf)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-Grafana-Alerting-Datasource-UID", datasourceUID)
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
|
||||
_, status, raw := sendRequest[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
|
||||
requireStatusCode(t, http.StatusAccepted, status, raw)
|
||||
}
|
||||
|
||||
func sendRequest[T any](t *testing.T, req *http.Request, successStatusCode int) (T, int, string) {
|
||||
t.Helper()
|
||||
client := &http.Client{}
|
||||
|
||||
@@ -22771,7 +22771,6 @@
|
||||
}
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
@@ -22896,7 +22895,6 @@
|
||||
}
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
|
||||
@@ -4,10 +4,9 @@ import * as React from 'react';
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2, NavModelItem, toIconName } from '@grafana/data';
|
||||
import { useStyles2, Text, IconButton, Icon, Stack, Badge } from '@grafana/ui';
|
||||
import { FeatureState, GrafanaTheme2, NavModelItem, toIconName } from '@grafana/data';
|
||||
import { useStyles2, Text, IconButton, Icon, Stack, FeatureBadge } from '@grafana/ui';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { Indent } from '../../Indent/Indent';
|
||||
|
||||
@@ -36,7 +35,7 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick, onPin, isPi
|
||||
const isActive = link === activeItem || (level === MAX_DEPTH && hasActiveChild);
|
||||
const [sectionExpanded, setSectionExpanded] = useLocalStorage(
|
||||
`grafana.navigation.expanded[${link.text}]`,
|
||||
Boolean(hasActiveChild || link.isNew)
|
||||
Boolean(hasActiveChild)
|
||||
);
|
||||
const showExpandButton = level < MAX_DEPTH && Boolean(linkHasChildren(link) || link.emptyMessage);
|
||||
const item = useRef<HTMLLIElement>(null);
|
||||
@@ -108,7 +107,7 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick, onPin, isPi
|
||||
>
|
||||
{level === 0 && iconElement && <FeatureHighlightWrapper>{iconElement}</FeatureHighlightWrapper>}
|
||||
<Text truncate>{link.text}</Text>
|
||||
{link.isNew && <Badge text={t('navigation.megamenu.new', 'New!')} color={'blue'} />}
|
||||
{link.isNew && <FeatureBadge featureState={FeatureState.new} />}
|
||||
</div>
|
||||
</MegaMenuItemText>
|
||||
</div>
|
||||
|
||||
@@ -35,21 +35,36 @@ export const enrichHelpItem = (helpItem: NavModelItem) => {
|
||||
return helpItem;
|
||||
};
|
||||
|
||||
export const enrichWithInteractionTracking = (item: NavModelItem, megaMenuDockedState: boolean) => {
|
||||
export const enrichWithInteractionTracking = (
|
||||
item: NavModelItem,
|
||||
megaMenuDockedState: boolean,
|
||||
ancestorIsNew = false
|
||||
) => {
|
||||
// creating a new object here to not mutate the original item object
|
||||
const newItem = { ...item };
|
||||
const onClick = newItem.onClick;
|
||||
|
||||
let isNew: 'item' | 'ancestor' | undefined = undefined;
|
||||
if (newItem.isNew) {
|
||||
isNew = 'item';
|
||||
} else if (ancestorIsNew) {
|
||||
isNew = 'ancestor';
|
||||
}
|
||||
|
||||
newItem.onClick = () => {
|
||||
reportInteraction('grafana_navigation_item_clicked', {
|
||||
path: newItem.url ?? newItem.id,
|
||||
menuIsDocked: megaMenuDockedState,
|
||||
itemIsBookmarked: Boolean(config.featureToggles.pinNavItems && newItem?.parentItem?.id === 'bookmarks'),
|
||||
bookmarkToggleOn: Boolean(config.featureToggles.pinNavItems),
|
||||
isNew,
|
||||
});
|
||||
onClick?.();
|
||||
};
|
||||
if (newItem.children) {
|
||||
newItem.children = newItem.children.map((item) => enrichWithInteractionTracking(item, megaMenuDockedState));
|
||||
newItem.children = newItem.children.map((item) =>
|
||||
enrichWithInteractionTracking(item, megaMenuDockedState, isNew !== undefined)
|
||||
);
|
||||
}
|
||||
return newItem;
|
||||
};
|
||||
|
||||
@@ -3146,7 +3146,8 @@ export const grafanaAlertNotifiers: Record<GrafanaNotifierType, NotifierDTO> = {
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Subject',
|
||||
description: 'Optional subject. You can use templates to customize this field',
|
||||
description:
|
||||
'Optional subject. By default, this field uses the default title template and can be customized using templates. It cannot be an empty string',
|
||||
placeholder: '{{ template "default.title" . }}',
|
||||
propertyName: 'subject',
|
||||
selectOptions: null,
|
||||
@@ -3165,7 +3166,7 @@ export const grafanaAlertNotifiers: Record<GrafanaNotifierType, NotifierDTO> = {
|
||||
inputType: '',
|
||||
label: 'Message',
|
||||
description:
|
||||
'Optional message. You can use templates to customize this field. Using a custom message will replace the default message',
|
||||
'Optional message. By default, this field uses the default message template and can be customized with templates and custom messages',
|
||||
placeholder: '{{ template "default.message" . }}',
|
||||
propertyName: 'message',
|
||||
selectOptions: null,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user