tools/amGithubNotifier/main.go (129 lines of code) (raw):

// Copyright 2019 The Prometheus Authors // 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 main import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "path/filepath" "github.com/google/go-github/v29/github" "github.com/prometheus/alertmanager/notify/webhook" "github.com/prometheus/alertmanager/template" "golang.org/x/oauth2" "gopkg.in/alecthomas/kingpin.v2" ) type ghWebhookReceiverConfig struct { authFile string org string repo string portNo string dryRun bool } type ghWebhookReceiver struct { ghClient *github.Client cfg ghWebhookReceiverConfig } type ghWebhookHandler struct { client *ghWebhookReceiver } func main() { /* Example `alerts.rules.yml`: ``` groups: - name: groupname rules: - alert: alertname expr: up == 0 labels: severity: info prNum: '{{ $labels.prNum }}' org: prometheus repo: prombench annotations: description: 'description of the alert' ``` */ log.SetFlags(log.Ltime | log.Lshortfile) cfg := ghWebhookReceiverConfig{} app := kingpin.New(filepath.Base(os.Args[0]), `alertmanager github webhook receiver Example: ./amGithubNotifier --org=prometheus --repo=prometheus --port=8080 Note: All alerts sent to amGithubNotifier must have the prNum label and description annotation, org and repo labels are optional but will take precedence over cli args if provided. `) app.Flag("authfile", "path to github oauth token file").Default("/etc/github/oauth").StringVar(&cfg.authFile) app.Flag("org", "name of the org").Required().StringVar(&cfg.org) app.Flag("repo", "name of the repo").Required().StringVar(&cfg.repo) app.Flag("port", "port number to run the server in").Default("8080").StringVar(&cfg.portNo) app.Flag("dryrun", "dry run for github api").BoolVar(&cfg.dryRun) kingpin.MustParse(app.Parse(os.Args[1:])) client, err := newGhWebhookReceiver(cfg) if err != nil { log.Fatalf("failed to create GitHub Webhook Receiver client: %v", err) } hl := ghWebhookHandler{client} http.Handle("/hook", hl) log.Printf("finished setting up gh client. starting amGithubNotifier with %v/%v", client.cfg.org, client.cfg.repo) err = http.ListenAndServe(fmt.Sprintf(":%v", client.cfg.portNo), nil) if err != nil { log.Fatal("amGithubNotifier exited unexpectedly") } } func (hl ghWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { log.Printf("unsupported request method: %v: %v", r.Method, r.RemoteAddr) http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) return } msg := &webhook.Message{} ctx := r.Context() // Decode the webhook request. err := json.NewDecoder(r.Body).Decode(msg) if err != nil { log.Println("failed to decode webhook data") http.Error(w, err.Error(), http.StatusBadRequest) return } // Handle the webhook message. log.Printf("handling alert: %v", alertID(msg)) if _, err := hl.client.processAlerts(ctx, msg); err != nil { log.Printf("failed to handle alert: %v: %v", alertID(msg), err) http.Error(w, err.Error(), http.StatusInternalServerError) return } log.Printf("completed alert: %v", alertID(msg)) w.WriteHeader(http.StatusOK) } func newGhWebhookReceiver(cfg ghWebhookReceiverConfig) (*ghWebhookReceiver, error) { if cfg.dryRun { return &ghWebhookReceiver{ ghClient: github.NewClient(nil), cfg: cfg, }, nil } // Add github token. oauth2token, err := os.ReadFile(cfg.authFile) if err != nil { return nil, err } ts := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: string(oauth2token)}, ) ctx := context.Background() tc := oauth2.NewClient(ctx, ts) return &ghWebhookReceiver{ ghClient: github.NewClient(tc), cfg: cfg, }, nil } // processAlert formats and posts the alert to GitHub. func (g ghWebhookReceiver) processAlert(ctx context.Context, alert template.Alert) (string, error) { msgBody, err := formatIssueCommentBody(alert) if err != nil { return "", err } issueComment := github.IssueComment{Body: &msgBody} prNum, err := getTargetPR(alert) if err != nil { return "", err } if g.cfg.dryRun { return msgBody, err } _, _, err = g.ghClient.Issues.CreateComment(ctx, g.getTargetOrg(alert), g.getTargetRepo(alert), prNum, &issueComment) return msgBody, err } func (g ghWebhookReceiver) processAlerts(ctx context.Context, msg *webhook.Message) ([]string, error) { var alertcomments []string // Each alert will have its own comment. for _, a := range msg.Alerts { alertcomment, err := g.processAlert(ctx, a) if err != nil { return nil, err } alertcomments = append(alertcomments, alertcomment) } return alertcomments, nil }