[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:
Sofia Papagiannaki
2021-04-01 11:11:45 +03:00
committed by GitHub
co-authored by kyle
parent e499585271
commit ee06970d72
19 changed files with 1433 additions and 356 deletions
+2 -1
View File
@@ -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})
+219
View File
@@ -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),
},
}
}
+3 -3
View File
@@ -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")
-295
View File
@@ -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"})
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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}}