diff --git a/.gitignore b/.gitignore index aa4b780003..4d9e6f8031 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,10 @@ /.release /.tarballs /vendor - +/.idea/workspace.xml +*.idea +/.idea/* +/template/slack.tmpl !.golangci.yml !/cli/testdata/*.yml !/cli/config/testdata/*.yml diff --git a/api/v2/api.go b/api/v2/api.go index 5148d0573a..eb1ebedd6d 100644 --- a/api/v2/api.go +++ b/api/v2/api.go @@ -388,6 +388,8 @@ func (api *API) getAlertGroupsHandler(params alertgroup_ops.GetAlertGroupsParams res := make(open_api_models.AlertGroups, 0, len(alertGroups)) + dedup := make(map[prometheus_model.Fingerprint]bool) + for _, alertGroup := range alertGroups { ag := &open_api_models.AlertGroup{ Receiver: &open_api_models.Receiver{Name: &alertGroup.Receiver}, @@ -397,12 +399,17 @@ func (api *API) getAlertGroupsHandler(params alertgroup_ops.GetAlertGroupsParams for _, alert := range alertGroup.Alerts { fp := alert.Fingerprint() - receivers := allReceivers[fp] - status := api.getAlertStatus(fp) - apiAlert := AlertToOpenAPIAlert(alert, status, receivers) - ag.Alerts = append(ag.Alerts, apiAlert) + if _, ok := dedup[fp]; !ok { + dedup[fp] = true + receivers := allReceivers[fp] + status := api.getAlertStatus(fp) + apiAlert := AlertToOpenAPIAlert(alert, status, receivers) + ag.Alerts = append(ag.Alerts, apiAlert) + } + } + if len(ag.Alerts) > 0 { + res = append(res, ag) } - res = append(res, ag) } return alertgroup_ops.NewGetAlertGroupsOK().WithPayload(res) diff --git a/api/v2/client/alert/alert_client.go b/api/v2/client/alert/alert_client.go index 24cbe8c950..4dfa2564be 100644 --- a/api/v2/client/alert/alert_client.go +++ b/api/v2/client/alert/alert_client.go @@ -49,7 +49,7 @@ type ClientService interface { } /* - GetAlerts Get a list of alerts +GetAlerts Get a list of alerts */ func (a *Client) GetAlerts(params *GetAlertsParams) (*GetAlertsOK, error) { // TODO: Validate the params before sending @@ -83,7 +83,7 @@ func (a *Client) GetAlerts(params *GetAlertsParams) (*GetAlertsOK, error) { } /* - PostAlerts Create new Alerts +PostAlerts Create new Alerts */ func (a *Client) PostAlerts(params *PostAlertsParams) (*PostAlertsOK, error) { // TODO: Validate the params before sending diff --git a/api/v2/client/alert/get_alerts_parameters.go b/api/v2/client/alert/get_alerts_parameters.go index de05f6889e..03d04d54fc 100644 --- a/api/v2/client/alert/get_alerts_parameters.go +++ b/api/v2/client/alert/get_alerts_parameters.go @@ -106,7 +106,8 @@ func NewGetAlertsParamsWithHTTPClient(client *http.Client) *GetAlertsParams { } } -/*GetAlertsParams contains all the parameters to send to the API endpoint +/* +GetAlertsParams contains all the parameters to send to the API endpoint for the get alerts operation typically these are written to a http.Request */ type GetAlertsParams struct { diff --git a/api/v2/client/alert/get_alerts_responses.go b/api/v2/client/alert/get_alerts_responses.go index 0fd6e282be..d5e5c074fe 100644 --- a/api/v2/client/alert/get_alerts_responses.go +++ b/api/v2/client/alert/get_alerts_responses.go @@ -66,7 +66,8 @@ func NewGetAlertsOK() *GetAlertsOK { return &GetAlertsOK{} } -/*GetAlertsOK handles this case with default header values. +/* +GetAlertsOK handles this case with default header values. Get alerts response */ @@ -97,7 +98,8 @@ func NewGetAlertsBadRequest() *GetAlertsBadRequest { return &GetAlertsBadRequest{} } -/*GetAlertsBadRequest handles this case with default header values. +/* +GetAlertsBadRequest handles this case with default header values. Bad request */ @@ -128,7 +130,8 @@ func NewGetAlertsInternalServerError() *GetAlertsInternalServerError { return &GetAlertsInternalServerError{} } -/*GetAlertsInternalServerError handles this case with default header values. +/* +GetAlertsInternalServerError handles this case with default header values. Internal server error */ diff --git a/api/v2/client/alert/post_alerts_parameters.go b/api/v2/client/alert/post_alerts_parameters.go index 3e5c99805a..a81735b1c7 100644 --- a/api/v2/client/alert/post_alerts_parameters.go +++ b/api/v2/client/alert/post_alerts_parameters.go @@ -71,7 +71,8 @@ func NewPostAlertsParamsWithHTTPClient(client *http.Client) *PostAlertsParams { } } -/*PostAlertsParams contains all the parameters to send to the API endpoint +/* +PostAlertsParams contains all the parameters to send to the API endpoint for the post alerts operation typically these are written to a http.Request */ type PostAlertsParams struct { diff --git a/api/v2/client/alert/post_alerts_responses.go b/api/v2/client/alert/post_alerts_responses.go index 693efd5b19..46a3ab6f61 100644 --- a/api/v2/client/alert/post_alerts_responses.go +++ b/api/v2/client/alert/post_alerts_responses.go @@ -64,7 +64,8 @@ func NewPostAlertsOK() *PostAlertsOK { return &PostAlertsOK{} } -/*PostAlertsOK handles this case with default header values. +/* +PostAlertsOK handles this case with default header values. Create alerts response */ @@ -85,7 +86,8 @@ func NewPostAlertsBadRequest() *PostAlertsBadRequest { return &PostAlertsBadRequest{} } -/*PostAlertsBadRequest handles this case with default header values. +/* +PostAlertsBadRequest handles this case with default header values. Bad request */ @@ -116,7 +118,8 @@ func NewPostAlertsInternalServerError() *PostAlertsInternalServerError { return &PostAlertsInternalServerError{} } -/*PostAlertsInternalServerError handles this case with default header values. +/* +PostAlertsInternalServerError handles this case with default header values. Internal server error */ diff --git a/api/v2/client/alertgroup/alertgroup_client.go b/api/v2/client/alertgroup/alertgroup_client.go index f7c4a1faa7..ca81de0fa3 100644 --- a/api/v2/client/alertgroup/alertgroup_client.go +++ b/api/v2/client/alertgroup/alertgroup_client.go @@ -47,7 +47,7 @@ type ClientService interface { } /* - GetAlertGroups Get a list of alert groups +GetAlertGroups Get a list of alert groups */ func (a *Client) GetAlertGroups(params *GetAlertGroupsParams) (*GetAlertGroupsOK, error) { // TODO: Validate the params before sending diff --git a/api/v2/client/alertgroup/get_alert_groups_parameters.go b/api/v2/client/alertgroup/get_alert_groups_parameters.go index b76d0b1b94..767bd61c6c 100644 --- a/api/v2/client/alertgroup/get_alert_groups_parameters.go +++ b/api/v2/client/alertgroup/get_alert_groups_parameters.go @@ -98,7 +98,8 @@ func NewGetAlertGroupsParamsWithHTTPClient(client *http.Client) *GetAlertGroupsP } } -/*GetAlertGroupsParams contains all the parameters to send to the API endpoint +/* +GetAlertGroupsParams contains all the parameters to send to the API endpoint for the get alert groups operation typically these are written to a http.Request */ type GetAlertGroupsParams struct { diff --git a/api/v2/client/alertgroup/get_alert_groups_responses.go b/api/v2/client/alertgroup/get_alert_groups_responses.go index 6c686ef3ee..fef8ba9416 100644 --- a/api/v2/client/alertgroup/get_alert_groups_responses.go +++ b/api/v2/client/alertgroup/get_alert_groups_responses.go @@ -66,7 +66,8 @@ func NewGetAlertGroupsOK() *GetAlertGroupsOK { return &GetAlertGroupsOK{} } -/*GetAlertGroupsOK handles this case with default header values. +/* +GetAlertGroupsOK handles this case with default header values. Get alert groups response */ @@ -97,7 +98,8 @@ func NewGetAlertGroupsBadRequest() *GetAlertGroupsBadRequest { return &GetAlertGroupsBadRequest{} } -/*GetAlertGroupsBadRequest handles this case with default header values. +/* +GetAlertGroupsBadRequest handles this case with default header values. Bad request */ @@ -128,7 +130,8 @@ func NewGetAlertGroupsInternalServerError() *GetAlertGroupsInternalServerError { return &GetAlertGroupsInternalServerError{} } -/*GetAlertGroupsInternalServerError handles this case with default header values. +/* +GetAlertGroupsInternalServerError handles this case with default header values. Internal server error */ diff --git a/api/v2/client/general/general_client.go b/api/v2/client/general/general_client.go index 35304b1790..3d9b14dc36 100644 --- a/api/v2/client/general/general_client.go +++ b/api/v2/client/general/general_client.go @@ -47,7 +47,7 @@ type ClientService interface { } /* - GetStatus Get current status of an Alertmanager instance and its cluster +GetStatus Get current status of an Alertmanager instance and its cluster */ func (a *Client) GetStatus(params *GetStatusParams) (*GetStatusOK, error) { // TODO: Validate the params before sending diff --git a/api/v2/client/general/get_status_parameters.go b/api/v2/client/general/get_status_parameters.go index 814e8b31ab..e0cb35170f 100644 --- a/api/v2/client/general/get_status_parameters.go +++ b/api/v2/client/general/get_status_parameters.go @@ -69,7 +69,8 @@ func NewGetStatusParamsWithHTTPClient(client *http.Client) *GetStatusParams { } } -/*GetStatusParams contains all the parameters to send to the API endpoint +/* +GetStatusParams contains all the parameters to send to the API endpoint for the get status operation typically these are written to a http.Request */ type GetStatusParams struct { diff --git a/api/v2/client/general/get_status_responses.go b/api/v2/client/general/get_status_responses.go index d457ccc92b..54227f7464 100644 --- a/api/v2/client/general/get_status_responses.go +++ b/api/v2/client/general/get_status_responses.go @@ -54,7 +54,8 @@ func NewGetStatusOK() *GetStatusOK { return &GetStatusOK{} } -/*GetStatusOK handles this case with default header values. +/* +GetStatusOK handles this case with default header values. Get status response */ diff --git a/api/v2/client/receiver/get_receivers_parameters.go b/api/v2/client/receiver/get_receivers_parameters.go index 090b9e4283..32a0ce4a4b 100644 --- a/api/v2/client/receiver/get_receivers_parameters.go +++ b/api/v2/client/receiver/get_receivers_parameters.go @@ -69,7 +69,8 @@ func NewGetReceiversParamsWithHTTPClient(client *http.Client) *GetReceiversParam } } -/*GetReceiversParams contains all the parameters to send to the API endpoint +/* +GetReceiversParams contains all the parameters to send to the API endpoint for the get receivers operation typically these are written to a http.Request */ type GetReceiversParams struct { diff --git a/api/v2/client/receiver/get_receivers_responses.go b/api/v2/client/receiver/get_receivers_responses.go index 3bc473d867..369ff9cdb2 100644 --- a/api/v2/client/receiver/get_receivers_responses.go +++ b/api/v2/client/receiver/get_receivers_responses.go @@ -54,7 +54,8 @@ func NewGetReceiversOK() *GetReceiversOK { return &GetReceiversOK{} } -/*GetReceiversOK handles this case with default header values. +/* +GetReceiversOK handles this case with default header values. Get receivers response */ diff --git a/api/v2/client/receiver/receiver_client.go b/api/v2/client/receiver/receiver_client.go index 1cda82018c..0f4915f988 100644 --- a/api/v2/client/receiver/receiver_client.go +++ b/api/v2/client/receiver/receiver_client.go @@ -47,7 +47,7 @@ type ClientService interface { } /* - GetReceivers Get list of all receivers (name of notification integrations) +GetReceivers Get list of all receivers (name of notification integrations) */ func (a *Client) GetReceivers(params *GetReceiversParams) (*GetReceiversOK, error) { // TODO: Validate the params before sending diff --git a/api/v2/client/silence/delete_silence_parameters.go b/api/v2/client/silence/delete_silence_parameters.go index 2b4e9b8c83..bdfd385cbe 100644 --- a/api/v2/client/silence/delete_silence_parameters.go +++ b/api/v2/client/silence/delete_silence_parameters.go @@ -69,7 +69,8 @@ func NewDeleteSilenceParamsWithHTTPClient(client *http.Client) *DeleteSilencePar } } -/*DeleteSilenceParams contains all the parameters to send to the API endpoint +/* +DeleteSilenceParams contains all the parameters to send to the API endpoint for the delete silence operation typically these are written to a http.Request */ type DeleteSilenceParams struct { diff --git a/api/v2/client/silence/delete_silence_responses.go b/api/v2/client/silence/delete_silence_responses.go index 848c53dc03..704b26ace7 100644 --- a/api/v2/client/silence/delete_silence_responses.go +++ b/api/v2/client/silence/delete_silence_responses.go @@ -58,7 +58,8 @@ func NewDeleteSilenceOK() *DeleteSilenceOK { return &DeleteSilenceOK{} } -/*DeleteSilenceOK handles this case with default header values. +/* +DeleteSilenceOK handles this case with default header values. Delete silence response */ @@ -79,7 +80,8 @@ func NewDeleteSilenceInternalServerError() *DeleteSilenceInternalServerError { return &DeleteSilenceInternalServerError{} } -/*DeleteSilenceInternalServerError handles this case with default header values. +/* +DeleteSilenceInternalServerError handles this case with default header values. Internal server error */ diff --git a/api/v2/client/silence/get_silence_parameters.go b/api/v2/client/silence/get_silence_parameters.go index e8cb7f00e2..5ecc340a9c 100644 --- a/api/v2/client/silence/get_silence_parameters.go +++ b/api/v2/client/silence/get_silence_parameters.go @@ -69,7 +69,8 @@ func NewGetSilenceParamsWithHTTPClient(client *http.Client) *GetSilenceParams { } } -/*GetSilenceParams contains all the parameters to send to the API endpoint +/* +GetSilenceParams contains all the parameters to send to the API endpoint for the get silence operation typically these are written to a http.Request */ type GetSilenceParams struct { diff --git a/api/v2/client/silence/get_silence_responses.go b/api/v2/client/silence/get_silence_responses.go index 7fc3f53da6..4d7f88afea 100644 --- a/api/v2/client/silence/get_silence_responses.go +++ b/api/v2/client/silence/get_silence_responses.go @@ -66,7 +66,8 @@ func NewGetSilenceOK() *GetSilenceOK { return &GetSilenceOK{} } -/*GetSilenceOK handles this case with default header values. +/* +GetSilenceOK handles this case with default header values. Get silence response */ @@ -99,7 +100,8 @@ func NewGetSilenceNotFound() *GetSilenceNotFound { return &GetSilenceNotFound{} } -/*GetSilenceNotFound handles this case with default header values. +/* +GetSilenceNotFound handles this case with default header values. A silence with the specified ID was not found */ @@ -120,7 +122,8 @@ func NewGetSilenceInternalServerError() *GetSilenceInternalServerError { return &GetSilenceInternalServerError{} } -/*GetSilenceInternalServerError handles this case with default header values. +/* +GetSilenceInternalServerError handles this case with default header values. Internal server error */ diff --git a/api/v2/client/silence/get_silences_parameters.go b/api/v2/client/silence/get_silences_parameters.go index 940fd8e9ef..5d0d5ec1a3 100644 --- a/api/v2/client/silence/get_silences_parameters.go +++ b/api/v2/client/silence/get_silences_parameters.go @@ -70,7 +70,8 @@ func NewGetSilencesParamsWithHTTPClient(client *http.Client) *GetSilencesParams } } -/*GetSilencesParams contains all the parameters to send to the API endpoint +/* +GetSilencesParams contains all the parameters to send to the API endpoint for the get silences operation typically these are written to a http.Request */ type GetSilencesParams struct { diff --git a/api/v2/client/silence/get_silences_responses.go b/api/v2/client/silence/get_silences_responses.go index 819a242793..827847f5e2 100644 --- a/api/v2/client/silence/get_silences_responses.go +++ b/api/v2/client/silence/get_silences_responses.go @@ -60,7 +60,8 @@ func NewGetSilencesOK() *GetSilencesOK { return &GetSilencesOK{} } -/*GetSilencesOK handles this case with default header values. +/* +GetSilencesOK handles this case with default header values. Get silences response */ @@ -91,7 +92,8 @@ func NewGetSilencesInternalServerError() *GetSilencesInternalServerError { return &GetSilencesInternalServerError{} } -/*GetSilencesInternalServerError handles this case with default header values. +/* +GetSilencesInternalServerError handles this case with default header values. Internal server error */ diff --git a/api/v2/client/silence/post_silences_parameters.go b/api/v2/client/silence/post_silences_parameters.go index 5fadff8e2e..d63b9bfbd5 100644 --- a/api/v2/client/silence/post_silences_parameters.go +++ b/api/v2/client/silence/post_silences_parameters.go @@ -71,7 +71,8 @@ func NewPostSilencesParamsWithHTTPClient(client *http.Client) *PostSilencesParam } } -/*PostSilencesParams contains all the parameters to send to the API endpoint +/* +PostSilencesParams contains all the parameters to send to the API endpoint for the post silences operation typically these are written to a http.Request */ type PostSilencesParams struct { diff --git a/api/v2/client/silence/post_silences_responses.go b/api/v2/client/silence/post_silences_responses.go index c71931f73a..0f15f0d3f9 100644 --- a/api/v2/client/silence/post_silences_responses.go +++ b/api/v2/client/silence/post_silences_responses.go @@ -65,7 +65,8 @@ func NewPostSilencesOK() *PostSilencesOK { return &PostSilencesOK{} } -/*PostSilencesOK handles this case with default header values. +/* +PostSilencesOK handles this case with default header values. Create / update silence response */ @@ -98,7 +99,8 @@ func NewPostSilencesBadRequest() *PostSilencesBadRequest { return &PostSilencesBadRequest{} } -/*PostSilencesBadRequest handles this case with default header values. +/* +PostSilencesBadRequest handles this case with default header values. Bad request */ @@ -129,7 +131,8 @@ func NewPostSilencesNotFound() *PostSilencesNotFound { return &PostSilencesNotFound{} } -/*PostSilencesNotFound handles this case with default header values. +/* +PostSilencesNotFound handles this case with default header values. A silence with the specified ID was not found */ @@ -155,7 +158,8 @@ func (o *PostSilencesNotFound) readResponse(response runtime.ClientResponse, con return nil } -/*PostSilencesOKBody post silences o k body +/* +PostSilencesOKBody post silences o k body swagger:model PostSilencesOKBody */ type PostSilencesOKBody struct { diff --git a/api/v2/client/silence/silence_client.go b/api/v2/client/silence/silence_client.go index b601f9d8c9..cba92be67d 100644 --- a/api/v2/client/silence/silence_client.go +++ b/api/v2/client/silence/silence_client.go @@ -53,7 +53,7 @@ type ClientService interface { } /* - DeleteSilence Delete a silence by its ID +DeleteSilence Delete a silence by its ID */ func (a *Client) DeleteSilence(params *DeleteSilenceParams) (*DeleteSilenceOK, error) { // TODO: Validate the params before sending @@ -87,7 +87,7 @@ func (a *Client) DeleteSilence(params *DeleteSilenceParams) (*DeleteSilenceOK, e } /* - GetSilence Get a silence by its ID +GetSilence Get a silence by its ID */ func (a *Client) GetSilence(params *GetSilenceParams) (*GetSilenceOK, error) { // TODO: Validate the params before sending @@ -121,7 +121,7 @@ func (a *Client) GetSilence(params *GetSilenceParams) (*GetSilenceOK, error) { } /* - GetSilences Get a list of silences +GetSilences Get a list of silences */ func (a *Client) GetSilences(params *GetSilencesParams) (*GetSilencesOK, error) { // TODO: Validate the params before sending @@ -155,7 +155,7 @@ func (a *Client) GetSilences(params *GetSilencesParams) (*GetSilencesOK, error) } /* - PostSilences Post a new silence or update an existing one +PostSilences Post a new silence or update an existing one */ func (a *Client) PostSilences(params *PostSilencesParams) (*PostSilencesOK, error) { // TODO: Validate the params before sending diff --git a/api/v2/restapi/doc.go b/api/v2/restapi/doc.go index d8ee687c48..ea249db26c 100644 --- a/api/v2/restapi/doc.go +++ b/api/v2/restapi/doc.go @@ -15,19 +15,19 @@ // Package restapi Alertmanager API // -// API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager) -// Schemes: -// http -// Host: localhost -// BasePath: /api/v2/ -// Version: 0.0.1 -// License: Apache 2.0 http://www.apache.org/licenses/LICENSE-2.0.html +// API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager) +// Schemes: +// http +// Host: localhost +// BasePath: /api/v2/ +// Version: 0.0.1 +// License: Apache 2.0 http://www.apache.org/licenses/LICENSE-2.0.html // -// Consumes: -// - application/json +// Consumes: +// - application/json // -// Produces: -// - application/json +// Produces: +// - application/json // // swagger:meta package restapi diff --git a/api/v2/restapi/operations/alert/get_alerts.go b/api/v2/restapi/operations/alert/get_alerts.go index 27caf27b11..df6d4d64ec 100644 --- a/api/v2/restapi/operations/alert/get_alerts.go +++ b/api/v2/restapi/operations/alert/get_alerts.go @@ -43,10 +43,10 @@ func NewGetAlerts(ctx *middleware.Context, handler GetAlertsHandler) *GetAlerts return &GetAlerts{Context: ctx, Handler: handler} } -/*GetAlerts swagger:route GET /alerts alert getAlerts +/* +GetAlerts swagger:route GET /alerts alert getAlerts Get a list of alerts - */ type GetAlerts struct { Context *middleware.Context diff --git a/api/v2/restapi/operations/alert/get_alerts_responses.go b/api/v2/restapi/operations/alert/get_alerts_responses.go index 8c4f4b8f55..1a4d19415c 100644 --- a/api/v2/restapi/operations/alert/get_alerts_responses.go +++ b/api/v2/restapi/operations/alert/get_alerts_responses.go @@ -30,7 +30,8 @@ import ( // GetAlertsOKCode is the HTTP code returned for type GetAlertsOK const GetAlertsOKCode int = 200 -/*GetAlertsOK Get alerts response +/* +GetAlertsOK Get alerts response swagger:response getAlertsOK */ @@ -77,7 +78,8 @@ func (o *GetAlertsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Pro // GetAlertsBadRequestCode is the HTTP code returned for type GetAlertsBadRequest const GetAlertsBadRequestCode int = 400 -/*GetAlertsBadRequest Bad request +/* +GetAlertsBadRequest Bad request swagger:response getAlertsBadRequest */ @@ -119,7 +121,8 @@ func (o *GetAlertsBadRequest) WriteResponse(rw http.ResponseWriter, producer run // GetAlertsInternalServerErrorCode is the HTTP code returned for type GetAlertsInternalServerError const GetAlertsInternalServerErrorCode int = 500 -/*GetAlertsInternalServerError Internal server error +/* +GetAlertsInternalServerError Internal server error swagger:response getAlertsInternalServerError */ diff --git a/api/v2/restapi/operations/alert/post_alerts.go b/api/v2/restapi/operations/alert/post_alerts.go index 858db94bbc..25860564f1 100644 --- a/api/v2/restapi/operations/alert/post_alerts.go +++ b/api/v2/restapi/operations/alert/post_alerts.go @@ -43,10 +43,10 @@ func NewPostAlerts(ctx *middleware.Context, handler PostAlertsHandler) *PostAler return &PostAlerts{Context: ctx, Handler: handler} } -/*PostAlerts swagger:route POST /alerts alert postAlerts +/* +PostAlerts swagger:route POST /alerts alert postAlerts Create new Alerts - */ type PostAlerts struct { Context *middleware.Context diff --git a/api/v2/restapi/operations/alert/post_alerts_responses.go b/api/v2/restapi/operations/alert/post_alerts_responses.go index 7e273c3106..87302a9800 100644 --- a/api/v2/restapi/operations/alert/post_alerts_responses.go +++ b/api/v2/restapi/operations/alert/post_alerts_responses.go @@ -28,7 +28,8 @@ import ( // PostAlertsOKCode is the HTTP code returned for type PostAlertsOK const PostAlertsOKCode int = 200 -/*PostAlertsOK Create alerts response +/* +PostAlertsOK Create alerts response swagger:response postAlertsOK */ @@ -52,7 +53,8 @@ func (o *PostAlertsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Pr // PostAlertsBadRequestCode is the HTTP code returned for type PostAlertsBadRequest const PostAlertsBadRequestCode int = 400 -/*PostAlertsBadRequest Bad request +/* +PostAlertsBadRequest Bad request swagger:response postAlertsBadRequest */ @@ -94,7 +96,8 @@ func (o *PostAlertsBadRequest) WriteResponse(rw http.ResponseWriter, producer ru // PostAlertsInternalServerErrorCode is the HTTP code returned for type PostAlertsInternalServerError const PostAlertsInternalServerErrorCode int = 500 -/*PostAlertsInternalServerError Internal server error +/* +PostAlertsInternalServerError Internal server error swagger:response postAlertsInternalServerError */ diff --git a/api/v2/restapi/operations/alertgroup/get_alert_groups.go b/api/v2/restapi/operations/alertgroup/get_alert_groups.go index 3ea3975d53..f355c45d00 100644 --- a/api/v2/restapi/operations/alertgroup/get_alert_groups.go +++ b/api/v2/restapi/operations/alertgroup/get_alert_groups.go @@ -43,10 +43,10 @@ func NewGetAlertGroups(ctx *middleware.Context, handler GetAlertGroupsHandler) * return &GetAlertGroups{Context: ctx, Handler: handler} } -/*GetAlertGroups swagger:route GET /alerts/groups alertgroup getAlertGroups +/* +GetAlertGroups swagger:route GET /alerts/groups alertgroup getAlertGroups Get a list of alert groups - */ type GetAlertGroups struct { Context *middleware.Context diff --git a/api/v2/restapi/operations/alertgroup/get_alert_groups_responses.go b/api/v2/restapi/operations/alertgroup/get_alert_groups_responses.go index bb0d90faee..3b9227aaaa 100644 --- a/api/v2/restapi/operations/alertgroup/get_alert_groups_responses.go +++ b/api/v2/restapi/operations/alertgroup/get_alert_groups_responses.go @@ -30,7 +30,8 @@ import ( // GetAlertGroupsOKCode is the HTTP code returned for type GetAlertGroupsOK const GetAlertGroupsOKCode int = 200 -/*GetAlertGroupsOK Get alert groups response +/* +GetAlertGroupsOK Get alert groups response swagger:response getAlertGroupsOK */ @@ -77,7 +78,8 @@ func (o *GetAlertGroupsOK) WriteResponse(rw http.ResponseWriter, producer runtim // GetAlertGroupsBadRequestCode is the HTTP code returned for type GetAlertGroupsBadRequest const GetAlertGroupsBadRequestCode int = 400 -/*GetAlertGroupsBadRequest Bad request +/* +GetAlertGroupsBadRequest Bad request swagger:response getAlertGroupsBadRequest */ @@ -119,7 +121,8 @@ func (o *GetAlertGroupsBadRequest) WriteResponse(rw http.ResponseWriter, produce // GetAlertGroupsInternalServerErrorCode is the HTTP code returned for type GetAlertGroupsInternalServerError const GetAlertGroupsInternalServerErrorCode int = 500 -/*GetAlertGroupsInternalServerError Internal server error +/* +GetAlertGroupsInternalServerError Internal server error swagger:response getAlertGroupsInternalServerError */ diff --git a/api/v2/restapi/operations/general/get_status.go b/api/v2/restapi/operations/general/get_status.go index 8b6a6f5c7c..8579c67eb8 100644 --- a/api/v2/restapi/operations/general/get_status.go +++ b/api/v2/restapi/operations/general/get_status.go @@ -43,10 +43,10 @@ func NewGetStatus(ctx *middleware.Context, handler GetStatusHandler) *GetStatus return &GetStatus{Context: ctx, Handler: handler} } -/*GetStatus swagger:route GET /status general getStatus +/* +GetStatus swagger:route GET /status general getStatus Get current status of an Alertmanager instance and its cluster - */ type GetStatus struct { Context *middleware.Context diff --git a/api/v2/restapi/operations/general/get_status_responses.go b/api/v2/restapi/operations/general/get_status_responses.go index 60d9e62075..ddfc323df6 100644 --- a/api/v2/restapi/operations/general/get_status_responses.go +++ b/api/v2/restapi/operations/general/get_status_responses.go @@ -30,7 +30,8 @@ import ( // GetStatusOKCode is the HTTP code returned for type GetStatusOK const GetStatusOKCode int = 200 -/*GetStatusOK Get status response +/* +GetStatusOK Get status response swagger:response getStatusOK */ diff --git a/api/v2/restapi/operations/receiver/get_receivers.go b/api/v2/restapi/operations/receiver/get_receivers.go index 40197940a2..35da8baf44 100644 --- a/api/v2/restapi/operations/receiver/get_receivers.go +++ b/api/v2/restapi/operations/receiver/get_receivers.go @@ -43,10 +43,10 @@ func NewGetReceivers(ctx *middleware.Context, handler GetReceiversHandler) *GetR return &GetReceivers{Context: ctx, Handler: handler} } -/*GetReceivers swagger:route GET /receivers receiver getReceivers +/* +GetReceivers swagger:route GET /receivers receiver getReceivers Get list of all receivers (name of notification integrations) - */ type GetReceivers struct { Context *middleware.Context diff --git a/api/v2/restapi/operations/receiver/get_receivers_responses.go b/api/v2/restapi/operations/receiver/get_receivers_responses.go index ee0e6f2c31..0727afc9fb 100644 --- a/api/v2/restapi/operations/receiver/get_receivers_responses.go +++ b/api/v2/restapi/operations/receiver/get_receivers_responses.go @@ -30,7 +30,8 @@ import ( // GetReceiversOKCode is the HTTP code returned for type GetReceiversOK const GetReceiversOKCode int = 200 -/*GetReceiversOK Get receivers response +/* +GetReceiversOK Get receivers response swagger:response getReceiversOK */ diff --git a/api/v2/restapi/operations/silence/delete_silence.go b/api/v2/restapi/operations/silence/delete_silence.go index d0a227484a..5692e931b5 100644 --- a/api/v2/restapi/operations/silence/delete_silence.go +++ b/api/v2/restapi/operations/silence/delete_silence.go @@ -43,10 +43,10 @@ func NewDeleteSilence(ctx *middleware.Context, handler DeleteSilenceHandler) *De return &DeleteSilence{Context: ctx, Handler: handler} } -/*DeleteSilence swagger:route DELETE /silence/{silenceID} silence deleteSilence +/* +DeleteSilence swagger:route DELETE /silence/{silenceID} silence deleteSilence Delete a silence by its ID - */ type DeleteSilence struct { Context *middleware.Context diff --git a/api/v2/restapi/operations/silence/delete_silence_responses.go b/api/v2/restapi/operations/silence/delete_silence_responses.go index de87e29a3b..0878e35dd3 100644 --- a/api/v2/restapi/operations/silence/delete_silence_responses.go +++ b/api/v2/restapi/operations/silence/delete_silence_responses.go @@ -28,7 +28,8 @@ import ( // DeleteSilenceOKCode is the HTTP code returned for type DeleteSilenceOK const DeleteSilenceOKCode int = 200 -/*DeleteSilenceOK Delete silence response +/* +DeleteSilenceOK Delete silence response swagger:response deleteSilenceOK */ @@ -52,7 +53,8 @@ func (o *DeleteSilenceOK) WriteResponse(rw http.ResponseWriter, producer runtime // DeleteSilenceInternalServerErrorCode is the HTTP code returned for type DeleteSilenceInternalServerError const DeleteSilenceInternalServerErrorCode int = 500 -/*DeleteSilenceInternalServerError Internal server error +/* +DeleteSilenceInternalServerError Internal server error swagger:response deleteSilenceInternalServerError */ diff --git a/api/v2/restapi/operations/silence/get_silence.go b/api/v2/restapi/operations/silence/get_silence.go index afaf6dd5f4..e19b12b63b 100644 --- a/api/v2/restapi/operations/silence/get_silence.go +++ b/api/v2/restapi/operations/silence/get_silence.go @@ -43,10 +43,10 @@ func NewGetSilence(ctx *middleware.Context, handler GetSilenceHandler) *GetSilen return &GetSilence{Context: ctx, Handler: handler} } -/*GetSilence swagger:route GET /silence/{silenceID} silence getSilence +/* +GetSilence swagger:route GET /silence/{silenceID} silence getSilence Get a silence by its ID - */ type GetSilence struct { Context *middleware.Context diff --git a/api/v2/restapi/operations/silence/get_silence_responses.go b/api/v2/restapi/operations/silence/get_silence_responses.go index 5ece7ad73c..51255fe83c 100644 --- a/api/v2/restapi/operations/silence/get_silence_responses.go +++ b/api/v2/restapi/operations/silence/get_silence_responses.go @@ -30,7 +30,8 @@ import ( // GetSilenceOKCode is the HTTP code returned for type GetSilenceOK const GetSilenceOKCode int = 200 -/*GetSilenceOK Get silence response +/* +GetSilenceOK Get silence response swagger:response getSilenceOK */ @@ -74,7 +75,8 @@ func (o *GetSilenceOK) WriteResponse(rw http.ResponseWriter, producer runtime.Pr // GetSilenceNotFoundCode is the HTTP code returned for type GetSilenceNotFound const GetSilenceNotFoundCode int = 404 -/*GetSilenceNotFound A silence with the specified ID was not found +/* +GetSilenceNotFound A silence with the specified ID was not found swagger:response getSilenceNotFound */ @@ -98,7 +100,8 @@ func (o *GetSilenceNotFound) WriteResponse(rw http.ResponseWriter, producer runt // GetSilenceInternalServerErrorCode is the HTTP code returned for type GetSilenceInternalServerError const GetSilenceInternalServerErrorCode int = 500 -/*GetSilenceInternalServerError Internal server error +/* +GetSilenceInternalServerError Internal server error swagger:response getSilenceInternalServerError */ diff --git a/api/v2/restapi/operations/silence/get_silences.go b/api/v2/restapi/operations/silence/get_silences.go index 0346a421b8..be03f28ca1 100644 --- a/api/v2/restapi/operations/silence/get_silences.go +++ b/api/v2/restapi/operations/silence/get_silences.go @@ -43,10 +43,10 @@ func NewGetSilences(ctx *middleware.Context, handler GetSilencesHandler) *GetSil return &GetSilences{Context: ctx, Handler: handler} } -/*GetSilences swagger:route GET /silences silence getSilences +/* +GetSilences swagger:route GET /silences silence getSilences Get a list of silences - */ type GetSilences struct { Context *middleware.Context diff --git a/api/v2/restapi/operations/silence/get_silences_responses.go b/api/v2/restapi/operations/silence/get_silences_responses.go index 314edf7b21..f2f7b88f74 100644 --- a/api/v2/restapi/operations/silence/get_silences_responses.go +++ b/api/v2/restapi/operations/silence/get_silences_responses.go @@ -30,7 +30,8 @@ import ( // GetSilencesOKCode is the HTTP code returned for type GetSilencesOK const GetSilencesOKCode int = 200 -/*GetSilencesOK Get silences response +/* +GetSilencesOK Get silences response swagger:response getSilencesOK */ @@ -77,7 +78,8 @@ func (o *GetSilencesOK) WriteResponse(rw http.ResponseWriter, producer runtime.P // GetSilencesInternalServerErrorCode is the HTTP code returned for type GetSilencesInternalServerError const GetSilencesInternalServerErrorCode int = 500 -/*GetSilencesInternalServerError Internal server error +/* +GetSilencesInternalServerError Internal server error swagger:response getSilencesInternalServerError */ diff --git a/api/v2/restapi/operations/silence/post_silences.go b/api/v2/restapi/operations/silence/post_silences.go index 98cc0576d4..e8f8dc131d 100644 --- a/api/v2/restapi/operations/silence/post_silences.go +++ b/api/v2/restapi/operations/silence/post_silences.go @@ -45,10 +45,10 @@ func NewPostSilences(ctx *middleware.Context, handler PostSilencesHandler) *Post return &PostSilences{Context: ctx, Handler: handler} } -/*PostSilences swagger:route POST /silences silence postSilences +/* +PostSilences swagger:route POST /silences silence postSilences Post a new silence or update an existing one - */ type PostSilences struct { Context *middleware.Context diff --git a/api/v2/restapi/operations/silence/post_silences_responses.go b/api/v2/restapi/operations/silence/post_silences_responses.go index 7570855d27..a20e89a6ee 100644 --- a/api/v2/restapi/operations/silence/post_silences_responses.go +++ b/api/v2/restapi/operations/silence/post_silences_responses.go @@ -28,7 +28,8 @@ import ( // PostSilencesOKCode is the HTTP code returned for type PostSilencesOK const PostSilencesOKCode int = 200 -/*PostSilencesOK Create / update silence response +/* +PostSilencesOK Create / update silence response swagger:response postSilencesOK */ @@ -72,7 +73,8 @@ func (o *PostSilencesOK) WriteResponse(rw http.ResponseWriter, producer runtime. // PostSilencesBadRequestCode is the HTTP code returned for type PostSilencesBadRequest const PostSilencesBadRequestCode int = 400 -/*PostSilencesBadRequest Bad request +/* +PostSilencesBadRequest Bad request swagger:response postSilencesBadRequest */ @@ -114,7 +116,8 @@ func (o *PostSilencesBadRequest) WriteResponse(rw http.ResponseWriter, producer // PostSilencesNotFoundCode is the HTTP code returned for type PostSilencesNotFound const PostSilencesNotFoundCode int = 404 -/*PostSilencesNotFound A silence with the specified ID was not found +/* +PostSilencesNotFound A silence with the specified ID was not found swagger:response postSilencesNotFound */ diff --git a/ci/docker/Dockerfile b/ci/docker/Dockerfile new file mode 100644 index 0000000000..4ef7dd72a1 --- /dev/null +++ b/ci/docker/Dockerfile @@ -0,0 +1,26 @@ +# Build image +FROM nexus.adsrv.wtf/click/golang:1.18.3-202206230758 as build + +WORKDIR /app +COPY ./ ./ +RUN make build + +# Main image +FROM quay.io/prometheus/busybox-linux-amd64:latest +LABEL maintainer="The Prometheus Authors " + + +COPY --from=build /app/amtool /bin/amtool +COPY --from=build /app/alertmanager /bin/alertmanager +COPY --from=build /app/examples/ha/alertmanager.yml /etc/alertmanager/alertmanager.yml + +RUN mkdir -p /alertmanager && \ + chown -R nobody:nobody etc/alertmanager /alertmanager + +USER nobody +EXPOSE 9093 +VOLUME [ "/alertmanager" ] +WORKDIR /alertmanager +ENTRYPOINT [ "/bin/alertmanager" ] +CMD [ "--config.file=/etc/alertmanager/alertmanager.yml", \ + "--storage.path=/alertmanager" ] diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go index d8d3ddfe76..f0bc02a2f7 100644 --- a/cmd/alertmanager/main.go +++ b/cmd/alertmanager/main.go @@ -16,6 +16,9 @@ package main import ( "context" "fmt" + "github.com/prometheus/alertmanager/notify/sigma" + "github.com/prometheus/alertmanager/notify/slackV2" + "github.com/prometheus/alertmanager/notify/twilio" "net" "net/http" "net/url" @@ -161,6 +164,9 @@ func buildReceiverIntegrations(nc *config.Receiver, tmpl *template.Template, log for i, c := range nc.SlackConfigs { add("slack", i, c, func(l log.Logger) (notify.Notifier, error) { return slack.New(c, tmpl, l) }) } + for i, c := range nc.SlackConfigV2 { + add("slackV2", i, c, func(l log.Logger) (notify.Notifier, error) { return slackV2.New(c, tmpl, l) }) + } for i, c := range nc.VictorOpsConfigs { add("victorops", i, c, func(l log.Logger) (notify.Notifier, error) { return victorops.New(c, tmpl, l) }) } @@ -170,6 +176,12 @@ func buildReceiverIntegrations(nc *config.Receiver, tmpl *template.Template, log for i, c := range nc.SNSConfigs { add("sns", i, c, func(l log.Logger) (notify.Notifier, error) { return sns.New(c, tmpl, l) }) } + for i, c := range nc.SigmaConfigs { + add("sigma", i, c, func(l log.Logger) (notify.Notifier, error) { return sigma.New(c, tmpl, l) }) + } + for i, c := range nc.TwilioConfigs { + add("twilio", i, c, func(l log.Logger) (notify.Notifier, error) { return twilio.New(c, tmpl, l) }) + } for i, c := range nc.TelegramConfigs { add("telegram", i, c, func(l log.Logger) (notify.Notifier, error) { return telegram.New(c, tmpl, l) }) } @@ -232,7 +244,7 @@ func run() int { level.Info(logger).Log("msg", "Starting Alertmanager", "version", version.Info()) level.Info(logger).Log("build_context", version.BuildContext()) - err := os.MkdirAll(*dataDir, 0o777) + err := os.MkdirAll(*dataDir, 0777) if err != nil { level.Error(logger).Log("msg", "Unable to create data directory", "err", err) return 1 @@ -374,6 +386,7 @@ func run() int { Registry: prometheus.DefaultRegisterer, GroupFunc: groupFn, }) + if err != nil { level.Error(logger).Log("err", errors.Wrap(err, "failed to create API")) return 1 @@ -528,6 +541,20 @@ func run() int { ui.Register(router, webReload, logger) + router.Get("/callback/twilio", func(w http.ResponseWriter, req *http.Request) { + id := req.URL.Query().Get("id") + twilio.Storage.Get(id) + w.Header().Add("Content-Type", "text/xml") + if val := twilio.Storage.Get(id); val == nil { + w.WriteHeader(http.StatusNotFound) + return + } else { + w.WriteHeader(http.StatusOK) + w.Write(val.Data) + return + } + }) + mux := api.Register(router, *routePrefix) srv := &http.Server{Addr: *listenAddress, Handler: mux} diff --git a/config/config.go b/config/config.go index efc1a9f3c6..60bc72bfc0 100644 --- a/config/config.go +++ b/config/config.go @@ -248,6 +248,9 @@ func resolveFilepaths(baseDir string, cfg *Config) { for _, cfg := range receiver.TelegramConfigs { cfg.HTTPConfig.SetDirectory(baseDir) } + for _, cfg := range receiver.SigmaConfigs { + cfg.HTTPConfig.SetDirectory(baseDir) + } } } @@ -483,7 +486,6 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { sns.HTTPConfig = c.Global.HTTPConfig } } - for _, telegram := range rcv.TelegramConfigs { if telegram.HTTPConfig == nil { telegram.HTTPConfig = c.Global.HTTPConfig @@ -492,7 +494,6 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { telegram.APIUrl = c.Global.TelegramAPIUrl } } - names[rcv.Name] = struct{}{} } @@ -847,12 +848,15 @@ type Receiver struct { EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"` PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"` SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` + SlackConfigV2 []*SlackConfigV2 `yaml:"slackV2_configs,omitempty" json:"slackV2_configs,omitempty"` WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"` WechatConfigs []*WechatConfig `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"` PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"` SNSConfigs []*SNSConfig `yaml:"sns_configs,omitempty" json:"sns_configs,omitempty"` + SigmaConfigs []*SigmaConfig `yaml:"sigma_configs,omitempty" json:"sigma_configs,omitempty"` + TwilioConfigs []*TwilioConfig `yaml:"twilio_configs,omitempty" json:"twilio_configs,omitempty"` TelegramConfigs []*TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"` } diff --git a/config/notifiers.go b/config/notifiers.go index 95df28db43..a583ac6b45 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -15,6 +15,7 @@ package config import ( "fmt" + "net/url" "regexp" "strings" "time" @@ -34,6 +35,18 @@ var ( }, } + DefaultSigmaConfig = SigmaConfig{ + NotifierConfig: NotifierConfig{ + VSendResolved: true, + }, + } + + DefaultTwilioConfig = TwilioConfig{ + NotifierConfig: NotifierConfig{ + VSendResolved: true, + }, + } + // DefaultEmailConfig defines default values for Email configurations. DefaultEmailConfig = EmailConfig{ NotifierConfig: NotifierConfig{ @@ -82,6 +95,14 @@ var ( Footer: `{{ template "slack.default.footer" . }}`, } + // DefaultSlackV2Config defines default values for Slack configurations. + DefaultSlackV2Config = SlackConfigV2{ + NotifierConfig: NotifierConfig{ + VSendResolved: true, + }, + Color: `{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}`, + } + // DefaultOpsGenieConfig defines default values for OpsGenie configurations. DefaultOpsGenieConfig = OpsGenieConfig{ NotifierConfig: NotifierConfig{ @@ -90,7 +111,6 @@ var ( Message: `{{ template "opsgenie.default.message" . }}`, Description: `{{ template "opsgenie.default.description" . }}`, Source: `{{ template "opsgenie.default.source" . }}`, - // TODO: Add a details field with all the alerts. } // DefaultWechatConfig defines default values for wechat configurations. @@ -391,6 +411,37 @@ func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +type SlackConfigV2 struct { + NotifierConfig `yaml:",inline" json:",inline"` + + Token Secret `yaml:"token,omitempty" json:"token,omitempty"` + GrafanaToken Secret `yaml:"grafana_token,omitempty" json:"grafana_token,omitempty"` + UserToken Secret `yaml:"user_token,omitempty" json:"user_token,omitempty"` + GrafanaUrl string `yaml:"grafana_url,omitempty" json:"grafana_url,omitempty"` + GrafanaTZ string `yaml:"grafana_tz,omitempty" json:"grafana_tz,omitempty"` + Channel string `yaml:"channel,omitempty" json:"channel,omitempty"` + Color string `yaml:"color,omitempty" json:"color,omitempty"` + Debug bool `yaml:"debug" json:"debug"` + Mentions []SlackMention `yaml:"mentions,omitempty" json:"mentions,omitempty"` + MentionDelay duration `yaml:"mentionDelay" json:"mentionDelay"` +} + +type SlackMention struct { + Type string `yaml:"type" json:"type"` + Name string `yaml:"name" json:"name"` + ID string `yaml:"id" json:"id"` +} + +func (c *SlackConfigV2) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultSlackV2Config + type plain SlackConfigV2 + if err := unmarshal((*plain)(c)); err != nil { + return err + } + + return nil +} + // WebhookConfig configures notifications via a generic webhook. type WebhookConfig struct { NotifierConfig `yaml:",inline" json:",inline"` @@ -421,21 +472,102 @@ func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } -// WechatConfig configures notifications via Wechat. -type WechatConfig struct { +type SigmaConfig struct { NotifierConfig `yaml:",inline" json:",inline"` + HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + // URL to send POST request to. + URL *URL `yaml:"url" json:"url"` + APIKey Secret `yaml:"api_key" json:"api_key"` + Recipient []string `yaml:"recipient" json:"recipient"` + NotificationType string `yaml:"notification_type" json:"notification_type"` + SenderName string `yaml:"sender_name" json:"sender_name"` + Text string `yaml:"text" json:"text"` + TTS string `yaml:"tts" json:"tts"` +} - HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *SigmaConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultSigmaConfig + type plain SigmaConfig + if err := unmarshal((*plain)(c)); err != nil { + return err + } + if c.URL == nil { + defaultUrl, _ := url.Parse("https://online.sigmasms.ru/api/sendings") + c.URL = &URL{URL: defaultUrl} + } + if c.APIKey == "" { + return fmt.Errorf("api_key must be configured") + } + switch strings.ToLower(c.NotificationType) { + case "sms", "": + c.NotificationType = "sms" + case "voice": + c.NotificationType = "voice" + default: + return fmt.Errorf("unknown notification type: %s", c.NotificationType) + } + if c.TTS == "" { + c.TTS = "yandex:alena" + } + return nil +} + +type TwilioConfig struct { + NotifierConfig `yaml:",inline" json:",inline"` + HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + AccountID string `yaml:"account_id" json:"account_id"` + Token Secret `yaml:"token" json:"token"` + AlertManagerUrl *URL `yaml:"alert_manager_url" json:"alert_manager_url"` + PlayFileUrl *URL `yaml:"play_file_url" json:"play_file_url"` + Recipient []string `yaml:"recipient" json:"recipient"` + NotificationType string `yaml:"notification_type" json:"notification_type"` + SenderName string `yaml:"sender_name" json:"sender_name"` + Text string `yaml:"text" json:"text"` +} - APISecret Secret `yaml:"api_secret,omitempty" json:"api_secret,omitempty"` - CorpID string `yaml:"corp_id,omitempty" json:"corp_id,omitempty"` - Message string `yaml:"message,omitempty" json:"message,omitempty"` - APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` - ToUser string `yaml:"to_user,omitempty" json:"to_user,omitempty"` - ToParty string `yaml:"to_party,omitempty" json:"to_party,omitempty"` - ToTag string `yaml:"to_tag,omitempty" json:"to_tag,omitempty"` - AgentID string `yaml:"agent_id,omitempty" json:"agent_id,omitempty"` - MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"` +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *TwilioConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultTwilioConfig + type plain TwilioConfig + if err := unmarshal((*plain)(c)); err != nil { + return err + } + + if c.AccountID == "" { + return fmt.Errorf("account_id must be configured") + } + if c.Token == "" { + return fmt.Errorf("token must be configured") + } + switch strings.ToLower(c.NotificationType) { + case "sms", "": + c.NotificationType = "sms" + case "voice": + c.NotificationType = "voice" + if c.AlertManagerUrl == nil { + return fmt.Errorf("missing Alert manager URL in Twilio voice config") + } + default: + return fmt.Errorf("unknown notification type: %s", c.NotificationType) + } + + return nil +} + +// WechatConfig configures notifications via Wechat. +type WechatConfig struct { + NotifierConfig `yaml:",inline" json:",inline"` + HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + APISecret Secret `yaml:"api_secret,omitempty" json:"api_secret,omitempty"` + CorpID string `yaml:"corp_id,omitempty" json:"corp_id,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` + ToUser string `yaml:"to_user,omitempty" json:"to_user,omitempty"` + ToParty string `yaml:"to_party,omitempty" json:"to_party,omitempty"` + ToTag string `yaml:"to_tag,omitempty" json:"to_tag,omitempty"` + AgentID string `yaml:"agent_id,omitempty" json:"agent_id,omitempty"` + MessageType string `yaml:"message_type,omitempty" json:"message_type,omitempty"` } const wechatValidTypesRe = `^(text|markdown)$` diff --git a/go.mod b/go.mod index 017d76b35f..408d5f9537 100644 --- a/go.mod +++ b/go.mod @@ -18,9 +18,10 @@ require ( github.com/go-openapi/validate v0.22.0 github.com/gofrs/uuid v4.2.0+incompatible github.com/gogo/protobuf v1.3.2 + github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/go-sockaddr v1.0.2 github.com/hashicorp/golang-lru v0.5.4 - github.com/hashicorp/memberlist v0.3.2 + github.com/hashicorp/memberlist v0.5.0 github.com/jessevdk/go-flags v1.5.0 github.com/kylelemons/godebug v1.1.0 github.com/matttproud/golang_protobuf_extensions v1.0.1 @@ -32,9 +33,12 @@ require ( github.com/prometheus/common/sigv4 v0.1.0 github.com/prometheus/exporter-toolkit v0.7.1 github.com/rs/cors v1.8.2 + github.com/satori/go.uuid v1.2.0 github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 + github.com/slack-go/slack v0.10.4-0.20220503131901-0e14c9d4a15c github.com/stretchr/testify v1.8.0 + github.com/twilio/twilio-go v0.26.0 github.com/xlab/treeprint v1.1.0 go.uber.org/atomic v1.9.0 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 @@ -57,6 +61,7 @@ require ( github.com/go-openapi/analysis v0.21.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/btree v1.0.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect @@ -79,7 +84,7 @@ require ( go.mongodb.org/mongo-driver v1.10.0 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect google.golang.org/appengine v1.6.6 // indirect google.golang.org/protobuf v1.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 9d9f4f974e..c29893db62 100644 --- a/go.sum +++ b/go.sum @@ -134,6 +134,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= @@ -164,6 +166,7 @@ github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -175,6 +178,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -205,8 +210,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -222,6 +228,9 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +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/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= @@ -239,8 +248,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/memberlist v0.3.2 h1:Bfe9LGSoSU+GlIbwdGd3fUrOFrVP1IrLU6z5ax0Wlcc= -github.com/hashicorp/memberlist v0.3.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= +github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= @@ -366,6 +375,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= @@ -377,6 +388,8 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/slack-go/slack v0.10.4-0.20220503131901-0e14c9d4a15c h1:fOqgV3BG04rDivs8IKXQATLgQHfsWaOfYu4XZK3XNMM= +github.com/slack-go/slack v0.10.4-0.20220503131901-0e14c9d4a15c/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -393,6 +406,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/twilio/twilio-go v0.26.0 h1:wFW4oTe3/LKt6bvByP7eio8JsjtaLHjMQKOUEzQry7U= +github.com/twilio/twilio-go v0.26.0/go.mod h1:lz62Hopu4vicpQ056H5TJ0JE4AP0rS3sQ35/ejmgOwE= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= @@ -405,6 +420,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= @@ -462,6 +478,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -496,6 +513,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -568,8 +586,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -577,8 +597,9 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -641,6 +662,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/notify/sigma/sigma.go b/notify/sigma/sigma.go new file mode 100644 index 0000000000..314c4d7247 --- /dev/null +++ b/notify/sigma/sigma.go @@ -0,0 +1,166 @@ +// Copyright 2019 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sigma + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/go-kit/log" + "github.com/pkg/errors" + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" + commoncfg "github.com/prometheus/common/config" + "io/ioutil" + "net/http" +) + +// Notifier implements a Notifier for generic sigma. +type Notifier struct { + conf *config.SigmaConfig + tmpl *template.Template + logger log.Logger + client *http.Client +} + +// New returns a new Sigma. +func New(conf *config.SigmaConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) { + client, err := commoncfg.NewClientFromConfig(*conf.HTTPConfig, "sigma", httpOpts...) + if err != nil { + return nil, err + } + return &Notifier{ + conf: conf, + tmpl: t, + logger: l, + client: client, + }, nil +} + +// RequestSms Message defines the JSON object send to Sigma endpoints. +type RecipientVoice struct { + Include []string `json:"include"` + Exclude []string `json:"exclude"` +} + +type RequestVoice struct { + Recipient RecipientVoice `json:"recipient"` + Type string `json:"type"` + Payload RequestPayload `json:"payload"` +} + +type RequestSms struct { + Recipient []string `json:"recipient"` + Type string `json:"type"` + Payload RequestPayload `json:"payload"` +} + +type RequestPayload struct { + Sender string `json:"sender"` + Text string `json:"text"` +} + +type Response struct { + Id string `json:"id"` + Status string `json:"status"` + Error int `json:"error"` + Name string `json:"name"` + Message string `json:"message"` +} + +func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { + var err error + var ( + data = notify.GetTemplateData(ctx, n.tmpl, as, n.logger) + tmplText = notify.TmplText(n.tmpl, data, &err) + ) + + var body []byte + switch n.conf.NotificationType { + case "sms": + msg := RequestSms{ + Recipient: n.conf.Recipient, + Type: n.conf.NotificationType, + Payload: RequestPayload{ + Sender: n.conf.SenderName, + Text: tmplText(n.conf.Text), + }, + } + body, err = json.Marshal(msg) + if err != nil { + return false, err + } + case "voice": + msg := RequestVoice{ + Recipient: RecipientVoice{ + Include: n.conf.Recipient, + }, + Type: n.conf.NotificationType, + Payload: RequestPayload{ + Sender: n.conf.SenderName, + Text: tmplText(n.conf.Text), + }, + } + body, err = json.Marshal(msg) + if err != nil { + return false, err + } + } + + bodyReader := bytes.NewReader(body) + req, err := http.NewRequest("POST", n.conf.URL.String(), bodyReader) + if err != nil { + return false, errors.Wrap(err, "request error") + } + + req.Header.Set("Authorization", string(n.conf.APIKey)) + req.Header.Set("Content-Type", "application/json") + req.WithContext(ctx) + + resp, err := n.client.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false, err + } + + r := Response{} + if err := json.Unmarshal(respBody, &r); err != nil { + return false, err + } + + if resp.StatusCode != http.StatusOK { + return false, &Error{NotificationType: n.conf.NotificationType, StatusCode: resp.StatusCode, Response: r} + } + + return false, nil + +} + +type Error struct { + NotificationType string + StatusCode int + Response +} + +func (e *Error) Error() string { + return fmt.Sprintf("Sigma error. Type: %s; Code: %d; Response: %+v", e.NotificationType, e.StatusCode, e.Response) +} diff --git a/notify/slackV2/blocks.go b/notify/slackV2/blocks.go new file mode 100644 index 0000000000..4eed1657aa --- /dev/null +++ b/notify/slackV2/blocks.go @@ -0,0 +1,164 @@ +package slackV2 + +import ( + "fmt" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/common/model" + "github.com/slack-go/slack" + url2 "net/url" + "strings" +) + +type Text struct { + Type string `json:"type"` + Text string `json:"text"` +} +type Element struct { + Type string `json:"type"` + Text string `json:"text"` +} +type Field struct { + Type string `json:"type"` + Text string `json:"text"` +} + +type Block struct { + Type slack.MessageBlockType `json:"type"` + Text *Text `json:"text,omitempty"` + Fields []*Field `json:"fields,omitempty"` + Elements []*Element `json:"elements,omitempty"` + ImageURL string `json:"image_url,omitempty"` + AltText string `json:"alt_text,omitempty"` +} + +func (b Block) BlockType() slack.MessageBlockType { + return b.Type +} + +func (n *Notifier) formatMessage(data *template.Data) slack.Blocks { + firing := make([]string, 0) + resolved := make([]string, 0) + severity := make([]string, 0) + envs := make([]string, 0) + + blocks := make([]slack.Block, 0) + + for _, alert := range data.Alerts { + for _, v := range alert.Labels.SortedPairs() { + switch v.Name { + case "host_name": + switch model.AlertStatus(alert.Status) { + case model.AlertFiring: + firing = append(firing, v.Value) + case model.AlertResolved: + resolved = append(resolved, v.Value) + } + case "severity": + severity = append(severity, v.Value) + case "env": + envs = append(envs, v.Value) + } + } + } + + severity = UniqStr(severity) + resolved = UniqStr(resolved) + firing = UniqStr(firing) + envs = UniqStr(envs) + + blocks = append(blocks, Block{Type: slack.MBTHeader, Text: &Text{Type: slack.PlainTextType, Text: getMapValue(data.CommonLabels, "alertname")}}) + blocks = append(blocks, Block{Type: slack.MBTDivider}) + + { + url := "" + if urlParsed, err := url2.Parse(data.ExternalURL); err == nil { + urlParsed.Path = "/#/silences/new" + args := urlParsed.Query() + filters := make([]string, 0) + for _, v := range data.CommonLabels.SortedPairs() { + filters = append(filters, fmt.Sprintf("%s=\"%s\"", v.Name, v.Value)) + } + args.Add("filter", fmt.Sprintf("{%s}", strings.Join(filters, ","))) + urlParsed.RawQuery = EncodeUrlArgs(args) + url = urlParsed.String() + url = strings.Replace(url, "%23", "#", 1) + } + + graphUrl := "" + for _, alert := range data.Alerts { + if alert.GeneratorURL != "" { + graphUrl = alert.GeneratorURL + break + } + } + + fields := make([]*Field, 0) + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Env: %s*", strings.ToUpper(strings.Join(envs, ", ")))}) + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Severety: %s*", strings.ToUpper(strings.Join(severity, ", ")))}) + if graphUrl != "" { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*<%s|:chart_with_upwards_trend:Graph>*", graphUrl)}) + } else { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf(":chart_with_upwards_trend:~Graph~")}) + } + if url != "" { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*<%s|:no_bell:Silence>*", url)}) + } else { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*:no_bell:~Silence~")}) + } + + blocks = append(blocks, Block{Type: slack.MBTSection, Fields: fields}) + } + + if len(firing) > 0 && len(resolved) > 0 { + fields := make([]*Field, 0) + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Firing:* `%s`", strings.Join(firing, ", "))}) + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Resolved:* `%s`", strings.Join(resolved, ", "))}) + blocks = append(blocks, Block{Type: slack.MBTSection, Fields: fields}) + } else { + fields := make([]*Field, 0) + if len(resolved) > 0 { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Resolved: *`%s`", strings.Join(resolved, ", "))}) + } else { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Firing: *`%s`", strings.Join(firing, ", "))}) + } + blocks = append(blocks, Block{Type: slack.MBTSection, Fields: fields}) + } + + { + block := Block{Type: slack.MBTContext, Elements: make([]*Element, 0)} + + if val := getMapValue(data.CommonAnnotations, "summary"); len(val) > 0 { + block.Elements = append(block.Elements, &Element{Type: slack.MarkdownType, Text: fmt.Sprintf("*Summary:* %s", val)}) + } else { + summary := make([]string, 0) + for _, al := range data.Alerts { + if val, ok := al.Annotations["summary"]; ok && len(val) > 0 { + summary = append(summary, val) + } + } + summary = mergeSameMessages(summary) + if len(summary) > 0 { + block.Elements = append(block.Elements, &Element{Type: slack.MarkdownType, Text: fmt.Sprintf("*Summary:* %s", cut(strings.Join(summary, ";\n"), 500))}) + } + } + + if val := getMapValue(data.CommonAnnotations, "description"); len(val) > 0 { + block.Elements = append(block.Elements, &Element{Type: slack.MarkdownType, Text: fmt.Sprintf("*Description:* %s", val)}) + } else { + for _, al := range data.Alerts { + if val, ok := al.Annotations["description"]; ok && len(val) > 0 { + block.Elements = append(block.Elements, &Element{Type: slack.MarkdownType, Text: fmt.Sprintf("*Description:* %s", val)}) + break + } + } + } + + if len(block.Elements) > 0 { + blocks = append(blocks, block) + } + } + + result := slack.Blocks{BlockSet: blocks} + + return result +} diff --git a/notify/slackV2/grafana.go b/notify/slackV2/grafana.go new file mode 100644 index 0000000000..971e45b4c3 --- /dev/null +++ b/notify/slackV2/grafana.go @@ -0,0 +1,337 @@ +package slackV2 + +import ( + "fmt" + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/common/model" + "github.com/satori/go.uuid" + "github.com/slack-go/slack" + "net/http" + url2 "net/url" + "path" + "regexp" + "strconv" + "strings" + "time" +) + +func genGrafanaRenderUrl(grafanaUrl string, grafanaTZ string, org string, dash string, panel string) (string, error) { + + const fromShift = -time.Hour + const toShift = -time.Second * 10 + const imageWidth = "999" + const imageHeight = "333" + const urlPath = "/render/d-solo/" + + if grafanaUrl == "" { + return "", fmt.Errorf("grafanaUrl is empty") + } + + u, err := url2.Parse(grafanaUrl) + if err != nil { + return "", err + } + + u.Path = path.Join(u.Path, urlPath, dash) + q := u.Query() + q.Set("orgId", org) + q.Set("from", strconv.Itoa(int(time.Now().Add(fromShift).UnixMilli()))) + q.Set("to", strconv.Itoa(int(time.Now().Add(toShift).UnixMilli()))) + q.Set("panelId", panel) + q.Set("width", imageWidth) + q.Set("height", imageHeight) + q.Set("tz", grafanaTZ) + u.RawQuery = EncodeUrlArgs(q) + return u.String(), nil + +} + +func genGrafanaUrl(grafanaUrl string, org string, dash string, panel string) (string, error) { + + if grafanaUrl == "" { + return "", fmt.Errorf("grafanaUrl is empty") + } + + u, err := url2.Parse(grafanaUrl) + if err != nil { + return "", err + } + + u.Path = path.Join(u.Path, "/d/"+dash) + q := u.Query() + q.Set("orgId", org) + if panel != "" { + q.Set("viewPanel", panel) + } + u.RawQuery = EncodeUrlArgs(q) + return u.String(), nil +} + +func urlMerger(publicUrl string, privateUrl string) (string, error) { + u, err := url2.Parse(publicUrl) + if err != nil { + return "", err + } + + trunc := []rune(u.Path) + key := string(trunc[len(trunc)-10:]) + + u, err = url2.Parse(privateUrl) + if err != nil { + return "", err + } + q := u.Query() + q.Set("pub_secret", key) + u.RawQuery = EncodeUrlArgs(q) + + return u.String(), nil +} + +func getUploadedImageUrl(url string, token config.Secret, grafanaToken config.Secret) (string, error) { + const imageExtension = "jpg" + client := &http.Client{} + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "Bearer "+string(grafanaToken)) + + response, err := client.Do(req) + if err != nil { + return "", err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return "", fmt.Errorf("request status code %d != %d", response.StatusCode, http.StatusOK) + } + + fileName := fmt.Sprintf("%s.%s", strings.Replace(uuid.NewV4().String(), "-", "", -1), imageExtension) + api := slack.New(string(token)) + params := slack.FileUploadParameters{ + Reader: response.Body, + Filetype: "jpg", + Filename: fileName, + } + + image, err := api.UploadFile(params) + if err != nil { + return "", fmt.Errorf("upload error, image: %s, error: %w", image.Name, err) + } + + sharedUrl, _, _, err := api.ShareFilePublicURL(image.ID) + if err != nil { + return "", fmt.Errorf("share error: %w", err) + } + + imageUrl, err := urlMerger(sharedUrl.PermalinkPublic, sharedUrl.URLPrivate) + if err != nil { + return "", fmt.Errorf("url merge error: %w", err) + } + + return imageUrl, nil + +} + +func (n *Notifier) formatGrafanaMessage(data *template.Data) slack.Blocks { + dashboardUid := "" + panelId := "" + orgId := "" + grafanaValues := "" + runBook := "" + firing := make([]string, 0) + resolved := make([]string, 0) + severity := make([]string, 0) + envs := make([]string, 0) + blocks := make([]slack.Block, 0) + + for _, alert := range data.Alerts { + for _, v := range alert.Labels.SortedPairs() { + switch v.Name { + case "host_name": + switch model.AlertStatus(alert.Status) { + case model.AlertFiring: + firing = append(firing, v.Value) + case model.AlertResolved: + resolved = append(resolved, v.Value) + } + case "severity": + severity = append(severity, v.Value) + case "env": + envs = append(envs, v.Value) + } + } + for _, v := range alert.Annotations.SortedPairs() { + switch strings.ToLower(v.Name) { + case strings.ToLower("__dashboardUid__"): + dashboardUid = v.Value + case strings.ToLower("__panelId__"): + panelId = v.Value + case strings.ToLower("OrgID"): + orgId = v.Value + case strings.ToLower("__value_string__"): + grafanaValues = v.Value + case strings.ToLower("runbook_url"): + runBook = v.Value + } + } + } + + severity = UniqStr(severity) + resolved = UniqStr(resolved) + firing = UniqStr(firing) + envs = UniqStr(envs) + + { + url := "" + if urlParsed, err := url2.Parse(data.ExternalURL); err == nil { + urlParsed.Path = "/#/silences/new" + args := urlParsed.Query() + filters := make([]string, 0) + for _, v := range data.CommonLabels.SortedPairs() { + filters = append(filters, fmt.Sprintf("%s=\"%s\"", v.Name, v.Value)) + } + + args.Add("filter", fmt.Sprintf("{%s}", strings.Join(filters, ","))) + urlParsed.RawQuery = EncodeUrlArgs(args) + url = urlParsed.String() + url = strings.Replace(url, "%23", "#", 1) + } + + alertEditUrl := "" + for _, alert := range data.Alerts { + if alert.GeneratorURL != "" { + if urlParsed, err := url2.Parse(alert.GeneratorURL); err == nil { + args := urlParsed.Query() + args.Add("orgId", orgId) + urlParsed.RawQuery = EncodeUrlArgs(args) + alertEditUrl = urlParsed.String() + break + } + } + } + + //Header + blocks = append(blocks, Block{Type: slack.MBTHeader, Text: &Text{Type: slack.PlainTextType, Text: getMapValue(data.CommonLabels, "alertname")}}) + + //Divider + //blocks = append(blocks, Block{Type: slack.MBTDivider}) + + //Env and severity + fields := make([]*Field, 0) + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Env: %s*", strings.ToUpper(strings.Join(envs, ", ")))}) + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Severety: %s*", strings.ToUpper(strings.Join(severity, ", ")))}) + + //Buttons + if url, err := genGrafanaUrl(n.conf.GrafanaUrl, orgId, dashboardUid, panelId); err == nil { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*<%s|:chart_with_upwards_trend:Panel>*", url)}) + } else { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf(":chart_with_upwards_trend:~Panel~")}) + } + + if url != "" { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*<%s|:no_bell:Silence>*", url)}) + } else { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*<%s|:no_bell:Silence>*", url)}) + } + if url, err := genGrafanaUrl(n.conf.GrafanaUrl, orgId, dashboardUid, ""); err == nil { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*<%s|:dashboard:Dash>*", url)}) + } else { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf(":dashboard:~Dash~")}) + } + if alertEditUrl != "" { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*<%s|:gear:Edit>*", alertEditUrl)}) + } else { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*:gear:~Edit~")}) + } + + blocks = append(blocks, Block{Type: slack.MBTSection, Fields: fields}) + } + + //Firing > Resolved + if len(firing) > 0 && len(resolved) > 0 { + fields := make([]*Field, 0) + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Firing:* `%s`", strings.Join(firing, ", "))}) + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Resolved:* `%s`", strings.Join(resolved, ", "))}) + blocks = append(blocks, Block{Type: slack.MBTSection, Fields: fields}) + } else { + fields := make([]*Field, 0) + if len(resolved) > 0 { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Resolved: *`%s`", strings.Join(resolved, ", "))}) + } else { + fields = append(fields, &Field{Type: slack.MarkdownType, Text: fmt.Sprintf("*Firing: *`%s`", strings.Join(firing, ", "))}) + } + blocks = append(blocks, Block{Type: slack.MBTSection, Fields: fields}) + } + + //GrafanaImage + if imageUrl, err := genGrafanaRenderUrl(n.conf.GrafanaUrl, n.conf.GrafanaTZ, orgId, dashboardUid, panelId); err == nil { + if slackImageUrl, err := getUploadedImageUrl(imageUrl, n.conf.UserToken, n.conf.GrafanaToken); err == nil { + blocks = append(blocks, Block{Type: slack.MBTImage, ImageURL: slackImageUrl, AltText: "inspiration"}) + } + } + + //Summary Description and Metrics + { + block := Block{Type: slack.MBTContext, Elements: make([]*Element, 0)} + + if (grafanaValues != "[no value]") || (grafanaValues != "") { + regexpForParseMetric := regexp.MustCompile(`(?m) labels={[a-zA-z0-9=:,_@{ -.]+} value=`) + valueStringCollection := regexpForParseMetric.ReplaceAllString(grafanaValues, ", value=") + regexpForParseParams := regexp.MustCompile(`(?m)metric='(?P.*)', value=(?P.*)`) + + grafanaMapParams := make(map[string]string) + for _, parsedCollection := range strings.Split(valueStringCollection, "], [ ") { + match := regexpForParseParams.FindStringSubmatch(parsedCollection) + if len(match) >= 3 { + grafanaMapParams[match[1]] = match[2] + } + } + if valueStringCollection != "" { + block.Elements = append(block.Elements, &Element{Type: slack.MarkdownType, Text: fmt.Sprintf("*Metric:* %s\n", valueStringCollection)}) + } + } + + if val := getMapValue(data.CommonAnnotations, "description"); len(val) > 0 { + block.Elements = append(block.Elements, &Element{Type: slack.MarkdownType, Text: fmt.Sprintf("*Description:* %s\n", val)}) + } else { + for _, al := range data.Alerts { + if val, ok := al.Annotations["description"]; ok && len(val) > 0 { + block.Elements = append(block.Elements, &Element{Type: slack.MarkdownType, Text: fmt.Sprintf("*Description:* %s\n", val)}) + break + } + } + } + + if val := getMapValue(data.CommonAnnotations, "summary"); len(val) > 0 { + if runBook != "" { + block.Elements = append(block.Elements, &Element{Type: slack.MarkdownType, Text: fmt.Sprintf("*<%s|Summary:>* %s", runBook, val)}) + } else { + block.Elements = append(block.Elements, &Element{Type: slack.MarkdownType, Text: fmt.Sprintf("*Summary:* %s", val)}) + } + } else { + summary := make([]string, 0) + for _, al := range data.Alerts { + if val, ok := al.Annotations["summary"]; ok && len(val) > 0 { + summary = append(summary, val) + } + } + summary = mergeSameMessages(summary) + if len(summary) > 0 { + if runBook != "" { + block.Elements = append(block.Elements, &Element{Type: slack.MarkdownType, Text: fmt.Sprintf("*<%s|Summary:>* %s", runBook, cut(strings.Join(summary, ";\n"), 500))}) + } else { + block.Elements = append(block.Elements, &Element{Type: slack.MarkdownType, Text: fmt.Sprintf("*Summary:* %s", cut(strings.Join(summary, ";\n"), 500))}) + } + } + } + + if len(block.Elements) > 0 { + blocks = append(blocks, block) + } + } + + result := slack.Blocks{BlockSet: blocks} + return result +} diff --git a/notify/slackV2/slackV2.go b/notify/slackV2/slackV2.go new file mode 100644 index 0000000000..7f374439c4 --- /dev/null +++ b/notify/slackV2/slackV2.go @@ -0,0 +1,215 @@ +// Copyright 2019 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package slackV2 + +import ( + "context" + "fmt" + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" + "github.com/slack-go/slack" + "strings" + "sync" + "time" +) + +// Notifier implements a Notifier for Slack notifications. +type Notifier struct { + conf *config.SlackConfigV2 + tmpl *template.Template + logger log.Logger + client *slack.Client + storage map[string]Data + mu sync.RWMutex +} + +type Data struct { + *template.Data + SendAt time.Time +} + +// New returns a new Slack notification handler. +func New(c *config.SlackConfigV2, t *template.Template, l log.Logger) (*Notifier, error) { + token := string(c.Token) + client := slack.New(token, slack.OptionDebug(c.Debug)) + + notifier := &Notifier{ + conf: c, + tmpl: t, + logger: l, + client: client, + storage: make(map[string]Data), + } + go notifier.storageCleaner() + return notifier, nil +} + +// Notify implements the Notifier interface. +func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { + data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger) + + if n.conf.Debug { + level.Debug(n.logger).Log("Alert Data", data) + } + + changedMessages := make([]string, 0) + notifyMessages := make([]string, 0) + for _, newAlert := range data.Alerts { + messages := n.getMessagesByFingerprint(newAlert.Fingerprint) + changedMessages = append(changedMessages, messages...) + if len(messages) > 0 { + n.mu.Lock() + for _, ts := range messages { + changed := false + for i := range n.storage[ts].Alerts { + if n.storage[ts].Alerts[i].Fingerprint == newAlert.Fingerprint { + if n.storage[ts].Alerts[i].Status != newAlert.Status { + n.storage[ts].Alerts[i].Status = newAlert.Status + changed = true + } + n.storage[ts].Alerts[i].EndsAt = newAlert.EndsAt + n.storage[ts].Data.CommonAnnotations = data.CommonAnnotations + + } + } + if !changed { + notifyMessages = append(notifyMessages, ts) + } + } + n.mu.Unlock() + } else { + // Делаем проверку, что бы не отправлять резолвы на "осиратевшие алерты", у которых 0 firing + if len(data.Alerts.Firing()) > 0 { + ts, err := n.send(data, "") + if err != nil { + return false, err + } + n.mu.Lock() + n.storage[ts] = Data{Data: data} + n.mu.Unlock() + notifyMessages = append(notifyMessages, ts) + } + } + + for _, ts := range UniqStr(notifyMessages) { + if n.storage[ts].SendAt.IsZero() || n.storage[ts].SendAt.Add(time.Duration(n.conf.MentionDelay)).Before(time.Now()) { + if err := n.sendNotify(ts); err != nil { + return false, err + } + n.mu.Lock() + n.storage[ts] = Data{Data: n.storage[ts].Data, SendAt: time.Now()} + n.mu.Unlock() + } + } + } + + n.mu.RLock() + defer n.mu.RUnlock() + + for _, msg := range changedMessages { + _, err := n.send(n.storage[msg].Data, msg) + if err != nil { + return false, err + } + } + + return true, nil +} + +func (n *Notifier) send(data *template.Data, ts string) (string, error) { + + channel := n.conf.Channel + attachment := slack.Attachment{} + + if n.conf.GrafanaToken != "" { + attachment = slack.Attachment{ + Color: n.conf.Color, + Blocks: n.formatGrafanaMessage(data), + } + } else { + attachment = slack.Attachment{ + Color: n.conf.Color, + Blocks: n.formatMessage(data), + } + } + + if len(data.Alerts.Firing()) == 0 { + attachment.Color = "#1aad21" + } + + att := slack.MsgOptionAttachments(attachment) + + if ts != "" { + _, _, messageTs, err := n.client.UpdateMessage(channel, ts, att) + return messageTs, err + } else { + _, messageTs, err := n.client.PostMessage(channel, att) + return messageTs, err + } +} + +func (n *Notifier) sendNotify(ts string) error { + if len(n.conf.Mentions) == 0 { + return nil + } + users := make([]string, len(n.conf.Mentions)) + for i, val := range n.conf.Mentions { + switch strings.ToLower(val.Type) { + case "group": + users[i] = fmt.Sprintf("", val.ID, val.Name) + case "user": + users[i] = fmt.Sprintf("<@%s>", val.ID) + } + } + + text := fmt.Sprintf("Look here %s", strings.Join(users, " ")) + opts := make([]slack.MsgOption, 0) + opts = append(opts, slack.MsgOptionTS(ts)) + opts = append(opts, slack.MsgOptionText(text, false)) + // + _, _, err := n.client.PostMessage(n.conf.Channel, opts...) + return err + +} + +func (n *Notifier) getMessagesByFingerprint(fp string) []string { + n.mu.RLock() + defer n.mu.RUnlock() + + ts := make([]string, 0) + for i, msg := range n.storage { + for _, alert := range msg.Alerts { + if fp == alert.Fingerprint { + ts = append(ts, i) + } + } + } + return ts +} + +func (n *Notifier) storageCleaner() { + for range time.Tick(time.Minute * 10) { + n.mu.Lock() + for k, data := range n.storage { + if len(data.Alerts.Firing()) == 0 { + delete(n.storage, k) + } + } + n.mu.Unlock() + } +} diff --git a/notify/slackV2/utils.go b/notify/slackV2/utils.go new file mode 100644 index 0000000000..a32bc09bde --- /dev/null +++ b/notify/slackV2/utils.go @@ -0,0 +1,115 @@ +package slackV2 + +import ( + "github.com/prometheus/alertmanager/template" + url2 "net/url" + "strings" + "unicode/utf8" +) + +const SummaryMessageDiffThreshold = 3 + +func UniqStr(input []string) []string { + u := make([]string, 0, len(input)) + m := make(map[string]bool) + + for _, val := range input { + if _, ok := m[val]; !ok { + m[val] = true + u = append(u, val) + } + } + return u +} + +func getMapValue(data template.KV, key string) string { + if value, ok := data[key]; ok { + return value + } else { + return "" + } +} + +func levenshteinDistance(s1, s2 string) int { + if len(s1) == 0 { + return utf8.RuneCountInString(s2) + } else if len(s2) == 0 { + return utf8.RuneCountInString(s1) + } else if s1 == s2 { + return 0 + } + + min := func(values ...int) int { + m := values[0] + for _, v := range values { + if v < m { + m = v + } + } + return m + } + r1, r2 := []rune(s1), []rune(s2) + n, m := len(r1), len(r2) + if n > m { + r1, r2 = r2, r1 + n, m = m, n + } + currentRow := make([]int, n+1) + previousRow := make([]int, n+1) + for i := range currentRow { + currentRow[i] = i + } + for i := 1; i <= m; i++ { + for j := range currentRow { + previousRow[j] = currentRow[j] + if j == 0 { + currentRow[j] = i + continue + } else { + currentRow[j] = 0 + } + add, del, change := previousRow[j]+1, currentRow[j-1]+1, previousRow[j-1] + if r1[j-1] != r2[i-1] { + change++ + } + currentRow[j] = min(add, del, change) + } + } + return currentRow[n] +} + +func mergeSameMessages(arr []string) []string { + result := make([]string, 0) + if len(arr) > 0 { + result = append(result, arr[0]) + } + + for _, val := range arr { + differs := 0 + for _, res := range result { + if levenshteinDistance(val, res) > SummaryMessageDiffThreshold { + differs++ + } + } + if differs == len(result) { + result = append(result, val) + } + } + + result = UniqStr(result) + return result +} + +func cut(text string, limit int) string { + runes := []rune(text) + if len(runes) >= limit { + return string(runes[:limit]) + } + return text +} + +func EncodeUrlArgs(values url2.Values) string { + result := values.Encode() + result = strings.Replace(result, "+", "%20", -1) + return result +} diff --git a/notify/twilio/main.go b/notify/twilio/main.go new file mode 100644 index 0000000000..a1792a9860 --- /dev/null +++ b/notify/twilio/main.go @@ -0,0 +1,123 @@ +// Copyright 2019 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package twilio + +import ( + "context" + "encoding/json" + "encoding/xml" + "fmt" + "github.com/go-kit/log" + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" + "github.com/twilio/twilio-go" + twapi "github.com/twilio/twilio-go/rest/api/v2010" +) + +// Notifier implements a Notifier for generic sigma. +type Notifier struct { + conf *config.TwilioConfig + tmpl *template.Template + logger log.Logger +} + +// New returns a new Sigma. +func New(conf *config.TwilioConfig, t *template.Template, l log.Logger) (*Notifier, error) { + return &Notifier{ + conf: conf, + tmpl: t, + logger: l, + }, nil +} + +func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { + var err error + var ( + data = notify.GetTemplateData(ctx, n.tmpl, as, n.logger) + tmplText = notify.TmplText(n.tmpl, data, &err) + ) + + tw := twilio.NewRestClientWithParams(twilio.ClientParams{ + Username: n.conf.AccountID, + Password: string(n.conf.Token), + }) + + allErrors := make([]error, 0) + switch n.conf.NotificationType { + case "sms": + for _, recepient := range n.conf.Recipient { + req := &twapi.CreateMessageParams{From: &n.conf.SenderName, To: &recepient} + req.SetBody(tmplText(n.conf.Text)) + resp, err := tw.Api.CreateMessage(req) + if err != nil { + allErrors = append(allErrors, err) + } else { + r, _ := json.Marshal(*resp) + n.logger.Log("Twilio response", r) + } + } + case "voice": + voiceReq := VoiceRequest{Say: tmplText(n.conf.Text)} + if n.conf.PlayFileUrl != nil { + voiceReq.Play = n.conf.PlayFileUrl.String() + } + voiceData, err := xml.Marshal(voiceReq) + if err != nil { + return false, err + } + + id := Storage.Put(voiceData) + url := *n.conf.AlertManagerUrl + url.Path = "/callback/twilio" + args := url.Query() + args.Set("id", id) + url.RawQuery = args.Encode() + + for _, recepient := range n.conf.Recipient { + req := &twapi.CreateCallParams{From: &n.conf.SenderName, To: &recepient} + req.SetMethod("GET") + req.SetUrl(url.String()) + resp, err := tw.Api.CreateCall(req) + if err != nil { + allErrors = append(allErrors, err) + } else { + r, _ := json.Marshal(*resp) + n.logger.Log("callback_url", url.String(), "Twilio response", r) + } + } + } + + if len(allErrors) > 0 { + return false, &Error{Errors: allErrors} + } + + return false, nil +} + +type Error struct { + Errors []error +} + +func (e *Error) Error() string { + return fmt.Sprintf("Errors count: %d", len(e.Errors)) +} + +type VoiceRequest struct { + XMLName xml.Name `xml:"Response"` + Text string `xml:",chardata"` + Play string `xml:"Play"` + Say string `xml:"Say"` +} diff --git a/notify/twilio/storage.go b/notify/twilio/storage.go new file mode 100644 index 0000000000..716b908cc4 --- /dev/null +++ b/notify/twilio/storage.go @@ -0,0 +1,56 @@ +package twilio + +import ( + "github.com/gofrs/uuid" + "sync" + "time" +) + +var Storage = NewStorage() + +type Entity struct { + Data []byte + CreatedAt time.Time +} + +type storage struct { + store map[string]Entity + mutex sync.RWMutex + Lifetime time.Duration +} + +func (st *storage) cleaner() { + for range time.Tick(time.Minute * 10) { + st.mutex.Lock() + for k, v := range st.store { + if v.CreatedAt.Add(st.Lifetime).Before(time.Now()) { + delete(st.store, k) + } + } + st.mutex.Unlock() + } +} + +func (st *storage) Get(id string) *Entity { + st.mutex.RLock() + defer st.mutex.RUnlock() + if val, ok := st.store[id]; !ok { + return nil + } else { + return &val + } +} + +func (st *storage) Put(data []byte) string { + st.mutex.Lock() + defer st.mutex.Unlock() + id, _ := uuid.NewV1() + st.store[id.String()] = Entity{Data: data, CreatedAt: time.Now()} + return id.String() +} + +func NewStorage() *storage { + st := &storage{store: make(map[string]Entity), mutex: sync.RWMutex{}, Lifetime: time.Hour * 24} + go st.cleaner() + return st +} diff --git a/pkg/labels/matcher_test.go b/pkg/labels/matcher_test.go index bd89a2b7ea..21d1b7f089 100644 --- a/pkg/labels/matcher_test.go +++ b/pkg/labels/matcher_test.go @@ -177,10 +177,10 @@ line`, want: `foo="new\nline"`, }, { - name: `foo`, - op: MatchEqual, + name: `foo`, + op: MatchEqual, value: `tab stop`, - want: `foo="tab stop"`, + want: `foo="tab stop"`, }, } diff --git a/pkg/labels/parse.go b/pkg/labels/parse.go index 0a242d506f..4652a7b7e2 100644 --- a/pkg/labels/parse.go +++ b/pkg/labels/parse.go @@ -45,11 +45,12 @@ var ( // this comma and whitespace will be trimmed. // // Examples for valid input strings: -// {foo = "bar", dings != "bums", } -// foo=bar,dings!=bums -// foo=bar, dings!=bums -// {quote="She said: \"Hi, ladies! That's gender-neutral…\""} -// statuscode=~"5.." +// +// {foo = "bar", dings != "bums", } +// foo=bar,dings!=bums +// foo=bar, dings!=bums +// {quote="She said: \"Hi, ladies! That's gender-neutral…\""} +// statuscode=~"5.." // // See ParseMatcher for details on how an individual Matcher is parsed. func ParseMatchers(s string) ([]*Matcher, error) { diff --git a/scripts/test_alert.sh b/scripts/test_alert.sh new file mode 100755 index 0000000000..b174183d08 --- /dev/null +++ b/scripts/test_alert.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail + +curl -H 'Content-Type: application/json' -d '[{"labels":{"alertname":"Test Alert", "severity": "critical", "env": "prod", "host_name": "testmachine"}}]' http://127.0.0.1:9093/api/v1/alerts