Files
grafana/pkg/services/ngalert/api/api_ruler.go
T
Owen Diehl e37a780e14 Inhouse alerting api (#33129)
* init

* autogens AM route

* POST dashboards/db spec

* POST alert-notifications spec

* fix description

* re inits vendor, updates grafana to master

* go mod updates

* alerting routes

* renames to receivers

* prometheus endpoints

* align config endpoint with cortex, include templates

* Change grafana receiver type

* Update receivers.go

* rename struct to stop swagger thrashing

* add rules API

* index html

* standalone swagger ui html page

* Update README.md

* Expose GrafanaManagedAlert properties

* Some fixes

- /api/v1/rules/{Namespace} should return a map
- update ExtendedUpsertAlertDefinitionCommand properties

* am alerts routes

* rename prom swagger section for clarity, remove example endpoints

* Add missing json and yaml tags

* folder perms

* make folders POST again

* fix grafana receiver type

* rename fodler->namespace for perms

* make ruler json again

* PR fixes

* silences

* fix Ok -> Ack

* Add id to POST /api/v1/silences (#9)

Signed-off-by: Ganesh Vernekar <cs15btech11018@iith.ac.in>

* Add POST /api/v1/alerts (#10)

Signed-off-by: Ganesh Vernekar <cs15btech11018@iith.ac.in>

* fix silences

* Add testing endpoints

* removes grpc replace directives

* [wip] starts validation

* pkg cleanup

* go mod tidy

* ignores vendor dir

* Change response type for Cortex/Loki alerts

* receiver unmarshaling tests

* ability to split routes between AM & Grafana

* api marshaling & validation

* begins work on routing lib

* [hack] ignores embedded field in generation

* path specific datasource for alerting

* align endpoint names with cloud

* single route per Alerting config

* removes unused routing pkg

* regens spec

* adds datasource param to ruler/prom route paths

* Modifications for supporting migration

* Apply suggestions from code review

* hack for cleaning circular refs in swagger definition

* generates files

* minor fixes for prom endpoints

* decorate prom apis with required: true where applicable

* Revert "generates files"

This reverts commit ef7e975584.

* removes server autogen

* Update imported structs from ngalert

* Fix listing rules response

* Update github.com/prometheus/common dependency

* Update get silence response

* Update get silences response

* adds ruler validation & backend switching

* Fix GET /alertmanager/{DatasourceId}/config/api/v1/alerts response

* Distinct gettable and postable grafana receivers

* Remove permissions routes

* Latest JSON specs

* Fix testing routes

* inline yaml annotation on apirulenode

* yaml test & yamlv3 + comments

* Fix yaml annotations for embedded type

* Rename DatasourceId path parameter

* Implement Backend.String()

* backend zero value is a real backend

* exports DiscoveryBase

* Fix GO initialisms

* Silences: Use PostableSilence as the base struct for creating silences

* Use type alias instead of struct embedding

* More fixes to alertmanager silencing routes

* post and spec JSONs

* Split rule config to postable/gettable

* Fix empty POST /silences payload

Recreating the generated JSON specs fixes the issue
without further modifications

* better yaml unmarshaling for nested yaml docs in cortex-am configs

* regens spec

* re-adds config.receivers

* omitempty to align with prometheus API behavior

* Prefix routes with /api

* Update Alertmanager models

* Make adjustments to follow the Alertmanager API

* ruler: add for and annotations to grafana alert (#45)

* Modify testing API routes

* Fix grafana rule for field type

* Move PostableUserConfig validation to this library

* Fix PostableUserConfig YAML encoding/decoding

* Use common fields for grafana and lotex rules

* Add namespace id in GettableGrafanaRule

* Apply suggestions from code review

* fixup

* more changes

* Apply suggestions from code review

* aligns structure pre merge

* fix new imports & tests

* updates tooling readme

* goimports

* lint

* more linting!!

* revive lint

Co-authored-by: Sofia Papagiannaki <papagian@gmail.com>
Co-authored-by: Domas <domasx2@gmail.com>
Co-authored-by: Sofia Papagiannaki <papagian@users.noreply.github.com>
Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com>
Co-authored-by: gotjosh <josue@grafana.com>
Co-authored-by: David Parrott <stomp.box.yo@gmail.com>
Co-authored-by: Kyle Brandt <kyle@grafana.com>
2021-04-19 14:26:04 -04:00

269 lines
9.4 KiB
Go

package api
import (
"errors"
"net/http"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/store"
coreapi "github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
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 {
namespaceTitle := c.Params(":Namespace")
namespace, err := srv.store.GetNamespaceByTitle(namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true)
if err != nil {
return toNamespaceErrorResponse(err)
}
if err := srv.store.DeleteNamespaceAlertRules(c.SignedInUser.OrgId, namespace.Uid); 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 {
namespaceTitle := c.Params(":Namespace")
namespace, err := srv.store.GetNamespaceByTitle(namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true)
if err != nil {
return toNamespaceErrorResponse(err)
}
ruleGroup := c.Params(":Groupname")
if err := srv.store.DeleteRuleGroupAlertRules(c.SignedInUser.OrgId, namespace.Uid, ruleGroup); err != nil {
if errors.Is(err, ngmodels.ErrRuleGroupNamespaceNotFound) {
return response.Error(http.StatusNotFound, "failed to delete rule group", err)
}
return response.Error(http.StatusInternalServerError, "failed to delete rule group", err)
}
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"})
}
func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.Response {
namespaceTitle := c.Params(":Namespace")
namespace, err := srv.store.GetNamespaceByTitle(namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, false)
if err != nil {
return toNamespaceErrorResponse(err)
}
q := ngmodels.ListNamespaceAlertRulesQuery{
OrgID: c.SignedInUser.OrgId,
NamespaceUID: namespace.Uid,
}
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, namespace.Id),
},
}
} else {
ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, namespace.Id))
ruleGroupConfigs[r.RuleGroup] = ruleGroupConfig
}
}
for _, ruleGroupConfig := range ruleGroupConfigs {
result[namespaceTitle] = append(result[namespaceTitle], ruleGroupConfig)
}
return response.JSON(http.StatusAccepted, result)
}
func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Response {
namespaceTitle := c.Params(":Namespace")
namespace, err := srv.store.GetNamespaceByTitle(namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, false)
if err != nil {
return toNamespaceErrorResponse(err)
}
ruleGroup := c.Params(":Groupname")
q := ngmodels.ListRuleGroupAlertRulesQuery{
OrgID: c.SignedInUser.OrgId,
NamespaceUID: namespace.Uid,
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, namespace.Id))
}
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 {
folder, err := srv.store.GetNamespaceByUID(r.NamespaceUID, c.SignedInUser.OrgId, c.SignedInUser)
if err != nil {
return toNamespaceErrorResponse(err)
}
namespace := folder.Title
_, 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, folder.Id),
},
}
} 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, folder.Id),
},
}
} else {
ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, folder.Id))
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 {
namespaceTitle := c.Params(":Namespace")
namespace, err := srv.store.GetNamespaceByTitle(namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true)
if err != nil {
return toNamespaceErrorResponse(err)
}
// TODO check permissions
// TODO check quota
// TODO validate UID uniqueness in the payload
//TODO: Should this belong in alerting-api?
if ruleGroupConfig.Name == "" {
return response.Error(http.StatusBadRequest, "rule group name is not valid", nil)
}
if err := srv.store.UpdateRuleGroup(store.UpdateRuleGroupCmd{
OrgID: c.SignedInUser.OrgId,
NamespaceUID: namespace.Uid,
RuleGroupConfig: ruleGroupConfig,
}); err != nil {
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) {
return response.Error(http.StatusNotFound, "failed to update rule group", err)
}
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, namespaceID int64) apimodels.GettableExtendedRuleNode {
gettableExtendedRuleNode := 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,
NamespaceID: namespaceID,
RuleGroup: r.RuleGroup,
NoDataState: apimodels.NoDataState(r.NoDataState),
ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState),
},
}
gettableExtendedRuleNode.ApiRuleNode = &apimodels.ApiRuleNode{
For: model.Duration(r.For),
Annotations: r.Annotations,
Labels: r.Labels,
}
return gettableExtendedRuleNode
}
func toPostableExtendedRuleNode(r ngmodels.AlertRule) apimodels.PostableExtendedRuleNode {
postableExtendedRuleNode := apimodels.PostableExtendedRuleNode{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
OrgID: r.OrgID,
Title: r.Title,
Condition: r.Condition,
Data: r.Data,
UID: r.UID,
NoDataState: apimodels.NoDataState(r.NoDataState),
ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState),
},
}
postableExtendedRuleNode.ApiRuleNode = &apimodels.ApiRuleNode{
For: model.Duration(r.For),
Annotations: r.Annotations,
Labels: r.Labels,
}
return postableExtendedRuleNode
}
func toNamespaceErrorResponse(err error) response.Response {
if errors.Is(err, ngmodels.ErrCannotEditNamespace) {
return response.Error(http.StatusForbidden, err.Error(), err)
}
if errors.Is(err, models.ErrDashboardIdentifierNotSet) {
return response.Error(http.StatusBadRequest, err.Error(), err)
}
return coreapi.ToFolderErrorResponse(err)
}