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/ngalert/api/prometheus/api_prometheus.go

811 lines
26 KiB

package api
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/url"
"slices"
"sort"
"strconv"
"strings"
"github.com/prometheus/alertmanager/pkg/labels"
apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/infra/log"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"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/eval"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/state"
"github.com/grafana/grafana/pkg/util"
)
type RuleStoreReader interface {
GetUserVisibleNamespaces(context.Context, int64, identity.Requester) (map[string]*folder.Folder, error)
ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) (ngmodels.RulesGroup, error)
}
type RuleGroupAccessControlService interface {
HasAccessInFolder(ctx context.Context, user identity.Requester, folder ngmodels.Namespaced) (bool, error)
}
type StatusReader interface {
Status(key ngmodels.AlertRuleKey) (ngmodels.RuleStatus, bool)
}
type ProvenanceStore interface {
GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]ngmodels.Provenance, error)
}
type PrometheusSrv struct {
log log.Logger
manager state.AlertInstanceManager
status StatusReader
store RuleStoreReader
authz RuleGroupAccessControlService
provenanceStore ProvenanceStore
}
func NewPrometheusSrv(log log.Logger, manager state.AlertInstanceManager, status StatusReader, store RuleStoreReader, authz RuleGroupAccessControlService, provenanceStore ProvenanceStore) *PrometheusSrv {
return &PrometheusSrv{
log,
manager,
status,
store,
authz,
provenanceStore,
}
}
const queryIncludeInternalLabels = "includeInternalLabels"
func getBoolWithDefault(vals url.Values, field string, d bool) bool {
f := vals.Get(field)
if f == "" {
return d
}
v, _ := strconv.ParseBool(f)
return v
}
func getInt64WithDefault(vals url.Values, field string, d int64) int64 {
f := vals.Get(field)
if f == "" {
return d
}
v, err := strconv.ParseInt(f, 10, 64)
if err != nil {
return d
}
return v
}
func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) response.Response {
// As we are using req.Form directly, this triggers a call to ParseForm() if needed.
c.Query("")
resp := PrepareAlertStatuses(srv.manager, AlertStatusesOptions{
OrgID: c.GetOrgID(),
Query: c.Req.Form,
})
return response.JSON(resp.HTTPStatusCode(), resp)
}
type AlertStatusesOptions struct {
OrgID int64
Query url.Values
}
func PrepareAlertStatuses(manager state.AlertInstanceManager, opts AlertStatusesOptions) apimodels.AlertResponse {
alertResponse := apimodels.AlertResponse{
DiscoveryBase: apimodels.DiscoveryBase{
Status: "success",
},
Data: apimodels.AlertDiscovery{
Alerts: []*apimodels.Alert{},
},
}
var labelOptions []ngmodels.LabelOption
if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) {
labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels())
}
for _, alertState := range manager.GetAll(opts.OrgID) {
startsAt := alertState.StartsAt
valString := ""
if alertState.State == eval.Alerting || alertState.State == eval.Pending || alertState.State == eval.Recovering {
valString = FormatValues(alertState)
}
alertResponse.Data.Alerts = append(alertResponse.Data.Alerts, &apimodels.Alert{
Labels: apimodels.LabelsFromMap(alertState.GetLabels(labelOptions...)),
Annotations: apimodels.LabelsFromMap(alertState.Annotations),
// TODO: or should we make this two fields? Using one field lets the
// frontend use the same logic for parsing text on annotations and this.
State: state.FormatStateAndReason(alertState.State, alertState.StateReason),
ActiveAt: &startsAt,
Value: valString,
})
}
return alertResponse
}
func FormatValues(alertState *state.State) string {
var fv string
values := alertState.GetLastEvaluationValuesForCondition()
switch len(values) {
case 0:
fv = alertState.LastEvaluationString
case 1:
for _, v := range values {
fv = strconv.FormatFloat(v, 'e', -1, 64)
break
}
default:
vs := make([]string, 0, len(values))
for k, v := range values {
vs = append(vs, fmt.Sprintf("%s: %s", k, strconv.FormatFloat(v, 'e', -1, 64)))
}
// Ensure we have a consistent natural ordering after formatting e.g. A0, A1, A10, A11, A3, etc.
sort.Strings(vs)
fv = strings.Join(vs, ", ")
}
return fv
}
func getPanelIDFromQuery(v url.Values) (int64, error) {
if s := strings.TrimSpace(v.Get("panel_id")); s != "" {
return strconv.ParseInt(s, 10, 64)
}
return 0, nil
}
func getMatchersFromQuery(v url.Values) (labels.Matchers, error) {
var matchers labels.Matchers
for _, s := range v["matcher"] {
var m labels.Matcher
if err := json.Unmarshal([]byte(s), &m); err != nil {
return nil, err
}
if len(m.Name) == 0 {
return nil, errors.New("bad matcher: the name cannot be blank")
}
matchers = append(matchers, &m)
}
return matchers, nil
}
func getStatesFromQuery(v url.Values) (map[eval.State]struct{}, error) {
states := make(map[eval.State]struct{})
for _, s := range v["state"] {
s = strings.ToLower(s)
switch s {
case "normal", "inactive":
states[eval.Normal] = struct{}{}
case "alerting", "firing":
states[eval.Alerting] = struct{}{}
case "pending":
states[eval.Pending] = struct{}{}
case "nodata":
states[eval.NoData] = struct{}{}
case "error":
states[eval.Error] = struct{}{}
case "recovering":
states[eval.Recovering] = struct{}{}
default:
return states, fmt.Errorf("unknown state '%s'", s)
}
}
return states, nil
}
func getHealthFromQuery(v url.Values) (map[string]struct{}, error) {
health := make(map[string]struct{})
for _, s := range v["health"] {
s = strings.ToLower(s)
switch s {
case "ok", "error", "nodata", "unknown":
health[s] = struct{}{}
default:
return nil, fmt.Errorf("unknown health '%s'", s)
}
}
return health, nil
}
type RuleGroupStatusesOptions struct {
Ctx context.Context
OrgID int64
Query url.Values
AllowedNamespaces map[string]string
}
type ListAlertRulesStore interface {
ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) (ngmodels.RulesGroup, error)
}
func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) response.Response {
// As we are using req.Form directly, this triggers a call to ParseForm() if needed.
c.Query("")
ruleResponse := apimodels.RuleResponse{
DiscoveryBase: apimodels.DiscoveryBase{
Status: "success",
},
Data: apimodels.RuleDiscovery{
RuleGroups: []apimodels.RuleGroup{},
},
}
namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.GetOrgID(), c.SignedInUser)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = fmt.Sprintf("failed to get namespaces visible to the user: %s", err.Error())
ruleResponse.ErrorType = apiv1.ErrServer
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
}
allowedNamespaces := map[string]string{}
for namespaceUID, folder := range namespaceMap {
// only add namespaces that the user has access to rules in
hasAccess, err := srv.authz.HasAccessInFolder(c.Req.Context(), c.SignedInUser, ngmodels.Namespace(*folder.ToFolderReference()))
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = fmt.Sprintf("failed to get namespaces visible to the user: %s", err.Error())
ruleResponse.ErrorType = apiv1.ErrServer
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
}
if hasAccess {
allowedNamespaces[namespaceUID] = folder.Fullpath
}
}
provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.GetOrgID(), (&ngmodels.AlertRule{}).ResourceType())
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = fmt.Sprintf("failed to get provenances visible to the user: %s", err.Error())
ruleResponse.ErrorType = apiv1.ErrServer
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
}
ruleResponse = PrepareRuleGroupStatuses(
srv.log,
srv.store,
RuleGroupStatusesOptions{
Ctx: c.Req.Context(),
OrgID: c.OrgID,
Query: c.Req.Form,
AllowedNamespaces: allowedNamespaces,
},
RuleStatusMutatorGenerator(srv.status),
RuleAlertStateMutatorGenerator(srv.manager),
provenanceRecords,
)
return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse)
}
// mutator function used to attach status to the rule
type RuleStatusMutator func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule)
// mutator function used to attach alert states to the rule and returns the totals and filtered totals
type RuleAlertStateMutator func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (total map[string]int64, filteredTotal map[string]int64)
func RuleStatusMutatorGenerator(statusReader StatusReader) RuleStatusMutator {
return func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule) {
status, ok := statusReader.Status(source.GetKey())
// Grafana by design return "ok" health and default other fields for unscheduled rules.
// This differs from Prometheus.
if !ok {
status = ngmodels.RuleStatus{
Health: "ok",
}
}
toMutate.Health = status.Health
toMutate.LastError = errorOrEmpty(status.LastError)
toMutate.LastEvaluation = status.EvaluationTimestamp
toMutate.EvaluationTime = status.EvaluationDuration.Seconds()
}
}
func RuleAlertStateMutatorGenerator(manager state.AlertInstanceManager) RuleAlertStateMutator {
return func(source *ngmodels.AlertRule, toMutate *apimodels.AlertingRule, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (map[string]int64, map[string]int64) {
states := manager.GetStatesForRuleUID(source.OrgID, source.UID)
totals := make(map[string]int64)
totalsFiltered := make(map[string]int64)
for _, alertState := range states {
activeAt := alertState.StartsAt
valString := ""
if alertState.State == eval.Alerting || alertState.State == eval.Pending || alertState.State == eval.Recovering {
valString = FormatValues(alertState)
}
stateKey := strings.ToLower(alertState.State.String())
totals[stateKey] += 1
// Do not add error twice when execution error state is Error
if alertState.Error != nil && source.ExecErrState != ngmodels.ErrorErrState {
totals["error"] += 1
}
alert := apimodels.Alert{
Labels: apimodels.LabelsFromMap(alertState.GetLabels(labelOptions...)),
Annotations: apimodels.LabelsFromMap(alertState.Annotations),
// TODO: or should we make this two fields? Using one field lets the
// frontend use the same logic for parsing text on annotations and this.
State: state.FormatStateAndReason(alertState.State, alertState.StateReason),
ActiveAt: &activeAt,
Value: valString,
}
// Set the state of the rule based on the state of its alerts.
// Only update the rule state with 'pending' or 'recovering' if the current state is 'inactive'.
// This prevents overwriting a higher-severity 'firing' state in the case of a rule with multiple alerts.
switch alertState.State {
case eval.Normal:
case eval.Pending:
if toMutate.State == "inactive" {
toMutate.State = "pending"
}
case eval.Recovering:
if toMutate.State == "inactive" {
toMutate.State = "recovering"
}
case eval.Alerting:
if toMutate.ActiveAt == nil || toMutate.ActiveAt.After(activeAt) {
toMutate.ActiveAt = &activeAt
}
toMutate.State = "firing"
case eval.Error:
case eval.NoData:
}
if len(stateFilterSet) > 0 {
if _, ok := stateFilterSet[alertState.State]; !ok {
continue
}
}
if !matchersMatch(matchers, alertState.Labels) {
continue
}
totalsFiltered[stateKey] += 1
// Do not add error twice when execution error state is Error
if alertState.Error != nil && source.ExecErrState != ngmodels.ErrorErrState {
totalsFiltered["error"] += 1
}
toMutate.Alerts = append(toMutate.Alerts, alert)
}
return totals, totalsFiltered
}
}
func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts RuleGroupStatusesOptions, ruleStatusMutator RuleStatusMutator, alertStateMutator RuleAlertStateMutator, provenanceRecords map[string]ngmodels.Provenance) apimodels.RuleResponse {
ruleResponse := apimodels.RuleResponse{
DiscoveryBase: apimodels.DiscoveryBase{
Status: "success",
},
Data: apimodels.RuleDiscovery{
RuleGroups: []apimodels.RuleGroup{},
},
}
dashboardUID := opts.Query.Get("dashboard_uid")
panelID, err := getPanelIDFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = fmt.Sprintf("invalid panel_id: %s", err.Error())
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
}
if dashboardUID == "" && panelID != 0 {
ruleResponse.Status = "error"
ruleResponse.Error = "panel_id must be set with dashboard_uid"
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
}
limitRulesPerGroup := getInt64WithDefault(opts.Query, "limit_rules", -1)
limitAlertsPerRule := getInt64WithDefault(opts.Query, "limit_alerts", -1)
matchers, err := getMatchersFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
}
stateFilterSet, err := getStatesFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
}
healthFilterSet, err := getHealthFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
}
var labelOptions []ngmodels.LabelOption
if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) {
labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels())
}
if len(opts.AllowedNamespaces) == 0 {
log.Debug("User does not have access to any namespaces")
return ruleResponse
}
namespaceUIDs := make([]string, 0, len(opts.AllowedNamespaces))
folderUID := opts.Query.Get("folder_uid")
_, exists := opts.AllowedNamespaces[folderUID]
if folderUID != "" && exists {
namespaceUIDs = append(namespaceUIDs, folderUID)
} else {
for k := range opts.AllowedNamespaces {
namespaceUIDs = append(namespaceUIDs, k)
}
}
ruleGroups := opts.Query["rule_group"]
receiverName := opts.Query.Get("receiver_name")
alertRuleQuery := ngmodels.ListAlertRulesQuery{
OrgID: opts.OrgID,
NamespaceUIDs: namespaceUIDs,
DashboardUID: dashboardUID,
PanelID: panelID,
RuleGroups: ruleGroups,
ReceiverName: receiverName,
}
ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = fmt.Sprintf("failure getting rules: %s", err.Error())
ruleResponse.ErrorType = apiv1.ErrServer
return ruleResponse
}
ruleNames := opts.Query["rule_name"]
ruleNamesSet := make(map[string]struct{}, len(ruleNames))
for _, rn := range ruleNames {
ruleNamesSet[rn] = struct{}{}
}
maxGroups := getInt64WithDefault(opts.Query, "group_limit", -1)
nextToken := opts.Query.Get("group_next_token")
if nextToken != "" {
if _, err := base64.URLEncoding.DecodeString(nextToken); err != nil {
nextToken = ""
}
}
groupedRules := getGroupedRules(log, ruleList, ruleNamesSet, opts.AllowedNamespaces)
rulesTotals := make(map[string]int64, len(groupedRules))
var newToken string
foundToken := false
for _, rg := range groupedRules {
if nextToken != "" && !foundToken {
if !tokenGreaterThanOrEqual(getRuleGroupNextToken(rg.Folder, rg.GroupKey.RuleGroup), nextToken) {
continue
}
foundToken = true
}
if maxGroups > -1 && len(ruleResponse.Data.RuleGroups) == int(maxGroups) {
newToken = getRuleGroupNextToken(rg.Folder, rg.GroupKey.RuleGroup)
break
}
ruleGroup, totals := toRuleGroup(log, rg.GroupKey, rg.Folder, rg.Rules, provenanceRecords, limitAlertsPerRule, stateFilterSet, matchers, labelOptions, ruleStatusMutator, alertStateMutator)
ruleGroup.Totals = totals
for k, v := range totals {
rulesTotals[k] += v
}
if len(stateFilterSet) > 0 {
filterRulesByState(ruleGroup, stateFilterSet)
}
if len(healthFilterSet) > 0 {
filterRulesByHealth(ruleGroup, healthFilterSet)
}
if limitRulesPerGroup > -1 && int64(len(ruleGroup.Rules)) > limitRulesPerGroup {
ruleGroup.Rules = ruleGroup.Rules[0:limitRulesPerGroup]
}
if len(ruleGroup.Rules) > 0 {
ruleResponse.Data.RuleGroups = append(ruleResponse.Data.RuleGroups, *ruleGroup)
}
}
ruleResponse.Data.NextToken = newToken
// Only return Totals if there is no pagination
if maxGroups == -1 {
ruleResponse.Data.Totals = rulesTotals
}
return ruleResponse
}
func getRuleGroupNextToken(namespace, group string) string {
return base64.URLEncoding.EncodeToString([]byte(namespace + "/" + group))
}
// Returns true if tokenA >= tokenB
func tokenGreaterThanOrEqual(tokenA string, tokenB string) bool {
decodedTokenA, _ := base64.URLEncoding.DecodeString(tokenA)
decodedTokenB, _ := base64.URLEncoding.DecodeString(tokenB)
return string(decodedTokenA) >= string(decodedTokenB)
}
type ruleGroup struct {
Folder string
GroupKey ngmodels.AlertRuleGroupKey
Rules []*ngmodels.AlertRule
}
// Returns a slice of rule groups ordered by namespace and group name
func getGroupedRules(log log.Logger, ruleList ngmodels.RulesGroup, ruleNamesSet map[string]struct{}, namespaceMap map[string]string) []*ruleGroup {
// Group rules together by Namespace and Rule Group. Rules are also grouped by Org ID,
// but in this API all rules belong to the same organization. Also filter by rule name if
// it was provided as a query param.
groupedRules := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule)
for _, rule := range ruleList {
if len(ruleNamesSet) > 0 {
if _, exists := ruleNamesSet[rule.Title]; !exists {
continue
}
}
groupKey := rule.GetGroupKey()
ruleGroup := groupedRules[groupKey]
ruleGroup = append(ruleGroup, rule)
groupedRules[groupKey] = ruleGroup
}
ruleGroups := make([]*ruleGroup, 0, len(groupedRules))
for groupKey, groupRules := range groupedRules {
folder, ok := namespaceMap[groupKey.NamespaceUID]
if !ok {
log.Warn("Query returned rules that belong to folder the user does not have access to. All rules that belong to that namespace will not be added to the response", "folder_uid", groupKey.NamespaceUID)
continue
}
// Sort the rules in each rule group by index. We do this at the end instead of
// after each append to avoid having to sort each group multiple times.
ngmodels.AlertRulesBy(ngmodels.AlertRulesByIndex).Sort(groupRules)
ruleGroups = append(ruleGroups, &ruleGroup{
Folder: folder,
GroupKey: groupKey,
Rules: groupRules,
})
}
// Sort the groups first by namespace, then group name
slices.SortFunc(ruleGroups, func(a, b *ruleGroup) int {
nsCmp := strings.Compare(a.Folder, b.Folder)
if nsCmp != 0 {
return nsCmp
}
// If Namespaces are equal, check the group names
return strings.Compare(a.GroupKey.RuleGroup, b.GroupKey.RuleGroup)
})
return ruleGroups
}
func filterRulesByState(ruleGroup *apimodels.RuleGroup, withStatesFast map[eval.State]struct{}) {
// Filtering is weird but firing, pending, and normal filters also need to be
// applied to the rule. Others such as nodata and error should have no effect.
// This is to match the current behavior in the UI.
filteredRules := make([]apimodels.AlertingRule, 0, len(ruleGroup.Rules))
for _, rule := range ruleGroup.Rules {
var state *eval.State
switch rule.State {
case "normal", "inactive":
state = util.Pointer(eval.Normal)
case "alerting", "firing":
state = util.Pointer(eval.Alerting)
case "pending":
state = util.Pointer(eval.Pending)
case "recovering":
state = util.Pointer(eval.Recovering)
}
if state != nil {
if _, ok := withStatesFast[*state]; ok {
filteredRules = append(filteredRules, rule)
}
}
}
ruleGroup.Rules = filteredRules
}
func filterRulesByHealth(ruleGroup *apimodels.RuleGroup, withHealthFast map[string]struct{}) {
// Filtering is weird but error and nodata filters also need to be
// applied to the rule. Others such as firing, pending, and normal should have no effect.
// This is to match the current behavior in the UI.
filteredRules := make([]apimodels.AlertingRule, 0, len(ruleGroup.Rules))
for _, rule := range ruleGroup.Rules {
if _, ok := withHealthFast[rule.Health]; ok {
filteredRules = append(filteredRules, rule)
}
}
ruleGroup.Rules = filteredRules
}
// This is the same as matchers.Matches but avoids the need to create a LabelSet
func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool {
for _, m := range matchers {
if !m.Matches(labels[m.Name]) {
return false
}
}
return true
}
func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance, limitAlerts int64, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption, ruleStatusMutator RuleStatusMutator, ruleAlertStateMutator RuleAlertStateMutator) (*apimodels.RuleGroup, map[string]int64) {
newGroup := &apimodels.RuleGroup{
Name: groupKey.RuleGroup,
// file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana.
File: folderFullPath,
FolderUID: groupKey.NamespaceUID,
}
rulesTotals := make(map[string]int64, len(rules))
ngmodels.RulesGroup(rules).SortByGroupIndex()
for _, rule := range rules {
provenance := ngmodels.ProvenanceNone
if prov, exists := provenanceRecords[rule.ResourceID()]; exists {
provenance = prov
}
alertingRule := apimodels.AlertingRule{
State: "inactive",
Name: rule.Title,
Query: ruleToQuery(log, rule),
QueriedDatasourceUIDs: extractDatasourceUIDs(rule),
Duration: rule.For.Seconds(),
KeepFiringFor: rule.KeepFiringFor.Seconds(),
Annotations: apimodels.LabelsFromMap(rule.Annotations),
Rule: apimodels.Rule{
UID: rule.UID,
Name: rule.Title,
FolderUID: rule.NamespaceUID,
Labels: apimodels.LabelsFromMap(rule.GetLabels(labelOptions...)),
Type: rule.Type().String(),
IsPaused: rule.IsPaused,
Provenance: apimodels.Provenance(provenance),
},
}
// mutate rule to apply status fields
ruleStatusMutator(rule, &alertingRule)
if len(rule.NotificationSettings) > 0 {
alertingRule.NotificationSettings = (*apimodels.AlertRuleNotificationSettings)(&rule.NotificationSettings[0])
}
// mutate rule for alert states
totals, totalsFiltered := ruleAlertStateMutator(rule, &alertingRule, stateFilterSet, matchers, labelOptions)
if alertingRule.State != "" {
rulesTotals[alertingRule.State] += 1
}
if alertingRule.Health == "error" || alertingRule.Health == "nodata" {
rulesTotals[alertingRule.Health] += 1
}
alertsBy := apimodels.AlertsBy(apimodels.AlertsByImportance)
if limitAlerts > -1 && int64(len(alertingRule.Alerts)) > limitAlerts {
alertingRule.Alerts = alertsBy.TopK(alertingRule.Alerts, int(limitAlerts))
} else {
// If there is no effective limit, then just sort the alerts.
// For large numbers of alerts, this can be faster.
alertsBy.Sort(alertingRule.Alerts)
}
alertingRule.Totals = totals
alertingRule.TotalsFiltered = totalsFiltered
newGroup.Rules = append(newGroup.Rules, alertingRule)
newGroup.Interval = float64(rule.IntervalSeconds)
// TODO yuri. Change that when scheduler will process alerts in groups
newGroup.EvaluationTime = alertingRule.EvaluationTime
newGroup.LastEvaluation = alertingRule.LastEvaluation
}
return newGroup, rulesTotals
}
// extractDatasourceUIDs extracts datasource UIDs from a rule
func extractDatasourceUIDs(rule *ngmodels.AlertRule) []string {
queriedDatasourceUIDs := make([]string, 0, len(rule.Data))
for _, query := range rule.Data {
// Skip expression datasources (UID -100 or __expr__)
if expr.IsDataSource(query.DatasourceUID) {
continue
}
queriedDatasourceUIDs = append(queriedDatasourceUIDs, query.DatasourceUID)
}
return queriedDatasourceUIDs
}
// ruleToQuery attempts to extract the datasource queries from the alert query model.
// Returns the whole JSON model as a string if it fails to extract a minimum of 1 query.
func ruleToQuery(logger log.Logger, rule *ngmodels.AlertRule) string {
var queryErr error
queries := make([]string, 0, len(rule.Data))
for _, q := range rule.Data {
q, err := q.GetQuery()
if err != nil {
// If we can't find the query simply omit it, and try the rest.
// Even single query alerts would have 2 `AlertQuery`, one for the query and one for the condition.
if errors.Is(err, ngmodels.ErrNoQuery) {
continue
}
// For any other type of error, it is unexpected abort and return the whole JSON.
logger.Debug("Failed to parse a query", "error", err)
queryErr = err
break
}
queries = append(queries, q)
}
// If we were able to extract at least one query without failure use it.
if queryErr == nil && len(queries) > 0 {
return strings.Join(queries, " | ")
}
return encodedQueriesOrError(rule.Data)
}
// encodedQueriesOrError tries to encode rule query data into JSON if it fails returns the encoding error as a string.
func encodedQueriesOrError(rules []ngmodels.AlertQuery) string {
encodedQueries, err := json.Marshal(rules)
if err == nil {
return string(encodedQueries)
}
return err.Error()
}
func errorOrEmpty(err error) string {
if err != nil {
return err.Error()
}
return ""
}