The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/annotations/annotationsimpl/xorm_store_test.go

739 lines
23 KiB

package annotationsimpl
import (
"context"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
annotation_ac "github.com/grafana/grafana/pkg/services/annotations/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations/testutil"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/tag"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
func TestIntegrationAnnotations(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sql := db.InitTestDB(t)
cfg := setting.NewCfg()
cfg.AnnotationMaximumTagsLength = 60
store := NewXormStore(cfg, log.New("annotation.test"), sql, tagimpl.ProvideService(sql))
testUser := &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAnnotationsRead: []string{accesscontrol.ScopeAnnotationsAll},
dashboards.ActionDashboardsRead: []string{dashboards.ScopeDashboardsAll},
},
},
}
t.Run("Testing annotation create, read, update and delete", func(t *testing.T) {
t.Cleanup(func() {
err := sql.WithDbSession(context.Background(), func(dbSession *db.Session) error {
_, err := dbSession.Exec("DELETE FROM annotation WHERE 1=1")
if err != nil {
return err
}
_, err = dbSession.Exec("DELETE FROM annotation_tag WHERE 1=1")
return err
})
assert.NoError(t, err)
})
dashboard := testutil.CreateDashboard(t, sql, cfg, featuremgmt.WithFeatures(), dashboards.SaveDashboardCommand{
UserID: 1,
OrgID: 1,
Dashboard: simplejson.NewFromAny(map[string]any{
"title": "Dashboard 1",
}),
})
dashboard2 := testutil.CreateDashboard(t, sql, cfg, featuremgmt.WithFeatures(), dashboards.SaveDashboardCommand{
UserID: 1,
OrgID: 1,
Dashboard: simplejson.NewFromAny(map[string]any{
"title": "Dashboard 2",
}),
})
var err error
annotation := &annotations.Item{
OrgID: 1,
UserID: 1,
DashboardID: dashboard.ID, // nolint: staticcheck
DashboardUID: dashboard.UID,
Text: "hello",
Type: "alert",
Epoch: 10,
Tags: []string{"outage", "error", "type:outage", "server:server-1"},
Data: simplejson.NewFromAny(map[string]any{"data1": "I am a cool data", "data2": "I am another cool data"}),
}
err = store.Add(context.Background(), annotation)
require.NoError(t, err)
assert.Greater(t, annotation.ID, int64(0))
assert.Equal(t, annotation.Epoch, annotation.EpochEnd)
annotation2 := &annotations.Item{
OrgID: 1,
UserID: 1,
DashboardID: dashboard2.ID, // nolint: staticcheck
DashboardUID: dashboard2.UID,
Text: "hello",
Type: "alert",
Epoch: 21, // Should swap epoch & epochEnd
EpochEnd: 20,
Tags: []string{"outage", "type:outage", "server:server-1", "error"},
}
err = store.Add(context.Background(), annotation2)
require.NoError(t, err)
assert.Greater(t, annotation2.ID, int64(0))
assert.Equal(t, int64(20), annotation2.Epoch)
assert.Equal(t, int64(21), annotation2.EpochEnd)
organizationAnnotation1 := &annotations.Item{
OrgID: 1,
UserID: 1,
Text: "deploy",
Type: "",
Epoch: 15,
Tags: []string{"deploy"},
}
err = store.Add(context.Background(), organizationAnnotation1)
require.NoError(t, err)
assert.Greater(t, organizationAnnotation1.ID, int64(0))
organizationAnnotation2 := &annotations.Item{
OrgID: 1,
UserID: 1,
Text: "rollback",
Type: "",
Epoch: 17,
Tags: []string{"rollback"},
}
err = store.Add(context.Background(), organizationAnnotation2)
require.NoError(t, err)
assert.Greater(t, organizationAnnotation2.ID, int64(0))
t.Run("Can query for annotation by dashboard id", func(t *testing.T) {
items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1,
DashboardID: dashboard.ID, // nolint: staticcheck
From: 0,
To: 15,
SignedInUser: testUser,
}, &annotation_ac.AccessResources{
Dashboards: map[string]int64{
dashboard.UID: dashboard.ID,
},
CanAccessDashAnnotations: true,
})
require.NoError(t, err)
assert.Len(t, items, 1)
assert.Equal(t, []string{"outage", "error", "type:outage", "server:server-1"}, items[0].Tags)
assert.GreaterOrEqual(t, items[0].Created, int64(0))
assert.GreaterOrEqual(t, items[0].Updated, int64(0))
assert.Equal(t, items[0].Updated, items[0].Created)
})
t.Run("Can query for annotation by dashboard uid", func(t *testing.T) {
items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1,
DashboardUID: dashboard.UID,
From: 0,
To: 15,
SignedInUser: testUser,
}, &annotation_ac.AccessResources{
Dashboards: map[string]int64{
dashboard.UID: dashboard.ID,
},
CanAccessDashAnnotations: true,
})
require.NoError(t, err)
assert.Len(t, items, 1)
assert.Equal(t, []string{"outage", "error", "type:outage", "server:server-1"}, items[0].Tags)
assert.GreaterOrEqual(t, items[0].Created, int64(0))
assert.GreaterOrEqual(t, items[0].Updated, int64(0))
assert.Equal(t, items[0].Updated, items[0].Created)
})
badAnnotation := &annotations.Item{
OrgID: 1,
UserID: 1,
Text: "rollback",
Type: "",
Epoch: 17,
Tags: []string{strings.Repeat("a", int(cfg.AnnotationMaximumTagsLength+1))},
}
err = store.Add(context.Background(), badAnnotation)
require.Error(t, err)
require.ErrorIs(t, err, annotations.ErrBaseTagLimitExceeded)
t.Run("Can batch-insert annotations", func(t *testing.T) {
count := 10
items := make([]annotations.Item, count)
for i := 0; i < count; i++ {
items[i] = annotations.Item{
OrgID: 100,
Type: "batch",
Epoch: 12,
}
}
err := store.AddMany(context.Background(), items)
require.NoError(t, err)
query := annotations.ItemQuery{OrgID: 100, SignedInUser: testUser}
accRes := &annotation_ac.AccessResources{CanAccessOrgAnnotations: true}
inserted, err := store.Get(context.Background(), query, accRes)
require.NoError(t, err)
assert.Len(t, inserted, count)
for _, ins := range inserted {
require.Equal(t, int64(12), ins.Time)
require.Equal(t, int64(12), ins.TimeEnd)
require.Equal(t, ins.Created, ins.Updated)
}
})
t.Run("Can batch-insert annotations with tags", func(t *testing.T) {
count := 10
items := make([]annotations.Item, count)
for i := 0; i < count; i++ {
items[i] = annotations.Item{
OrgID: 101,
Type: "batch",
Epoch: 12,
}
}
items[0].Tags = []string{"type:test"}
err := store.AddMany(context.Background(), items)
require.NoError(t, err)
query := annotations.ItemQuery{OrgID: 101, SignedInUser: testUser}
accRes := &annotation_ac.AccessResources{CanAccessOrgAnnotations: true}
inserted, err := store.Get(context.Background(), query, accRes)
require.NoError(t, err)
assert.Len(t, inserted, count)
})
t.Run("Can query for annotation by id", func(t *testing.T) {
items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1,
AnnotationID: annotation2.ID,
SignedInUser: testUser,
}, &annotation_ac.AccessResources{
Dashboards: map[string]int64{
dashboard2.UID: dashboard2.ID,
},
CanAccessDashAnnotations: true,
})
require.NoError(t, err)
assert.Len(t, items, 1)
assert.Equal(t, annotation2.ID, items[0].ID)
})
t.Run("Should not find any when item is outside time range", func(t *testing.T) {
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true,
}
items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1,
DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 12,
To: 15,
SignedInUser: testUser,
}, accRes)
require.NoError(t, err)
assert.Empty(t, items)
})
t.Run("Should not find one when tag filter does not match", func(t *testing.T) {
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true,
}
items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1,
DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 1,
To: 15,
Tags: []string{"asd"},
SignedInUser: testUser,
}, accRes)
require.NoError(t, err)
assert.Empty(t, items)
})
t.Run("Should not find one when type filter does not match", func(t *testing.T) {
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true,
}
items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1,
DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 1,
To: 15,
Type: "alert",
SignedInUser: testUser,
}, accRes)
require.NoError(t, err)
assert.Empty(t, items)
})
t.Run("Should find one when all tag filters does match", func(t *testing.T) {
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true,
}
items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1,
DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 1,
To: 15, // this will exclude the second test annotation
Tags: []string{"outage", "error"},
SignedInUser: testUser,
}, accRes)
require.NoError(t, err)
assert.Len(t, items, 1)
})
t.Run("Should find two annotations using partial match", func(t *testing.T) {
accRes := &annotation_ac.AccessResources{CanAccessOrgAnnotations: true}
items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1,
From: 1,
To: 25,
MatchAny: true,
Tags: []string{"rollback", "deploy"},
SignedInUser: testUser,
}, accRes)
require.NoError(t, err)
assert.Len(t, items, 2)
})
t.Run("Should find one when all key value tag filters does match", func(t *testing.T) {
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true,
}
items, err := store.Get(context.Background(), annotations.ItemQuery{
OrgID: 1,
DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 1,
To: 15,
Tags: []string{"type:outage", "server:server-1"},
SignedInUser: testUser,
}, accRes)
require.NoError(t, err)
assert.Len(t, items, 1)
})
t.Run("Can update annotation and remove all tags", func(t *testing.T) {
query := annotations.ItemQuery{
OrgID: 1,
DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 0,
To: 15,
SignedInUser: testUser,
}
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true,
}
items, err := store.Get(context.Background(), query, accRes)
require.NoError(t, err)
annotationId := items[0].ID
err = store.Update(context.Background(), &annotations.Item{
ID: annotationId,
OrgID: 1,
Text: "something new",
Tags: []string{},
})
require.NoError(t, err)
items, err = store.Get(context.Background(), query, accRes)
require.NoError(t, err)
assert.Equal(t, annotationId, items[0].ID)
assert.Empty(t, items[0].Tags)
assert.Equal(t, "something new", items[0].Text)
data, err := items[0].Data.Map()
assert.NoError(t, err)
assert.Equal(t, data, map[string]any{"data1": "I am a cool data", "data2": "I am another cool data"})
})
t.Run("Can update annotation with new tags", func(t *testing.T) {
query := annotations.ItemQuery{
OrgID: 1,
DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 0,
To: 15,
SignedInUser: testUser,
}
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true,
}
items, err := store.Get(context.Background(), query, accRes)
require.NoError(t, err)
annotationId := items[0].ID
err = store.Update(context.Background(), &annotations.Item{
ID: annotationId,
OrgID: 1,
Text: "something new",
Tags: []string{"newtag1", "newtag2"},
})
require.NoError(t, err)
items, err = store.Get(context.Background(), query, accRes)
require.NoError(t, err)
assert.Equal(t, annotationId, items[0].ID)
assert.Equal(t, []string{"newtag1", "newtag2"}, items[0].Tags)
assert.Equal(t, "something new", items[0].Text)
assert.Greater(t, items[0].Updated, items[0].Created)
})
t.Run("Can update annotation with additional tags", func(t *testing.T) {
query := annotations.ItemQuery{
OrgID: 1,
DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 0,
To: 15,
SignedInUser: testUser,
}
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true,
}
items, err := store.Get(context.Background(), query, accRes)
require.NoError(t, err)
annotationId := items[0].ID
err = store.Update(context.Background(), &annotations.Item{
ID: annotationId,
OrgID: 1,
Text: "something new",
Tags: []string{"newtag1", "newtag3"},
})
require.NoError(t, err)
items, err = store.Get(context.Background(), query, accRes)
require.NoError(t, err)
assert.Equal(t, annotationId, items[0].ID)
assert.Equal(t, []string{"newtag1", "newtag3"}, items[0].Tags)
assert.Equal(t, "something new", items[0].Text)
assert.Greater(t, items[0].Updated, items[0].Created)
})
t.Run("Can update annotations with data", func(t *testing.T) {
query := annotations.ItemQuery{
OrgID: 1,
DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 0,
To: 15,
SignedInUser: testUser,
}
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true,
}
items, err := store.Get(context.Background(), query, accRes)
require.NoError(t, err)
annotationId := items[0].ID
data := simplejson.NewFromAny(map[string]any{"data": "I am a data", "data2": "I am also a data"})
err = store.Update(context.Background(), &annotations.Item{
ID: annotationId,
OrgID: 1,
Text: "something new",
Tags: []string{"newtag1", "newtag2"},
Data: data,
})
require.NoError(t, err)
items, err = store.Get(context.Background(), query, accRes)
require.NoError(t, err)
assert.Equal(t, annotationId, items[0].ID)
assert.Equal(t, []string{"newtag1", "newtag2"}, items[0].Tags)
assert.Equal(t, "something new", items[0].Text)
assert.Greater(t, items[0].Updated, items[0].Created)
assert.Equal(t, data, items[0].Data)
})
t.Run("Can delete annotation", func(t *testing.T) {
query := annotations.ItemQuery{
OrgID: 1,
DashboardID: 1, // nolint: staticcheck
DashboardUID: dashboard.UID,
From: 0,
To: 15,
SignedInUser: testUser,
}
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{dashboard.UID: 1},
CanAccessDashAnnotations: true,
}
items, err := store.Get(context.Background(), query, accRes)
require.NoError(t, err)
annotationId := items[0].ID
err = store.Delete(context.Background(), &annotations.DeleteParams{ID: annotationId, OrgID: 1})
require.NoError(t, err)
items, err = store.Get(context.Background(), query, accRes)
require.NoError(t, err)
assert.Empty(t, items)
})
t.Run("Can delete annotation using dashboard id and panel id", func(t *testing.T) {
annotation3 := &annotations.Item{
OrgID: 1,
UserID: 1,
DashboardID: dashboard2.ID, // nolint: staticcheck
DashboardUID: dashboard2.UID,
Text: "toBeDeletedWithPanelId",
Type: "alert",
Epoch: 11,
Tags: []string{"test"},
PanelID: 20,
}
err = store.Add(context.Background(), annotation3)
require.NoError(t, err)
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{
dashboard2.UID: dashboard2.ID,
},
CanAccessDashAnnotations: true,
}
query := annotations.ItemQuery{
OrgID: 1,
AnnotationID: annotation3.ID,
SignedInUser: testUser,
}
items, err := store.Get(context.Background(), query, accRes)
require.NoError(t, err)
// nolint:staticcheck
err = store.Delete(context.Background(), &annotations.DeleteParams{DashboardID: items[0].DashboardID, PanelID: items[0].PanelID, OrgID: 1})
require.NoError(t, err)
items, err = store.Get(context.Background(), query, accRes)
require.NoError(t, err)
assert.Empty(t, items)
})
t.Run("Can delete annotation using dashboard uid and panel id", func(t *testing.T) {
annotation3 := &annotations.Item{
OrgID: 1,
UserID: 1,
DashboardUID: dashboard2.UID,
Text: "toBeDeletedWithPanelId",
Type: "alert",
Epoch: 11,
Tags: []string{"test"},
PanelID: 20,
}
err = store.Add(context.Background(), annotation3)
require.NoError(t, err)
accRes := &annotation_ac.AccessResources{
Dashboards: map[string]int64{
dashboard2.UID: dashboard2.ID,
},
CanAccessDashAnnotations: true,
}
query := annotations.ItemQuery{
OrgID: 1,
AnnotationID: annotation3.ID,
SignedInUser: testUser,
}
items, err := store.Get(context.Background(), query, accRes)
require.NoError(t, err)
// nolint:staticcheck
err = store.Delete(context.Background(), &annotations.DeleteParams{DashboardUID: *items[0].DashboardUID, PanelID: items[0].PanelID, OrgID: 1})
require.NoError(t, err)
items, err = store.Get(context.Background(), query, accRes)
require.NoError(t, err)
assert.Empty(t, items)
})
t.Run("Should find tags by key", func(t *testing.T) {
result, err := store.GetTags(context.Background(), annotations.TagsQuery{
OrgID: 1,
Tag: "SeRvEr", // Use mixed-case to test LIKE case-insensitivity
})
require.NoError(t, err)
require.Len(t, result.Tags, 1)
require.Equal(t, "server:server-1", result.Tags[0].Tag)
require.Equal(t, int64(1), result.Tags[0].Count)
})
t.Run("Should find tags by value", func(t *testing.T) {
result, err := store.GetTags(context.Background(), annotations.TagsQuery{
OrgID: 1,
Tag: "Outage", // Use mixed-case to test LIKE case-insensitivity
})
require.NoError(t, err)
require.Len(t, result.Tags, 2)
require.Equal(t, "outage", result.Tags[0].Tag)
require.Equal(t, "type:outage", result.Tags[1].Tag)
require.Equal(t, int64(1), result.Tags[0].Count)
require.Equal(t, int64(1), result.Tags[1].Count)
})
t.Run("Should not find tags in other org", func(t *testing.T) {
result, err := store.GetTags(context.Background(), annotations.TagsQuery{
OrgID: 0,
Tag: "server-1",
})
require.NoError(t, err)
require.Len(t, result.Tags, 0)
})
t.Run("Should not find tags that do not exist", func(t *testing.T) {
result, err := store.GetTags(context.Background(), annotations.TagsQuery{
OrgID: 0,
Tag: "unknown:tag",
})
require.NoError(t, err)
require.Len(t, result.Tags, 0)
})
})
}
func BenchmarkFindTags_10k(b *testing.B) {
benchmarkFindTags(b, 10000)
}
func BenchmarkFindTags_100k(b *testing.B) {
benchmarkFindTags(b, 100000)
}
func benchmarkFindTags(b *testing.B, numAnnotations int) {
sql := db.InitTestDB(b)
cfg := setting.NewCfg()
cfg.AnnotationMaximumTagsLength = 60
store := xormRepositoryImpl{db: sql, cfg: cfg, log: log.New("annotation.test"), tagService: tagimpl.ProvideService(sql)}
type annotationTag struct {
ID int64 `xorm:"pk autoincr 'id'"`
AnnotationID int64 `xorm:"annotation_id"`
TagID int64 `xorm:"tag_id"`
}
newAnnotations := make([]annotations.Item, 0, numAnnotations)
newTags := make([]tag.Tag, 0, numAnnotations)
newAnnotationTags := make([]annotationTag, 0, numAnnotations)
for i := 0; i < numAnnotations; i++ {
newAnnotations = append(newAnnotations, annotations.Item{
ID: int64(i),
OrgID: 1,
UserID: 1,
DashboardID: int64(i),
Text: "hello",
Type: "alert",
Epoch: 10,
Data: simplejson.NewFromAny(map[string]any{"data1": "I am a cool data", "data2": "I am another cool data"}),
})
newTags = append(newTags, tag.Tag{
Id: int64(i),
Key: fmt.Sprintf("tag%d", i),
})
newAnnotationTags = append(newAnnotationTags, annotationTag{
AnnotationID: int64(i),
TagID: int64(1),
})
}
err := sql.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
batchSize := 1000
numOfBatches := numAnnotations / batchSize
for i := 0; i < numOfBatches; i++ {
_, err := sess.Insert(newAnnotations[i*batchSize : (i+1)*batchSize-1])
require.NoError(b, err)
_, err = sess.Insert(newTags[i*batchSize : (i+1)*batchSize-1])
require.NoError(b, err)
_, err = sess.Insert(newAnnotationTags[i*batchSize : (i+1)*batchSize-1])
require.NoError(b, err)
}
return nil
})
require.NoError(b, err)
annotationWithTheTag := annotations.Item{
ID: int64(numAnnotations) + 1,
OrgID: 1,
UserID: 1,
DashboardID: 1, // nolint: staticcheck
DashboardUID: "uid",
Text: "hello",
Type: "alert",
Epoch: 10,
Tags: []string{"outage", "error", "type:outage", "server:server-1"},
Data: simplejson.NewFromAny(map[string]any{"data1": "I am a cool data", "data2": "I am another cool data"}),
}
err = store.Add(context.Background(), &annotationWithTheTag)
require.NoError(b, err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
result, err := store.GetTags(context.Background(), annotations.TagsQuery{
OrgID: 1,
Tag: "outage",
})
require.NoError(b, err)
require.Len(b, result.Tags, 2)
require.Equal(b, "outage", result.Tags[0].Tag)
require.Equal(b, "type:outage", result.Tags[1].Tag)
require.Equal(b, int64(1), result.Tags[0].Count)
require.Equal(b, int64(1), result.Tags[1].Count)
}
}