mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Template Testing API (#67450)
This commit is contained in:
parent
9eb10bee1f
commit
91471ac7ae
2
go.mod
2
go.mod
@ -57,7 +57,7 @@ require (
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/google/wire v0.5.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/grafana/alerting v0.0.0-20230426193323-4f09f516596b
|
||||
github.com/grafana/alerting v0.0.0-20230428095912-33c5aa68a5ba
|
||||
github.com/grafana/grafana-aws-sdk v0.12.0
|
||||
github.com/grafana/grafana-azure-sdk-go v1.6.0
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.160.0
|
||||
|
8
go.sum
8
go.sum
@ -1275,8 +1275,12 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20230426193323-4f09f516596b h1:dAHlNtW62HcSh+XmhV7kmcWqnOSnYsw/pGHiEyix544=
|
||||
github.com/grafana/alerting v0.0.0-20230426193323-4f09f516596b/go.mod h1:5edgy6tQY4+W2wuJdi8g2GjbVmpJufguy7QGIRl2q4o=
|
||||
github.com/grafana/alerting v0.0.0-20230427200804-f8565cd8b74b h1:SyatotlHGK+05AxICctry09ZEawhE0AasWpOifljH4M=
|
||||
github.com/grafana/alerting v0.0.0-20230427200804-f8565cd8b74b/go.mod h1:nHfrSTdV7/l74N5/ezqlQ+JwSvIChhN3G5+PjCfwG/E=
|
||||
github.com/grafana/alerting v0.0.0-20230428094731-e067f119be06 h1:g+J4UGslzpi9Kt+846NDAjGtVyt8K9Lgjj/vUh61yK0=
|
||||
github.com/grafana/alerting v0.0.0-20230428094731-e067f119be06/go.mod h1:5edgy6tQY4+W2wuJdi8g2GjbVmpJufguy7QGIRl2q4o=
|
||||
github.com/grafana/alerting v0.0.0-20230428095912-33c5aa68a5ba h1:aNTu22ojw4XY24DYNAuvw8v/5iFUNk2bkdTeeWQ5+0o=
|
||||
github.com/grafana/alerting v0.0.0-20230428095912-33c5aa68a5ba/go.mod h1:5edgy6tQY4+W2wuJdi8g2GjbVmpJufguy7QGIRl2q4o=
|
||||
github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw=
|
||||
github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
|
||||
github.com/grafana/cuetsy v0.1.8 h1:l0AKXfHr0clu6qPirirDzNC/W5mqq5gG7iruOVolG34=
|
||||
|
@ -52,6 +52,7 @@ type Alertmanager interface {
|
||||
// Receivers
|
||||
GetReceivers(ctx context.Context) []apimodels.Receiver
|
||||
TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*notifier.TestReceiversResult, error)
|
||||
TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*notifier.TestTemplatesResults, error)
|
||||
}
|
||||
|
||||
type AlertingStore interface {
|
||||
|
@ -342,6 +342,20 @@ func (srv AlertmanagerSrv) RoutePostTestReceivers(c *contextmodel.ReqContext, bo
|
||||
return response.JSON(statusForTestReceivers(result.Receivers), newTestReceiversResult(result))
|
||||
}
|
||||
|
||||
func (srv AlertmanagerSrv) RoutePostTestTemplates(c *contextmodel.ReqContext, body apimodels.TestTemplatesConfigBodyParams) response.Response {
|
||||
am, errResp := srv.AlertmanagerFor(c.OrgID)
|
||||
if errResp != nil {
|
||||
return errResp
|
||||
}
|
||||
|
||||
res, err := am.TestTemplate(c.Req.Context(), body)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "", err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, newTestTemplateResult(res))
|
||||
}
|
||||
|
||||
// contextWithTimeoutFromRequest returns a context with a deadline set from the
|
||||
// Request-Timeout header in the HTTP request. If the header is absent then the
|
||||
// context will use the default timeout. The timeout in the Request-Timeout
|
||||
@ -433,6 +447,24 @@ func statusForTestReceivers(v []notifier.TestReceiverResult) int {
|
||||
}
|
||||
}
|
||||
|
||||
func newTestTemplateResult(res *notifier.TestTemplatesResults) apimodels.TestTemplatesResults {
|
||||
apiRes := apimodels.TestTemplatesResults{}
|
||||
for _, r := range res.Results {
|
||||
apiRes.Results = append(apiRes.Results, apimodels.TestTemplatesResult{
|
||||
Name: r.Name,
|
||||
Text: r.Text,
|
||||
})
|
||||
}
|
||||
for _, e := range res.Errors {
|
||||
apiRes.Errors = append(apiRes.Errors, apimodels.TestTemplatesErrorResult{
|
||||
Name: e.Name,
|
||||
Kind: apimodels.TemplateErrorKind(e.Kind),
|
||||
Message: e.Error.Error(),
|
||||
})
|
||||
}
|
||||
return apiRes
|
||||
}
|
||||
|
||||
func (srv AlertmanagerSrv) AlertmanagerFor(orgID int64) (Alertmanager, *response.NormalResponse) {
|
||||
am, err := srv.mam.AlertmanagerFor(orgID)
|
||||
if err == nil {
|
||||
|
@ -421,6 +421,46 @@ func TestRoutePostGrafanaAlertingConfigHistoryActivate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoutePostTestTemplates(t *testing.T) {
|
||||
sut := createSut(t, nil)
|
||||
|
||||
t.Run("assert 404 when no alertmanager found", func(tt *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://grafana.net", nil)
|
||||
require.NoError(tt, err)
|
||||
q := req.URL.Query()
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rc := createRequestCtxInOrg(10)
|
||||
|
||||
response := sut.RoutePostTestTemplates(rc, apimodels.TestTemplatesConfigBodyParams{})
|
||||
require.Equal(tt, 404, response.Status())
|
||||
})
|
||||
|
||||
t.Run("assert 409 when alertmanager not ready", func(tt *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://grafana.net", nil)
|
||||
require.NoError(tt, err)
|
||||
q := req.URL.Query()
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rc := createRequestCtxInOrg(3)
|
||||
|
||||
response := sut.RoutePostTestTemplates(rc, apimodels.TestTemplatesConfigBodyParams{})
|
||||
require.Equal(tt, 409, response.Status())
|
||||
})
|
||||
|
||||
t.Run("assert 200 for a valid alertmanager", func(tt *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://grafana.net", nil)
|
||||
require.NoError(tt, err)
|
||||
q := req.URL.Query()
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rc := createRequestCtxInOrg(1)
|
||||
|
||||
response := sut.RoutePostTestTemplates(rc, apimodels.TestTemplatesConfigBodyParams{})
|
||||
require.Equal(tt, 200, response.Status())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSilenceCreate(t *testing.T) {
|
||||
makeSilence := func(comment string, createdBy string,
|
||||
startsAt, endsAt strfmt.DateTime, matchers amv2.Matchers) amv2.Silence {
|
||||
|
@ -173,6 +173,9 @@ func (api *API) authorize(method, path string) web.Handler {
|
||||
case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/receivers/test":
|
||||
fallback = middleware.ReqEditorRole
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
|
||||
case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/templates/test":
|
||||
fallback = middleware.ReqSignedIn
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
|
||||
|
||||
// External Alertmanager Paths
|
||||
case http.MethodDelete + "/api/alertmanager/{DatasourceUID}/config/api/v1/alerts":
|
||||
|
@ -49,7 +49,7 @@ func TestAuthorize(t *testing.T) {
|
||||
}
|
||||
paths[p] = methods
|
||||
}
|
||||
require.Len(t, paths, 47)
|
||||
require.Len(t, paths, 48)
|
||||
|
||||
ac := acmock.New()
|
||||
api := &API{AccessControl: ac}
|
||||
|
@ -189,3 +189,7 @@ func (f *AlertmanagerApiHandler) handleRouteGetGrafanaReceivers(ctx *contextmode
|
||||
func (f *AlertmanagerApiHandler) handleRoutePostTestGrafanaReceivers(ctx *contextmodel.ReqContext, conf apimodels.TestReceiversConfigBodyParams) response.Response {
|
||||
return f.GrafanaSvc.RoutePostTestReceivers(ctx, conf)
|
||||
}
|
||||
|
||||
func (f *AlertmanagerApiHandler) handleRoutePostTestGrafanaTemplates(ctx *contextmodel.ReqContext, conf apimodels.TestTemplatesConfigBodyParams) response.Response {
|
||||
return f.GrafanaSvc.RoutePostTestTemplates(ctx, conf)
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ type AlertmanagerApi interface {
|
||||
RoutePostGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response
|
||||
RoutePostGrafanaAlertingConfigHistoryActivate(*contextmodel.ReqContext) response.Response
|
||||
RoutePostTestGrafanaReceivers(*contextmodel.ReqContext) response.Response
|
||||
RoutePostTestGrafanaTemplates(*contextmodel.ReqContext) response.Response
|
||||
}
|
||||
|
||||
func (f *AlertmanagerApiHandler) RouteCreateGrafanaSilence(ctx *contextmodel.ReqContext) response.Response {
|
||||
@ -181,6 +182,14 @@ func (f *AlertmanagerApiHandler) RoutePostTestGrafanaReceivers(ctx *contextmodel
|
||||
}
|
||||
return f.handleRoutePostTestGrafanaReceivers(ctx, conf)
|
||||
}
|
||||
func (f *AlertmanagerApiHandler) RoutePostTestGrafanaTemplates(ctx *contextmodel.ReqContext) response.Response {
|
||||
// Parse Request Body
|
||||
conf := apimodels.TestTemplatesConfigBodyParams{}
|
||||
if err := web.Bind(ctx.Req, &conf); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
return f.handleRoutePostTestGrafanaTemplates(ctx, conf)
|
||||
}
|
||||
|
||||
func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApi, m *metrics.API) {
|
||||
api.RouteRegister.Group("", func(group routing.RouteRegister) {
|
||||
@ -434,5 +443,15 @@ func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApi, m *metrics
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Post(
|
||||
toMacaronPath("/api/alertmanager/grafana/config/api/v1/templates/test"),
|
||||
api.authorize(http.MethodPost, "/api/alertmanager/grafana/config/api/v1/templates/test"),
|
||||
metrics.Instrument(
|
||||
http.MethodPost,
|
||||
"/api/alertmanager/grafana/config/api/v1/templates/test",
|
||||
api.Hooks.Wrap(srv.RoutePostTestGrafanaTemplates),
|
||||
m,
|
||||
),
|
||||
)
|
||||
}, middleware.ReqSignedIn)
|
||||
}
|
||||
|
@ -299,6 +299,10 @@
|
||||
"AlertingRule": {
|
||||
"description": "adapted from cortex",
|
||||
"properties": {
|
||||
"activeAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"alerts": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/Alert"
|
||||
@ -339,6 +343,20 @@
|
||||
"description": "State can be \"pending\", \"firing\", \"inactive\".",
|
||||
"type": "string"
|
||||
},
|
||||
"totals": {
|
||||
"additionalProperties": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"totalsFiltered": {
|
||||
"additionalProperties": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/RuleType"
|
||||
}
|
||||
@ -350,7 +368,7 @@
|
||||
"type",
|
||||
"state",
|
||||
"annotations",
|
||||
"alerts"
|
||||
"activeAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@ -2845,6 +2863,13 @@
|
||||
"$ref": "#/definitions/RuleGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"totals": {
|
||||
"additionalProperties": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -2878,6 +2903,13 @@
|
||||
"$ref": "#/definitions/AlertingRule"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"totals": {
|
||||
"additionalProperties": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -3343,6 +3375,77 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TestTemplatesConfigBodyParams": {
|
||||
"properties": {
|
||||
"alerts": {
|
||||
"description": "Alerts to use as data when testing the template.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/postableAlert"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the template file.",
|
||||
"type": "string"
|
||||
},
|
||||
"template": {
|
||||
"description": "Template string to test.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TestTemplatesErrorResult": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"description": "Kind of template error that occurred.",
|
||||
"enum": [
|
||||
"invalid_template",
|
||||
"execution_error"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"description": "Error message.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the associated template for this error. Will be empty if the Kind is \"invalid_template\".",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TestTemplatesResult": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name of the associated template definition for this result.",
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"description": "Interpolated value of the template.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TestTemplatesResults": {
|
||||
"properties": {
|
||||
"errors": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/TestTemplatesErrorResult"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"results": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/TestTemplatesResult"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Threshold": {
|
||||
"description": "Threshold a single step on the threshold list",
|
||||
"properties": {
|
||||
@ -3644,7 +3747,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroup": {
|
||||
"description": "AlertGroup alert group",
|
||||
"properties": {
|
||||
"alerts": {
|
||||
"description": "alerts",
|
||||
@ -3829,13 +3931,13 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableAlert"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"gettableSilence": {
|
||||
"description": "GettableSilence gettable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -3891,7 +3993,6 @@
|
||||
"type": "array"
|
||||
},
|
||||
"integration": {
|
||||
"description": "Integration integration",
|
||||
"properties": {
|
||||
"lastNotifyAttempt": {
|
||||
"description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time",
|
||||
@ -4035,7 +4136,6 @@
|
||||
"type": "array"
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -4073,6 +4173,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"receiver": {
|
||||
"description": "Receiver receiver",
|
||||
"properties": {
|
||||
"active": {
|
||||
"description": "active",
|
||||
|
@ -168,6 +168,19 @@ import (
|
||||
// 408: Failure
|
||||
// 409: AlertManagerNotReady
|
||||
|
||||
// swagger:route POST /api/alertmanager/grafana/config/api/v1/templates/test alertmanager RoutePostTestGrafanaTemplates
|
||||
//
|
||||
// Test Grafana managed templates without saving them.
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
//
|
||||
// 200: TestTemplatesResults
|
||||
// 400: ValidationError
|
||||
// 403: PermissionDenied
|
||||
// 409: AlertManagerNotReady
|
||||
|
||||
// swagger:route GET /api/alertmanager/grafana/api/v2/silences alertmanager RouteGetGrafanaSilences
|
||||
//
|
||||
// get silences
|
||||
@ -293,6 +306,56 @@ type TestReceiverConfigResult struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// swagger:parameters RoutePostTestGrafanaTemplates
|
||||
type TestTemplatesConfigParams struct {
|
||||
// in:body
|
||||
Body TestTemplatesConfigBodyParams
|
||||
}
|
||||
|
||||
type TestTemplatesConfigBodyParams struct {
|
||||
// Alerts to use as data when testing the template.
|
||||
Alerts []*amv2.PostableAlert `json:"alerts"`
|
||||
|
||||
// Template string to test.
|
||||
Template string `json:"template"`
|
||||
|
||||
// Name of the template file.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
type TestTemplatesResults struct {
|
||||
Results []TestTemplatesResult `json:"results,omitempty"`
|
||||
Errors []TestTemplatesErrorResult `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
type TestTemplatesResult struct {
|
||||
// Name of the associated template definition for this result.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Interpolated value of the template.
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type TestTemplatesErrorResult struct {
|
||||
// Name of the associated template for this error. Will be empty if the Kind is "invalid_template".
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// Kind of template error that occurred.
|
||||
Kind TemplateErrorKind `json:"kind"`
|
||||
|
||||
// Error message.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// swagger:enum TemplateErrorKind
|
||||
type TemplateErrorKind string
|
||||
|
||||
const (
|
||||
InvalidTemplate TemplateErrorKind = "invalid_template"
|
||||
ExecutionError TemplateErrorKind = "execution_error"
|
||||
)
|
||||
|
||||
// swagger:parameters RouteCreateSilence RouteCreateGrafanaSilence
|
||||
type CreateSilenceParams struct {
|
||||
// in:body
|
||||
|
@ -299,6 +299,10 @@
|
||||
"AlertingRule": {
|
||||
"description": "adapted from cortex",
|
||||
"properties": {
|
||||
"activeAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"alerts": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/Alert"
|
||||
@ -339,6 +343,20 @@
|
||||
"description": "State can be \"pending\", \"firing\", \"inactive\".",
|
||||
"type": "string"
|
||||
},
|
||||
"totals": {
|
||||
"additionalProperties": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"totalsFiltered": {
|
||||
"additionalProperties": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/RuleType"
|
||||
}
|
||||
@ -350,7 +368,7 @@
|
||||
"type",
|
||||
"state",
|
||||
"annotations",
|
||||
"alerts"
|
||||
"activeAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@ -2845,6 +2863,13 @@
|
||||
"$ref": "#/definitions/RuleGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"totals": {
|
||||
"additionalProperties": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -2878,6 +2903,13 @@
|
||||
"$ref": "#/definitions/AlertingRule"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"totals": {
|
||||
"additionalProperties": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -3343,6 +3375,77 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TestTemplatesConfigBodyParams": {
|
||||
"properties": {
|
||||
"alerts": {
|
||||
"description": "Alerts to use as data when testing the template.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/postableAlert"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the template file.",
|
||||
"type": "string"
|
||||
},
|
||||
"template": {
|
||||
"description": "Template string to test.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TestTemplatesErrorResult": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"description": "Kind of template error that occurred.",
|
||||
"enum": [
|
||||
"invalid_template",
|
||||
"execution_error"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"description": "Error message.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the associated template for this error. Will be empty if the Kind is \"invalid_template\".",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TestTemplatesResult": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name of the associated template definition for this result.",
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"description": "Interpolated value of the template.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TestTemplatesResults": {
|
||||
"properties": {
|
||||
"errors": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/TestTemplatesErrorResult"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"results": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/TestTemplatesResult"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Threshold": {
|
||||
"description": "Threshold a single step on the threshold list",
|
||||
"properties": {
|
||||
@ -3668,6 +3771,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"items": {
|
||||
"$ref": "#/definitions/alertGroup"
|
||||
},
|
||||
@ -3888,6 +3992,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"integration": {
|
||||
"description": "Integration integration",
|
||||
"properties": {
|
||||
"lastNotifyAttempt": {
|
||||
"description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time",
|
||||
@ -4031,7 +4136,6 @@
|
||||
"type": "array"
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -4606,6 +4710,53 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/alertmanager/grafana/config/api/v1/templates/test": {
|
||||
"post": {
|
||||
"operationId": "RoutePostTestGrafanaTemplates",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "Body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/TestTemplatesConfigBodyParams"
|
||||
}
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "TestTemplatesResults",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/TestTemplatesResults"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "PermissionDenied",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/PermissionDenied"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "AlertManagerNotReady",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/AlertManagerNotReady"
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Test Grafana managed templates without saving them.",
|
||||
"tags": [
|
||||
"alertmanager"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/alertmanager/grafana/config/history": {
|
||||
"get": {
|
||||
"description": "gets Alerting configurations that were successfully applied in the past",
|
||||
|
@ -435,6 +435,53 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/alertmanager/grafana/config/api/v1/templates/test": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"alertmanager"
|
||||
],
|
||||
"summary": "Test Grafana managed templates without saving them.",
|
||||
"operationId": "RoutePostTestGrafanaTemplates",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/TestTemplatesConfigBodyParams"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "TestTemplatesResults",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/TestTemplatesResults"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "ValidationError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ValidationError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "PermissionDenied",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/PermissionDenied"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "AlertManagerNotReady",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/AlertManagerNotReady"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/alertmanager/grafana/config/history": {
|
||||
"get": {
|
||||
"description": "gets Alerting configurations that were successfully applied in the past",
|
||||
@ -3016,9 +3063,13 @@
|
||||
"type",
|
||||
"state",
|
||||
"annotations",
|
||||
"alerts"
|
||||
"activeAt"
|
||||
],
|
||||
"properties": {
|
||||
"activeAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"alerts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -3059,6 +3110,20 @@
|
||||
"description": "State can be \"pending\", \"firing\", \"inactive\".",
|
||||
"type": "string"
|
||||
},
|
||||
"totals": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
"totalsFiltered": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/RuleType"
|
||||
}
|
||||
@ -5563,6 +5628,13 @@
|
||||
"items": {
|
||||
"$ref": "#/definitions/RuleGroup"
|
||||
}
|
||||
},
|
||||
"totals": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -5599,6 +5671,13 @@
|
||||
"items": {
|
||||
"$ref": "#/definitions/AlertingRule"
|
||||
}
|
||||
},
|
||||
"totals": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -6057,6 +6136,77 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TestTemplatesConfigBodyParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"alerts": {
|
||||
"description": "Alerts to use as data when testing the template.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/postableAlert"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the template file.",
|
||||
"type": "string"
|
||||
},
|
||||
"template": {
|
||||
"description": "Template string to test.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TestTemplatesErrorResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"description": "Kind of template error that occurred.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_template",
|
||||
"execution_error"
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"description": "Error message.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the associated template for this error. Will be empty if the Kind is \"invalid_template\".",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TestTemplatesResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name of the associated template definition for this result.",
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"description": "Interpolated value of the template.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TestTemplatesResults": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"errors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TestTemplatesErrorResult"
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TestTemplatesResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Threshold": {
|
||||
"description": "Threshold a single step on the threshold list",
|
||||
"type": "object",
|
||||
@ -6383,6 +6533,7 @@
|
||||
"$ref": "#/definitions/alertGroup"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/alertGroup"
|
||||
@ -6608,6 +6759,7 @@
|
||||
"$ref": "#/definitions/gettableSilences"
|
||||
},
|
||||
"integration": {
|
||||
"description": "Integration integration",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
@ -6752,7 +6904,6 @@
|
||||
}
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment",
|
||||
|
@ -117,7 +117,8 @@ func newAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A
|
||||
}
|
||||
|
||||
amcfg := &alertingNotify.GrafanaAlertmanagerConfig{
|
||||
WorkingDirectory: workingDir,
|
||||
WorkingDirectory: filepath.Join(cfg.DataPath, workingDir, strconv.Itoa(int(orgID))),
|
||||
ExternalURL: cfg.AppURL,
|
||||
AlertStoreCallback: nil,
|
||||
PeerTimeout: cfg.UnifiedAlerting.HAPeerTimeout,
|
||||
Silences: silencesOptions,
|
||||
@ -259,9 +260,10 @@ func (am *Alertmanager) applyConfig(cfg *apimodels.PostableUserConfig, rawConfig
|
||||
cfg.TemplateFiles = map[string]string{}
|
||||
}
|
||||
cfg.TemplateFiles["__default__.tmpl"] = alertingTemplates.DefaultTemplateString
|
||||
cfg.AlertmanagerConfig.Templates = append(cfg.AlertmanagerConfig.Templates, "__default__.tmpl")
|
||||
|
||||
// next, we need to make sure we persist the templates to disk.
|
||||
paths, templatesChanged, err := PersistTemplates(cfg, am.WorkingDirPath())
|
||||
_, templatesChanged, err := PersistTemplates(cfg, am.Base.WorkingDirectory())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -272,18 +274,11 @@ func (am *Alertmanager) applyConfig(cfg *apimodels.PostableUserConfig, rawConfig
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// With the templates persisted, create the template list using the paths.
|
||||
tmpl, err := am.Base.TemplateFromPaths(am.Settings.AppURL, paths...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = am.Base.ApplyConfig(AlertingConfiguration{
|
||||
RawAlertmanagerConfig: rawConfig,
|
||||
AlertmanagerConfig: cfg.AlertmanagerConfig,
|
||||
AlertmanagerTemplates: tmpl,
|
||||
IntegrationsFunc: am.buildIntegrationsMap,
|
||||
ReceiverIntegrationsFunc: am.buildReceiverIntegration,
|
||||
rawAlertmanagerConfig: rawConfig,
|
||||
alertmanagerConfig: cfg.AlertmanagerConfig,
|
||||
receivers: PostableApiAlertingConfigToApiReceivers(cfg.AlertmanagerConfig),
|
||||
receiverIntegrationsFunc: am.buildReceiverIntegrations,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -310,22 +305,8 @@ func (am *Alertmanager) applyAndMarkConfig(ctx context.Context, hash string, cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *Alertmanager) WorkingDirPath() string {
|
||||
return filepath.Join(am.Settings.DataPath, workingDir, strconv.Itoa(int(am.orgID)))
|
||||
}
|
||||
|
||||
// buildIntegrationsMap builds a map of name to the list of Grafana integration notifiers off of a list of receiver config.
|
||||
func (am *Alertmanager) buildIntegrationsMap(receivers []*alertingNotify.APIReceiver, templates *alertingTemplates.Template) (map[string][]*alertingNotify.Integration, error) {
|
||||
integrationsMap := make(map[string][]*alertingNotify.Integration, len(receivers))
|
||||
for _, receiver := range receivers {
|
||||
integrations, err := am.buildReceiverIntegrations(receiver, templates)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
integrationsMap[receiver.Name] = integrations
|
||||
}
|
||||
|
||||
return integrationsMap, nil
|
||||
func (am *Alertmanager) AppURL() string {
|
||||
return am.Settings.AppURL
|
||||
}
|
||||
|
||||
// buildReceiverIntegrations builds a list of integration notifiers off of a receiver config.
|
||||
@ -356,23 +337,6 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *alertingNotify.APIRe
|
||||
return integrations, nil
|
||||
}
|
||||
|
||||
func (am *Alertmanager) buildReceiverIntegration(r *alertingNotify.GrafanaIntegrationConfig, tmpl *alertingTemplates.Template) (*alertingNotify.Integration, error) {
|
||||
apiReceiver := &alertingNotify.APIReceiver{
|
||||
GrafanaIntegrations: alertingNotify.GrafanaIntegrations{
|
||||
Integrations: []*alertingNotify.GrafanaIntegrationConfig{r},
|
||||
},
|
||||
}
|
||||
integrations, err := am.buildReceiverIntegrations(apiReceiver, tmpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(integrations) == 0 {
|
||||
// This should not happen, but it is better to return some error rather than having a panic.
|
||||
return nil, fmt.Errorf("failed to build integration")
|
||||
}
|
||||
return integrations[0], nil
|
||||
}
|
||||
|
||||
// PutAlerts receives the alerts and then sends them through the corresponding route based on whenever the alert has a receiver embedded or not
|
||||
func (am *Alertmanager) PutAlerts(postableAlerts apimodels.PostableAlerts) error {
|
||||
alerts := make(alertingNotify.PostableAlerts, 0, len(postableAlerts.PostableAlerts))
|
||||
|
@ -22,6 +22,7 @@ func setupAMTest(t *testing.T) *Alertmanager {
|
||||
dir := t.TempDir()
|
||||
cfg := &setting.Cfg{
|
||||
DataPath: dir,
|
||||
AppURL: "http://localhost:9093",
|
||||
}
|
||||
|
||||
m := metrics.NewAlertmanagerMetrics(prometheus.NewRegistry())
|
||||
|
@ -92,18 +92,16 @@ func Load(rawConfig []byte) (*api.PostableUserConfig, error) {
|
||||
// AlertingConfiguration provides configuration for an Alertmanager.
|
||||
// It implements the notify.Configuration interface.
|
||||
type AlertingConfiguration struct {
|
||||
AlertmanagerConfig api.PostableApiAlertingConfig
|
||||
RawAlertmanagerConfig []byte
|
||||
alertmanagerConfig api.PostableApiAlertingConfig
|
||||
rawAlertmanagerConfig []byte
|
||||
|
||||
AlertmanagerTemplates *alertingTemplates.Template
|
||||
|
||||
IntegrationsFunc func(receivers []*alertingNotify.APIReceiver, templates *alertingTemplates.Template) (map[string][]*alertingNotify.Integration, error)
|
||||
ReceiverIntegrationsFunc func(r *alertingNotify.GrafanaIntegrationConfig, tmpl *alertingTemplates.Template) (*alertingNotify.Integration, error)
|
||||
receivers []*alertingNotify.APIReceiver
|
||||
receiverIntegrationsFunc func(r *alertingNotify.APIReceiver, tmpl *alertingTemplates.Template) ([]*alertingNotify.Integration, error)
|
||||
}
|
||||
|
||||
func (a AlertingConfiguration) BuildReceiverIntegrationsFunc() func(next *alertingNotify.GrafanaIntegrationConfig, tmpl *alertingTemplates.Template) (alertingNotify.Notifier, error) {
|
||||
return func(next *alertingNotify.GrafanaIntegrationConfig, tmpl *alertingTemplates.Template) (alertingNotify.Notifier, error) {
|
||||
return a.ReceiverIntegrationsFunc(next, tmpl)
|
||||
func (a AlertingConfiguration) BuildReceiverIntegrationsFunc() func(next *alertingNotify.APIReceiver, tmpl *alertingTemplates.Template) ([]*alertingNotify.Integration, error) {
|
||||
return func(next *alertingNotify.APIReceiver, tmpl *alertingTemplates.Template) ([]*alertingNotify.Integration, error) {
|
||||
return a.receiverIntegrationsFunc(next, tmpl)
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,29 +110,29 @@ func (a AlertingConfiguration) DispatcherLimits() alertingNotify.DispatcherLimit
|
||||
}
|
||||
|
||||
func (a AlertingConfiguration) InhibitRules() []alertingNotify.InhibitRule {
|
||||
return a.AlertmanagerConfig.InhibitRules
|
||||
return a.alertmanagerConfig.InhibitRules
|
||||
}
|
||||
|
||||
func (a AlertingConfiguration) MuteTimeIntervals() []alertingNotify.MuteTimeInterval {
|
||||
return a.AlertmanagerConfig.MuteTimeIntervals
|
||||
return a.alertmanagerConfig.MuteTimeIntervals
|
||||
}
|
||||
|
||||
func (a AlertingConfiguration) ReceiverIntegrations() (map[string][]*alertingNotify.Integration, error) {
|
||||
return a.IntegrationsFunc(PostableApiAlertingConfigToApiReceivers(a.AlertmanagerConfig), a.AlertmanagerTemplates)
|
||||
func (a AlertingConfiguration) Receivers() []*alertingNotify.APIReceiver {
|
||||
return a.receivers
|
||||
}
|
||||
|
||||
func (a AlertingConfiguration) RoutingTree() *alertingNotify.Route {
|
||||
return a.AlertmanagerConfig.Route.AsAMRoute()
|
||||
return a.alertmanagerConfig.Route.AsAMRoute()
|
||||
}
|
||||
|
||||
func (a AlertingConfiguration) Templates() *alertingTemplates.Template {
|
||||
return a.AlertmanagerTemplates
|
||||
func (a AlertingConfiguration) Templates() []string {
|
||||
return a.alertmanagerConfig.Templates
|
||||
}
|
||||
|
||||
func (a AlertingConfiguration) Hash() [16]byte {
|
||||
return md5.Sum(a.RawAlertmanagerConfig)
|
||||
return md5.Sum(a.rawAlertmanagerConfig)
|
||||
}
|
||||
|
||||
func (a AlertingConfiguration) Raw() []byte {
|
||||
return a.RawAlertmanagerConfig
|
||||
return a.rawAlertmanagerConfig
|
||||
}
|
||||
|
63
pkg/services/ngalert/notifier/templates.go
Normal file
63
pkg/services/ngalert/notifier/templates.go
Normal file
@ -0,0 +1,63 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
alertingModels "github.com/grafana/alerting/models"
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
amv2 "github.com/prometheus/alertmanager/api/v2/models"
|
||||
prometheusModel "github.com/prometheus/common/model"
|
||||
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
)
|
||||
|
||||
type TestTemplatesResults = alertingNotify.TestTemplatesResults
|
||||
|
||||
var (
|
||||
DefaultLabels = map[string]string{
|
||||
prometheusModel.AlertNameLabel: `alert title`,
|
||||
alertingModels.FolderTitleLabel: `folder title`,
|
||||
}
|
||||
DefaultAnnotations = map[string]string{
|
||||
alertingModels.ValuesAnnotation: `{"B":22,"C":1}`,
|
||||
alertingModels.ValueStringAnnotation: `[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]`,
|
||||
alertingModels.OrgIDAnnotation: `1`,
|
||||
alertingModels.DashboardUIDAnnotation: `dashboard_uid`,
|
||||
alertingModels.PanelIDAnnotation: `1`,
|
||||
}
|
||||
)
|
||||
|
||||
// TestTemplate tests the given template string against the given alerts. Existing templates are used to provide context for the test.
|
||||
// If an existing template of the same filename as the one being tested is found, it will not be used as context.
|
||||
func (am *Alertmanager) TestTemplate(ctx context.Context, c apimodels.TestTemplatesConfigBodyParams) (*TestTemplatesResults, error) {
|
||||
for _, alert := range c.Alerts {
|
||||
addDefaultLabelsAndAnnotations(alert)
|
||||
}
|
||||
|
||||
return am.Base.TestTemplate(ctx, alertingNotify.TestTemplatesConfigBodyParams{
|
||||
Alerts: c.Alerts,
|
||||
Template: c.Template,
|
||||
Name: c.Name,
|
||||
})
|
||||
}
|
||||
|
||||
// addDefaultLabelsAndAnnotations is a slimmed down version of schedule.stateToPostableAlert and schedule.getRuleExtraLabels using default values.
|
||||
func addDefaultLabelsAndAnnotations(alert *amv2.PostableAlert) {
|
||||
if alert.Labels == nil {
|
||||
alert.Labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range DefaultLabels {
|
||||
if _, ok := alert.Labels[k]; !ok {
|
||||
alert.Labels[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if alert.Annotations == nil {
|
||||
alert.Annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range DefaultAnnotations {
|
||||
if _, ok := alert.Annotations[k]; !ok {
|
||||
alert.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
}
|
174
pkg/services/ngalert/notifier/templates_test.go
Normal file
174
pkg/services/ngalert/notifier/templates_test.go
Normal file
@ -0,0 +1,174 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
alertingModels "github.com/grafana/alerting/models"
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
amv2 "github.com/prometheus/alertmanager/api/v2/models"
|
||||
prometheusModel "github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
)
|
||||
|
||||
var (
|
||||
simpleAlert = amv2.PostableAlert{
|
||||
Alert: amv2.Alert{
|
||||
Labels: amv2.LabelSet{"__alert_rule_uid__": "rule uid", "alertname": "alert1", "lbl1": "val1"},
|
||||
},
|
||||
Annotations: amv2.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh", "__alertImageToken__": "test-image-1"},
|
||||
StartsAt: strfmt.DateTime{},
|
||||
EndsAt: strfmt.DateTime{},
|
||||
}
|
||||
)
|
||||
|
||||
func TestTemplateDefaultData(t *testing.T) {
|
||||
am := setupAMTest(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input apimodels.TestTemplatesConfigBodyParams
|
||||
expected TestTemplatesResults
|
||||
}{{
|
||||
name: "check various extended data",
|
||||
input: apimodels.TestTemplatesConfigBodyParams{
|
||||
Alerts: []*amv2.PostableAlert{&simpleAlert},
|
||||
Name: "slack.title",
|
||||
Template: `{{ define "slack.title" }}
|
||||
Receiver: {{ .Receiver }}
|
||||
Status: {{ .Status }}
|
||||
ExternalURL: {{ .ExternalURL }}
|
||||
Alerts: {{ len .Alerts }}
|
||||
Firing Alerts: {{ len .Alerts.Firing }}
|
||||
Resolved Alerts: {{ len .Alerts.Resolved }}
|
||||
GroupLabels: {{ range .GroupLabels.SortedPairs }}{{ .Name }}={{ .Value }} {{ end }}
|
||||
CommonLabels: {{ range .CommonLabels.SortedPairs }}{{ .Name }}={{ .Value }} {{ end }}
|
||||
CommonAnnotations: {{ range .CommonAnnotations.SortedPairs }}{{ .Name }}={{ .Value }} {{ end }}
|
||||
{{ end }}`,
|
||||
},
|
||||
expected: TestTemplatesResults{
|
||||
Results: []alertingNotify.TestTemplatesResult{{
|
||||
Name: "slack.title",
|
||||
Text: "\nReceiver: TestReceiver\nStatus: firing\nExternalURL: http://localhost:9093\nAlerts: 1\nFiring Alerts: 1\nResolved Alerts: 0\nGroupLabels: group_label=group_label_value \nCommonLabels: alertname=alert1 grafana_folder=folder title lbl1=val1 \nCommonAnnotations: ann1=annv1 \n",
|
||||
}},
|
||||
Errors: nil,
|
||||
},
|
||||
}, {
|
||||
name: "AlertNameLabel",
|
||||
input: apimodels.TestTemplatesConfigBodyParams{
|
||||
Alerts: []*amv2.PostableAlert{{}},
|
||||
Name: "slack.title",
|
||||
Template: fmt.Sprintf(`{{ define "slack.title" }}{{ index (index .Alerts 0 ).Labels "%s" }}{{ end }}`, prometheusModel.AlertNameLabel),
|
||||
},
|
||||
expected: TestTemplatesResults{
|
||||
Results: []alertingNotify.TestTemplatesResult{{
|
||||
Name: "slack.title",
|
||||
Text: DefaultLabels[prometheusModel.AlertNameLabel],
|
||||
}},
|
||||
Errors: nil,
|
||||
},
|
||||
}, {
|
||||
name: "FolderTitleLabel",
|
||||
input: apimodels.TestTemplatesConfigBodyParams{
|
||||
Alerts: []*amv2.PostableAlert{{}},
|
||||
Name: "slack.title",
|
||||
Template: fmt.Sprintf(`{{ define "slack.title" }}{{ index (index .Alerts 0 ).Labels "%s" }}{{ end }}`, alertingModels.FolderTitleLabel),
|
||||
},
|
||||
expected: TestTemplatesResults{
|
||||
Results: []alertingNotify.TestTemplatesResult{{
|
||||
Name: "slack.title",
|
||||
Text: DefaultLabels[alertingModels.FolderTitleLabel],
|
||||
}},
|
||||
Errors: nil,
|
||||
},
|
||||
}, {
|
||||
name: "ValuesAnnotation",
|
||||
input: apimodels.TestTemplatesConfigBodyParams{
|
||||
Alerts: []*amv2.PostableAlert{{}},
|
||||
Name: "slack.title",
|
||||
Template: `{{ define "slack.title" }}{{ range $key, $value := (index .Alerts 0 ).Values }}{{ $key }}={{ $value }} {{ end }}{{ end }}`,
|
||||
},
|
||||
expected: TestTemplatesResults{
|
||||
Results: []alertingNotify.TestTemplatesResult{{
|
||||
Name: "slack.title",
|
||||
Text: "B=22 C=1 ",
|
||||
}},
|
||||
Errors: nil,
|
||||
},
|
||||
}, {
|
||||
name: "ValueStringAnnotation",
|
||||
input: apimodels.TestTemplatesConfigBodyParams{
|
||||
Alerts: []*amv2.PostableAlert{{}},
|
||||
Name: "slack.title",
|
||||
Template: `{{ define "slack.title" }}{{ (index .Alerts 0 ).ValueString }}{{ end }}`,
|
||||
},
|
||||
expected: TestTemplatesResults{
|
||||
Results: []alertingNotify.TestTemplatesResult{{
|
||||
Name: "slack.title",
|
||||
Text: DefaultAnnotations[alertingModels.ValueStringAnnotation],
|
||||
}},
|
||||
Errors: nil,
|
||||
},
|
||||
}, {
|
||||
name: "DashboardURL generation contains DashboardUIDAnnotation and OrgIDAnnotation ",
|
||||
input: apimodels.TestTemplatesConfigBodyParams{
|
||||
Alerts: []*amv2.PostableAlert{{}},
|
||||
Name: "slack.title",
|
||||
Template: `{{ define "slack.title" }}{{ (index .Alerts 0 ).DashboardURL }}{{ end }}`,
|
||||
},
|
||||
expected: TestTemplatesResults{
|
||||
Results: []alertingNotify.TestTemplatesResult{{
|
||||
Name: "slack.title",
|
||||
Text: fmt.Sprintf("http://localhost:9093/d/%s?orgId=%s",
|
||||
DefaultAnnotations[alertingModels.DashboardUIDAnnotation],
|
||||
DefaultAnnotations[alertingModels.OrgIDAnnotation]),
|
||||
}},
|
||||
Errors: nil,
|
||||
},
|
||||
}, {
|
||||
name: "PanelURL generation contains DashboardUIDAnnotation, PanelIDAnnotation, and OrgIDAnnotation ",
|
||||
input: apimodels.TestTemplatesConfigBodyParams{
|
||||
Alerts: []*amv2.PostableAlert{{}},
|
||||
Name: "slack.title",
|
||||
Template: `{{ define "slack.title" }}{{ (index .Alerts 0 ).PanelURL }}{{ end }}`,
|
||||
},
|
||||
expected: TestTemplatesResults{
|
||||
Results: []alertingNotify.TestTemplatesResult{{
|
||||
Name: "slack.title",
|
||||
Text: fmt.Sprintf("http://localhost:9093/d/%s?orgId=%s&viewPanel=%s",
|
||||
DefaultAnnotations[alertingModels.DashboardUIDAnnotation],
|
||||
DefaultAnnotations[alertingModels.OrgIDAnnotation],
|
||||
DefaultAnnotations[alertingModels.PanelIDAnnotation]),
|
||||
}},
|
||||
Errors: nil,
|
||||
},
|
||||
}, {
|
||||
name: "GeneratorURL generation ",
|
||||
input: apimodels.TestTemplatesConfigBodyParams{
|
||||
Alerts: []*amv2.PostableAlert{{Alert: amv2.Alert{GeneratorURL: "http://localhost:3000"}}},
|
||||
Name: "slack.title",
|
||||
Template: `{{ define "slack.title" }}{{ (index .Alerts 0 ).GeneratorURL }}{{ end }}`,
|
||||
},
|
||||
expected: TestTemplatesResults{
|
||||
Results: []alertingNotify.TestTemplatesResult{{
|
||||
Name: "slack.title",
|
||||
Text: fmt.Sprintf("http://localhost:3000?orgId=%s", DefaultAnnotations[alertingModels.OrgIDAnnotation]),
|
||||
}},
|
||||
Errors: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res, err := am.TestTemplate(context.Background(), test.input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, *res)
|
||||
})
|
||||
}
|
||||
}
|
@ -102,7 +102,7 @@ func TestIntegrationAlertmanagerConfigurationIsTransactional(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
var res map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(b, &res))
|
||||
require.Regexp(t, `^failed to save and apply Alertmanager configuration: failed to build integration map: failed to validate integration "slack.receiver" \(UID [^\)]+\) of type "slack": token must be specified when using the Slack chat API`, res["message"])
|
||||
require.Regexp(t, `^failed to save and apply Alertmanager configuration: failed to validate integration "slack.receiver" \(UID [^\)]+\) of type "slack": token must be specified when using the Slack chat API`, res["message"])
|
||||
resp = getRequest(t, alertConfigURL, http.StatusOK) // nolint
|
||||
|
||||
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
|
||||
|
Loading…
Reference in New Issue
Block a user