[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>
This commit is contained in:
co-authored by
kyle
parent
e499585271
commit
ee06970d72
@@ -44,6 +44,7 @@ type API struct {
|
||||
DataService *tsdb.Service
|
||||
Schedule schedule.ScheduleService
|
||||
Store store.Store
|
||||
RuleStore store.RuleStore
|
||||
AlertingStore store.AlertingStore
|
||||
DataProxy *datasourceproxy.DatasourceProxyService
|
||||
Alertmanager Alertmanager
|
||||
@@ -68,7 +69,7 @@ func (api *API) RegisterAPIEndpoints() {
|
||||
api.RegisterRulerApiEndpoints(NewForkedRuler(
|
||||
api.DatasourceCache,
|
||||
NewLotexRuler(proxy, logger),
|
||||
RulerApiMock{log: logger},
|
||||
RulerSrv{store: api.RuleStore, log: logger},
|
||||
))
|
||||
api.RegisterTestingApiEndpoints(TestingApiMock{log: logger})
|
||||
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ type RulerApiService interface {
|
||||
RouteGetNamespaceRulesConfig(*models.ReqContext) response.Response
|
||||
RouteGetRulegGroupConfig(*models.ReqContext) response.Response
|
||||
RouteGetRulesConfig(*models.ReqContext) response.Response
|
||||
RoutePostNameRulesConfig(*models.ReqContext, apimodels.RuleGroupConfig) response.Response
|
||||
RoutePostNameRulesConfig(*models.ReqContext, apimodels.PostableRuleGroupConfig) response.Response
|
||||
}
|
||||
|
||||
type RulerApiBase struct {
|
||||
@@ -37,7 +37,7 @@ func (api *API) RegisterRulerApiEndpoints(srv RulerApiService) {
|
||||
group.Get(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}"), routing.Wrap(srv.RouteGetNamespaceRulesConfig))
|
||||
group.Get(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}/{Groupname}"), routing.Wrap(srv.RouteGetRulegGroupConfig))
|
||||
group.Get(toMacaronPath("/ruler/{Recipient}/api/v1/rules"), routing.Wrap(srv.RouteGetRulesConfig))
|
||||
group.Post(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}"), binding.Bind(apimodels.RuleGroupConfig{}), routing.Wrap(srv.RoutePostNameRulesConfig))
|
||||
group.Post(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}"), binding.Bind(apimodels.PostableRuleGroupConfig{}), routing.Wrap(srv.RoutePostNameRulesConfig))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func (base RulerApiBase) RouteGetRulesConfig(c *models.ReqContext) response.Resp
|
||||
return response.Error(http.StatusNotImplemented, "", nil)
|
||||
}
|
||||
|
||||
func (base RulerApiBase) RoutePostNameRulesConfig(c *models.ReqContext, body apimodels.RuleGroupConfig) response.Response {
|
||||
func (base RulerApiBase) RoutePostNameRulesConfig(c *models.ReqContext, body apimodels.PostableRuleGroupConfig) response.Response {
|
||||
recipient := c.Params(":Recipient")
|
||||
base.log.Info("RoutePostNameRulesConfig: ", "Recipient", recipient)
|
||||
namespace := c.Params(":Namespace")
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func (r *ForkedRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Respo
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ForkedRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.RuleGroupConfig) response.Response {
|
||||
func (r *ForkedRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig) response.Response {
|
||||
backendType, err := backendType(ctx, r.DatasourceCache)
|
||||
if err != nil {
|
||||
return response.Error(400, err.Error(), nil)
|
||||
|
||||
@@ -133,7 +133,7 @@ func (r *LotexRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Respon
|
||||
)
|
||||
}
|
||||
|
||||
func (r *LotexRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.RuleGroupConfig) response.Response {
|
||||
func (r *LotexRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig) response.Response {
|
||||
legacyRulerPrefix, err := r.getPrefix(ctx)
|
||||
if err != nil {
|
||||
return response.Error(500, err.Error(), nil)
|
||||
|
||||
@@ -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}}
|
||||
Reference in New Issue
Block a user