2021-04-29 12:24:37 -05:00
|
|
|
package ualert
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"time"
|
2021-06-14 14:13:45 -05:00
|
|
|
|
|
|
|
"github.com/grafana/grafana/pkg/util"
|
2021-04-29 12:24:37 -05:00
|
|
|
)
|
|
|
|
|
2021-04-30 06:47:01 -05:00
|
|
|
func transConditions(set dashAlertSettings, orgID int64, dsUIDMap dsUIDLookup) (*condition, error) {
|
2021-04-29 12:24:37 -05:00
|
|
|
refIDtoCondIdx := make(map[string][]int) // a map of original refIds to their corresponding condition index
|
|
|
|
for i, cond := range set.Conditions {
|
|
|
|
if len(cond.Query.Params) != 3 {
|
|
|
|
return nil, fmt.Errorf("unexpected number of query parameters in cond %v, want 3 got %v", i+1, len(cond.Query.Params))
|
|
|
|
}
|
|
|
|
refID := cond.Query.Params[0]
|
|
|
|
refIDtoCondIdx[refID] = append(refIDtoCondIdx[refID], i)
|
|
|
|
}
|
|
|
|
|
|
|
|
newRefIDstoCondIdx := make(map[string][]int) // a map of the new refIds to their coresponding condition index
|
|
|
|
|
|
|
|
refIDs := make([]string, 0, len(refIDtoCondIdx)) // a unique sorted list of the original refIDs
|
|
|
|
for refID := range refIDtoCondIdx {
|
|
|
|
refIDs = append(refIDs, refID)
|
|
|
|
}
|
|
|
|
sort.Strings(refIDs)
|
|
|
|
|
|
|
|
newRefIDsToTimeRanges := make(map[string][2]string) // a map of new RefIDs to their time range string tuple representation
|
|
|
|
for _, refID := range refIDs {
|
|
|
|
condIdxes := refIDtoCondIdx[refID]
|
|
|
|
|
|
|
|
if len(condIdxes) == 1 {
|
|
|
|
// If the refID is used in only condition, keep the letter a new refID
|
|
|
|
newRefIDstoCondIdx[refID] = append(newRefIDstoCondIdx[refID], condIdxes[0])
|
|
|
|
newRefIDsToTimeRanges[refID] = [2]string{set.Conditions[condIdxes[0]].Query.Params[1], set.Conditions[condIdxes[0]].Query.Params[2]}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// track unique time ranges within the same refID
|
|
|
|
timeRangesToCondIdx := make(map[[2]string][]int) // a map of the time range tuple to the condition index
|
|
|
|
for _, idx := range condIdxes {
|
|
|
|
timeParamFrom := set.Conditions[idx].Query.Params[1]
|
|
|
|
timeParamTo := set.Conditions[idx].Query.Params[2]
|
|
|
|
key := [2]string{timeParamFrom, timeParamTo}
|
|
|
|
timeRangesToCondIdx[key] = append(timeRangesToCondIdx[key], idx)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(timeRangesToCondIdx) == 1 {
|
|
|
|
// if all shared time range, no need to create a new query with a new RefID
|
|
|
|
for i := range condIdxes {
|
|
|
|
newRefIDstoCondIdx[refID] = append(newRefIDstoCondIdx[refID], condIdxes[i])
|
|
|
|
newRefIDsToTimeRanges[refID] = [2]string{set.Conditions[condIdxes[i]].Query.Params[1], set.Conditions[condIdxes[i]].Query.Params[2]}
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// This referenced query/refID has different time ranges, so new queries are needed for each unique time range.
|
|
|
|
timeRanges := make([][2]string, 0, len(timeRangesToCondIdx)) // a sorted list of unique time ranges for the query
|
|
|
|
for tr := range timeRangesToCondIdx {
|
|
|
|
timeRanges = append(timeRanges, tr)
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Slice(timeRanges, func(i, j int) bool {
|
|
|
|
switch {
|
|
|
|
case timeRanges[i][0] < timeRanges[j][0]:
|
|
|
|
return true
|
|
|
|
case timeRanges[i][0] > timeRanges[j][0]:
|
|
|
|
return false
|
|
|
|
default:
|
|
|
|
return timeRanges[i][1] < timeRanges[j][1]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
for _, tr := range timeRanges {
|
|
|
|
idxes := timeRangesToCondIdx[tr]
|
|
|
|
for i := 0; i < len(idxes); i++ {
|
|
|
|
newLetter, err := getNewRefID(newRefIDstoCondIdx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
newRefIDstoCondIdx[newLetter] = append(newRefIDstoCondIdx[newLetter], idxes[i])
|
|
|
|
newRefIDsToTimeRanges[newLetter] = [2]string{set.Conditions[idxes[i]].Query.Params[1], set.Conditions[idxes[i]].Query.Params[2]}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
newRefIDs := make([]string, 0, len(newRefIDstoCondIdx)) // newRefIds is a sorted list of the unique refIds of new queries
|
|
|
|
for refID := range newRefIDstoCondIdx {
|
|
|
|
newRefIDs = append(newRefIDs, refID)
|
|
|
|
}
|
|
|
|
sort.Strings(newRefIDs)
|
|
|
|
|
|
|
|
newCond := &condition{}
|
|
|
|
condIdxToNewRefID := make(map[int]string) // a map of condition indices to the RefIDs of new queries
|
|
|
|
|
|
|
|
// build the new data source queries
|
|
|
|
for _, refID := range newRefIDs {
|
|
|
|
condIdxes := newRefIDstoCondIdx[refID]
|
|
|
|
for i, condIdx := range condIdxes {
|
|
|
|
condIdxToNewRefID[condIdx] = refID
|
|
|
|
if i > 0 {
|
|
|
|
// only create each unique query once
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var queryObj map[string]interface{} // copy the model
|
|
|
|
err := json.Unmarshal(set.Conditions[condIdx].Query.Model, &queryObj)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var queryType string
|
|
|
|
if v, ok := queryObj["queryType"]; ok {
|
|
|
|
if s, ok := v.(string); ok {
|
|
|
|
queryType = s
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// one could have an alert saved but datasource deleted, so can not require match.
|
2021-04-30 06:47:01 -05:00
|
|
|
dsUID := dsUIDMap.GetUID(orgID, set.Conditions[condIdx].Query.DatasourceID)
|
2021-04-29 12:24:37 -05:00
|
|
|
queryObj["refId"] = refID
|
|
|
|
|
|
|
|
encodedObj, err := json.Marshal(queryObj)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
rawFrom := newRefIDsToTimeRanges[refID][0]
|
|
|
|
rawTo := newRefIDsToTimeRanges[refID][1]
|
|
|
|
|
|
|
|
rTR, err := getRelativeDuration(rawFrom, rawTo)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
alertQuery := alertQuery{
|
|
|
|
RefID: refID,
|
|
|
|
Model: encodedObj,
|
|
|
|
RelativeTimeRange: *rTR,
|
2021-04-30 06:47:01 -05:00
|
|
|
DatasourceUID: dsUID,
|
2021-04-29 12:24:37 -05:00
|
|
|
QueryType: queryType,
|
|
|
|
}
|
|
|
|
newCond.Data = append(newCond.Data, alertQuery)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// build the new classic condition pointing our new equivalent queries
|
|
|
|
conditions := make([]classicConditionJSON, len(set.Conditions))
|
|
|
|
for i, cond := range set.Conditions {
|
|
|
|
newCond := classicConditionJSON{}
|
|
|
|
newCond.Evaluator = conditionEvalJSON{
|
|
|
|
Type: cond.Evaluator.Type,
|
|
|
|
Params: cond.Evaluator.Params,
|
|
|
|
}
|
|
|
|
newCond.Operator.Type = cond.Operator.Type
|
|
|
|
newCond.Query.Params = append(newCond.Query.Params, condIdxToNewRefID[i])
|
|
|
|
newCond.Reducer.Type = cond.Reducer.Type
|
|
|
|
|
|
|
|
conditions[i] = newCond
|
|
|
|
}
|
|
|
|
|
|
|
|
ccRefID, err := getNewRefID(newRefIDstoCondIdx) // get refID for the classic condition
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
newCond.Condition = ccRefID // set the alert condition to point to the classic condition
|
|
|
|
newCond.OrgID = orgID
|
|
|
|
|
|
|
|
exprModel := struct {
|
|
|
|
Type string `json:"type"`
|
|
|
|
RefID string `json:"refId"`
|
|
|
|
Conditions []classicConditionJSON `json:"conditions"`
|
|
|
|
}{
|
|
|
|
"classic_conditions",
|
|
|
|
ccRefID,
|
|
|
|
conditions,
|
|
|
|
}
|
|
|
|
|
|
|
|
exprModelJSON, err := json.Marshal(&exprModel)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
ccAlertQuery := alertQuery{
|
|
|
|
RefID: ccRefID,
|
|
|
|
Model: exprModelJSON,
|
2023-03-23 15:55:54 -05:00
|
|
|
DatasourceUID: expressionDatasourceUID,
|
2021-04-29 12:24:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
newCond.Data = append(newCond.Data, ccAlertQuery)
|
|
|
|
|
|
|
|
sort.Slice(newCond.Data, func(i, j int) bool {
|
|
|
|
return newCond.Data[i].RefID < newCond.Data[j].RefID
|
|
|
|
})
|
|
|
|
|
|
|
|
return newCond, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type condition struct {
|
|
|
|
// Condition is the RefID of the query or expression from
|
|
|
|
// the Data property to get the results for.
|
|
|
|
Condition string `json:"condition"`
|
|
|
|
OrgID int64 `json:"-"`
|
|
|
|
|
|
|
|
// Data is an array of data source queries and/or server side expressions.
|
|
|
|
Data []alertQuery `json:"data"`
|
|
|
|
}
|
|
|
|
|
|
|
|
const alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
|
|
|
|
|
|
// getNewRefID finds first capital letter in the alphabet not in use
|
|
|
|
// to use for a new RefID. It errors if it runs out of letters.
|
|
|
|
func getNewRefID(refIDs map[string][]int) (string, error) {
|
|
|
|
for _, r := range alpha {
|
|
|
|
sR := string(r)
|
|
|
|
if _, ok := refIDs[sR]; ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return sR, nil
|
|
|
|
}
|
2021-06-14 14:13:45 -05:00
|
|
|
for i := 0; i < 20; i++ {
|
|
|
|
sR := util.GenerateShortUID()
|
|
|
|
if _, ok := refIDs[sR]; ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return sR, nil
|
|
|
|
}
|
|
|
|
return "", fmt.Errorf("failed to generate unique RefID")
|
2021-04-29 12:24:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// getRelativeDuration turns the alerting durations for dashboard conditions
|
|
|
|
// into a relative time range.
|
|
|
|
func getRelativeDuration(rawFrom, rawTo string) (*relativeTimeRange, error) {
|
|
|
|
fromD, err := getFrom(rawFrom)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
toD, err := getTo(rawTo)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &relativeTimeRange{
|
|
|
|
From: duration(fromD),
|
|
|
|
To: duration(toD),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getFrom(from string) (time.Duration, error) {
|
|
|
|
fromRaw := strings.Replace(from, "now-", "", 1)
|
|
|
|
|
|
|
|
d, err := time.ParseDuration("-" + fromRaw)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return -d, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func getTo(to string) (time.Duration, error) {
|
|
|
|
if to == "now" {
|
|
|
|
return 0, nil
|
|
|
|
} else if strings.HasPrefix(to, "now-") {
|
|
|
|
withoutNow := strings.Replace(to, "now-", "", 1)
|
|
|
|
|
|
|
|
d, err := time.ParseDuration("-" + withoutNow)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return -d, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
d, err := time.ParseDuration(to)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return -d, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type classicConditionJSON struct {
|
|
|
|
Evaluator conditionEvalJSON `json:"evaluator"`
|
|
|
|
|
|
|
|
Operator struct {
|
|
|
|
Type string `json:"type"`
|
|
|
|
} `json:"operator"`
|
|
|
|
|
|
|
|
Query struct {
|
|
|
|
Params []string `json:"params"`
|
|
|
|
} `json:"query"`
|
|
|
|
|
|
|
|
Reducer struct {
|
|
|
|
// Params []interface{} `json:"params"` (Unused)
|
|
|
|
Type string `json:"type"`
|
|
|
|
} `json:"reducer"`
|
|
|
|
}
|