Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions alerts/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,13 @@ type ReceiverHandler struct {

// titleTmpl is used to format the title of the new issue.
titleTmpl *template.Template

// alertTmpl is used to format the context of the new issue.
alertTmpl *template.Template
}

// NewReceiver creates a new ReceiverHandler.
func NewReceiver(client ReceiverClient, githubRepo string, autoClose bool, resolvedLabel string, extraLabels []string, titleTmplStr string) (*ReceiverHandler, error) {
func NewReceiver(client ReceiverClient, githubRepo string, autoClose bool, resolvedLabel string, extraLabels []string, titleTmplStr string, alertTmplStr string) (*ReceiverHandler, error) {
rh := ReceiverHandler{
Client: client,
DefaultRepo: githubRepo,
Expand All @@ -97,6 +100,11 @@ func NewReceiver(client ReceiverClient, githubRepo string, autoClose bool, resol
return nil, err
}

rh.alertTmpl, err = template.New("alert").Parse(alertTmplStr)
if err != nil {
return nil, err
}

return &rh, nil
}

Expand Down Expand Up @@ -171,7 +179,10 @@ func (rh *ReceiverHandler) processAlert(msg *webhook.Message) error {
// issue from github, so create a new issue.
if msg.Data.Status == "firing" {
if foundIssue == nil {
msgBody := formatIssueBody(msg)
msgBody, err := rh.formatIssueBody(msg)
if err != nil {
return fmt.Errorf("format body for %q: %s", msg.GroupKey, err)
}
_, err = rh.Client.CreateIssue(rh.getTargetRepo(msg), msgTitle, msgBody, rh.ExtraLabels)
if err == nil {
createdIssues.WithLabelValues(alertName).Inc()
Expand Down
59 changes: 52 additions & 7 deletions alerts/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
// 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
// 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 alerts

import (
Expand Down Expand Up @@ -125,6 +125,7 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
msgRepo string
fakeClient *fakeClient
titleTmpl string
alertTmpl string
httpStatus int
expectReceiverErr bool
wantMessageErr bool
Expand All @@ -140,6 +141,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
createIssue("DiskRunningFull", "body1", ""),
},
},
titleTmpl: DefaultTitleTmpl,
alertTmpl: DefaultAlertTmpl,
httpStatus: http.StatusOK,
},
{
Expand All @@ -148,6 +151,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
msgAlert: "DiskRunningFull",
msgAlertStatus: "resolved",
fakeClient: &fakeClient{},
titleTmpl: DefaultTitleTmpl,
alertTmpl: DefaultAlertTmpl,
httpStatus: http.StatusOK,
},
{
Expand All @@ -156,6 +161,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
msgAlert: "DiskRunningFull",
msgAlertStatus: "firing",
fakeClient: &fakeClient{},
titleTmpl: DefaultTitleTmpl,
alertTmpl: DefaultAlertTmpl,
httpStatus: http.StatusOK,
},
{
Expand All @@ -165,6 +172,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
msgAlertStatus: "firing",
msgRepo: "custom-repo",
fakeClient: &fakeClient{},
titleTmpl: DefaultTitleTmpl,
alertTmpl: DefaultAlertTmpl,
httpStatus: http.StatusOK,
},
{
Expand All @@ -177,6 +186,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
createIssue("DiskRunningFull", "body1", ""),
},
},
titleTmpl: DefaultTitleTmpl,
alertTmpl: DefaultAlertTmpl,
httpStatus: http.StatusOK,
},
{
Expand All @@ -190,6 +201,7 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
},
},
titleTmpl: `{{ (index .Data.Alerts 0).Labels.alertname }}`,
alertTmpl: `Disk is running full on {{ (index .Data.Alerts 0).Labels.instance }}`,
httpStatus: http.StatusOK,
},
{
Expand All @@ -203,6 +215,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
},
labelError: errors.New("No such label"),
},
titleTmpl: DefaultTitleTmpl,
alertTmpl: DefaultAlertTmpl,
httpStatus: http.StatusInternalServerError,
},
{
Expand All @@ -216,6 +230,7 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
},
},
titleTmpl: `{{ (index .Data.Alerts 1).Status }}`,
alertTmpl: DefaultAlertTmpl,
httpStatus: http.StatusInternalServerError,
},
{
Expand All @@ -229,18 +244,38 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
},
},
titleTmpl: `{{ x }}`,
alertTmpl: DefaultAlertTmpl,
expectReceiverErr: true,
httpStatus: http.StatusInternalServerError,
},
{
name: "failure-alert-template-bad-syntax",
method: http.MethodPost,
msgAlert: "DiskRunningFull",
msgAlertStatus: "firing",
fakeClient: &fakeClient{
listIssues: []*github.Issue{
createIssue("DiskRunningFull", "body1", ""),
},
},
titleTmpl: `{{ (index .Data.Alerts 1).Status }}`,
alertTmpl: `{{ x }}`,
expectReceiverErr: true,
httpStatus: http.StatusInternalServerError,
},
{
name: "failure-unmarshal-error",
method: http.MethodPost,
titleTmpl: DefaultTitleTmpl,
alertTmpl: DefaultAlertTmpl,
httpStatus: http.StatusBadRequest,
wantMessageErr: true,
},
{
name: "failure-reader-error",
method: http.MethodPost,
titleTmpl: DefaultTitleTmpl,
alertTmpl: DefaultAlertTmpl,
httpStatus: http.StatusInternalServerError,
wantReadErr: true,
},
Expand All @@ -250,13 +285,27 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
fakeClient: &fakeClient{
listError: fmt.Errorf("Fake error listing current issues"),
},
titleTmpl: DefaultTitleTmpl,
alertTmpl: DefaultAlertTmpl,
httpStatus: http.StatusInternalServerError,
},
{
name: "failure-wrong-method",
method: http.MethodGet,
titleTmpl: DefaultTitleTmpl,
alertTmpl: DefaultAlertTmpl,
httpStatus: http.StatusMethodNotAllowed,
},
{
name: "failure-body-template",
method: http.MethodPost,
msgAlert: "DiskRunningFull",
msgAlertStatus: "firing",
fakeClient: &fakeClient{},
titleTmpl: DefaultTitleTmpl,
alertTmpl: `{{ .NOTAREAL_FIELD }}`,
httpStatus: http.StatusInternalServerError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -284,11 +333,7 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
return
}

