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>
This commit is contained in:
Owen Diehl 2021-04-19 14:26:04 -04:00 committed by GitHub
parent 6238a7c4c4
commit e37a780e14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 9572 additions and 27 deletions

1
go.mod
View File

@ -68,6 +68,7 @@ require (
github.com/prometheus/client_golang v1.10.0
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.20.0
github.com/prometheus/prometheus v1.8.2-0.20210217141258-a6be548dbc17
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/goxmldsig v1.1.0

1
go.sum
View File

@ -1044,6 +1044,7 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=

View File

@ -7,12 +7,12 @@ import (
"github.com/go-macaron/binding"
apimodels "github.com/grafana/alerting-api/pkg/api"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources"
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/services/ngalert/schedule"
"github.com/grafana/grafana/pkg/services/ngalert/store"

View File

@ -4,10 +4,10 @@ import (
"errors"
"net/http"
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"
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/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/store"

View File

@ -10,10 +10,10 @@ import (
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"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"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/state"
)

View File

@ -7,11 +7,11 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/store"
apimodels "github.com/grafana/alerting-api/pkg/api"
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"

View File

@ -6,11 +6,11 @@ import (
"net/url"
"strconv"
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"
"github.com/grafana/grafana/pkg/services/datasources"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/util"

View File

@ -3,10 +3,10 @@ package api
import (
"fmt"
apimodels "github.com/grafana/alerting-api/pkg/api"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasources"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
// ForkedRuler will validate and proxy requests to the correct backend type depending on the datasource.

View File

@ -3,10 +3,10 @@ package api
import (
"fmt"
apimodels "github.com/grafana/alerting-api/pkg/api"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasources"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
type ForkedAMSvc struct {

View File

@ -3,10 +3,10 @@ package api
import (
"fmt"
apimodels "github.com/grafana/alerting-api/pkg/api"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasources"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
type ForkedPromSvc struct {

View File

@ -9,11 +9,11 @@ package api
import (
"github.com/go-macaron/binding"
apimodels "github.com/grafana/alerting-api/pkg/api"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
type AlertmanagerApiService interface {

View File

@ -9,11 +9,11 @@ package api
import (
"github.com/go-macaron/binding"
apimodels "github.com/grafana/alerting-api/pkg/api"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
type RulerApiService interface {

View File

@ -9,11 +9,11 @@ package api
import (
"github.com/go-macaron/binding"
apimodels "github.com/grafana/alerting-api/pkg/api"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
type TestingApiService interface {

View File

@ -6,12 +6,12 @@ import (
"github.com/prometheus/common/model"
apimodels "github.com/grafana/alerting-api/pkg/api"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/expr/translate"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"

View File

@ -6,10 +6,10 @@ import (
"fmt"
"net/http"
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"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"gopkg.in/yaml.v3"
)

View File

@ -4,10 +4,10 @@ import (
"fmt"
"net/http"
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"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
type promEndpoints struct {

View File

@ -6,7 +6,7 @@ import (
"net/http"
"net/url"
apimodels "github.com/grafana/alerting-api/pkg/api"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"gopkg.in/yaml.v3"
"github.com/grafana/grafana/pkg/api/response"

View File

@ -0,0 +1,14 @@
.DEFAULT_GOAL := openapi
API_DIR = pkg/api
GO_PKG_FILES = $(shell find $(API_DIR) -name *.go -print)
spec.json: $(GO_PKG_FILES)
swagger generate spec -m -w $(API_DIR) -o $@
post.json: spec.json
go run cmd/clean-swagger/main.go -if $(<) -of $@
.PHONY: openapi
openapi: post.json
docker run --rm -p 80:8080 -v $$(pwd):/tmp -e SWAGGER_FILE=/tmp/$(<) swaggerapi/swagger-editor

View File

@ -0,0 +1,13 @@
## What
[view api](http://localhost)
This aims to define the unified alerting API as code. It generates OpenAPI definitions from go structs
## Running
`make openapi`
## Requires
- [go-swagger](https://github.com/go-swagger/go-swagger)

View File

@ -0,0 +1,63 @@
package main
import (
"encoding/json"
"flag"
"io/ioutil"
"log"
"strings"
)
const RefKey = "$ref"
func main() {
var input, output string
flag.StringVar(&input, "if", "", "input file")
flag.StringVar(&output, "of", "", "output file")
flag.Parse()
if input == "" || output == "" {
log.Fatal("no file specified, input", input, ", output", output)
}
//nolint
b, err := ioutil.ReadFile(input)
if err != nil {
log.Fatal(err)
}
data := make(map[string]interface{})
if err := json.Unmarshal(b, &data); err != nil {
log.Fatal(err)
}
definitions, ok := data["definitions"]
if !ok {
log.Fatal("no definitions")
}
defs := definitions.(map[string]interface{})
for k, v := range defs {
vMap := v.(map[string]interface{})
refKey, ok := vMap[RefKey]
if !ok {
continue
}
if strings.TrimPrefix(refKey.(string), "#/definitions/") == k {
log.Println("removing circular ref key", refKey)
delete(vMap, RefKey)
}
}
out, err := json.MarshalIndent(data, "", " ")
if err != nil {
log.Fatal(err)
}
err = ioutil.WriteFile(output, out, 0644)
if err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,565 @@
package api
import (
"encoding/json"
"fmt"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/models"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/config"
"gopkg.in/yaml.v3"
)
// swagger:route POST /api/alertmanager/{Recipient}/config/api/v1/alerts alertmanager RoutePostAlertingConfig
//
// sets an Alerting config
//
// Responses:
// 201: Ack
// 400: ValidationError
// swagger:route GET /api/alertmanager/{Recipient}/config/api/v1/alerts alertmanager RouteGetAlertingConfig
//
// gets an Alerting config
//
// Responses:
// 200: GettableUserConfig
// 400: ValidationError
// swagger:route DELETE /api/alertmanager/{Recipient}/config/api/v1/alerts alertmanager RouteDeleteAlertingConfig
//
// deletes the Alerting config for a tenant
//
// Responses:
// 200: Ack
// 400: ValidationError
// swagger:route GET /api/alertmanager/{Recipient}/api/v2/alerts alertmanager RouteGetAMAlerts
//
// get alertmanager alerts
//
// Responses:
// 200: GettableAlerts
// 400: ValidationError
// swagger:route POST /api/alertmanager/{Recipient}/api/v2/alerts alertmanager RoutePostAMAlerts
//
// create alertmanager alerts
//
// Responses:
// 200: Ack
// 400: ValidationError
// swagger:route GET /api/alertmanager/{Recipient}/api/v2/alerts/groups alertmanager RouteGetAMAlertGroups
//
// get alertmanager alerts
//
// Responses:
// 200: AlertGroups
// 400: ValidationError
// swagger:route GET /api/alertmanager/{Recipient}/api/v2/silences alertmanager RouteGetSilences
//
// get silences
//
// Responses:
// 200: GettableSilences
// 400: ValidationError
// swagger:route POST /api/alertmanager/{Recipient}/api/v2/silences alertmanager RouteCreateSilence
//
// create silence
//
// Responses:
// 201: GettableSilence
// 400: ValidationError
// swagger:route GET /api/alertmanager/{Recipient}/api/v2/silence/{SilenceId} alertmanager RouteGetSilence
//
// get silence
//
// Responses:
// 200: GettableSilence
// 400: ValidationError
// swagger:route DELETE /api/alertmanager/{Recipient}/api/v2/silence/{SilenceId} alertmanager RouteDeleteSilence
//
// delete silence
//
// Responses:
// 200: Ack
// 400: ValidationError
// swagger:parameters RouteCreateSilence
type CreateSilenceParams struct {
// in:body
Silence PostableSilence
}
// swagger:parameters RouteGetSilence RouteDeleteSilence
type GetDeleteSilenceParams struct {
// in:path
SilenceId string
}
// swagger:parameters RouteGetSilences
type GetSilencesParams struct {
// in:query
Filter []string `json:"filter"`
}
// swagger:model
type PostableSilence = amv2.PostableSilence
// swagger:model
type GettableSilences = amv2.GettableSilences
// swagger:model
type GettableSilence = amv2.GettableSilence
// swagger:model
type GettableAlerts = amv2.GettableAlerts
// swagger:model
type GettableAlert = amv2.GettableAlert
// swagger:model
type AlertGroups = amv2.AlertGroups
// swagger:model
type AlertGroup = amv2.AlertGroup
// swagger:model
type Receiver = amv2.Receiver
// swagger:parameters RouteGetAMAlerts RouteGetAMAlertGroups
type AlertsParams struct {
// Show active alerts
// in: query
// required: false
// default: true
Active bool `json:"active"`
// Show silenced alerts
// in: query
// required: false
// default: true
Silenced bool `json:"silenced"`
// Show inhibited alerts
// in: query
// required: false
// default: true
Inhibited bool `json:"inhibited"`
// A list of matchers to filter alerts by
// in: query
// required: false
Matchers []string `json:"filter"`
// A regex matching receivers to filter alerts by
// in: query
// required: false
Receivers string `json:"receiver"`
}
// swagger:parameters RoutePostAMAlerts
type PostableAlerts struct {
// in:body
PostableAlerts []amv2.PostableAlert `yaml:"" json:""`
}
// swagger:parameters RoutePostAlertingConfig
type BodyAlertingConfig struct {
// in:body
Body PostableUserConfig
}
// alertmanager routes
// swagger:parameters RoutePostAlertingConfig RouteGetAlertingConfig RouteDeleteAlertingConfig RouteGetAMAlerts RoutePostAMAlerts RouteGetAMAlertGroups RouteGetSilences RouteCreateSilence RouteGetSilence RouteDeleteSilence RoutePostAlertingConfig
// ruler routes
// swagger:parameters RouteGetRulesConfig RoutePostNameRulesConfig RouteGetNamespaceRulesConfig RouteDeleteNamespaceRulesConfig RouteGetRulegGroupConfig RouteDeleteRuleGroupConfig
// prom routes
// swagger:parameters RouteGetRuleStatuses RouteGetAlertStatuses
// testing routes
// swagger:parameters RouteTestReceiverConfig RouteTestRuleConfig
type DatasourceReference struct {
// Recipient should be "grafana" for requests to be handled by grafana
// and the numeric datasource id for requests to be forwarded to a datasource
// in:path
Recipient string
}
// swagger:model
type PostableUserConfig struct {
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
}
func (c *PostableUserConfig) UnmarshalJSON(b []byte) error {
type plain PostableUserConfig
if err := json.Unmarshal(b, (*plain)(c)); err != nil {
return err
}
return c.validate()
}
func (c *PostableUserConfig) validate() error {
// Taken from https://github.com/prometheus/alertmanager/blob/master/config/config.go#L170-L191
// Check if we have a root route. We cannot check for it in the
// UnmarshalYAML method because it won't be called if the input is empty
// (e.g. the config file is empty or only contains whitespace).
if c.AlertmanagerConfig.Route == nil {
return fmt.Errorf("no route provided in config")
}
// Check if continue in root route.
if c.AlertmanagerConfig.Route.Continue {
return fmt.Errorf("cannot have continue in root route")
}
return nil
}
// MarshalYAML implements yaml.Marshaller.
func (c *PostableUserConfig) MarshalYAML() (interface{}, error) {
yml, err := yaml.Marshal(c.AlertmanagerConfig)
if err != nil {
return nil, err
}
// cortex/loki actually pass the AM config as a string.
cortexPostableUserConfig := struct {
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
AlertmanagerConfig string `yaml:"alertmanager_config" json:"alertmanager_config"`
}{
TemplateFiles: c.TemplateFiles,
AlertmanagerConfig: string(yml),
}
return cortexPostableUserConfig, nil
}
func (c *PostableUserConfig) UnmarshalYAML(value *yaml.Node) error {
// cortex/loki actually pass the AM config as a string.
type cortexPostableUserConfig struct {
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
AlertmanagerConfig string `yaml:"alertmanager_config" json:"alertmanager_config"`
}
var tmp cortexPostableUserConfig
if err := value.Decode(&tmp); err != nil {
return err
}
if err := yaml.Unmarshal([]byte(tmp.AlertmanagerConfig), &c.AlertmanagerConfig); err != nil {
return err
}
c.TemplateFiles = tmp.TemplateFiles
return nil
}
// swagger:model
type GettableUserConfig struct {
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
AlertmanagerConfig GettableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
}
func (c *GettableUserConfig) UnmarshalYAML(value *yaml.Node) error {
// cortex/loki actually pass the AM config as a string.
type cortexGettableUserConfig struct {
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
AlertmanagerConfig string `yaml:"alertmanager_config" json:"alertmanager_config"`
}
var tmp cortexGettableUserConfig
if err := value.Decode(&tmp); err != nil {
return err
}
if err := yaml.Unmarshal([]byte(tmp.AlertmanagerConfig), &c.AlertmanagerConfig); err != nil {
return err
}
c.TemplateFiles = tmp.TemplateFiles
return nil
}
type GettableApiAlertingConfig struct {
Config `yaml:",inline"`
// Override with our superset receiver type
Receivers []*GettableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"`
}
func (c *GettableApiAlertingConfig) UnmarshalJSON(b []byte) error {
type plain GettableApiAlertingConfig
if err := json.Unmarshal(b, (*plain)(c)); err != nil {
return err
}
return c.validate()
}
// validate ensures that the two routing trees use the correct receiver types.
func (c *GettableApiAlertingConfig) validate() error {
receivers := make(map[string]struct{}, len(c.Receivers))
var hasGrafReceivers, hasAMReceivers bool
for _, r := range c.Receivers {
receivers[r.Name] = struct{}{}
switch r.Type() {
case GrafanaReceiverType:
hasGrafReceivers = true
case AlertmanagerReceiverType:
hasAMReceivers = true
}
}
if hasGrafReceivers && hasAMReceivers {
return fmt.Errorf("cannot mix Alertmanager & Grafana receiver types")
}
for _, receiver := range AllReceivers(c.Route) {
_, ok := receivers[receiver]
if !ok {
return fmt.Errorf("unexpected receiver (%s) is undefined", receiver)
}
}
return nil
}
// Type requires validate has been called and just checks the first receiver type
func (c *GettableApiAlertingConfig) Type() (backend Backend) {
for _, r := range c.Receivers {
switch r.Type() {
case GrafanaReceiverType:
return GrafanaBackend
case AlertmanagerReceiverType:
return AlertmanagerBackend
}
}
return
}
// Config is the top-level configuration for Alertmanager's config files.
type Config struct {
Global *config.GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"`
Route *config.Route `yaml:"route,omitempty" json:"route,omitempty"`
InhibitRules []*config.InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"`
Receivers []*config.Receiver `yaml:"-" json:"receivers,omitempty"`
Templates []string `yaml:"templates" json:"templates"`
}
type PostableApiAlertingConfig struct {
Config `yaml:",inline"`
// Override with our superset receiver type
Receivers []*PostableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"`
}
func (c *PostableApiAlertingConfig) UnmarshalJSON(b []byte) error {
type plain PostableApiAlertingConfig
if err := json.Unmarshal(b, (*plain)(c)); err != nil {
return err
}
return c.validate()
}
// validate ensures that the two routing trees use the correct receiver types.
func (c *PostableApiAlertingConfig) validate() error {
receivers := make(map[string]struct{}, len(c.Receivers))
var hasGrafReceivers, hasAMReceivers bool
for _, r := range c.Receivers {
receivers[r.Name] = struct{}{}
switch r.Type() {
case GrafanaReceiverType:
hasGrafReceivers = true
case AlertmanagerReceiverType:
hasAMReceivers = true
}
}
if hasGrafReceivers && hasAMReceivers {
return fmt.Errorf("cannot mix Alertmanager & Grafana receiver types")
}
for _, receiver := range AllReceivers(c.Route) {
_, ok := receivers[receiver]
if !ok {
return fmt.Errorf("unexpected receiver (%s) is undefined", receiver)
}
}
return nil
}
// Type requires validate has been called and just checks the first receiver type
func (c *PostableApiAlertingConfig) Type() (backend Backend) {
for _, r := range c.Receivers {
switch r.Type() {
case GrafanaReceiverType:
return GrafanaBackend
case AlertmanagerReceiverType:
return AlertmanagerBackend
}
}
return
}
// AllReceivers will recursively walk a routing tree and return a list of all the
// referenced receiver names.
func AllReceivers(route *config.Route) (res []string) {
res = append(res, route.Receiver)
for _, subRoute := range route.Routes {
res = append(res, AllReceivers(subRoute)...)
}
return res
}
type GettableGrafanaReceiver dtos.AlertNotification
type PostableGrafanaReceiver models.CreateAlertNotificationCommand
type ReceiverType int
const (
GrafanaReceiverType ReceiverType = iota
AlertmanagerReceiverType
)
type GettableApiReceiver struct {
config.Receiver `yaml:",inline"`
GettableGrafanaReceivers `yaml:",inline"`
}
func (r *GettableApiReceiver) UnmarshalJSON(b []byte) error {
type plain GettableApiReceiver
if err := json.Unmarshal(b, (*plain)(r)); err != nil {
return err
}
hasGrafanaReceivers := len(r.GettableGrafanaReceivers.GrafanaManagedReceivers) > 0
if hasGrafanaReceivers {
if len(r.EmailConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager EmailConfigs & Grafana receivers together")
}
if len(r.PagerdutyConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager PagerdutyConfigs & Grafana receivers together")
}
if len(r.SlackConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager SlackConfigs & Grafana receivers together")
}
if len(r.WebhookConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager WebhookConfigs & Grafana receivers together")
}
if len(r.OpsGenieConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager OpsGenieConfigs & Grafana receivers together")
}
if len(r.WechatConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager WechatConfigs & Grafana receivers together")
}
if len(r.PushoverConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager PushoverConfigs & Grafana receivers together")
}
if len(r.VictorOpsConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager VictorOpsConfigs & Grafana receivers together")
}
}
return nil
}
func (r *GettableApiReceiver) Type() ReceiverType {
if len(r.GettableGrafanaReceivers.GrafanaManagedReceivers) > 0 {
return GrafanaReceiverType
}
return AlertmanagerReceiverType
}
type PostableApiReceiver struct {
config.Receiver `yaml:",inline"`
PostableGrafanaReceivers `yaml:",inline"`
}
func (r *PostableApiReceiver) UnmarshalYAML(unmarshal func(interface{}) error) error {
var grafanaReceivers PostableGrafanaReceivers
if err := unmarshal(&grafanaReceivers); err != nil {
return err
}
r.PostableGrafanaReceivers = grafanaReceivers
var cfg config.Receiver
if err := unmarshal(&cfg); err != nil {
return err
}
r.Name = cfg.Name
r.EmailConfigs = cfg.EmailConfigs
r.PagerdutyConfigs = cfg.PagerdutyConfigs
r.SlackConfigs = cfg.SlackConfigs
r.WebhookConfigs = cfg.WebhookConfigs
r.OpsGenieConfigs = cfg.OpsGenieConfigs
r.WechatConfigs = cfg.WechatConfigs
r.PushoverConfigs = cfg.PushoverConfigs
r.VictorOpsConfigs = cfg.VictorOpsConfigs
return nil
}
func (r *PostableApiReceiver) UnmarshalJSON(b []byte) error {
type plain PostableApiReceiver
if err := json.Unmarshal(b, (*plain)(r)); err != nil {
return err
}
hasGrafanaReceivers := len(r.PostableGrafanaReceivers.GrafanaManagedReceivers) > 0
if hasGrafanaReceivers {
if len(r.EmailConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager EmailConfigs & Grafana receivers together")
}
if len(r.PagerdutyConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager PagerdutyConfigs & Grafana receivers together")
}
if len(r.SlackConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager SlackConfigs & Grafana receivers together")
}
if len(r.WebhookConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager WebhookConfigs & Grafana receivers together")
}
if len(r.OpsGenieConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager OpsGenieConfigs & Grafana receivers together")
}
if len(r.WechatConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager WechatConfigs & Grafana receivers together")
}
if len(r.PushoverConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager PushoverConfigs & Grafana receivers together")
}
if len(r.VictorOpsConfigs) > 0 {
return fmt.Errorf("cannot have both Alertmanager VictorOpsConfigs & Grafana receivers together")
}
}
return nil
}
func (r *PostableApiReceiver) Type() ReceiverType {
if len(r.PostableGrafanaReceivers.GrafanaManagedReceivers) > 0 {
return GrafanaReceiverType
}
return AlertmanagerReceiverType
}
type GettableGrafanaReceivers struct {
GrafanaManagedReceivers []*GettableGrafanaReceiver `yaml:"grafana_managed_receiver_configs,omitempty" json:"grafana_managed_receiver_configs,omitempty"`
}
type PostableGrafanaReceivers struct {
GrafanaManagedReceivers []*PostableGrafanaReceiver `yaml:"grafana_managed_receiver_configs,omitempty" json:"grafana_managed_receiver_configs,omitempty"`
}

View File

@ -0,0 +1,378 @@
package api
import (
"encoding/json"
"testing"
"github.com/prometheus/alertmanager/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func Test_ApiReceiver_Marshaling(t *testing.T) {
for _, tc := range []struct {
desc string
input PostableApiReceiver
err bool
}{
{
desc: "success AM",
input: PostableApiReceiver{
Receiver: config.Receiver{
Name: "foo",
EmailConfigs: []*config.EmailConfig{{}},
},
},
},
{
desc: "success GM",
input: PostableApiReceiver{
Receiver: config.Receiver{
Name: "foo",
},
PostableGrafanaReceivers: PostableGrafanaReceivers{
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}},
},
},
},
{
desc: "failure mixed",
input: PostableApiReceiver{
Receiver: config.Receiver{
Name: "foo",
EmailConfigs: []*config.EmailConfig{{}},
},
PostableGrafanaReceivers: PostableGrafanaReceivers{
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}},
},
},
err: true,
},
} {
t.Run(tc.desc, func(t *testing.T) {
encoded, err := json.Marshal(tc.input)
require.Nil(t, err)
var out PostableApiReceiver
err = json.Unmarshal(encoded, &out)
if tc.err {
require.Error(t, err)
} else {
require.Nil(t, err)
require.Equal(t, tc.input, out)
}
})
}
}
func Test_AllReceivers(t *testing.T) {
input := &config.Route{
Receiver: "foo",
Routes: []*config.Route{
{
Receiver: "bar",
Routes: []*config.Route{
{
Receiver: "bazz",
},
},
},
{
Receiver: "buzz",
},
},
}
require.Equal(t, []string{"foo", "bar", "bazz", "buzz"}, AllReceivers(input))
}
func Test_ApiAlertingConfig_Marshaling(t *testing.T) {
for _, tc := range []struct {
desc string
input PostableApiAlertingConfig
err bool
}{
{
desc: "success am",
input: PostableApiAlertingConfig{
Config: Config{
Route: &config.Route{
Receiver: "am",
Routes: []*config.Route{
{
Receiver: "am",
},
},
},
},
Receivers: []*PostableApiReceiver{
{
Receiver: config.Receiver{
Name: "am",
EmailConfigs: []*config.EmailConfig{{}},
},
},
},
},
},
{
desc: "success graf",
input: PostableApiAlertingConfig{
Config: Config{
Route: &config.Route{
Receiver: "graf",
Routes: []*config.Route{
{
Receiver: "graf",
},
},
},
},
Receivers: []*PostableApiReceiver{
{
Receiver: config.Receiver{
Name: "graf",
},
PostableGrafanaReceivers: PostableGrafanaReceivers{
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}},
},
},
},
},
},
{
desc: "failure undefined am receiver",
input: PostableApiAlertingConfig{
Config: Config{
Route: &config.Route{
Receiver: "am",
Routes: []*config.Route{
{
Receiver: "unmentioned",
},
},
},
},
Receivers: []*PostableApiReceiver{
{
Receiver: config.Receiver{
Name: "am",
EmailConfigs: []*config.EmailConfig{{}},
},
},
},
},
err: true,
},
{
desc: "failure undefined graf receiver",
input: PostableApiAlertingConfig{
Config: Config{
Route: &config.Route{
Receiver: "graf",
Routes: []*config.Route{
{
Receiver: "unmentioned",
},
},
},
},
Receivers: []*PostableApiReceiver{
{
Receiver: config.Receiver{
Name: "graf",
},
PostableGrafanaReceivers: PostableGrafanaReceivers{
GrafanaManagedReceivers: []*PostableGrafanaReceiver{{}},
},
},
},
},
err: true,
},
} {
t.Run(tc.desc, func(t *testing.T) {
encoded, err := json.Marshal(tc.input)
require.Nil(t, err)
var out PostableApiAlertingConfig
err = json.Unmarshal(encoded, &out)
if tc.err {
require.Error(t, err)
} else {
require.Nil(t, err)
require.Equal(t, tc.input, out)
}
})
}
}
func Test_PostableApiReceiver_Unmarshaling_YAML(t *testing.T) {
for _, tc := range []struct {
desc string
input string
rtype ReceiverType
}{
{
desc: "grafana receivers",
input: `
name: grafana_managed
grafana_managed_receiver_configs:
- uid: alertmanager UID
name: an alert manager receiver
type: prometheus-alertmanager
sendreminder: false
disableresolvemessage: false
frequency: 5m
isdefault: false
settings: {}
securesettings:
basicAuthPassword: <basicAuthPassword>
- uid: dingding UID
name: a dingding receiver
type: dingding
sendreminder: false
disableresolvemessage: false
frequency: 5m
isdefault: false`,
rtype: GrafanaReceiverType,
},
{
desc: "receiver",
input: `
name: example-email
email_configs:
- to: 'youraddress@example.org'`,
rtype: AlertmanagerReceiverType,
},
} {
t.Run(tc.desc, func(t *testing.T) {
var r PostableApiReceiver
err := yaml.Unmarshal([]byte(tc.input), &r)
require.Nil(t, err)
assert.Equal(t, tc.rtype, r.Type())
})
}
}
func Test_GettableUserConfigUnmarshaling(t *testing.T) {
for _, tc := range []struct {
desc, input string
output GettableUserConfig
err bool
}{
{
desc: "empty",
input: ``,
output: GettableUserConfig{},
},
{
desc: "empty-ish",
input: `
template_files: {}
alertmanager_config: ""
`,
output: GettableUserConfig{
TemplateFiles: map[string]string{},
},
},
{
desc: "bad type for template",
input: `
template_files: abc
alertmanager_config: ""
`,
err: true,
},
{
desc: "existing templates",
input: `
template_files:
foo: bar
alertmanager_config: ""
`,
output: GettableUserConfig{
TemplateFiles: map[string]string{"foo": "bar"},
},
},
{
desc: "existing templates inline",
input: `
template_files: {foo: bar}
alertmanager_config: ""
`,
output: GettableUserConfig{
TemplateFiles: map[string]string{"foo": "bar"},
},
},
{
desc: "existing am config",
input: `
template_files: {foo: bar}
alertmanager_config: |
route:
receiver: am
continue: false
routes:
- receiver: am
continue: false
templates: []
receivers:
- name: am
email_configs:
- to: foo
from: bar
headers:
Bazz: buzz
text: hi
html: there
`,
output: GettableUserConfig{
TemplateFiles: map[string]string{"foo": "bar"},
AlertmanagerConfig: GettableApiAlertingConfig{
Config: Config{
Templates: []string{},
Route: &config.Route{
Receiver: "am",
Routes: []*config.Route{
{
Receiver: "am",
},
},
},
},
Receivers: []*GettableApiReceiver{
{
Receiver: config.Receiver{
Name: "am",
EmailConfigs: []*config.EmailConfig{{
To: "foo",
From: "bar",
Headers: map[string]string{
"Bazz": "buzz",
},
Text: "hi",
HTML: "there",
}},
},
},
},
},
},
},
} {
t.Run(tc.desc, func(t *testing.T) {
var out GettableUserConfig
err := yaml.Unmarshal([]byte(tc.input), &out)
if tc.err {
require.Error(t, err)
return
}
require.Nil(t, err)
require.Equal(t, tc.output, out)
})
}
}

View File

@ -0,0 +1,51 @@
// Documentation of the API.
//
// Schemes: http, https
// BasePath: /api/v1
// Version: 1.0.0
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Security:
// - basic
//
// SecurityDefinitions:
// basic:
// type: basic
//
// swagger:meta
package api
// swagger:model
type ValidationError struct {
Msg string `json:"msg"`
}
// swagger:model
type Ack struct{}
type Backend int
const (
GrafanaBackend Backend = iota
AlertmanagerBackend
LoTexRulerBackend
)
func (b Backend) String() string {
switch b {
case GrafanaBackend:
return "grafana"
case AlertmanagerBackend:
return "alertmanager"
case LoTexRulerBackend:
return "lotex"
default:
return ""
}
}

View File

@ -0,0 +1,323 @@
package api
import (
"encoding/json"
"fmt"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/prometheus/common/model"
)
// swagger:route Get /api/ruler/{Recipient}/api/v1/rules ruler RouteGetRulesConfig
//
// List rule groups
//
// Produces:
// - application/json
//
// Responses:
// 202: NamespaceConfigResponse
// swagger:route POST /api/ruler/{Recipient}/api/v1/rules/{Namespace} ruler RoutePostNameRulesConfig
//
// Creates or updates a rule group
//
// Consumes:
// - application/json
// - application/yaml
//
// Responses:
// 202: Ack
// swagger:route Get /api/ruler/{Recipient}/api/v1/rules/{Namespace} ruler RouteGetNamespaceRulesConfig
//
// Get rule groups by namespace
//
// Produces:
// - application/json
//
// Responses:
// 202: NamespaceConfigResponse
// swagger:route Delete /api/ruler/{Recipient}/api/v1/rules/{Namespace} ruler RouteDeleteNamespaceRulesConfig
//
// Delete namespace
//
// Responses:
// 202: Ack
// swagger:route Get /api/ruler/{Recipient}/api/v1/rules/{Namespace}/{Groupname} ruler RouteGetRulegGroupConfig
//
// Get rule group
//
// Produces:
// - application/json
//
// Responses:
// 202: RuleGroupConfigResponse
// swagger:route Delete /api/ruler/{Recipient}/api/v1/rules/{Namespace}/{Groupname} ruler RouteDeleteRuleGroupConfig
//
// Delete rule group
//
// Responses:
// 202: Ack
// swagger:parameters RoutePostNameRulesConfig
type NamespaceConfig struct {
// in:path
Namespace string
// in:body
Body PostableRuleGroupConfig
}
// swagger:parameters RouteGetNamespaceRulesConfig RouteDeleteNamespaceRulesConfig
type PathNamespaceConfig struct {
// in: path
Namespace string
}
// swagger:parameters RouteGetRulegGroupConfig RouteDeleteRuleGroupConfig
type PathRouleGroupConfig struct {
// in: path
Namespace string
// in: path
Groupname string
}
// swagger:model
type RuleGroupConfigResponse struct {
GettableRuleGroupConfig
}
// swagger:model
type NamespaceConfigResponse map[string][]GettableRuleGroupConfig
// swagger:model
type PostableRuleGroupConfig struct {
Name string `yaml:"name" json:"name"`
Interval model.Duration `yaml:"interval,omitempty" json:"interval,omitempty"`
Rules []PostableExtendedRuleNode `yaml:"rules" json:"rules"`
}
func (c *PostableRuleGroupConfig) UnmarshalJSON(b []byte) error {
type plain PostableRuleGroupConfig
if err := json.Unmarshal(b, (*plain)(c)); err != nil {
return err
}
return c.validate()
}
// Type requires validate has been called and just checks the first rule type
func (c *PostableRuleGroupConfig) Type() (backend Backend) {
for _, rule := range c.Rules {
switch rule.Type() {
case GrafanaManagedRule:
return GrafanaBackend
case LoTexManagedRule:
return LoTexRulerBackend
}
}
return
}
func (c *PostableRuleGroupConfig) validate() error {
var hasGrafRules, hasLotexRules bool
for _, rule := range c.Rules {
switch rule.Type() {
case GrafanaManagedRule:
hasGrafRules = true
case LoTexManagedRule:
hasLotexRules = true
}
}
if hasGrafRules && hasLotexRules {
return fmt.Errorf("cannot mix Grafana & Prometheus style rules")
}
return nil
}
// swagger:model
type GettableRuleGroupConfig struct {
Name string `yaml:"name" json:"name"`
Interval model.Duration `yaml:"interval,omitempty" json:"interval,omitempty"`
Rules []GettableExtendedRuleNode `yaml:"rules" json:"rules"`
}
func (c *GettableRuleGroupConfig) UnmarshalJSON(b []byte) error {
type plain GettableRuleGroupConfig
if err := json.Unmarshal(b, (*plain)(c)); err != nil {
return err
}
return c.validate()
}
// Type requires validate has been called and just checks the first rule type
func (c *GettableRuleGroupConfig) Type() (backend Backend) {
for _, rule := range c.Rules {
switch rule.Type() {
case GrafanaManagedRule:
return GrafanaBackend
case LoTexManagedRule:
return LoTexRulerBackend
}
}
return
}
func (c *GettableRuleGroupConfig) validate() error {
var hasGrafRules, hasLotexRules bool
for _, rule := range c.Rules {
switch rule.Type() {
case GrafanaManagedRule:
hasGrafRules = true
case LoTexManagedRule:
hasLotexRules = true
}
}
if hasGrafRules && hasLotexRules {
return fmt.Errorf("cannot mix Grafana & Prometheus style rules")
}
return nil
}
type ApiRuleNode struct {
Record string `yaml:"record,omitempty" json:"record,omitempty"`
Alert string `yaml:"alert,omitempty" json:"alert,omitempty"`
Expr string `yaml:"expr" json:"expr"`
For model.Duration `yaml:"for,omitempty" json:"for,omitempty"`
Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"`
}
type RuleType int
const (
GrafanaManagedRule RuleType = iota
LoTexManagedRule
)
type PostableExtendedRuleNode struct {
// note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2)
*ApiRuleNode `yaml:",inline"`
//GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"`
GrafanaManagedAlert *PostableGrafanaRule `yaml:"grafana_alert,omitempty" json:"grafana_alert,omitempty"`
}
func (n *PostableExtendedRuleNode) Type() RuleType {
if n.GrafanaManagedAlert != nil {
return GrafanaManagedRule
}
return LoTexManagedRule
}
func (n *PostableExtendedRuleNode) UnmarshalJSON(b []byte) error {
type plain PostableExtendedRuleNode
if err := json.Unmarshal(b, (*plain)(n)); err != nil {
return err
}
return n.validate()
}
func (n *PostableExtendedRuleNode) validate() error {
if n.ApiRuleNode == nil && n.GrafanaManagedAlert == nil {
return fmt.Errorf("cannot have empty rule")
}
if n.GrafanaManagedAlert != nil {
if n.ApiRuleNode != nil && (n.ApiRuleNode.Expr != "" || n.ApiRuleNode.Record != "") {
return fmt.Errorf("cannot have both Prometheus style rules and Grafana rules together")
}
}
return nil
}
type GettableExtendedRuleNode struct {
// note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2)
*ApiRuleNode `yaml:",inline"`
//GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"`
GrafanaManagedAlert *GettableGrafanaRule `yaml:"grafana_alert,omitempty" json:"grafana_alert,omitempty"`
}
func (n *GettableExtendedRuleNode) Type() RuleType {
if n.GrafanaManagedAlert != nil {
return GrafanaManagedRule
}
return LoTexManagedRule
}
func (n *GettableExtendedRuleNode) UnmarshalJSON(b []byte) error {
type plain GettableExtendedRuleNode
if err := json.Unmarshal(b, (*plain)(n)); err != nil {
return err
}
return n.validate()
}
func (n *GettableExtendedRuleNode) validate() error {
if n.ApiRuleNode == nil && n.GrafanaManagedAlert == nil {
return fmt.Errorf("cannot have empty rule")
}
if n.GrafanaManagedAlert != nil {
if n.ApiRuleNode != nil && (n.ApiRuleNode.Expr != "" || n.ApiRuleNode.Record != "") {
return fmt.Errorf("cannot have both Prometheus style rules and Grafana rules together")
}
}
return nil
}
// swagger:enum NoDataState
type NoDataState string
const (
Alerting NoDataState = "Alerting"
NoData NoDataState = "NoData"
KeepLastState NoDataState = "KeepLastState"
OK NoDataState = "OK"
)
// swagger:enum ExecutionErrorState
type ExecutionErrorState string
const (
AlertingErrState ExecutionErrorState = "Alerting"
KeepLastStateErrState ExecutionErrorState = "KeepLastState"
)
// swagger:model
type PostableGrafanaRule struct {
OrgID int64 `json:"-" yaml:"-"`
Title string `json:"title" yaml:"title"`
Condition string `json:"condition" yaml:"condition"`
Data []models.AlertQuery `json:"data" yaml:"data"`
UID string `json:"uid" yaml:"uid"`
NoDataState NoDataState `json:"no_data_state" yaml:"no_data_state"`
ExecErrState ExecutionErrorState `json:"exec_err_state" yaml:"exec_err_state"`
}
// swagger:model
type GettableGrafanaRule struct {
ID int64 `json:"id" yaml:"id"`
OrgID int64 `json:"orgId" yaml:"orgId"`
Title string `json:"title" yaml:"title"`
Condition string `json:"condition" yaml:"condition"`
Data []models.AlertQuery `json:"data" yaml:"data"`
Updated time.Time `json:"updated" yaml:"updated"`
IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"`
Version int64 `json:"version" yaml:"version"`
UID string `json:"uid" yaml:"uid"`
NamespaceUID string `json:"namespace_uid" yaml:"namespace_uid"`
NamespaceID int64 `json:"namespace_id" yaml:"namespace_id"`
RuleGroup string `json:"rule_group" yaml:"rule_group"`
NoDataState NoDataState `json:"no_data_state" yaml:"no_data_state"`
ExecErrState ExecutionErrorState `json:"exec_err_state" yaml:"exec_err_state"`
}

View File

@ -0,0 +1,238 @@
package api
import (
"encoding/json"
"testing"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func Test_Rule_Marshaling(t *testing.T) {
dur, err := model.ParseDuration("1m")
require.NoError(t, err)
for _, tc := range []struct {
desc string
input PostableExtendedRuleNode
err bool
}{
{
desc: "success lotex",
input: PostableExtendedRuleNode{
ApiRuleNode: &ApiRuleNode{},
},
},
{
desc: "success grafana",
input: PostableExtendedRuleNode{
GrafanaManagedAlert: &PostableGrafanaRule{},
},
},
{
desc: "failure mixed with alert lotex rule",
input: PostableExtendedRuleNode{
ApiRuleNode: &ApiRuleNode{Expr: "<string>"},
GrafanaManagedAlert: &PostableGrafanaRule{},
},
err: true,
},
{
desc: "failure mixed with record lotex rule",
input: PostableExtendedRuleNode{
ApiRuleNode: &ApiRuleNode{Record: "<string>"},
GrafanaManagedAlert: &PostableGrafanaRule{},
},
err: true,
},
{
desc: "grafana with for, annotation and label properties",
input: PostableExtendedRuleNode{
ApiRuleNode: &ApiRuleNode{
For: dur,
Annotations: map[string]string{"foo": "bar"},
Labels: map[string]string{"label1": "val1"}},
GrafanaManagedAlert: &PostableGrafanaRule{},
},
},
} {
t.Run(tc.desc, func(t *testing.T) {
encoded, err := json.Marshal(tc.input)
require.Nil(t, err)
var out PostableExtendedRuleNode
err = json.Unmarshal(encoded, &out)
if tc.err {
require.Error(t, err)
} else {
require.Nil(t, err)
require.Equal(t, tc.input, out)
}
})
}
}
func Test_Rule_Group_Marshaling(t *testing.T) {
dur, err := model.ParseDuration("1m")
require.NoError(t, err)
for _, tc := range []struct {
desc string
input PostableRuleGroupConfig
err bool
}{
{
desc: "success lotex",
input: PostableRuleGroupConfig{
Name: "foo",
Interval: 0,
Rules: []PostableExtendedRuleNode{
PostableExtendedRuleNode{
ApiRuleNode: &ApiRuleNode{},
},
PostableExtendedRuleNode{
ApiRuleNode: &ApiRuleNode{},
},
},
},
},
{
desc: "success grafana",
input: PostableRuleGroupConfig{
Name: "foo",
Interval: 0,
Rules: []PostableExtendedRuleNode{
PostableExtendedRuleNode{
GrafanaManagedAlert: &PostableGrafanaRule{},
},
PostableExtendedRuleNode{
GrafanaManagedAlert: &PostableGrafanaRule{},
},
},
},
},
{
desc: "success grafana with for and annotations",
input: PostableRuleGroupConfig{
Name: "foo",
Interval: 0,
Rules: []PostableExtendedRuleNode{
PostableExtendedRuleNode{
ApiRuleNode: &ApiRuleNode{
For: dur,
Annotations: map[string]string{"foo": "bar"},
Labels: map[string]string{"label1": "val1"},
},
GrafanaManagedAlert: &PostableGrafanaRule{},
},
PostableExtendedRuleNode{
GrafanaManagedAlert: &PostableGrafanaRule{},
},
},
},
},
{
desc: "failure mixed",
input: PostableRuleGroupConfig{
Name: "foo",
Interval: 0,
Rules: []PostableExtendedRuleNode{
PostableExtendedRuleNode{
GrafanaManagedAlert: &PostableGrafanaRule{},
},
PostableExtendedRuleNode{
ApiRuleNode: &ApiRuleNode{},
},
},
},
err: true,
},
} {
t.Run(tc.desc, func(t *testing.T) {
encoded, err := json.Marshal(tc.input)
require.Nil(t, err)
var out PostableRuleGroupConfig
err = json.Unmarshal(encoded, &out)
if tc.err {
require.Error(t, err)
} else {
require.Nil(t, err)
require.Equal(t, tc.input, out)
}
})
}
}
func Test_Rule_Group_Type(t *testing.T) {
for _, tc := range []struct {
desc string
input PostableRuleGroupConfig
expected Backend
}{
{
desc: "success lotex",
input: PostableRuleGroupConfig{
Name: "foo",
Interval: 0,
Rules: []PostableExtendedRuleNode{
PostableExtendedRuleNode{
ApiRuleNode: &ApiRuleNode{},
},
PostableExtendedRuleNode{
ApiRuleNode: &ApiRuleNode{},
},
},
},
expected: LoTexRulerBackend,
},
{
desc: "success grafana",
input: PostableRuleGroupConfig{
Name: "foo",
Interval: 0,
Rules: []PostableExtendedRuleNode{
PostableExtendedRuleNode{
GrafanaManagedAlert: &PostableGrafanaRule{},
},
PostableExtendedRuleNode{
GrafanaManagedAlert: &PostableGrafanaRule{},
},
},
},
expected: GrafanaBackend,
},
} {
t.Run(tc.desc, func(t *testing.T) {
bt := tc.input.Type()
require.Equal(t, tc.expected, bt)
})
}
}
func TestNamespaceMarshalling(t *testing.T) {
var data = `copy:
- name: loki_alerts
rules:
- alert: logs_exist
expr: rate({cluster="us-central1", job="loki-prod/loki-canary"}[1m]) > 0
for: 1m
simple_rules:
- name: loki_alerts
rules:
- alert: logs_exist
expr: rate({cluster="us-central1", job="loki-prod/loki-canary"}[1m]) > 0
for: 1m
`
var res NamespaceConfigResponse
err := yaml.Unmarshal([]byte(data), &res)
require.Nil(t, err)
b, err := yaml.Marshal(res)
require.Nil(t, err)
require.Equal(t, data, string(b))
}

View File

@ -0,0 +1,130 @@
package api
import (
"time"
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
)
// swagger:route GET /api/prometheus/{Recipient}/api/v1/rules prometheus RouteGetRuleStatuses
//
// gets the evaluation statuses of all rules
//
// Responses:
// 200: RuleResponse
// swagger:route GET /api/prometheus/{Recipient}/api/v1/alerts prometheus RouteGetAlertStatuses
//
// gets the current alerts
//
// Responses:
// 200: AlertResponse
// swagger:model
type RuleResponse struct {
// in: body
DiscoveryBase
// in: body
Data RuleDiscovery `json:"data"`
}
// swagger:model
type AlertResponse struct {
// in: body
DiscoveryBase
// in: body
Data AlertDiscovery `json:"data"`
}
// swagger:model
type DiscoveryBase struct {
// required: true
Status string `json:"status"`
// required: false
ErrorType v1.ErrorType `json:"errorType,omitempty"`
// required: false
Error string `json:"error,omitempty"`
}
// swagger:model
type RuleDiscovery struct {
// required: true
RuleGroups []*RuleGroup `json:"groups"`
}
// AlertDiscovery has info for all active alerts.
// swagger:model
type AlertDiscovery struct {
// required: true
Alerts []*Alert `json:"alerts"`
}
// swagger:model
type RuleGroup struct {
// required: true
Name string `json:"name"`
// required: true
File string `json:"file"`
// In order to preserve rule ordering, while exposing type (alerting or recording)
// specific properties, both alerting and recording rules are exposed in the
// same array.
// required: true
Rules []AlertingRule `json:"rules"`
// required: true
Interval float64 `json:"interval"`
LastEvaluation time.Time `json:"lastEvaluation"`
EvaluationTime float64 `json:"evaluationTime"`
}
// adapted from cortex
// swagger:model
type AlertingRule struct {
// State can be "pending", "firing", "inactive".
// required: true
State string `json:"state,omitempty"`
// required: true
Name string `json:"name,omitempty"`
// required: true
Query string `json:"query,omitempty"`
Duration float64 `json:"duration,omitempty"`
// required: true
Annotations labels `json:"annotations,omitempty"`
// required: true
Alerts []*Alert `json:"alerts,omitempty"`
Rule
}
// adapted from cortex
// swagger:model
type Rule struct {
// required: true
Name string `json:"name"`
// required: true
Query string `json:"query"`
Labels labels `json:"labels"`
// required: true
Health string `json:"health"`
LastError string `json:"lastError"`
// required: true
Type v1.RuleType `json:"type"`
LastEvaluation time.Time `json:"lastEvaluation"`
EvaluationTime float64 `json:"evaluationTime"`
}
// Alert has info for an alert.
// swagger:model
type Alert struct {
// required: true
Labels labels `json:"labels"`
// required: true
Annotations labels `json:"annotations"`
// required: true
State string `json:"state"`
ActiveAt *time.Time `json:"activeAt"`
// required: true
Value string `json:"value"`
}
// override the labels type with a map for generation.
// The custom marshaling for labels.Labels ends up doing this anyways.
type labels map[string]string

View File

@ -0,0 +1,131 @@
package api
import (
"encoding/json"
"fmt"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/prometheus/promql"
)
// swagger:route Post /api/v1/receiver/test/{Recipient} testing RouteTestReceiverConfig
//
// Test receiver
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Responses:
// 200: Success
// 412: SmtpNotEnabled
// 500: Failure
// swagger:route Post /api/v1/rule/test/{Recipient} testing RouteTestRuleConfig
//
// Test rule
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Responses:
// 200: TestRuleResponse
// swagger:parameters RouteTestReceiverConfig
type TestReceiverRequest struct {
// in:body
Body ExtendedReceiver
}
// swagger:parameters RouteTestRuleConfig
type TestRuleRequest struct {
// in:body
Body TestRulePayload
}
// swagger:model
type TestRulePayload struct {
// Example: (node_filesystem_avail_bytes{fstype!="",job="integrations/node_exporter"} node_filesystem_size_bytes{fstype!="",job="integrations/node_exporter"} * 100 < 5 and node_filesystem_readonly{fstype!="",job="integrations/node_exporter"} == 0)
Expr string `json:"expr,omitempty"`
// GrafanaManagedCondition for grafana alerts
GrafanaManagedCondition *models.EvalAlertConditionCommand `json:"grafana_condition,omitempty"`
}
func (p *TestRulePayload) UnmarshalJSON(b []byte) error {
type plain TestRulePayload
if err := json.Unmarshal(b, (*plain)(p)); err != nil {
return err
}
return p.validate()
}
func (p *TestRulePayload) validate() error {
if p.Expr != "" && p.GrafanaManagedCondition != nil {
return fmt.Errorf("cannot mix Grafana & Prometheus style expressions")
}
if p.Expr == "" && p.GrafanaManagedCondition == nil {
return fmt.Errorf("missing either Grafana or Prometheus style expressions")
}
return nil
}
func (p *TestRulePayload) Type() (backend Backend) {
if p.Expr != "" {
return LoTexRulerBackend
}
if p.GrafanaManagedCondition != nil {
return GrafanaBackend
}
return
}
// swagger:model
type TestRuleResponse struct {
Alerts promql.Vector `json:"alerts"`
GrafanaAlertInstances AlertInstancesResponse `json:"grafana_alert_instances"`
}
// swagger:model
type AlertInstancesResponse struct {
// Instances is an array of arrow encoded dataframes
// each frame has a single row, and a column for each instance (alert identified by unique labels) with a boolean value (firing/not firing)
Instances [][]byte `json:"instances"`
}
// swagger:model
type ExtendedReceiver struct {
EmailConfigs config.EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"`
PagerdutyConfigs config.PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"`
SlackConfigs config.SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"`
WebhookConfigs config.WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"`
OpsGenieConfigs config.OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"`
WechatConfigs config.WechatConfig `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"`
PushoverConfigs config.PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"`
VictorOpsConfigs config.VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"`
GrafanaReceiver PostableGrafanaReceiver `yaml:"grafana_managed_receiver,omitempty" json:"grafana_managed_receiver,omitempty"`
}
// swagger:model
type Success ResponseDetails
// swagger:model
type SmtpNotEnabled ResponseDetails
// swagger:model
type Failure ResponseDetails
// swagger:model
type ResponseDetails struct {
Msg string `json:"msg"`
}

View File

@ -0,0 +1,69 @@
package api
import (
"encoding/json"
"testing"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/stretchr/testify/require"
)
func TestRulePayloadMarshaling(t *testing.T) {
for _, tc := range []struct {
desc string
input TestRulePayload
err bool
}{
{
desc: "success lotex",
input: TestRulePayload{
Expr: "rate({cluster=\"us-central1\", job=\"loki-prod/loki-canary\"}[1m]) > 0",
},
},
{
desc: "success grafana",
input: func() TestRulePayload {
data := models.AlertQuery{}
// hack around that the struct embeds the json message inside of it as well
raw, _ := json.Marshal(data)
data.Model = raw
return TestRulePayload{
GrafanaManagedCondition: &models.EvalAlertConditionCommand{
Condition: "placeholder",
Data: []models.AlertQuery{data},
},
}
}(),
},
{
desc: "failure mixed",
input: TestRulePayload{
Expr: "rate({cluster=\"us-central1\", job=\"loki-prod/loki-canary\"}[1m]) > 0",
GrafanaManagedCondition: &models.EvalAlertConditionCommand{},
},
err: true,
},
{
desc: "failure both empty",
input: TestRulePayload{},
err: true,
},
} {
t.Run(tc.desc, func(t *testing.T) {
encoded, err := json.Marshal(tc.input)
require.Nil(t, err)
var out TestRulePayload
err = json.Unmarshal(encoded, &out)
if tc.err {
require.Error(t, err)
} else {
require.Nil(t, err)
require.Equal(t, tc.input, out)
}
})
}
}

View File

@ -0,0 +1,35 @@
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.19.5/swagger-ui.css" >
<style>
.topbar {
display: none;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.19.5/swagger-ui-bundle.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.19.5/swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "https://grafana.github.io/alerting-api/spec.json?cachebuster=" + Math.random(),
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
window.ui = ui
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -11,12 +11,12 @@ import (
"strconv"
"strings"
apimodels "github.com/grafana/alerting-api/pkg/api"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/setting"

View File

@ -5,7 +5,7 @@ import (
"time"
gokit_log "github.com/go-kit/kit/log"
apimodels "github.com/grafana/alerting-api/pkg/api"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/provider"
"github.com/prometheus/alertmanager/provider/mem"

View File

@ -5,7 +5,7 @@ import (
"time"
"github.com/go-openapi/strfmt"
apimodels "github.com/grafana/alerting-api/pkg/api"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/provider"
"github.com/prometheus/alertmanager/types"

View File

@ -11,7 +11,7 @@ import (
"time"
gokit_log "github.com/go-kit/kit/log"
apimodels "github.com/grafana/alerting-api/pkg/api"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/pkg/errors"
"github.com/prometheus/alertmanager/dispatch"
"github.com/prometheus/alertmanager/nflog"

View File

@ -5,7 +5,7 @@ import (
"sort"
"time"
apimodels "github.com/grafana/alerting-api/pkg/api"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/pkg/errors"
v2 "github.com/prometheus/alertmanager/api/v2"
"github.com/prometheus/alertmanager/dispatch"

View File

@ -7,8 +7,8 @@ import (
"os"
"path/filepath"
"github.com/grafana/alerting-api/pkg/api"
"github.com/grafana/grafana/pkg/infra/log"
api "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/pkg/errors"
)

View File

@ -6,7 +6,7 @@ import (
"path/filepath"
"testing"
"github.com/grafana/alerting-api/pkg/api"
api "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

View File

@ -4,7 +4,7 @@ import (
"fmt"
"time"
apimodels "github.com/grafana/alerting-api/pkg/api"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/pkg/errors"
v2 "github.com/prometheus/alertmanager/api/v2"
"github.com/prometheus/alertmanager/silence"

View File

@ -2,7 +2,7 @@ package schedule
import (
"github.com/go-openapi/strfmt"
apimodels "github.com/grafana/alerting-api/pkg/api"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/grafana/grafana/pkg/services/ngalert/eval"

View File

@ -7,7 +7,7 @@ import (
"time"
"github.com/benbjohnson/clock"
apimodels "github.com/grafana/alerting-api/pkg/api"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/infra/log"

View File

@ -12,7 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
apimodels "github.com/grafana/alerting-api/pkg/api"
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/services/sqlstore"
"github.com/grafana/grafana/pkg/util"

View File

@ -7,7 +7,7 @@ import (
"testing"
"time"
apimodels "github.com/grafana/alerting-api/pkg/api"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/services/ngalert/models"