Skip to content

Commit 3f7b053

Browse files
stephen-solteszAlex Orfanosaorfanos
authored
Feature: Add support for body templates (#59)
* fix error strings should not be capitalized * add -alert-template-file flag * fix test and comment out portion * fix(template/tests): fix formatIssueBody initial * fix(template/tests): amend expected error text for issue body test * fix(alerts/handler): tidy up alert template checking * fix(alerts/handler): add alert template * fix(alerts/handler): fix alert template * fix(handler/tests): add test case for bad alert template syntax * fix(handler/tests): add another test case for alert template with present issue * fix(handler/tests): rename test to bad index testing * fix(handler/tests): remove alert template bad index * Update unit tests * Complete code coverage, explicit template values * Use consistent name for DefaultAlertTmpl * Omit redundant type cast Co-authored-by: Alex Orfanos <[email protected]> Co-authored-by: Alex Orfanos <[email protected]>
1 parent e018b3e commit 3f7b053

File tree

6 files changed

+140
-41
lines changed

6 files changed

+140
-41
lines changed

alerts/handler.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,13 @@ type ReceiverHandler struct {
7979

8080
// titleTmpl is used to format the title of the new issue.
8181
titleTmpl *template.Template
82+
83+
// alertTmpl is used to format the context of the new issue.
84+
alertTmpl *template.Template
8285
}
8386

8487
// NewReceiver creates a new ReceiverHandler.
85-
func NewReceiver(client ReceiverClient, githubRepo string, autoClose bool, resolvedLabel string, extraLabels []string, titleTmplStr string) (*ReceiverHandler, error) {
88+
func NewReceiver(client ReceiverClient, githubRepo string, autoClose bool, resolvedLabel string, extraLabels []string, titleTmplStr string, alertTmplStr string) (*ReceiverHandler, error) {
8689
rh := ReceiverHandler{
8790
Client: client,
8891
DefaultRepo: githubRepo,
@@ -97,6 +100,11 @@ func NewReceiver(client ReceiverClient, githubRepo string, autoClose bool, resol
97100
return nil, err
98101
}
99102

103+
rh.alertTmpl, err = template.New("alert").Parse(alertTmplStr)
104+
if err != nil {
105+
return nil, err
106+
}
107+
100108
return &rh, nil
101109
}
102110

@@ -171,7 +179,10 @@ func (rh *ReceiverHandler) processAlert(msg *webhook.Message) error {
171179
// issue from github, so create a new issue.
172180
if msg.Data.Status == "firing" {
173181
if foundIssue == nil {
174-
msgBody := formatIssueBody(msg)
182+
msgBody, err := rh.formatIssueBody(msg)
183+
if err != nil {
184+
return fmt.Errorf("format body for %q: %s", msg.GroupKey, err)
185+
}
175186
_, err = rh.Client.CreateIssue(rh.getTargetRepo(msg), msgTitle, msgBody, rh.ExtraLabels)
176187
if err == nil {
177188
createdIssues.WithLabelValues(alertName).Inc()

alerts/handler_test.go

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
66
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
7+
// http://www.apache.org/licenses/LICENSE-2.0
88
//
99
// Unless required by applicable law or agreed to in writing, software
1010
// distributed under the License is distributed on an "AS IS" BASIS,
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
14-
//////////////////////////////////////////////////////////////////////////////
14+
// ////////////////////////////////////////////////////////////////////////////
1515
package alerts
1616

1717
import (
@@ -125,6 +125,7 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
125125
msgRepo string
126126
fakeClient *fakeClient
127127
titleTmpl string
128+
alertTmpl string
128129
httpStatus int
129130
expectReceiverErr bool
130131
wantMessageErr bool
@@ -140,6 +141,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
140141
createIssue("DiskRunningFull", "body1", ""),
141142
},
142143
},
144+
titleTmpl: DefaultTitleTmpl,
145+
alertTmpl: DefaultAlertTmpl,
143146
httpStatus: http.StatusOK,
144147
},
145148
{
@@ -148,6 +151,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
148151
msgAlert: "DiskRunningFull",
149152
msgAlertStatus: "resolved",
150153
fakeClient: &fakeClient{},
154+
titleTmpl: DefaultTitleTmpl,
155+
alertTmpl: DefaultAlertTmpl,
151156
httpStatus: http.StatusOK,
152157
},
153158
{
@@ -156,6 +161,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
156161
msgAlert: "DiskRunningFull",
157162
msgAlertStatus: "firing",
158163
fakeClient: &fakeClient{},
164+
titleTmpl: DefaultTitleTmpl,
165+
alertTmpl: DefaultAlertTmpl,
159166
httpStatus: http.StatusOK,
160167
},
161168
{
@@ -165,6 +172,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
165172
msgAlertStatus: "firing",
166173
msgRepo: "custom-repo",
167174
fakeClient: &fakeClient{},
175+
titleTmpl: DefaultTitleTmpl,
176+
alertTmpl: DefaultAlertTmpl,
168177
httpStatus: http.StatusOK,
169178
},
170179
{
@@ -177,6 +186,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
177186
createIssue("DiskRunningFull", "body1", ""),
178187
},
179188
},
189+
titleTmpl: DefaultTitleTmpl,
190+
alertTmpl: DefaultAlertTmpl,
180191
httpStatus: http.StatusOK,
181192
},
182193
{
@@ -190,6 +201,7 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
190201
},
191202
},
192203
titleTmpl: `{{ (index .Data.Alerts 0).Labels.alertname }}`,
204+
alertTmpl: `Disk is running full on {{ (index .Data.Alerts 0).Labels.instance }}`,
193205
httpStatus: http.StatusOK,
194206
},
195207
{
@@ -203,6 +215,8 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
203215
},
204216
labelError: errors.New("No such label"),
205217
},
218+
titleTmpl: DefaultTitleTmpl,
219+
alertTmpl: DefaultAlertTmpl,
206220
httpStatus: http.StatusInternalServerError,
207221
},
208222
{
@@ -216,6 +230,7 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
216230
},
217231
},
218232
titleTmpl: `{{ (index .Data.Alerts 1).Status }}`,
233+
alertTmpl: DefaultAlertTmpl,
219234
httpStatus: http.StatusInternalServerError,
220235
},
221236
{
@@ -229,18 +244,38 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
229244
},
230245
},
231246
titleTmpl: `{{ x }}`,
247+
alertTmpl: DefaultAlertTmpl,
248+
expectReceiverErr: true,
249+
httpStatus: http.StatusInternalServerError,
250+
},
251+
{
252+
name: "failure-alert-template-bad-syntax",
253+
method: http.MethodPost,
254+
msgAlert: "DiskRunningFull",
255+
msgAlertStatus: "firing",
256+
fakeClient: &fakeClient{
257+
listIssues: []*github.Issue{
258+
createIssue("DiskRunningFull", "body1", ""),
259+
},
260+
},
261+
titleTmpl: `{{ (index .Data.Alerts 1).Status }}`,
262+
alertTmpl: `{{ x }}`,
232263
expectReceiverErr: true,
233264
httpStatus: http.StatusInternalServerError,
234265
},
235266
{
236267
name: "failure-unmarshal-error",
237268
method: http.MethodPost,
269+
titleTmpl: DefaultTitleTmpl,
270+
alertTmpl: DefaultAlertTmpl,
238271
httpStatus: http.StatusBadRequest,
239272
wantMessageErr: true,
240273
},
241274
{
242275
name: "failure-reader-error",
243276
method: http.MethodPost,
277+
titleTmpl: DefaultTitleTmpl,
278+
alertTmpl: DefaultAlertTmpl,
244279
httpStatus: http.StatusInternalServerError,
245280
wantReadErr: true,
246281
},
@@ -250,13 +285,27 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
250285
fakeClient: &fakeClient{
251286
listError: fmt.Errorf("Fake error listing current issues"),
252287
},
288+
titleTmpl: DefaultTitleTmpl,
289+
alertTmpl: DefaultAlertTmpl,
253290
httpStatus: http.StatusInternalServerError,
254291
},
255292
{
256293
name: "failure-wrong-method",
257294
method: http.MethodGet,
295+
titleTmpl: DefaultTitleTmpl,
296+
alertTmpl: DefaultAlertTmpl,
258297
httpStatus: http.StatusMethodNotAllowed,
259298
},
299+
{
300+
name: "failure-body-template",
301+
method: http.MethodPost,
302+
msgAlert: "DiskRunningFull",
303+
msgAlertStatus: "firing",
304+
fakeClient: &fakeClient{},
305+
titleTmpl: DefaultTitleTmpl,
306+
alertTmpl: `{{ .NOTAREAL_FIELD }}`,
307+
httpStatus: http.StatusInternalServerError,
308+
},
260309
}
261310
for _, tt := range tests {
262311
t.Run(tt.name, func(t *testing.T) {
@@ -284,11 +333,7 @@ func TestReceiverHandler_ServeHTTP(t *testing.T) {
284333
return
285334
}
286335

287-
titleTmpl := tt.titleTmpl
288-
if titleTmpl == "" {
289-
titleTmpl = DefaultTitleTmpl
290-
}
291-
rh, err := NewReceiver(tt.fakeClient, "default", true, "", nil, titleTmpl)
336+
rh, err := NewReceiver(tt.fakeClient, "default", true, "", nil, tt.titleTmpl, tt.alertTmpl)
292337
if tt.expectReceiverErr {
293338
if err == nil {
294339
t.Fatal()

alerts/template.go

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@ package alerts
1818
import (
1919
"bytes"
2020
"fmt"
21-
"html/template"
22-
"log"
2321

2422
"github.com/prometheus/alertmanager/notify/webhook"
2523
)
2624

2725
const (
28-
// alertMD reports all alert labels and annotations in a markdown format
26+
// DefaultAlertTmpl reports all alert labels and annotations in a markdown format
2927
// that renders correctly in github issues.
3028
//
3129
// Example:
@@ -51,7 +49,7 @@ const (
5149
// - alertname = DiskRunningFull
5250
// - dev = sda2
5351
// - instance = example2
54-
alertMD = `
52+
DefaultAlertTmpl = `
5553
Alertmanager URL: {{.Data.ExternalURL}}
5654
{{range .Data.Alerts}}
5755
* {{.Status}} {{.GeneratorURL}}
@@ -77,10 +75,6 @@ TODO: add graph url from annotations.
7775
DefaultTitleTmpl = `{{ .Data.GroupLabels.alertname }}`
7876
)
7977

80-
var (
81-
alertTemplate = template.Must(template.New("alert").Parse(alertMD))
82-
)
83-
8478
func id(msg *webhook.Message) string {
8579
return fmt.Sprintf("0x%x", msg.GroupKey)
8680
}
@@ -95,13 +89,10 @@ func (rh *ReceiverHandler) formatTitle(msg *webhook.Message) (string, error) {
9589
}
9690

9791
// formatIssueBody constructs an issue body from a webhook message.
98-
func formatIssueBody(msg *webhook.Message) string {
92+
func (rh *ReceiverHandler) formatIssueBody(msg *webhook.Message) (string, error) {
9993
var buf bytes.Buffer
100-
err := alertTemplate.Execute(&buf, msg)
101-
if err != nil {
102-
log.Printf("Error executing template: %s", err)
103-
return ""
94+
if err := rh.alertTmpl.Execute(&buf, msg); err != nil {
95+
return "", err
10496
}
105-
s := buf.String()
106-
return fmt.Sprintf("<!-- ID: %s -->\n%s", id(msg), s)
97+
return buf.String(), nil
10798
}

alerts/template_test.go

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,74 @@
1616
package alerts
1717

1818
import (
19-
"html/template"
2019
"testing"
2120

2221
"github.com/prometheus/alertmanager/notify/webhook"
2322
amtmpl "github.com/prometheus/alertmanager/template"
2423
)
2524

2625
func Test_formatIssueBody(t *testing.T) {
27-
wh := createWebhookMessage("FakeAlertName", "firing", "")
28-
brokenTemplate := `
29-
{{range .NOT_REAL_FIELD}}
30-
* {{.Status}}
31-
{{end}}
32-
`
33-
alertTemplate = template.Must(template.New("alert").Parse(brokenTemplate))
34-
got := formatIssueBody(wh)
35-
if got != "" {
36-
t.Errorf("formatIssueBody() = %q, want empty string", got)
26+
msg := webhook.Message{
27+
Data: &amtmpl.Data{
28+
Status: "firing",
29+
Alerts: []amtmpl.Alert{
30+
{
31+
Annotations: amtmpl.KV{"env": "prod", "svc": "foo"},
32+
},
33+
{
34+
Annotations: amtmpl.KV{"env": "stage", "svc": "foo"},
35+
},
36+
},
37+
},
38+
}
39+
tests := []struct {
40+
name string
41+
template string
42+
want string
43+
wantErr bool
44+
}{
45+
{
46+
name: "success",
47+
template: "foo",
48+
want: "foo",
49+
},
50+
{
51+
name: "success-data-status",
52+
template: "{{ .Data.Status }}",
53+
want: "firing",
54+
},
55+
{
56+
name: "success-status",
57+
template: "{{ .Status }}",
58+
want: "firing",
59+
},
60+
{
61+
name: "error-template",
62+
template: "{{ range .NOT_REAL_FIELD }}\n* {{.Status}}\n{{end}}",
63+
wantErr: true,
64+
},
65+
{
66+
name: "error-template-no-field",
67+
template: "{{ .Foo }}",
68+
wantErr: true,
69+
},
70+
}
71+
for _, tt := range tests {
72+
t.Run(tt.name, func(t *testing.T) {
73+
rh, err := NewReceiver(&fakeClient{}, "default", false, "", nil, "", tt.template)
74+
if err != nil {
75+
t.Fatal(err)
76+
}
77+
78+
got, err := rh.formatIssueBody(&msg)
79+
if (err != nil) != tt.wantErr {
80+
t.Errorf("formatIssueBody() error = %v, wantErr %v", err, tt.wantErr)
81+
return
82+
}
83+
if got != tt.want {
84+
t.Errorf("formatIssueBody() = %v, want %v", got, tt.want)
85+
}
86+
})
3787
}
3888
}
3989

@@ -80,7 +130,7 @@ func TestReceiverHandler_formatTitle(t *testing.T) {
80130
}
81131
for _, tt := range tests {
82132
t.Run(tt.name, func(t *testing.T) {
83-
rh, err := NewReceiver(&fakeClient{}, "default", false, "", nil, tt.template)
133+
rh, err := NewReceiver(&fakeClient{}, "default", false, "", nil, tt.template, "")
84134
if err != nil {
85135
t.Fatal(err)
86136
}

cmd/github_receiver/main.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ var (
5151
alertLabel = flag.String("alertlabel", "alert:boom:", "The default label applied to all alerts. Also used to search the repo to discover exisitng alerts.")
5252
extraLabels = flagx.StringArray{}
5353
titleTmplFile = flagx.FileBytes(alerts.DefaultTitleTmpl)
54+
alertTmplFile = flagx.FileBytes(alerts.DefaultAlertTmpl)
5455
)
5556

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

144-
receiver, err := alerts.NewReceiver(client, *githubRepo, *enableAutoClose, *labelOnResolved, extraLabels, string(titleTmplFile))
146+
receiver, err := alerts.NewReceiver(client, *githubRepo, *enableAutoClose, *labelOnResolved, extraLabels, string(titleTmplFile), string(alertTmplFile))
145147
if err != nil {
146148
fmt.Print(err)
147149
osExit(1)

0 commit comments

Comments
 (0)