Skip to content

Commit 4e1350b

Browse files
Merge pull request #10 from m-lab/sandbox-soltesz-reporoute
Enable routing alerts to multiple repos
2 parents 9dfa3d3 + a878452 commit 4e1350b

File tree

7 files changed

+159
-73
lines changed

7 files changed

+159
-73
lines changed

alerts/handler.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414
//////////////////////////////////////////////////////////////////////////////
15+
1516
package alerts
1617

1718
import (
@@ -26,18 +27,26 @@ import (
2627
"github.com/prometheus/alertmanager/notify"
2728
)
2829

30+
// ReceiverClient defines all issue operations needed by the ReceiverHandler.
2931
type ReceiverClient interface {
3032
CloseIssue(issue *github.Issue) (*github.Issue, error)
31-
CreateIssue(title, body string) (*github.Issue, error)
33+
CreateIssue(repo, title, body string) (*github.Issue, error)
3234
ListOpenIssues() ([]*github.Issue, error)
3335
}
3436

37+
// ReceiverHandler contains data needed for HTTP handlers.
3538
type ReceiverHandler struct {
36-
// Client is an implementation of the ReceiverClient interface. Client is used to handle requests.
39+
// Client is an implementation of the ReceiverClient interface. Client is used
40+
// to handle requests.
3741
Client ReceiverClient
3842

39-
// AutoClose indicates whether resolved issues that are still open should be closed automatically.
43+
// AutoClose indicates whether resolved issues that are still open should be
44+
// closed automatically.
4045
AutoClose bool
46+
47+
// DefaultRepo is the repository where all alerts without a "repo" label will
48+
// be created. Repo must exist.
49+
DefaultRepo string
4150
}
4251

4352
// ServeHTTP receives and processes alertmanager notifications. If the alert
@@ -106,7 +115,7 @@ func (rh *ReceiverHandler) processAlert(msg *notify.WebhookMessage) error {
106115
// issue from github, so create a new issue.
107116
if msg.Data.Status == "firing" && foundIssue == nil {
108117
msgBody := formatIssueBody(msg)
109-
_, err := rh.Client.CreateIssue(msgTitle, msgBody)
118+
_, err := rh.Client.CreateIssue(rh.getTargetRepo(msg), msgTitle, msgBody)
110119
return err
111120
}
112121

@@ -124,3 +133,14 @@ func (rh *ReceiverHandler) processAlert(msg *notify.WebhookMessage) error {
124133
// log.Printf("Unsupported WebhookMessage.Data.Status: %s", msg.Data.Status)
125134
return nil
126135
}
136+
137+
// getTargetRepo returns a suitable github repository for creating an issue for
138+
// the given alert message. If the alert includes a "repo" label, then getTargetRepo
139+
// uses that value. Otherwise, getTargetRepo uses the ReceiverHandler's default repo.
140+
func (rh *ReceiverHandler) getTargetRepo(msg *notify.WebhookMessage) string {
141+
repo := msg.CommonLabels["repo"]
142+
if repo != "" {
143+
return repo
144+
}
145+
return rh.DefaultRepo
146+
}

alerts/handler_test.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func (f *fakeClient) ListOpenIssues() ([]*github.Issue, error) {
4242
return f.listIssues, nil
4343
}
4444

45-
func (f *fakeClient) CreateIssue(title, body string) (*github.Issue, error) {
45+
func (f *fakeClient) CreateIssue(repo, title, body string) (*github.Issue, error) {
4646
fmt.Println("create issue")
4747
f.createdIssue = createIssue(title, body)
4848
return f.createdIssue, nil
@@ -110,7 +110,11 @@ func TestReceiverHandler(t *testing.T) {
110110
createIssue("DiskRunningFull", "body1"),
111111
},
112112
}
113-
handler := alerts.ReceiverHandler{f, true}
113+
handler := alerts.ReceiverHandler{
114+
Client: f,
115+
AutoClose: true,
116+
DefaultRepo: "default",
117+
}
114118
handler.ServeHTTP(rw, req)
115119
resp := rw.Result()
116120

@@ -143,7 +147,11 @@ func TestReceiverHandler(t *testing.T) {
143147

144148
// No pre-existing issues to close.
145149
f = &fakeClient{}
146-
handler = alerts.ReceiverHandler{f, true}
150+
handler = alerts.ReceiverHandler{
151+
Client: f,
152+
AutoClose: true,
153+
DefaultRepo: "default",
154+
}
147155
handler.ServeHTTP(rw, req)
148156
resp = rw.Result()
149157

cmd/github_receiver/main.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ import (
2525

2626
"github.com/m-lab/alertmanager-github-receiver/alerts"
2727
"github.com/m-lab/alertmanager-github-receiver/issues"
28+
// TODO: add prometheus metrics for errors and github api access.
2829
)
2930

3031
var (
3132
authtoken = flag.String("authtoken", "", "Oauth2 token for access to github API.")
32-
githubOwner = flag.String("owner", "", "The github user or organization name.")
33-
githubRepo = flag.String("repo", "", "The repository where issues are created.")
33+
githubOrg = flag.String("org", "", "The github user or organization name where all repos are found.")
34+
githubRepo = flag.String("repo", "", "The default repository for creating issues when alerts do not include a repo label.")
3435
enableAutoClose = flag.Bool("enable-auto-close", false, "Once an alert stops firing, automatically close open issues.")
3536
)
3637

@@ -52,17 +53,21 @@ func init() {
5253
}
5354

5455
func serveListener(client *issues.Client) {
55-
http.Handle("/", &issues.ListHandler{client})
56-
http.Handle("/v1/receiver", &alerts.ReceiverHandler{client, *enableAutoClose})
56+
http.Handle("/", &issues.ListHandler{ListClient: client})
57+
http.Handle("/v1/receiver", &alerts.ReceiverHandler{
58+
Client: client,
59+
DefaultRepo: *githubRepo,
60+
AutoClose: *enableAutoClose,
61+
})
5762
http.ListenAndServe(":9393", nil)
5863
}
5964

6065
func main() {
6166
flag.Parse()
62-
if *authtoken == "" || *githubOwner == "" || *githubRepo == "" {
67+
if *authtoken == "" || *githubOrg == "" || *githubRepo == "" {
6368
flag.Usage()
6469
os.Exit(1)
6570
}
66-
client := issues.NewClient(*githubOwner, *githubRepo, *authtoken)
71+
client := issues.NewClient(*githubOrg, *authtoken)
6772
serveListener(client)
6873
}

issues/handler.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,20 @@ package issues
1717

1818
import (
1919
"fmt"
20-
"github.com/google/go-github/github"
2120
"html/template"
2221
"net/http"
22+
23+
"github.com/google/go-github/github"
2324
)
2425

2526
const (
2627
listRawHTMLTemplate = `
2728
<html><body>
2829
<h1>Open Issues</h1>
2930
<table>
30-
{{range .}}
31-
<tr><td><a href={{.HTMLURL}}>{{.Title}}</a></td></tr>
32-
{{end}}
31+
{{range .}}
32+
<tr><td><a href={{.HTMLURL}}>{{.Title}}</a></td></tr>
33+
{{end}}
3334
</table>
3435
</body></html>`
3536
)
@@ -38,17 +39,19 @@ var (
3839
listTemplate = template.Must(template.New("list").Parse(listRawHTMLTemplate))
3940
)
4041

42+
// ListClient defines an interface for listing issues.
4143
type ListClient interface {
4244
ListOpenIssues() ([]*github.Issue, error)
4345
}
4446

47+
// ListHandler contains data needed for HTTP handlers.
4548
type ListHandler struct {
46-
Client ListClient
49+
ListClient
4750
}
4851

4952
// ServeHTTP lists open issues from github for view in a browser.
5053
func (lh *ListHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
51-
issues, err := lh.Client.ListOpenIssues()
54+
issues, err := lh.ListOpenIssues()
5255
if err != nil {
5356
rw.WriteHeader(http.StatusInternalServerError)
5457
fmt.Fprintf(rw, "%s\n", err)

issues/handler_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
package issues_test
1616

1717
import (
18-
"github.com/google/go-github/github"
1918
"io/ioutil"
2019
"net/http"
2120
"net/http/httptest"
2221
"testing"
2322

23+
"github.com/google/go-github/github"
24+
2425
"github.com/m-lab/alertmanager-github-receiver/issues"
2526
)
2627

@@ -37,9 +38,9 @@ func TestListHandler(t *testing.T) {
3738
<html><body>
3839
<h1>Open Issues</h1>
3940
<table>
40-
41-
<tr><td><a href=http://foo.bar>issue1 title</a></td></tr>
42-
41+
42+
<tr><td><a href=http://foo.bar>issue1 title</a></td></tr>
43+
4344
</table>
4445
</body></html>`
4546
f := &fakeClient{
@@ -59,7 +60,7 @@ func TestListHandler(t *testing.T) {
5960
}
6061

6162
// Run the list handler.
62-
handler := issues.ListHandler{f}
63+
handler := issues.ListHandler{ListClient: f}
6364
handler.ServeHTTP(rw, req)
6465
resp := rw.Result()
6566

issues/issues.go

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@
1313
// limitations under the License.
1414
//////////////////////////////////////////////////////////////////////////////
1515

16-
// A client interface wrapping the Github API for creating, listing, and closing
17-
// issues on a single repository.
16+
// Package issues defines a client interface wrapping the Github API for
17+
// creating, listing, and closing issues on a single repository.
1818
package issues
1919

2020
import (
21+
"fmt"
2122
"log"
23+
"net/url"
24+
"strings"
25+
"time"
2226

2327
"github.com/google/go-github/github"
2428
"github.com/kr/pretty"
@@ -30,40 +34,44 @@ import (
3034
type Client struct {
3135
// githubClient is an authenticated client for accessing the github API.
3236
GithubClient *github.Client
33-
// owner is the github project (e.g. github.com/<owner>/<repo>).
34-
owner string
35-
// repo is the github repository under the above owner.
36-
repo string
37+
// org is the github user or organization name (e.g. github.com/<org>/<repo>).
38+
org string
3739
}
3840

3941
// NewClient creates an Client authenticated using the Github authToken.
40-
// Future operations are only performed on the given github "owner/repo".
41-
func NewClient(owner, repo, authToken string) *Client {
42+
// Future operations are only performed on the given github "org/repo".
43+
func NewClient(org, authToken string) *Client {
4244
ctx := context.Background()
4345
tokenSource := oauth2.StaticTokenSource(
4446
&oauth2.Token{AccessToken: authToken},
4547
)
4648
client := &Client{
4749
GithubClient: github.NewClient(oauth2.NewClient(ctx, tokenSource)),
48-
owner: owner,
49-
repo: repo,
50+
org: org,
5051
}
5152
return client
5253
}
5354

54-
// CreateIssue creates a new Github issue. New issues are unassigned.
55-
func (c *Client) CreateIssue(title, body string) (*github.Issue, error) {
55+
// CreateIssue creates a new Github issue. New issues are unassigned. Issues are
56+
// labeled with with an alert named "alert:boom:". Labels are created automatically
57+
// if they do not already exist in a repo.
58+
func (c *Client) CreateIssue(repo, title, body string) (*github.Issue, error) {
5659
// Construct a minimal github issue request.
5760
issueReq := github.IssueRequest{
58-
Title: &title,
59-
Body: &body,
61+
Title: &title,
62+
Body: &body,
63+
Labels: &([]string{"alert:boom:"}), // Search using: label:"alert:boom:"
6064
}
6165

66+
// Enforce a timeout on the issue creation.
67+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
68+
defer cancel()
69+
6270
// Create the issue.
6371
// See also: https://developer.github.com/v3/issues/#create-an-issue
6472
// See also: https://godoc.org/github.com/google/go-github/github#IssuesService.Create
6573
issue, resp, err := c.GithubClient.Issues.Create(
66-
context.Background(), c.owner, c.repo, &issueReq)
74+
ctx, c.org, repo, &issueReq)
6775
if err != nil {
6876
log.Printf("Error in CreateIssue: response: %v\n%s",
6977
err, pretty.Sprint(resp))
@@ -72,30 +80,42 @@ func (c *Client) CreateIssue(title, body string) (*github.Issue, error) {
7280
return issue, nil
7381
}
7482

75-
// ListOpenIssues returns open issues from github Github issues are either
76-
// "open" or "closed". Closed issues have either been resolved automatically or
77-
// by a person. So, there will be an ever increasing number of "closed" issues.
78-
// By only listing "open" issues we limit the number of issues returned.
83+
// ListOpenIssues returns open issues created by past alerts within the
84+
// client organization. Because ListOpenIssues uses the Github Search API,
85+
// the *github.Issue instances returned will contain partial information.
86+
// See also: https://developer.github.com/v3/search/#search-issues
7987
func (c *Client) ListOpenIssues() ([]*github.Issue, error) {
8088
var allIssues []*github.Issue
8189

82-
opts := &github.IssueListByRepoOptions{State: "open"}
90+
sopts := &github.SearchOptions{}
8391
for {
84-
issues, resp, err := c.GithubClient.Issues.ListByRepo(
85-
context.Background(), c.owner, c.repo, opts)
92+
// Enforce a timeout on the issue listing.
93+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
94+
defer cancel()
95+
96+
// Github issues are either "open" or "closed". Closed issues have either been
97+
// resolved automatically or by a person. So, there will be an ever increasing
98+
// number of "closed" issues. By only listing "open" issues we limit the
99+
// number of issues returned.
100+
//
101+
// The search depends on all relevant issues including the "alert:boom:" label.
102+
issues, resp, err := c.GithubClient.Search.Issues(
103+
ctx, `is:issue in:title is:open org:`+c.org+` label:"alert:boom:"`, sopts)
86104
if err != nil {
87-
log.Printf("Failed to list open github issues: %v\n%s",
88-
err, pretty.Sprint(resp))
105+
log.Printf("Failed to list open github issues: %v\n", err)
89106
return nil, err
90107
}
91108
// Collect 'em all.
92-
allIssues = append(allIssues, issues...)
109+
for i := range issues.Issues {
110+
log.Println("ListOpenIssues:", issues.Issues[i].GetTitle())
111+
allIssues = append(allIssues, &issues.Issues[i])
112+
}
93113

94114
// Continue loading the next page until all issues are received.
95115
if resp.NextPage == 0 {
96116
break
97117
}
98-
opts.ListOptions.Page = resp.NextPage
118+
sopts.ListOptions.Page = resp.NextPage
99119
}
100120
return allIssues, nil
101121
}
@@ -107,14 +127,42 @@ func (c *Client) CloseIssue(issue *github.Issue) (*github.Issue, error) {
107127
State: github.String("closed"),
108128
}
109129

130+
org, repo, err := getOrgAndRepoFromIssue(issue)
131+
if err != nil {
132+
return nil, err
133+
}
134+
// Enforce a timeout on the issue edit.
135+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
136+
defer cancel()
137+
110138
// Edits the issue to have "closed" state.
111139
// See also: https://developer.github.com/v3/issues/#edit-an-issue
112140
// See also: https://godoc.org/github.com/google/go-github/github#IssuesService.Edit
113-
closedIssue, resp, err := c.GithubClient.Issues.Edit(
114-
context.Background(), c.owner, c.repo, *issue.Number, &issueReq)
141+
closedIssue, _, err := c.GithubClient.Issues.Edit(
142+
ctx, org, repo, *issue.Number, &issueReq)
115143
if err != nil {
116-
log.Printf("Failed to close issue: %v\n%s", err, pretty.Sprint(resp))
144+
log.Printf("Failed to close issue: %v", err)
117145
return nil, err
118146
}
119147
return closedIssue, nil
120148
}
149+
150+
// getOrgAndRepoFromIssue reads the issue RepositoryURL and extracts the
151+
// owner and repo names. Issues returned by the Search API contain partial
152+
// records.
153+
func getOrgAndRepoFromIssue(issue *github.Issue) (string, string, error) {
154+
repoURL := issue.GetRepositoryURL()
155+
if repoURL == "" {
156+
return "", "", fmt.Errorf("Issue has invalid RepositoryURL value")
157+
}
158+
u, err := url.Parse(repoURL)
159+
if err != nil {
160+
return "", "", err
161+
}
162+
fields := strings.Split(u.Path, "/")
163+
if len(fields) != 4 {
164+
return "", "", fmt.Errorf("Issue has invalid RepositoryURL value")
165+
}
166+
return fields[2], fields[3], nil
167+
168+
}

0 commit comments

Comments
 (0)