mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Alerting: In migration improve deduplication of title and group This change improves alert titles generated in the legacy migration that occur when we need to deduplicate titles. Now when duplicate titles are detected we will first attempt to append a sequential index, falling back to a random uid if none are unique within 10 attempts. This should cause shorter and more easily readable deduplicated titles in most cases. In addition, groups are no longer deduplicated. Instead we set them to a combination of truncated dashboard name and humanized alert frequency. This way, alerts from the same dashboard share a group if they have the same evaluation interval. In the event that truncation causes overlap, it won't be a big issue as all alerts will still be in a group with the correct evaluation interval.
319 lines
11 KiB
Go
319 lines
11 KiB
Go
package migration
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"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/logtest"
|
|
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
|
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
|
|
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
|
)
|
|
|
|
func TestMigrateAlertRuleQueries(t *testing.T) {
|
|
tc := []struct {
|
|
name string
|
|
input *simplejson.Json
|
|
expected string
|
|
err error
|
|
}{
|
|
{
|
|
name: "when a query has a sub query - it is extracted",
|
|
input: simplejson.NewFromAny(map[string]any{"targetFull": "thisisafullquery", "target": "ahalfquery"}),
|
|
expected: `{"target":"thisisafullquery"}`,
|
|
},
|
|
{
|
|
name: "when a query does not have a sub query - it no-ops",
|
|
input: simplejson.NewFromAny(map[string]any{"target": "ahalfquery"}),
|
|
expected: `{"target":"ahalfquery"}`,
|
|
},
|
|
{
|
|
name: "when query was hidden, it removes the flag",
|
|
input: simplejson.NewFromAny(map[string]any{"hide": true}),
|
|
expected: `{}`,
|
|
},
|
|
{
|
|
name: "when prometheus both type query, convert to range",
|
|
input: simplejson.NewFromAny(map[string]any{
|
|
"datasource": map[string]string{
|
|
"type": "prometheus",
|
|
},
|
|
"instant": true,
|
|
"range": true,
|
|
}),
|
|
expected: `{"datasource":{"type":"prometheus"},"instant":false,"range":true}`,
|
|
},
|
|
{
|
|
name: "when prometheus instant type query, do nothing",
|
|
input: simplejson.NewFromAny(map[string]any{
|
|
"datasource": map[string]string{
|
|
"type": "prometheus",
|
|
},
|
|
"instant": true,
|
|
}),
|
|
expected: `{"datasource":{"type":"prometheus"},"instant":true}`,
|
|
},
|
|
{
|
|
name: "when non-prometheus with instant and range, do nothing",
|
|
input: simplejson.NewFromAny(map[string]any{
|
|
"datasource": map[string]string{
|
|
"type": "something",
|
|
},
|
|
"instant": true,
|
|
"range": true,
|
|
}),
|
|
expected: `{"datasource":{"type":"something"},"instant":true,"range":true}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tc {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
model, err := tt.input.Encode()
|
|
require.NoError(t, err)
|
|
queries, err := migrateAlertRuleQueries(&logtest.Fake{}, []models.AlertQuery{{Model: model}})
|
|
if tt.err != nil {
|
|
require.Error(t, err)
|
|
require.EqualError(t, err, tt.err.Error())
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
r, err := queries[0].Model.MarshalJSON()
|
|
require.NoError(t, err)
|
|
require.JSONEq(t, tt.expected, string(r))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddMigrationInfo(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
alert *migrationStore.DashAlert
|
|
dashboard string
|
|
expectedLabels map[string]string
|
|
expectedAnnotations map[string]string
|
|
}{
|
|
{
|
|
name: "when alert rule tags are a JSON array, they're ignored.",
|
|
alert: &migrationStore.DashAlert{Alert: &legacymodels.Alert{ID: 43, PanelID: 42}, ParsedSettings: &migrationStore.DashAlertSettings{AlertRuleTags: []string{"one", "two", "three", "four"}}},
|
|
dashboard: "dashboard",
|
|
expectedLabels: map[string]string{},
|
|
expectedAnnotations: map[string]string{"__alertId__": "43", "__dashboardUid__": "dashboard", "__panelId__": "42"},
|
|
},
|
|
{
|
|
name: "when alert rule tags are a JSON object",
|
|
alert: &migrationStore.DashAlert{Alert: &legacymodels.Alert{ID: 43, PanelID: 42}, ParsedSettings: &migrationStore.DashAlertSettings{AlertRuleTags: map[string]any{"key": "value", "key2": "value2"}}},
|
|
dashboard: "dashboard",
|
|
expectedLabels: map[string]string{"key": "value", "key2": "value2"},
|
|
expectedAnnotations: map[string]string{"__alertId__": "43", "__dashboardUid__": "dashboard", "__panelId__": "42"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
labels, annotations := addMigrationInfo(tc.alert, tc.dashboard)
|
|
require.Equal(t, tc.expectedLabels, labels)
|
|
require.Equal(t, tc.expectedAnnotations, annotations)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMakeAlertRule(t *testing.T) {
|
|
sqlStore := db.InitTestDB(t)
|
|
info := migmodels.DashboardUpgradeInfo{
|
|
DashboardUID: "dashboarduid",
|
|
DashboardName: "dashboardname",
|
|
NewFolderUID: "ewfolderuid",
|
|
NewFolderName: "newfoldername",
|
|
}
|
|
t.Run("when mapping rule names", func(t *testing.T) {
|
|
t.Run("leaves basic names untouched", func(t *testing.T) {
|
|
service := NewTestMigrationService(t, sqlStore, nil)
|
|
m := service.newOrgMigration(1)
|
|
da := createTestDashAlert()
|
|
|
|
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, da.Name, ar.Title)
|
|
})
|
|
|
|
t.Run("truncates very long names to max length", func(t *testing.T) {
|
|
service := NewTestMigrationService(t, sqlStore, nil)
|
|
m := service.newOrgMigration(1)
|
|
da := createTestDashAlert()
|
|
da.Name = strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1)
|
|
|
|
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, ar.Title, store.AlertDefinitionMaxTitleLength)
|
|
})
|
|
|
|
t.Run("deduplicate names in same org and folder", func(t *testing.T) {
|
|
service := NewTestMigrationService(t, sqlStore, nil)
|
|
m := service.newOrgMigration(1)
|
|
da := createTestDashAlert()
|
|
da.Name = strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1)
|
|
|
|
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, ar.Title, store.AlertDefinitionMaxTitleLength)
|
|
|
|
da = createTestDashAlert()
|
|
da.Name = strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1)
|
|
|
|
ar, err = m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, ar.Title, store.AlertDefinitionMaxTitleLength)
|
|
require.Equal(t, ar.Title, fmt.Sprintf("%s #2", strings.Repeat("a", store.AlertDefinitionMaxTitleLength-3)))
|
|
})
|
|
})
|
|
|
|
t.Run("alert is not paused", func(t *testing.T) {
|
|
service := NewTestMigrationService(t, sqlStore, nil)
|
|
m := service.newOrgMigration(1)
|
|
da := createTestDashAlert()
|
|
|
|
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
|
require.NoError(t, err)
|
|
require.False(t, ar.IsPaused)
|
|
})
|
|
|
|
t.Run("paused dash alert is paused", func(t *testing.T) {
|
|
service := NewTestMigrationService(t, sqlStore, nil)
|
|
m := service.newOrgMigration(1)
|
|
da := createTestDashAlert()
|
|
da.State = "paused"
|
|
|
|
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
|
require.NoError(t, err)
|
|
require.True(t, ar.IsPaused)
|
|
})
|
|
|
|
t.Run("use default if execution of NoData is not known", func(t *testing.T) {
|
|
service := NewTestMigrationService(t, sqlStore, nil)
|
|
m := service.newOrgMigration(1)
|
|
da := createTestDashAlert()
|
|
da.ParsedSettings.NoDataState = uuid.NewString()
|
|
|
|
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
|
require.Nil(t, err)
|
|
require.Equal(t, models.NoData, ar.NoDataState)
|
|
})
|
|
|
|
t.Run("use default if execution of Error is not known", func(t *testing.T) {
|
|
service := NewTestMigrationService(t, sqlStore, nil)
|
|
m := service.newOrgMigration(1)
|
|
da := createTestDashAlert()
|
|
da.ParsedSettings.ExecutionErrorState = uuid.NewString()
|
|
|
|
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
|
require.Nil(t, err)
|
|
require.Equal(t, models.ErrorErrState, ar.ExecErrState)
|
|
})
|
|
|
|
t.Run("migrate message template", func(t *testing.T) {
|
|
service := NewTestMigrationService(t, sqlStore, nil)
|
|
m := service.newOrgMigration(1)
|
|
da := createTestDashAlert()
|
|
da.Message = "Instance ${instance} is down"
|
|
|
|
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
|
require.Nil(t, err)
|
|
expected :=
|
|
"{{- $mergedLabels := mergeLabelValues $values -}}\n" +
|
|
"Instance {{$mergedLabels.instance}} is down"
|
|
require.Equal(t, expected, ar.Annotations["message"])
|
|
})
|
|
|
|
t.Run("create unique group from dashboard title and humanized interval", func(t *testing.T) {
|
|
service := NewTestMigrationService(t, sqlStore, nil)
|
|
m := service.newOrgMigration(1)
|
|
da := createTestDashAlert()
|
|
da.PanelID = 42
|
|
|
|
intervalTests := []struct {
|
|
interval int64
|
|
expected string
|
|
}{
|
|
{interval: 10, expected: "10s"},
|
|
{interval: 30, expected: "30s"},
|
|
{interval: 60, expected: "1m"},
|
|
{interval: 120, expected: "2m"},
|
|
{interval: 3600, expected: "1h"},
|
|
{interval: 7200, expected: "2h"},
|
|
{interval: 86400, expected: "1d"},
|
|
{interval: 172800, expected: "2d"},
|
|
{interval: 604800, expected: "1w"},
|
|
{interval: 1209600, expected: "2w"},
|
|
{interval: 31536000, expected: "1y"},
|
|
{interval: 63072000, expected: "2y"},
|
|
{interval: 60 + 30, expected: "1m30s"},
|
|
{interval: 3600 + 10, expected: "1h10s"},
|
|
{interval: 3600 + 60, expected: "1h1m"},
|
|
{interval: 3600 + 60 + 10, expected: "1h1m10s"},
|
|
{interval: 86400 + 10, expected: "1d10s"},
|
|
{interval: 86400 + 60, expected: "1d1m"},
|
|
{interval: 86400 + 3600, expected: "1d1h"},
|
|
{interval: 86400 + 3600 + 60, expected: "1d1h1m"},
|
|
{interval: 86400 + 3600 + 10, expected: "1d1h10s"},
|
|
{interval: 86400 + 60 + 10, expected: "1d1m10s"},
|
|
{interval: 86400 + 3600 + 60 + 10, expected: "1d1h1m10s"},
|
|
{interval: 604800 + 86400 + 3600 + 60 + 10, expected: "8d1h1m10s"},
|
|
{interval: 31536000 + 604800 + 86400 + 3600 + 60 + 10, expected: "373d1h1m10s"},
|
|
}
|
|
|
|
for _, test := range intervalTests {
|
|
t.Run(fmt.Sprintf("interval %ds should be %s", test.interval, test.expected), func(t *testing.T) {
|
|
da.Frequency = test.interval
|
|
|
|
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, fmt.Sprintf("%s - %s", info.DashboardName, test.expected), ar.RuleGroup)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("truncate dashboard name part of rule group if too long", func(t *testing.T) {
|
|
service := NewTestMigrationService(t, sqlStore, nil)
|
|
m := service.newOrgMigration(1)
|
|
da := createTestDashAlert()
|
|
info := migmodels.DashboardUpgradeInfo{
|
|
DashboardUID: "dashboarduid",
|
|
DashboardName: strings.Repeat("a", store.AlertRuleMaxRuleGroupNameLength-1),
|
|
NewFolderUID: "newfolderuid",
|
|
NewFolderName: "newfoldername",
|
|
}
|
|
|
|
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, ar.RuleGroup, store.AlertRuleMaxRuleGroupNameLength)
|
|
suffix := fmt.Sprintf(" - %ds", ar.IntervalSeconds)
|
|
require.Equal(t, fmt.Sprintf("%s%s", strings.Repeat("a", store.AlertRuleMaxRuleGroupNameLength-len(suffix)), suffix), ar.RuleGroup)
|
|
})
|
|
}
|
|
|
|
func createTestDashAlert() migrationStore.DashAlert {
|
|
return migrationStore.DashAlert{
|
|
Alert: &legacymodels.Alert{
|
|
ID: 1,
|
|
Name: "test",
|
|
},
|
|
ParsedSettings: &migrationStore.DashAlertSettings{},
|
|
}
|
|
}
|