diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule.go b/pkg/services/sqlstore/migrations/ualert/alert_rule.go index 5c53806d7bd..f20452442ee 100644 --- a/pkg/services/sqlstore/migrations/ualert/alert_rule.go +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule.go @@ -118,7 +118,11 @@ func (m *migration) makeAlertRule(cond condition, da dashAlert, folderUID string if err != nil { return nil, fmt.Errorf("failed to migrate alert rule queries: related dashboard could not be loaded: %w", err) } - dsTypeMap, err := m.fetchDsTypes(cond.Data) + var uids []string + for _, d := range cond.Data { + uids = append(uids, d.DatasourceUID) + } + dsTypeMap, err := m.fetchDsTypes(uids) if err != nil { return nil, fmt.Errorf("failed to migrate alert rule queries: datasources could not be loaded: %w", err) } @@ -185,15 +189,15 @@ func (m *migration) makeAlertRule(cond condition, da dashAlert, folderUID string return ar, nil } -func (m *migration) fetchDashboard(orgID int64, dashboardUID string) (*dashboards.Dashboard, error) { +func (m *migration) fetchDashboard(orgID int64, dashboardUID string) (*dashboard, error) { // This is a hack for unit tests in 9.4. if m.sess == nil { - return &dashboards.Dashboard{}, nil + return &dashboard{}, nil } - var queryResult *dashboards.Dashboard + var queryResult *dashboard - dashboard := dashboards.Dashboard{OrgID: orgID, UID: dashboardUID} + dashboard := dashboard{OrgId: orgID, Uid: dashboardUID} has, err := m.sess.Get(&dashboard) @@ -203,14 +207,14 @@ func (m *migration) fetchDashboard(orgID int64, dashboardUID string) (*dashboard return nil, dashboards.ErrDashboardNotFound } - dashboard.SetID(dashboard.ID) - dashboard.SetUID(dashboard.UID) + dashboard.setUid(dashboard.Uid) queryResult = &dashboard return queryResult, err } type dsType struct { + ID int64 `xorm:"id"` UID string `xorm:"uid"` Type string `xorm:"type"` } @@ -219,31 +223,54 @@ func (dsType) TableName() string { return "data_source" } -func (m *migration) fetchDsTypes(data []alertQuery) (map[string]string, error) { +func (m *migration) fetchDsTypes(uids []string) (map[string]*dsType, error) { // This is a hack for unit tests in 9.4. if m.sess == nil { - return map[string]string{}, nil + return map[string]*dsType{}, nil } - result := make(map[string]string) - for _, q := range data { - result[q.DatasourceUID] = "" + result := make(map[string]*dsType) + for _, uid := range uids { + result[uid] = nil } var dsTypes []*dsType - for _, uid := range result { + for uid := range result { dsTypes = append(dsTypes, &dsType{UID: uid}) } err := m.sess.Find(&dsTypes) for _, ds := range dsTypes { - result[ds.UID] = ds.Type + result[ds.UID] = ds + } + return result, err +} + +func (m *migration) fetchDsTypesByIDs(ids []int64) (map[int64]*dsType, error) { + // This is a hack for unit tests in 9.4. + if m.sess == nil { + return map[int64]*dsType{}, nil + } + + result := make(map[int64]*dsType) + for _, id := range ids { + result[id] = nil + } + var dsTypes []*dsType + for id := range result { + dsTypes = append(dsTypes, &dsType{ID: id}) + } + + err := m.sess.Find(&dsTypes) + + for _, ds := range dsTypes { + result[ds.ID] = ds } return result, err } // migrateAlertRuleQueries attempts to fix alert rule queries so they can work in unified alerting. Queries of some data sources are not compatible with unified alerting. -func migrateAlertRuleQueries(l log.Logger, ruleID int64, data []alertQuery, panelID int64, dashboard *dashboards.Dashboard, dsTypes map[string]string) ([]alertQuery, error) { +func migrateAlertRuleQueries(l log.Logger, ruleID int64, data []alertQuery, panelID int64, dashboard *dashboard, dsTypes map[string]*dsType) ([]alertQuery, error) { result := make([]alertQuery, 0, len(data)) for _, d := range data { // queries that are expression are not relevant, skip them. @@ -256,7 +283,7 @@ func migrateAlertRuleQueries(l log.Logger, ruleID int64, data []alertQuery, pane l.Error("datasource not found", "uid", d.DatasourceUID) return nil, fmt.Errorf("datasource not found") } - if dsType != datasources.DS_GRAPHITE { + if dsType.Type != datasources.DS_GRAPHITE { continue } var fixedData map[string]json.RawMessage diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_graphite.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_graphite.go index dd525b0b1cf..3da7d0f2752 100644 --- a/pkg/services/sqlstore/migrations/ualert/alert_rule_graphite.go +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_graphite.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/tsdb/graphite" ) @@ -29,13 +29,14 @@ type panel struct { } type target struct { - RefID string `json:"refId"` - Target string `json:"target"` + RefID string `json:"refId"` + Target string `json:"target"` + TargetFull string `json:"targetFull"` } // fixGraphiteReferencedSubQueries attempts to fix graphite referenced sub queries, given unified alerting does not support this. // targetFull of Graphite data source contains the expanded version of field 'target', so let's copy that. -func fixGraphiteReferencedSubQueries(l log.Logger, queryData map[string]json.RawMessage, ruleID, panelID int64, dashboard *dashboards.Dashboard) map[string]json.RawMessage { +func fixGraphiteReferencedSubQueries(l log.Logger, queryData map[string]json.RawMessage, ruleID, panelID int64, dashboard *dashboard) map[string]json.RawMessage { if !isFixable(l, queryData) { return queryData } @@ -94,7 +95,7 @@ func isFixable(l log.Logger, queryData map[string]json.RawMessage) bool { return true } -func unwrapFromDashboard(l log.Logger, queryData map[string]json.RawMessage, panelID int64, dashboard *dashboards.Dashboard) (string, error) { +func unwrapFromDashboard(l log.Logger, queryData map[string]json.RawMessage, panelID int64, dashboard *dashboard) (string, error) { refIDRaw, ok := queryData["refId"] if !ok { return "", fmt.Errorf("query data does not have field 'refId'") @@ -203,3 +204,48 @@ func logGraphiteMigrationStats(l log.Logger) { "success_copy", successfulGraphiteMigrationCopy, "success_dashboard", successfulGraphiteMigrationDashboard) } + +func fixBrokenGraphitePlaceholders(l log.Logger, dash *dashboard, panelID int64, settings *dashAlertSettings, dsTypes map[int64]*dsType) error { + for i := range settings.Conditions { + cond := settings.Conditions[i] + val, present := dsTypes[cond.Query.DatasourceID] + if !present { + return fmt.Errorf("failed to find datasource with id %d", cond.Query.DatasourceID) + } + if val.Type != datasources.DS_GRAPHITE { + return nil + } + var t target + if err := json.Unmarshal(cond.Query.Model, &t); err != nil { + return err + } + if t.Target != "" && !hasPlaceholders(t.Target) { + continue + } + if t.TargetFull != "" && !hasPlaceholders(t.TargetFull) { + continue + } + var data map[string]json.RawMessage + if err := json.Unmarshal(cond.Query.Model, &data); err != nil { + return err + } + unwrapped, err := unwrapFromDashboard(nil, data, panelID, dash) + if err != nil { + return err + } + marshUnwrapped, err := json.Marshal(unwrapped) + if err != nil { + l.Error("error", "err", err) + return err + } + data[graphite.TargetFullModelField] = marshUnwrapped + b, err := json.Marshal(data) + if err != nil { + l.Error("error", "err", err) + return err + } + cond.Query.Model = b + settings.Conditions[i] = cond + } + return nil +} diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go index f6f4ebd2c9b..738df7a1a49 100644 --- a/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/datasources" ) func TestMigrateAlertRuleQueries(t *testing.T) { @@ -18,7 +18,7 @@ func TestMigrateAlertRuleQueries(t *testing.T) { input *simplejson.Json expected string err error - dashboard *dashboards.Dashboard + dashboard *dashboard }{ { name: "when a query has a sub query - it is extracted", @@ -27,7 +27,7 @@ func TestMigrateAlertRuleQueries(t *testing.T) { "target": "ahalfquery", }), expected: `{"target":"thisisafullquery"}`, - dashboard: &dashboards.Dashboard{}, + dashboard: &dashboard{}, }, { name: "when a query has a sub query that is not fully unwrapped, it unwraps it", @@ -37,7 +37,7 @@ func TestMigrateAlertRuleQueries(t *testing.T) { "target": "alias(#A, #A)", }), expected: `{"refId":"B", "target": "alias(xxx, xxx)"}`, - dashboard: &dashboards.Dashboard{ + dashboard: &dashboard{ Data: simplejson.MustJson([]byte(`{"panels":[{"id":0,"targets":[{"refId":"A","target":"xxx"},{"refId":"B","target":"alias(#A, #A)"}]}]}`)), }, }, @@ -47,7 +47,7 @@ func TestMigrateAlertRuleQueries(t *testing.T) { "target": "ahalfquery", }), expected: `{"target":"ahalfquery"}`, - dashboard: &dashboards.Dashboard{}, + dashboard: &dashboard{}, }, { name: "when query was hidden, it removes the flag", @@ -55,7 +55,7 @@ func TestMigrateAlertRuleQueries(t *testing.T) { "hide": true, }), expected: `{}`, - dashboard: &dashboards.Dashboard{}, + dashboard: &dashboard{}, }, } @@ -63,7 +63,7 @@ func TestMigrateAlertRuleQueries(t *testing.T) { t.Run(tt.name, func(t *testing.T) { model, err := tt.input.Encode() require.NoError(t, err) - queries, err := migrateAlertRuleQueries(log.NewNopLogger(), 0, []alertQuery{{Model: model, DatasourceUID: "a"}}, 0, tt.dashboard, map[string]string{"a": "graphite"}) + queries, err := migrateAlertRuleQueries(log.NewNopLogger(), 0, []alertQuery{{Model: model, DatasourceUID: "a"}}, 0, tt.dashboard, map[string]*dsType{"a": {Type: datasources.DS_GRAPHITE}}) if tt.err != nil { require.Error(t, err) require.EqualError(t, err, tt.err.Error()) diff --git a/pkg/services/sqlstore/migrations/ualert/ualert.go b/pkg/services/sqlstore/migrations/ualert/ualert.go index a695b78c148..af33daebc12 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert.go @@ -292,13 +292,8 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { for _, da := range dashAlerts { l := mg.Logger.New("ruleID", da.Id, "ruleName", da.Name, "dashboardUID", da.DashboardUID, "orgID", da.OrgId) - newCond, err := transConditions(*da.ParsedSettings, da.OrgId, dsIDMap) - if err != nil { - return err - } da.DashboardUID = dashIDMap[[2]int64{da.OrgId, da.DashboardId}] - // get dashboard dash := dashboard{} exists, err := m.sess.Where("org_id=? AND uid=?", da.OrgId, da.DashboardUID).Get(&dash) @@ -314,6 +309,27 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { AlertId: da.Id, } } + var ids []int64 + for _, d := range da.ParsedSettings.Conditions { + ids = append(ids, d.Query.DatasourceID) + } + dsTypes, err := m.fetchDsTypesByIDs(ids) + if err != nil { + return MigrationError{ + Err: err, + AlertId: da.Id, + } + } + // Make sure that targetFull is correctly set for Graphite based alerts before starting the migration + if err := fixBrokenGraphitePlaceholders(l, &dash, da.PanelId, da.ParsedSettings, dsTypes); err != nil { + l.Error("failed to fix broken graphite query", "err", err) + continue + } + + newCond, err := transConditions(*da.ParsedSettings, da.OrgId, dsIDMap) + if err != nil { + return err + } var folder *dashboard switch {