mirror of https://github.com/grafana/grafana
[Alerting]: Grafana managed ruler API implementation (#32537)
* [Alerting]: Grafana managed ruler API impl * Apply suggestions from code review * fix lint * Add validation for ruleGroup name length * Fix MySQL migration Co-authored-by: kyle <kyle@grafana.com>pull/32672/head
parent
e499585271
commit
ee06970d72
@ -0,0 +1,219 @@ |
||||
package api |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store" |
||||
|
||||
apimodels "github.com/grafana/alerting-api/pkg/api" |
||||
"github.com/grafana/grafana/pkg/api/response" |
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
"github.com/prometheus/common/model" |
||||
) |
||||
|
||||
type RulerSrv struct { |
||||
store store.RuleStore |
||||
log log.Logger |
||||
} |
||||
|
||||
func (srv RulerSrv) RouteDeleteNamespaceRulesConfig(c *models.ReqContext) response.Response { |
||||
namespace := c.Params(":Namespace") |
||||
namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser) |
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) |
||||
} |
||||
if err := srv.store.DeleteNamespaceAlertRules(c.SignedInUser.OrgId, namespaceUID); err != nil { |
||||
return response.Error(http.StatusInternalServerError, "failed to delete namespace alert rules", err) |
||||
} |
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "namespace rules deleted"}) |
||||
} |
||||
|
||||
func (srv RulerSrv) RouteDeleteRuleGroupConfig(c *models.ReqContext) response.Response { |
||||
namespace := c.Params(":Namespace") |
||||
namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser) |
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) |
||||
} |
||||
ruleGroup := c.Params(":Groupname") |
||||
if err := srv.store.DeleteRuleGroupAlertRules(c.SignedInUser.OrgId, namespaceUID, ruleGroup); err != nil { |
||||
return response.Error(http.StatusInternalServerError, "failed to delete group alert rules", err) |
||||
} |
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"}) |
||||
} |
||||
|
||||
func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.Response { |
||||
namespace := c.Params(":Namespace") |
||||
namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser) |
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) |
||||
} |
||||
|
||||
q := ngmodels.ListNamespaceAlertRulesQuery{ |
||||
OrgID: c.SignedInUser.OrgId, |
||||
NamespaceUID: namespaceUID, |
||||
} |
||||
if err := srv.store.GetNamespaceAlertRules(&q); err != nil { |
||||
return response.Error(http.StatusInternalServerError, "failed to update rule group", err) |
||||
} |
||||
|
||||
result := apimodels.NamespaceConfigResponse{} |
||||
ruleGroupConfigs := make(map[string]apimodels.GettableRuleGroupConfig) |
||||
for _, r := range q.Result { |
||||
ruleGroupConfig, ok := ruleGroupConfigs[r.RuleGroup] |
||||
if !ok { |
||||
ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second) |
||||
ruleGroupConfigs[r.RuleGroup] = apimodels.GettableRuleGroupConfig{ |
||||
Name: r.RuleGroup, |
||||
Interval: ruleGroupInterval, |
||||
Rules: []apimodels.GettableExtendedRuleNode{ |
||||
toGettableExtendedRuleNode(*r), |
||||
}, |
||||
} |
||||
} else { |
||||
ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r)) |
||||
ruleGroupConfigs[r.RuleGroup] = ruleGroupConfig |
||||
} |
||||
} |
||||
|
||||
for _, ruleGroupConfig := range ruleGroupConfigs { |
||||
result[namespace] = append(result[namespace], ruleGroupConfig) |
||||
} |
||||
|
||||
return response.JSON(http.StatusAccepted, result) |
||||
} |
||||
|
||||
func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Response { |
||||
namespace := c.Params(":Namespace") |
||||
namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser) |
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) |
||||
} |
||||
|
||||
ruleGroup := c.Params(":Groupname") |
||||
q := ngmodels.ListRuleGroupAlertRulesQuery{ |
||||
OrgID: c.SignedInUser.OrgId, |
||||
NamespaceUID: namespaceUID, |
||||
RuleGroup: ruleGroup, |
||||
} |
||||
if err := srv.store.GetRuleGroupAlertRules(&q); err != nil { |
||||
return response.Error(http.StatusInternalServerError, "failed to get group alert rules", err) |
||||
} |
||||
|
||||
var ruleGroupInterval model.Duration |
||||
ruleNodes := make([]apimodels.GettableExtendedRuleNode, 0, len(q.Result)) |
||||
for _, r := range q.Result { |
||||
ruleGroupInterval = model.Duration(time.Duration(r.IntervalSeconds) * time.Second) |
||||
ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r)) |
||||
} |
||||
|
||||
result := apimodels.RuleGroupConfigResponse{ |
||||
GettableRuleGroupConfig: apimodels.GettableRuleGroupConfig{ |
||||
Name: ruleGroup, |
||||
Interval: ruleGroupInterval, |
||||
Rules: ruleNodes, |
||||
}, |
||||
} |
||||
return response.JSON(http.StatusAccepted, result) |
||||
} |
||||
|
||||
func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response { |
||||
q := ngmodels.ListAlertRulesQuery{ |
||||
OrgID: c.SignedInUser.OrgId, |
||||
} |
||||
if err := srv.store.GetOrgAlertRules(&q); err != nil { |
||||
return response.Error(http.StatusInternalServerError, "failed to get alert rules", err) |
||||
} |
||||
|
||||
configs := make(map[string]map[string]apimodels.GettableRuleGroupConfig) |
||||
for _, r := range q.Result { |
||||
namespace, err := srv.store.GetNamespaceByUID(r.NamespaceUID, c.SignedInUser.OrgId, c.SignedInUser) |
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", r.NamespaceUID), err) |
||||
} |
||||
_, ok := configs[namespace] |
||||
if !ok { |
||||
ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second) |
||||
configs[namespace] = make(map[string]apimodels.GettableRuleGroupConfig) |
||||
configs[namespace][r.RuleGroup] = apimodels.GettableRuleGroupConfig{ |
||||
Name: r.RuleGroup, |
||||
Interval: ruleGroupInterval, |
||||
Rules: []apimodels.GettableExtendedRuleNode{ |
||||
toGettableExtendedRuleNode(*r), |
||||
}, |
||||
} |
||||
} else { |
||||
ruleGroupConfig, ok := configs[namespace][r.RuleGroup] |
||||
if !ok { |
||||
ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second) |
||||
configs[namespace][r.RuleGroup] = apimodels.GettableRuleGroupConfig{ |
||||
Name: r.RuleGroup, |
||||
Interval: ruleGroupInterval, |
||||
Rules: []apimodels.GettableExtendedRuleNode{ |
||||
toGettableExtendedRuleNode(*r), |
||||
}, |
||||
} |
||||
} else { |
||||
ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r)) |
||||
configs[namespace][r.RuleGroup] = ruleGroupConfig |
||||
} |
||||
} |
||||
} |
||||
|
||||
result := apimodels.NamespaceConfigResponse{} |
||||
for namespace, m := range configs { |
||||
for _, ruleGroupConfig := range m { |
||||
result[namespace] = append(result[namespace], ruleGroupConfig) |
||||
} |
||||
} |
||||
return response.JSON(http.StatusAccepted, result) |
||||
} |
||||
|
||||
func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig) response.Response { |
||||
namespace := c.Params(":Namespace") |
||||
namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser) |
||||
if err != nil { |
||||
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) |
||||
} |
||||
|
||||
// TODO check permissions
|
||||
// TODO check quota
|
||||
// TODO validate UID uniqueness in the payload
|
||||
|
||||
ruleGroup := ruleGroupConfig.Name |
||||
|
||||
if err := srv.store.UpdateRuleGroup(store.UpdateRuleGroupCmd{ |
||||
OrgID: c.SignedInUser.OrgId, |
||||
NamespaceUID: namespaceUID, |
||||
RuleGroup: ruleGroup, |
||||
RuleGroupConfig: ruleGroupConfig, |
||||
}); err != nil { |
||||
return response.Error(http.StatusInternalServerError, "failed to update rule group", err) |
||||
} |
||||
|
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group updated successfully"}) |
||||
} |
||||
|
||||
func toGettableExtendedRuleNode(r ngmodels.AlertRule) apimodels.GettableExtendedRuleNode { |
||||
return apimodels.GettableExtendedRuleNode{ |
||||
GrafanaManagedAlert: &apimodels.GettableGrafanaRule{ |
||||
ID: r.ID, |
||||
OrgID: r.OrgID, |
||||
Title: r.Title, |
||||
Condition: r.Condition, |
||||
Data: r.Data, |
||||
Updated: r.Updated, |
||||
IntervalSeconds: r.IntervalSeconds, |
||||
Version: r.Version, |
||||
UID: r.UID, |
||||
NamespaceUID: r.NamespaceUID, |
||||
RuleGroup: r.RuleGroup, |
||||
NoDataState: apimodels.NoDataState(r.NoDataState), |
||||
ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState), |
||||
}, |
||||
} |
||||
} |
@ -1,295 +0,0 @@ |
||||
/*Package api contains mock API implementation of unified alerting |
||||
* |
||||
* Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
|
||||
* |
||||
* Need to remove unused imports. |
||||
*/ |
||||
package api |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/http" |
||||
"time" |
||||
|
||||
apimodels "github.com/grafana/alerting-api/pkg/api" |
||||
"github.com/grafana/grafana/pkg/api/response" |
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
) |
||||
|
||||
var prometheusAlert = []ngmodels.AlertQuery{ |
||||
{ |
||||
Model: json.RawMessage(`{ |
||||
"datasource": "gdev-prometheus", |
||||
"datasourceUid": "000000002", |
||||
"expr": "http_request_duration_microseconds_count", |
||||
"hide": false, |
||||
"interval": "", |
||||
"intervalMs": 1000, |
||||
"legendFormat": "", |
||||
"maxDataPoints": 100, |
||||
"refId": "query" |
||||
}`), |
||||
RefID: "query", |
||||
RelativeTimeRange: ngmodels.RelativeTimeRange{ |
||||
From: ngmodels.Duration(time.Duration(5) * time.Hour), |
||||
To: ngmodels.Duration(time.Duration(3) * time.Hour), |
||||
}, |
||||
}, |
||||
{ |
||||
Model: json.RawMessage(`{ |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "-100", |
||||
"expression": "query", |
||||
"hide": false, |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"reducer": "mean", |
||||
"refId": "reduced", |
||||
"type": "reduce" |
||||
}`), |
||||
RefID: "reduced", |
||||
RelativeTimeRange: ngmodels.RelativeTimeRange{ |
||||
From: ngmodels.Duration(time.Duration(5) * time.Hour), |
||||
To: ngmodels.Duration(time.Duration(3) * time.Hour), |
||||
}, |
||||
}, |
||||
{ |
||||
Model: json.RawMessage(`{ |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "-100", |
||||
"expression": "$reduced > 10", |
||||
"hide": false, |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"refId": "condition", |
||||
"type": "math" |
||||
}`), |
||||
RefID: "condition", |
||||
RelativeTimeRange: ngmodels.RelativeTimeRange{ |
||||
From: ngmodels.Duration(time.Duration(5) * time.Hour), |
||||
To: ngmodels.Duration(time.Duration(3) * time.Hour), |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
var testAlert = []ngmodels.AlertQuery{ |
||||
{ |
||||
Model: json.RawMessage(`{ |
||||
"alias": "just-testing", |
||||
"datasource": "000000004", |
||||
"datasourceUid": "000000004", |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"orgId": 0, |
||||
"refId": "A", |
||||
"scenarioId": "csv_metric_values", |
||||
"stringInput": "1,20,90,30,5,0" |
||||
}`), |
||||
RefID: "A", |
||||
RelativeTimeRange: ngmodels.RelativeTimeRange{ |
||||
From: ngmodels.Duration(time.Duration(5) * time.Hour), |
||||
To: ngmodels.Duration(time.Duration(3) * time.Hour), |
||||
}, |
||||
}, |
||||
{ |
||||
Model: json.RawMessage(`{ |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "__expr__", |
||||
"expression": "$A", |
||||
"intervalMs": 2000, |
||||
"maxDataPoints": 200, |
||||
"orgId": 0, |
||||
"reducer": "mean", |
||||
"refId": "B", |
||||
"type": "reduce" |
||||
}`), |
||||
RefID: "B", |
||||
RelativeTimeRange: ngmodels.RelativeTimeRange{ |
||||
From: ngmodels.Duration(time.Duration(5) * time.Hour), |
||||
To: ngmodels.Duration(time.Duration(3) * time.Hour), |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
type RulerApiMock struct { |
||||
log log.Logger |
||||
} |
||||
|
||||
func (mock RulerApiMock) RouteDeleteNamespaceRulesConfig(c *models.ReqContext) response.Response { |
||||
recipient := c.Params(":Recipient") |
||||
mock.log.Info("RouteDeleteNamespaceRulesConfig: ", "Recipient", recipient) |
||||
namespace := c.Params(":Namespace") |
||||
mock.log.Info("RouteDeleteNamespaceRulesConfig: ", "Namespace", namespace) |
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "namespace rules deleted"}) |
||||
} |
||||
|
||||
func (mock RulerApiMock) RouteDeleteRuleGroupConfig(c *models.ReqContext) response.Response { |
||||
recipient := c.Params(":Recipient") |
||||
mock.log.Info("RouteDeleteRuleGroupConfig: ", "Recipient", recipient) |
||||
namespace := c.Params(":Namespace") |
||||
mock.log.Info("RouteDeleteRuleGroupConfig: ", "Namespace", namespace) |
||||
groupname := c.Params(":Groupname") |
||||
mock.log.Info("RouteDeleteRuleGroupConfig: ", "Groupname", groupname) |
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"}) |
||||
} |
||||
|
||||
func (mock RulerApiMock) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.Response { |
||||
recipient := c.Params(":Recipient") |
||||
mock.log.Info("RouteGetNamespaceRulesConfig: ", "Recipient", recipient) |
||||
namespace := c.Params(":Namespace") |
||||
mock.log.Info("RouteGetNamespaceRulesConfig: ", "Namespace", namespace) |
||||
result := apimodels.NamespaceConfigResponse{ |
||||
namespace: []apimodels.RuleGroupConfig{ |
||||
{ |
||||
Name: "group1", |
||||
Interval: 60, |
||||
Rules: []apimodels.ExtendedRuleNode{ |
||||
{ |
||||
GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ |
||||
NoDataState: apimodels.NoData, |
||||
ExecutionErrorState: apimodels.AlertingErrState, |
||||
UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ |
||||
UID: "UID", |
||||
OrgID: 1, |
||||
Title: "rule 1-1", |
||||
Condition: "condition", |
||||
Data: prometheusAlert, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ |
||||
NoDataState: apimodels.NoData, |
||||
ExecutionErrorState: apimodels.AlertingErrState, |
||||
UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ |
||||
UID: "UID", |
||||
OrgID: 1, |
||||
Title: "rule 1-2", |
||||
Condition: "B", |
||||
Data: testAlert, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
return response.JSON(http.StatusAccepted, result) |
||||
} |
||||
|
||||
func (mock RulerApiMock) RouteGetRulegGroupConfig(c *models.ReqContext) response.Response { |
||||
recipient := c.Params(":Recipient") |
||||
mock.log.Info("RouteGetRulegGroupConfig: ", "Recipient", recipient) |
||||
namespace := c.Params(":Namespace") |
||||
mock.log.Info("RouteGetRulegGroupConfig: ", "Namespace", namespace) |
||||
groupname := c.Params(":Groupname") |
||||
mock.log.Info("RouteGetRulegGroupConfig: ", "Groupname", groupname) |
||||
result := apimodels.RuleGroupConfigResponse{ |
||||
RuleGroupConfig: apimodels.RuleGroupConfig{ |
||||
Name: groupname, |
||||
Interval: 60, |
||||
Rules: []apimodels.ExtendedRuleNode{ |
||||
{ |
||||
GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ |
||||
NoDataState: apimodels.NoData, |
||||
ExecutionErrorState: apimodels.AlertingErrState, |
||||
UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ |
||||
UID: "UID", |
||||
OrgID: 1, |
||||
Title: "something completely different", |
||||
Condition: "A", |
||||
Data: testAlert, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
return response.JSON(http.StatusAccepted, result) |
||||
} |
||||
|
||||
func (mock RulerApiMock) RouteGetRulesConfig(c *models.ReqContext) response.Response { |
||||
recipient := c.Params(":Recipient") |
||||
mock.log.Info("RouteGetRulesConfig: ", "Recipient", recipient) |
||||
result := apimodels.NamespaceConfigResponse{ |
||||
"namespace1": []apimodels.RuleGroupConfig{ |
||||
{ |
||||
Name: "group1", |
||||
Interval: 60, |
||||
Rules: []apimodels.ExtendedRuleNode{ |
||||
{ |
||||
GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ |
||||
NoDataState: apimodels.NoData, |
||||
ExecutionErrorState: apimodels.AlertingErrState, |
||||
UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ |
||||
UID: "UID", |
||||
OrgID: 1, |
||||
Title: "rule 1-1", |
||||
Condition: "A", |
||||
Data: testAlert, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ |
||||
NoDataState: apimodels.NoData, |
||||
ExecutionErrorState: apimodels.AlertingErrState, |
||||
UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ |
||||
UID: "UID", |
||||
OrgID: 1, |
||||
Title: "rule 1-2", |
||||
Condition: "A", |
||||
Data: testAlert, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
Name: "group2", |
||||
Interval: 60, |
||||
Rules: []apimodels.ExtendedRuleNode{ |
||||
{ |
||||
GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ |
||||
NoDataState: apimodels.NoData, |
||||
ExecutionErrorState: apimodels.AlertingErrState, |
||||
UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ |
||||
UID: "UID", |
||||
OrgID: 1, |
||||
Title: "rule 2-1", |
||||
Condition: "A", |
||||
Data: prometheusAlert, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
GrafanaManagedAlert: &apimodels.ExtendedUpsertAlertDefinitionCommand{ |
||||
NoDataState: apimodels.NoData, |
||||
ExecutionErrorState: apimodels.AlertingErrState, |
||||
UpdateAlertDefinitionCommand: ngmodels.UpdateAlertDefinitionCommand{ |
||||
UID: "UID", |
||||
OrgID: 1, |
||||
Title: "rule 2-2", |
||||
Condition: "A", |
||||
Data: testAlert, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
return response.JSON(http.StatusAccepted, result) |
||||
} |
||||
|
||||
func (mock RulerApiMock) RoutePostNameRulesConfig(c *models.ReqContext, body apimodels.RuleGroupConfig) response.Response { |
||||
recipient := c.Params(":Recipient") |
||||
mock.log.Info("RoutePostNameRulesConfig: ", "Recipient", recipient) |
||||
namespace := c.Params(":Namespace") |
||||
mock.log.Info("RoutePostNameRulesConfig: ", "Namespace", namespace) |
||||
mock.log.Info("RoutePostNameRulesConfig: ", "body", body) |
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "namespace rules created"}) |
||||
} |
@ -0,0 +1,120 @@ |
||||
{ |
||||
"name": "group101", |
||||
"interval": "10s", |
||||
"rules": [ |
||||
{ |
||||
"grafana_alert": { |
||||
"title": "prom query with SSE - 2", |
||||
"condition": "condition", |
||||
"data": [ |
||||
{ |
||||
"refId": "query", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "gdev-prometheus", |
||||
"datasourceUid": "000000002", |
||||
"expr": "http_request_duration_microseconds_count", |
||||
"hide": false, |
||||
"interval": "", |
||||
"intervalMs": 1000, |
||||
"legendFormat": "", |
||||
"maxDataPoints": 100, |
||||
"refId": "query" |
||||
} |
||||
}, |
||||
{ |
||||
"refId": "reduced", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "-100", |
||||
"expression": "query", |
||||
"hide": false, |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"reducer": "mean", |
||||
"refId": "reduced", |
||||
"type": "reduce" |
||||
} |
||||
}, |
||||
{ |
||||
"refId": "condition", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "-100", |
||||
"expression": "$reduced > 10", |
||||
"hide": false, |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"refId": "condition", |
||||
"type": "math" |
||||
} |
||||
} |
||||
], |
||||
"no_data_state": "NoData", |
||||
"exec_err_state": "Alerting" |
||||
} |
||||
}, |
||||
{ |
||||
"grafana_alert": { |
||||
"title": "reduced testdata query - 2", |
||||
"condition": "B", |
||||
"data": [ |
||||
{ |
||||
"refId": "query", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"alias": "just-testing", |
||||
"datasource": "000000004", |
||||
"datasourceUid": "000000004", |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"orgId": 0, |
||||
"refId": "A", |
||||
"scenarioId": "csv_metric_values", |
||||
"stringInput": "1,20,90,30,5,0" |
||||
} |
||||
}, |
||||
{ |
||||
"refId": "B", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "__expr__", |
||||
"expression": "$A", |
||||
"intervalMs": 2000, |
||||
"maxDataPoints": 200, |
||||
"orgId": 0, |
||||
"reducer": "mean", |
||||
"refId": "B", |
||||
"type": "reduce" |
||||
} |
||||
} |
||||
], |
||||
"no_data_state": "NoData", |
||||
"exec_err_state": "Alerting" |
||||
} |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,120 @@ |
||||
{ |
||||
"name": "group42", |
||||
"interval": "10s", |
||||
"rules": [ |
||||
{ |
||||
"grafana_alert": { |
||||
"title": "prom query with SSE", |
||||
"condition": "condition", |
||||
"data": [ |
||||
{ |
||||
"refId": "query", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "gdev-prometheus", |
||||
"datasourceUid": "000000002", |
||||
"expr": "http_request_duration_microseconds_count", |
||||
"hide": false, |
||||
"interval": "", |
||||
"intervalMs": 1000, |
||||
"legendFormat": "", |
||||
"maxDataPoints": 100, |
||||
"refId": "query" |
||||
} |
||||
}, |
||||
{ |
||||
"refId": "reduced", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "-100", |
||||
"expression": "query", |
||||
"hide": false, |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"reducer": "mean", |
||||
"refId": "reduced", |
||||
"type": "reduce" |
||||
} |
||||
}, |
||||
{ |
||||
"refId": "condition", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "-100", |
||||
"expression": "$reduced > 10", |
||||
"hide": false, |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"refId": "condition", |
||||
"type": "math" |
||||
} |
||||
} |
||||
], |
||||
"no_data_state": "NoData", |
||||
"exec_err_state": "Alerting" |
||||
} |
||||
}, |
||||
{ |
||||
"grafana_alert": { |
||||
"title": "reduced testdata query", |
||||
"condition": "B", |
||||
"data": [ |
||||
{ |
||||
"refId": "query", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"alias": "just-testing", |
||||
"datasource": "000000004", |
||||
"datasourceUid": "000000004", |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"orgId": 0, |
||||
"refId": "A", |
||||
"scenarioId": "csv_metric_values", |
||||
"stringInput": "1,20,90,30,5,0" |
||||
} |
||||
}, |
||||
{ |
||||
"refId": "B", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "__expr__", |
||||
"expression": "$A", |
||||
"intervalMs": 2000, |
||||
"maxDataPoints": 200, |
||||
"orgId": 0, |
||||
"reducer": "mean", |
||||
"refId": "B", |
||||
"type": "reduce" |
||||
} |
||||
} |
||||
], |
||||
"no_data_state": "NoData", |
||||
"exec_err_state": "Alerting" |
||||
} |
||||
} |
||||
] |
||||
} |
@ -1,30 +0,0 @@ |
||||
{ |
||||
"name": "group42", |
||||
"interval": "10s", |
||||
"rules": [ |
||||
{ |
||||
"expr": "", |
||||
"grafana_alert": { |
||||
"title": "something completely different", |
||||
"condition": "A", |
||||
"data": [ |
||||
{ |
||||
"refId": "A", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "__expr__", |
||||
"type": "math", |
||||
"expression": "2 + 2 > 1" |
||||
} |
||||
} |
||||
], |
||||
"no_data_state": "NoData", |
||||
"exec_err_state": "Alerting" |
||||
} |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,246 @@ |
||||
@grafanaRecipient = grafana |
||||
|
||||
// should point to an existing folder named alerting |
||||
@namespace1 = alerting |
||||
|
||||
// create group42 under unknown namespace - it should fail |
||||
POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/unknown |
||||
content-type: application/json |
||||
|
||||
< ./post-rulegroup-42.json |
||||
|
||||
### |
||||
// create group42 |
||||
POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} |
||||
content-type: application/json |
||||
|
||||
< ./post-rulegroup-42.json |
||||
|
||||
### |
||||
// create group101 |
||||
POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} |
||||
content-type: application/json |
||||
|
||||
< ./post-rulegroup-101.json |
||||
|
||||
### |
||||
// get group42 rules |
||||
GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group42 |
||||
|
||||
### |
||||
// get group101 rules |
||||
GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group101 |
||||
|
||||
### |
||||
// get namespace rules |
||||
GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} |
||||
|
||||
### |
||||
// get org rules |
||||
GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules |
||||
|
||||
### |
||||
// delete group42 rules |
||||
DELETE http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group42 |
||||
|
||||
### |
||||
// get namespace rules - only group101 should be listed |
||||
GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} |
||||
|
||||
### |
||||
// delete namespace rules |
||||
DELETE http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} |
||||
|
||||
### |
||||
// get namespace rules - no rules |
||||
GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} |
||||
|
||||
### |
||||
// recreate group42 |
||||
POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} |
||||
content-type: application/json |
||||
|
||||
{ |
||||
"name": "group42", |
||||
"interval": "10s", |
||||
"rules": [ |
||||
{ |
||||
"grafana_alert": { |
||||
"title": "prom query with SSE", |
||||
"condition": "condition", |
||||
"data": [ |
||||
{ |
||||
"refId": "query", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "gdev-prometheus", |
||||
"datasourceUid": "000000002", |
||||
"expr": "http_request_duration_microseconds_count", |
||||
"hide": false, |
||||
"interval": "", |
||||
"intervalMs": 1000, |
||||
"legendFormat": "", |
||||
"maxDataPoints": 100, |
||||
"refId": "query" |
||||
} |
||||
}, |
||||
{ |
||||
"refId": "reduced", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "-100", |
||||
"expression": "query", |
||||
"hide": false, |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"reducer": "mean", |
||||
"refId": "reduced", |
||||
"type": "reduce" |
||||
} |
||||
}, |
||||
{ |
||||
"refId": "condition", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "-100", |
||||
"expression": "$reduced > 10", |
||||
"hide": false, |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"refId": "condition", |
||||
"type": "math" |
||||
} |
||||
} |
||||
], |
||||
"no_data_state": "NoData", |
||||
"exec_err_state": "Alerting" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
|
||||
|
||||
### |
||||
// get group42 rules |
||||
# @name getRule |
||||
GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group42 |
||||
|
||||
### |
||||
@ruleUID = {{getRule.response.body.$.rules[0].grafana_alert.uid}} |
||||
|
||||
### |
||||
// update group42 interval and condition threshold |
||||
|
||||
POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} |
||||
content-type: application/json |
||||
|
||||
{ |
||||
"name": "group42", |
||||
"interval": "20s", |
||||
"rules": [ |
||||
{ |
||||
"grafana_alert": { |
||||
"title": "prom query with SSE", |
||||
"condition": "condition", |
||||
"uid": "{{ruleUID}}", |
||||
"data": [ |
||||
{ |
||||
"refId": "query", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "gdev-prometheus", |
||||
"datasourceUid": "000000002", |
||||
"expr": "http_request_duration_microseconds_count", |
||||
"hide": false, |
||||
"interval": "", |
||||
"intervalMs": 1000, |
||||
"legendFormat": "", |
||||
"maxDataPoints": 100, |
||||
"refId": "query" |
||||
} |
||||
}, |
||||
{ |
||||
"refId": "reduced", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "-100", |
||||
"expression": "query", |
||||
"hide": false, |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"reducer": "mean", |
||||
"refId": "reduced", |
||||
"type": "reduce" |
||||
} |
||||
}, |
||||
{ |
||||
"refId": "condition", |
||||
"queryType": "", |
||||
"relativeTimeRange": { |
||||
"from": 18000, |
||||
"to": 10800 |
||||
}, |
||||
"model": { |
||||
"datasource": "__expr__", |
||||
"datasourceUid": "-100", |
||||
"expression": "$reduced > 42", |
||||
"hide": false, |
||||
"intervalMs": 1000, |
||||
"maxDataPoints": 100, |
||||
"refId": "condition", |
||||
"type": "math" |
||||
} |
||||
} |
||||
], |
||||
"no_data_state": "NoData", |
||||
"exec_err_state": "Alerting" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
|
||||
### |
||||
// get group42 rules |
||||
GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group42 |
||||
|
||||
### |
||||
// update group42 - delete all rules |
||||
POST http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} |
||||
content-type: application/json |
||||
|
||||
{ |
||||
"name": "group42", |
||||
"interval": "20s", |
||||
"rules": [] |
||||
} |
||||
|
||||
### |
||||
// get group42 rules |
||||
GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group42 |
||||
|
||||
### |
||||
// get namespace rules |
||||
GET http://admin:admin@localhost:3000/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} |
@ -0,0 +1,155 @@ |
||||
package models |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"time" |
||||
) |
||||
|
||||
var ( |
||||
// ErrAlertRuleNotFound is an error for an unknown alert rule.
|
||||
ErrAlertRuleNotFound = fmt.Errorf("could not find alert rule") |
||||
// ErrAlertRuleFailedGenerateUniqueUID is an error for failure to generate alert rule UID
|
||||
ErrAlertRuleFailedGenerateUniqueUID = errors.New("failed to generate alert rule UID") |
||||
) |
||||
|
||||
type NoDataState string |
||||
|
||||
func (noDataState NoDataState) String() string { |
||||
return string(noDataState) |
||||
} |
||||
|
||||
const ( |
||||
Alerting NoDataState = "Alerting" |
||||
NoData NoDataState = "NoData" |
||||
KeepLastState NoDataState = "KeepLastState" |
||||
OK NoDataState = "OK" |
||||
) |
||||
|
||||
type ExecutionErrorState string |
||||
|
||||
func (executionErrorState ExecutionErrorState) String() string { |
||||
return string(executionErrorState) |
||||
} |
||||
|
||||
const ( |
||||
AlertingErrState ExecutionErrorState = "Alerting" |
||||
KeepLastStateErrState ExecutionErrorState = "KeepLastState" |
||||
) |
||||
|
||||
// AlertRule is the model for alert rules in unified alerting.
|
||||
type AlertRule struct { |
||||
ID int64 `xorm:"pk autoincr 'id'"` |
||||
OrgID int64 `xorm:"org_id"` |
||||
Title string |
||||
Condition string |
||||
Data []AlertQuery |
||||
Updated time.Time |
||||
IntervalSeconds int64 |
||||
Version int64 |
||||
UID string `xorm:"uid"` |
||||
NamespaceUID string `xorm:"namespace_uid"` |
||||
RuleGroup string |
||||
NoDataState NoDataState |
||||
ExecErrState ExecutionErrorState |
||||
} |
||||
|
||||
// AlertRuleKey is the alert definition identifier
|
||||
type AlertRuleKey struct { |
||||
OrgID int64 |
||||
UID string |
||||
} |
||||
|
||||
func (k AlertRuleKey) String() string { |
||||
return fmt.Sprintf("{orgID: %d, UID: %s}", k.OrgID, k.UID) |
||||
} |
||||
|
||||
// GetKey returns the alert definitions identifier
|
||||
func (alertRule *AlertRule) GetKey() AlertRuleKey { |
||||
return AlertRuleKey{OrgID: alertRule.OrgID, UID: alertRule.UID} |
||||
} |
||||
|
||||
// PreSave sets default values and loads the updated model for each alert query.
|
||||
func (alertRule *AlertRule) PreSave(timeNow func() time.Time) error { |
||||
for i, q := range alertRule.Data { |
||||
err := q.PreSave() |
||||
if err != nil { |
||||
return fmt.Errorf("invalid alert query %s: %w", q.RefID, err) |
||||
} |
||||
alertRule.Data[i] = q |
||||
} |
||||
alertRule.Updated = timeNow() |
||||
return nil |
||||
} |
||||
|
||||
// AlertRuleVersion is the model for alert rule versions in unified alerting.
|
||||
type AlertRuleVersion struct { |
||||
ID int64 `xorm:"pk autoincr 'id'"` |
||||
RuleOrgID int64 `xorm:"rule_org_id"` |
||||
RuleUID string `xorm:"rule_uid"` |
||||
RuleNamespaceUID string `xorm:"rule_namespace_uid"` |
||||
RuleGroup string |
||||
ParentVersion int64 |
||||
RestoredFrom int64 |
||||
Version int64 |
||||
|
||||
Created time.Time |
||||
Title string |
||||
Condition string |
||||
Data []AlertQuery |
||||
IntervalSeconds int64 |
||||
NoDataState NoDataState |
||||
ExecErrState ExecutionErrorState |
||||
} |
||||
|
||||
// GetAlertRuleByUIDQuery is the query for retrieving/deleting an alert rule by UID and organisation ID.
|
||||
type GetAlertRuleByUIDQuery struct { |
||||
UID string |
||||
OrgID int64 |
||||
|
||||
Result *AlertRule |
||||
} |
||||
|
||||
// ListAlertRulesQuery is the query for listing alert rules
|
||||
type ListAlertRulesQuery struct { |
||||
OrgID int64 |
||||
|
||||
Result []*AlertRule |
||||
} |
||||
|
||||
// ListNamespaceAlertRulesQuery is the query for listing namespace alert rules
|
||||
type ListNamespaceAlertRulesQuery struct { |
||||
OrgID int64 |
||||
// Namespace is the folder slug
|
||||
NamespaceUID string |
||||
|
||||
Result []*AlertRule |
||||
} |
||||
|
||||
// ListRuleGroupAlertRulesQuery is the query for listing rule group alert rules
|
||||
type ListRuleGroupAlertRulesQuery struct { |
||||
OrgID int64 |
||||
// Namespace is the folder slug
|
||||
NamespaceUID string |
||||
RuleGroup string |
||||
|
||||
Result []*AlertRule |
||||
} |
||||
|
||||
// Condition contains backend expressions and queries and the RefID
|
||||
// of the query or expression that will be evaluated.
|
||||
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"` |
||||
} |
||||
|
||||
// IsValid checks the condition's validity.
|
||||
func (c Condition) IsValid() bool { |
||||
// TODO search for refIDs in QueriesAndExpressions
|
||||
return len(c.Data) != 0 |
||||
} |
@ -0,0 +1,450 @@ |
||||
package store |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana/pkg/models" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/dashboards" |
||||
|
||||
apimodels "github.com/grafana/alerting-api/pkg/api" |
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
) |
||||
|
||||
// AlertRuleMaxTitleLength is the maximum length of the alert rule title
|
||||
const AlertRuleMaxTitleLength = 190 |
||||
|
||||
// AlertRuleMaxRuleGroupNameLength is the maximum length of the alert rule group name
|
||||
const AlertRuleMaxRuleGroupNameLength = 190 |
||||
|
||||
type UpdateRuleGroupCmd struct { |
||||
OrgID int64 |
||||
NamespaceUID string |
||||
RuleGroup string |
||||
RuleGroupConfig apimodels.PostableRuleGroupConfig |
||||
} |
||||
|
||||
type UpsertRule struct { |
||||
Existing *ngmodels.AlertRule |
||||
New ngmodels.AlertRule |
||||
} |
||||
|
||||
// Store is the interface for persisting alert rules and instances
|
||||
type RuleStore interface { |
||||
DeleteAlertRuleByUID(orgID int64, ruleUID string) error |
||||
DeleteNamespaceAlertRules(orgID int64, namespaceUID string) error |
||||
DeleteRuleGroupAlertRules(orgID int64, namespaceUID string, ruleGroup string) error |
||||
GetAlertRuleByUID(*ngmodels.GetAlertRuleByUIDQuery) error |
||||
GetAlertRules(query *ngmodels.ListAlertRulesQuery) error |
||||
GetOrgAlertRules(query *ngmodels.ListAlertRulesQuery) error |
||||
GetNamespaceAlertRules(query *ngmodels.ListNamespaceAlertRulesQuery) error |
||||
GetRuleGroupAlertRules(query *ngmodels.ListRuleGroupAlertRulesQuery) error |
||||
GetNamespaceUIDBySlug(string, int64, *models.SignedInUser) (string, error) |
||||
GetNamespaceByUID(string, int64, *models.SignedInUser) (string, error) |
||||
UpsertAlertRules([]UpsertRule) error |
||||
UpdateRuleGroup(UpdateRuleGroupCmd) error |
||||
GetAlertInstance(*ngmodels.GetAlertInstanceQuery) error |
||||
ListAlertInstances(cmd *ngmodels.ListAlertInstancesQuery) error |
||||
SaveAlertInstance(cmd *ngmodels.SaveAlertInstanceCommand) error |
||||
ValidateAlertRule(ngmodels.AlertRule, bool) error |
||||
} |
||||
|
||||
func getAlertRuleByUID(sess *sqlstore.DBSession, alertRuleUID string, orgID int64) (*ngmodels.AlertRule, error) { |
||||
// we consider optionally enabling some caching
|
||||
alertRule := ngmodels.AlertRule{OrgID: orgID, UID: alertRuleUID} |
||||
has, err := sess.Get(&alertRule) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if !has { |
||||
return nil, ngmodels.ErrAlertRuleNotFound |
||||
} |
||||
return &alertRule, nil |
||||
} |
||||
|
||||
// DeleteAlertRuleByUID is a handler for deleting an alert rule.
|
||||
// It returns ngmodels.ErrAlertRuleNotFound if no alert rule is found for the provided ID.
|
||||
func (st DBstore) DeleteAlertRuleByUID(orgID int64, ruleUID string) error { |
||||
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { |
||||
_, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? AND uid = ?", orgID, ruleUID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_uid = ?", orgID, ruleUID) |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = sess.Exec("DELETE FROM alert_instance WHERE def_org_id = ? AND def_uid = ?", orgID, ruleUID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// DeleteNamespaceAlertRules is a handler for deleting namespace alert rules.
|
||||
func (st DBstore) DeleteNamespaceAlertRules(orgID int64, namespaceUID string) error { |
||||
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { |
||||
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ?", orgID, namespaceUID); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, err := sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_namespace_uid = ?", orgID, namespaceUID); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, err := sess.Exec(`DELETE FROM alert_instance WHERE def_org_id = ? AND def_uid NOT IN ( |
||||
SELECT uid FROM alert_rule where org_id = ? |
||||
)`, orgID, orgID); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// DeleteRuleGroupAlertRules is a handler for deleting rule group alert rules.
|
||||
func (st DBstore) DeleteRuleGroupAlertRules(orgID int64, namespaceUID string, ruleGroup string) error { |
||||
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { |
||||
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?", orgID, namespaceUID, ruleGroup); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, err := sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_namespace_uid = ? and rule_group = ?", orgID, namespaceUID, ruleGroup); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, err := sess.Exec(`DELETE FROM alert_instance WHERE def_org_id = ? AND def_uid NOT IN ( |
||||
SELECT uid FROM alert_rule where org_id = ? |
||||
)`, orgID, orgID); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// GetAlertRuleByUID is a handler for retrieving an alert rule from that database by its UID and organisation ID.
|
||||
// It returns ngmodels.ErrAlertRuleNotFound if no alert rule is found for the provided ID.
|
||||
func (st DBstore) GetAlertRuleByUID(query *ngmodels.GetAlertRuleByUIDQuery) error { |
||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { |
||||
alertRule, err := getAlertRuleByUID(sess, query.UID, query.OrgID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
query.Result = alertRule |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// UpsertAlertRules is a handler for creating/updating alert rules.
|
||||
func (st DBstore) UpsertAlertRules(rules []UpsertRule) error { |
||||
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { |
||||
newRules := make([]ngmodels.AlertRule, 0, len(rules)) |
||||
ruleVersions := make([]ngmodels.AlertRuleVersion, 0, len(rules)) |
||||
for _, r := range rules { |
||||
if r.Existing == nil && r.New.UID != "" { |
||||
// check by UID
|
||||
existingAlertRule, err := getAlertRuleByUID(sess, r.New.UID, r.New.OrgID) |
||||
if err != nil { |
||||
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) { |
||||
return nil |
||||
} |
||||
return err |
||||
} |
||||
r.Existing = existingAlertRule |
||||
} |
||||
|
||||
var parentVersion int64 |
||||
switch r.Existing { |
||||
case nil: // new rule
|
||||
uid, err := generateNewAlertRuleUID(sess, r.New.OrgID) |
||||
if err != nil { |
||||
return fmt.Errorf("failed to generate UID for alert rule %q: %w", r.New.Title, err) |
||||
} |
||||
r.New.UID = uid |
||||
|
||||
if r.New.IntervalSeconds == 0 { |
||||
r.New.IntervalSeconds = st.DefaultIntervalSeconds |
||||
} |
||||
|
||||
r.New.Version = 1 |
||||
|
||||
if err := st.ValidateAlertRule(r.New, true); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := (&r.New).PreSave(TimeNow); err != nil { |
||||
return err |
||||
} |
||||
|
||||
newRules = append(newRules, r.New) |
||||
default: |
||||
// explicitly set the existing properties if missing
|
||||
// do not rely on xorm
|
||||
if r.New.Title == "" { |
||||
r.New.Title = r.Existing.Title |
||||
} |
||||
|
||||
if r.New.Condition == "" { |
||||
r.New.Condition = r.Existing.Condition |
||||
} |
||||
|
||||
if len(r.New.Data) == 0 { |
||||
r.New.Data = r.Existing.Data |
||||
} |
||||
|
||||
if r.New.IntervalSeconds == 0 { |
||||
r.New.IntervalSeconds = r.Existing.IntervalSeconds |
||||
} |
||||
|
||||
r.New.ID = r.Existing.ID |
||||
r.New.OrgID = r.Existing.OrgID |
||||
r.New.NamespaceUID = r.Existing.NamespaceUID |
||||
r.New.RuleGroup = r.Existing.RuleGroup |
||||
r.New.Version = r.Existing.Version + 1 |
||||
|
||||
if err := st.ValidateAlertRule(r.New, true); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := (&r.New).PreSave(TimeNow); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// no way to update multiple rules at once
|
||||
if _, err := sess.ID(r.Existing.ID).Update(r.New); err != nil { |
||||
return fmt.Errorf("failed to update rule %s: %w", r.New.Title, err) |
||||
} |
||||
|
||||
parentVersion = r.Existing.Version |
||||
} |
||||
|
||||
ruleVersions = append(ruleVersions, ngmodels.AlertRuleVersion{ |
||||
RuleOrgID: r.New.OrgID, |
||||
RuleUID: r.New.UID, |
||||
RuleNamespaceUID: r.New.NamespaceUID, |
||||
RuleGroup: r.New.RuleGroup, |
||||
ParentVersion: parentVersion, |
||||
Version: r.New.Version, |
||||
Created: r.New.Updated, |
||||
Condition: r.New.Condition, |
||||
Title: r.New.Title, |
||||
Data: r.New.Data, |
||||
IntervalSeconds: r.New.IntervalSeconds, |
||||
NoDataState: r.New.NoDataState, |
||||
ExecErrState: r.New.ExecErrState, |
||||
}) |
||||
} |
||||
|
||||
if len(newRules) > 0 { |
||||
if _, err := sess.Insert(&newRules); err != nil { |
||||
return fmt.Errorf("failed to create new rules: %w", err) |
||||
} |
||||
} |
||||
|
||||
if len(ruleVersions) > 0 { |
||||
if _, err := sess.Insert(&ruleVersions); err != nil { |
||||
return fmt.Errorf("failed to create new rule versions: %w", err) |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// GetOrgAlertRules is a handler for retrieving alert rules of specific organisation.
|
||||
func (st DBstore) GetOrgAlertRules(query *ngmodels.ListAlertRulesQuery) error { |
||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { |
||||
alertRules := make([]*ngmodels.AlertRule, 0) |
||||
q := "SELECT * FROM alert_rule WHERE org_id = ?" |
||||
if err := sess.SQL(q, query.OrgID).Find(&alertRules); err != nil { |
||||
return err |
||||
} |
||||
|
||||
query.Result = alertRules |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// GetNamespaceAlertRules is a handler for retrieving namespace alert rules of specific organisation.
|
||||
func (st DBstore) GetNamespaceAlertRules(query *ngmodels.ListNamespaceAlertRulesQuery) error { |
||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { |
||||
alertRules := make([]*ngmodels.AlertRule, 0) |
||||
// TODO rewrite using group by namespace_uid, rule_group
|
||||
q := "SELECT * FROM alert_rule WHERE org_id = ? and namespace_uid = ?" |
||||
if err := sess.SQL(q, query.OrgID, query.NamespaceUID).Find(&alertRules); err != nil { |
||||
return err |
||||
} |
||||
|
||||
query.Result = alertRules |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// GetRuleGroupAlertRules is a handler for retrieving rule group alert rules of specific organisation.
|
||||
func (st DBstore) GetRuleGroupAlertRules(query *ngmodels.ListRuleGroupAlertRulesQuery) error { |
||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { |
||||
alertRules := make([]*ngmodels.AlertRule, 0) |
||||
q := "SELECT * FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?" |
||||
if err := sess.SQL(q, query.OrgID, query.NamespaceUID, query.RuleGroup).Find(&alertRules); err != nil { |
||||
return err |
||||
} |
||||
|
||||
query.Result = alertRules |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// GetNamespaceUIDBySlug is a handler for retrieving namespace UID by its name.
|
||||
func (st DBstore) GetNamespaceUIDBySlug(namespace string, orgID int64, user *models.SignedInUser) (string, error) { |
||||
s := dashboards.NewFolderService(orgID, user, st.SQLStore) |
||||
folder, err := s.GetFolderBySlug(namespace) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return folder.Uid, nil |
||||
} |
||||
|
||||
// GetNamespaceByUID is a handler for retrieving namespace by its UID.
|
||||
func (st DBstore) GetNamespaceByUID(UID string, orgID int64, user *models.SignedInUser) (string, error) { |
||||
s := dashboards.NewFolderService(orgID, user, st.SQLStore) |
||||
folder, err := s.GetFolderByUID(UID) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return folder.Title, nil |
||||
} |
||||
|
||||
// GetAlertRules returns alert rule identifier, interval, version and pause state
|
||||
// that are useful for it's scheduling.
|
||||
func (st DBstore) GetAlertRules(query *ngmodels.ListAlertRulesQuery) error { |
||||
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { |
||||
alerts := make([]*ngmodels.AlertRule, 0) |
||||
q := "SELECT uid, org_id, interval_seconds, version, paused FROM alert_rule" |
||||
if err := sess.SQL(q).Find(&alerts); err != nil { |
||||
return err |
||||
} |
||||
|
||||
query.Result = alerts |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
func generateNewAlertRuleUID(sess *sqlstore.DBSession, orgID int64) (string, error) { |
||||
for i := 0; i < 3; i++ { |
||||
uid := util.GenerateShortUID() |
||||
|
||||
exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&ngmodels.AlertRule{}) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
if !exists { |
||||
return uid, nil |
||||
} |
||||
} |
||||
|
||||
return "", ngmodels.ErrAlertRuleFailedGenerateUniqueUID |
||||
} |
||||
|
||||
// ValidateAlertRule validates the alert rule interval and organisation.
|
||||
// If requireData is true checks that it contains at least one alert query
|
||||
func (st DBstore) ValidateAlertRule(alertRule ngmodels.AlertRule, requireData bool) error { |
||||
if !requireData && len(alertRule.Data) == 0 { |
||||
return fmt.Errorf("no queries or expressions are found") |
||||
} |
||||
|
||||
if alertRule.Title == "" { |
||||
return ErrEmptyTitleError |
||||
} |
||||
|
||||
if alertRule.IntervalSeconds%int64(st.BaseInterval.Seconds()) != 0 { |
||||
return fmt.Errorf("invalid interval: %v: interval should be divided exactly by scheduler interval: %v", time.Duration(alertRule.IntervalSeconds)*time.Second, st.BaseInterval) |
||||
} |
||||
|
||||
// enfore max name length in SQLite
|
||||
if len(alertRule.Title) > AlertRuleMaxTitleLength { |
||||
return fmt.Errorf("name length should not be greater than %d", AlertRuleMaxTitleLength) |
||||
} |
||||
|
||||
// enfore max name length in SQLite
|
||||
if len(alertRule.RuleGroup) > AlertRuleMaxRuleGroupNameLength { |
||||
return fmt.Errorf("name length should not be greater than %d", AlertRuleMaxRuleGroupNameLength) |
||||
} |
||||
|
||||
if alertRule.OrgID == 0 { |
||||
return fmt.Errorf("no organisation is found") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// UpdateRuleGroup creates new rules and updates and/or deletes existing rules
|
||||
func (st DBstore) UpdateRuleGroup(cmd UpdateRuleGroupCmd) error { |
||||
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { |
||||
q := &ngmodels.ListRuleGroupAlertRulesQuery{ |
||||
OrgID: cmd.OrgID, |
||||
NamespaceUID: cmd.NamespaceUID, |
||||
RuleGroup: cmd.RuleGroup, |
||||
} |
||||
if err := st.GetRuleGroupAlertRules(q); err != nil { |
||||
return err |
||||
} |
||||
existingGroupRules := q.Result |
||||
|
||||
existingGroupRulesUIDs := make(map[string]ngmodels.AlertRule, len(existingGroupRules)) |
||||
for _, r := range existingGroupRules { |
||||
existingGroupRulesUIDs[r.UID] = *r |
||||
} |
||||
|
||||
upsertRules := make([]UpsertRule, 0) |
||||
for _, r := range cmd.RuleGroupConfig.Rules { |
||||
if r.GrafanaManagedAlert == nil { |
||||
continue |
||||
} |
||||
|
||||
upsertRule := UpsertRule{ |
||||
New: ngmodels.AlertRule{ |
||||
OrgID: cmd.OrgID, |
||||
Title: r.GrafanaManagedAlert.Title, |
||||
Condition: r.GrafanaManagedAlert.Condition, |
||||
Data: r.GrafanaManagedAlert.Data, |
||||
UID: r.GrafanaManagedAlert.UID, |
||||
IntervalSeconds: int64(time.Duration(cmd.RuleGroupConfig.Interval).Seconds()), |
||||
NamespaceUID: cmd.NamespaceUID, |
||||
RuleGroup: cmd.RuleGroup, |
||||
NoDataState: ngmodels.NoDataState(r.GrafanaManagedAlert.NoDataState), |
||||
ExecErrState: ngmodels.ExecutionErrorState(r.GrafanaManagedAlert.ExecErrState), |
||||
}, |
||||
} |
||||
|
||||
if existingGroupRule, ok := existingGroupRulesUIDs[r.GrafanaManagedAlert.UID]; ok { |
||||
upsertRule.Existing = &existingGroupRule |
||||
// remove the rule from existingGroupRulesUIDs
|
||||
delete(existingGroupRulesUIDs, r.GrafanaManagedAlert.UID) |
||||
} |
||||
upsertRules = append(upsertRules, upsertRule) |
||||
} |
||||
|
||||
if err := st.UpsertAlertRules(upsertRules); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// delete the remaining rules
|
||||
for ruleUID := range existingGroupRulesUIDs { |
||||
if err := st.DeleteAlertRuleByUID(cmd.OrgID, ruleUID); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
}) |
||||
} |
Loading…
Reference in new issue