titleTmpl := tt.titleTmpl
if titleTmpl == "" {
titleTmpl = DefaultTitleTmpl
}
rh, err := NewReceiver(tt.fakeClient, "default", true, "", nil, titleTmpl)
rh, err := NewReceiver(tt.fakeClient, "default", true, "", nil, tt.titleTmpl, tt.alertTmpl)
if tt.expectReceiverErr {
if err == nil {
t.Fatal()
Expand Down
21 changes: 6 additions & 15 deletions alerts/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@ package alerts
import (
"bytes"
"fmt"
"html/template"
"log"

"github.com/prometheus/alertmanager/notify/webhook"
)

const (
// alertMD reports all alert labels and annotations in a markdown format
// DefaultAlertTmpl reports all alert labels and annotations in a markdown format
// that renders correctly in github issues.
//
// Example:
Expand All @@ -51,7 +49,7 @@ const (
// - alertname = DiskRunningFull
// - dev = sda2
// - instance = example2
alertMD = `
DefaultAlertTmpl = `
Alertmanager URL: {{.Data.ExternalURL}}
{{range .Data.Alerts}}
* {{.Status}} {{.GeneratorURL}}
Expand All @@ -77,10 +75,6 @@ TODO: add graph url from annotations.
DefaultTitleTmpl = `{{ .Data.GroupLabels.alertname }}`
)

var (
alertTemplate = template.Must(template.New("alert").Parse(alertMD))
)

func id(msg *webhook.Message) string {
return fmt.Sprintf("0x%x", msg.GroupKey)
}
Expand All @@ -95,13 +89,10 @@ func (rh *ReceiverHandler) formatTitle(msg *webhook.Message) (string, error) {
}

// formatIssueBody constructs an issue body from a webhook message.
func formatIssueBody(msg *webhook.Message) string {
func (rh *ReceiverHandler) formatIssueBody(msg *webhook.Message) (string, error) {
var buf bytes.Buffer
err := alertTemplate.Execute(&buf, msg)
if err != nil {
log.Printf("Error executing template: %s", err)
return ""
if err := rh.alertTmpl.Execute(&buf, msg); err != nil {
return "", err
}
s := buf.String()
return fmt.Sprintf("<!-- ID: %s -->\n%s", id(msg), s)
return buf.String(), nil
}
74 changes: 62 additions & 12 deletions alerts/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,74 @@
package alerts

import (
"html/template"
"testing"

"github.com/prometheus/alertmanager/notify/webhook"
amtmpl "github.com/prometheus/alertmanager/template"
)

func Test_formatIssueBody(t *testing.T) {
wh := createWebhookMessage("FakeAlertName", "firing", "")
brokenTemplate := `
{{range .NOT_REAL_FIELD}}
* {{.Status}}
{{end}}
`
alertTemplate = template.Must(template.New("alert").Parse(brokenTemplate))
got := formatIssueBody(wh)
if got != "" {
t.Errorf("formatIssueBody() = %q, want empty string", got)
msg := webhook.Message{
Data: &amtmpl.Data{
Status: "firing",
Alerts: []amtmpl.Alert{
{
Annotations: amtmpl.KV{"env": "prod", "svc": "foo"},
},
{
Annotations: amtmpl.KV{"env": "stage", "svc": "foo"},
},
},
},
}
tests := []struct {
name string
template string
want string
wantErr bool
}{
{
name: "success",
template: "foo",
want: "foo",
},
{
name: "success-data-status",
template: "{{ .Data.Status }}",
want: "firing",
},
{
name: "success-status",
template: "{{ .Status }}",
want: "firing",
},
{
name: "error-template",
template: "{{ range .NOT_REAL_FIELD }}\n* {{.Status}}\n{{end}}",
wantErr: true,
},
{
name: "error-template-no-field",
template: "{{ .Foo }}",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rh, err := NewReceiver(&fakeClient{}, "default", false, "", nil, "", tt.template)
if err != nil {
t.Fatal(err)
}

got, err := rh.formatIssueBody(&msg)
if (err != nil) != tt.wantErr {
t.Errorf("formatIssueBody() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("formatIssueBody() = %v, want %v", got, tt.want)
}
})
}
}

Expand Down Expand Up @@ -80,7 +130,7 @@ func TestReceiverHandler_formatTitle(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rh, err := NewReceiver(&fakeClient{}, "default", false, "", nil, tt.template)
rh, err := NewReceiver(&fakeClient{}, "default", false, "", nil, tt.template, "")
if err != nil {
t.Fatal(err)
}
Expand Down
6 changes: 4 additions & 2 deletions cmd/github_receiver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ var (
alertLabel = flag.String("alertlabel", "alert:boom:", "The default label applied to all alerts. Also used to search the repo to discover exisitng alerts.")
extraLabels = flagx.StringArray{}
titleTmplFile = flagx.FileBytes(alerts.DefaultTitleTmpl)
alertTmplFile = flagx.FileBytes(alerts.DefaultAlertTmpl)
)

// Metrics.
Expand Down Expand Up @@ -89,8 +90,9 @@ func init() {
flag.Var(&extraLabels, "label", "Extra labels to add to issues at creation time.")
flag.Var(&authtokenFile, "authtoken-file", "Oauth2 token file for access to github API. When provided it takes precedence over authtoken.")
flag.Var(&titleTmplFile, "title-template-file", "File containing a template to generate issue titles.")
flag.Var(&alertTmplFile, "alert-template-file", "File containing Markdown template to generate issue context.")
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), usage)
fmt.Fprint(flag.CommandLine.Output(), usage)
flag.PrintDefaults()
}
}
Expand Down Expand Up @@ -141,7 +143,7 @@ func main() {
promSrv := prometheusx.MustServeMetrics()
defer promSrv.Close()

receiver, err := alerts.NewReceiver(client, *githubRepo, *enableAutoClose, *labelOnResolved, extraLabels, string(titleTmplFile))
receiver, err := alerts.NewReceiver(client, *githubRepo, *enableAutoClose, *labelOnResolved, extraLabels, string(titleTmplFile), string(alertTmplFile))
if err != nil {
fmt.Print(err)
osExit(1)
Expand Down
Loading