diff --git a/README.md b/README.md index 6dfdac1..1b621e9 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ OPTIONS: --github-app-installation-id value GitHub App installation ID (default: 0) [$ATG_GITHUB_APP_INSTALLATION_ID] --github-app-private-key value GitHub App private key (command line argument is not recommended) [$ATG_GITHUB_APP_PRIVATE_KEY] --github-token value GitHub API token (command line argument is not recommended) [$ATG_GITHUB_TOKEN] - --auto-close-resolved-issues Should issues be automatically closed when resolved (default: true) [$ATG_AUTO_CLOSE_RESOLVED_ISSUES] + --auto-close-resolved-issues Should issues be automatically closed when resolved. If alerts have 'atg-skip-auto-close=true' annotation, issues will not be auto-closed. (default: true) [$ATG_AUTO_CLOSE_RESOLVED_ISSUES] --reopen-window value Alerts will create a new issue instead of reopening closed issues if the specified duration has passed [$ATG_REOPEN_WINDOW] --help, -h show help ``` @@ -106,6 +106,21 @@ Issue title and body are rendered from [Go template](https://golang.org/pkg/text - `json`: Marshal an object to JSON string - `timeNow`: Get current time +### Automatically close issues when alerts are resolved + +You can use the `--auto-close-resolved-issues` flag to automatically close issues when alerts are resolved. + +If you want to skip auto-close for some alerts, add the `atg-skip-auto-close=true` annotation to them. + +```yaml +- alert: HighRequestLatency + expr: job:request_latency_seconds:mean5m{job="myjob"} > 0.5 + labels: + severity: critical + annotations: + atg-skip-auto-close: "true" +``` + ## Customize organization and repository The organization/repository where issues are raised can be customized per-alert by specifying the `atg_owner` label for the organization and/or the `atg_repo` label for the repository on the alert. diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 715117a..85621d5 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -182,7 +182,7 @@ func App() *cli.App { Name: flagAutoCloseResolvedIssues, Required: false, Value: true, - Usage: "Should issues be automatically closed when resolved", + Usage: "Should issues be automatically closed when resolved. If alerts have 'atg-skip-auto-close=true' annotation, issues will not be auto-closed.", EnvVars: []string{"ATG_AUTO_CLOSE_RESOLVED_ISSUES"}, }, &noDefaultDurationFlag{ diff --git a/pkg/cli/templates/body.tmpl b/pkg/cli/templates/body.tmpl index 5710e26..c71dcda 100644 --- a/pkg/cli/templates/body.tmpl +++ b/pkg/cli/templates/body.tmpl @@ -55,4 +55,9 @@ Previous Issue: {{ $previousIssue.HTMLURL }} {{end -}} +{{- if $payload.HasSkipAutoCloseAnnotation }} + +*This issue will not be auto-closed because the alerts have `atg-skip-auto-close=true` annotation.* +{{- end }} + diff --git a/pkg/notifier/github.go b/pkg/notifier/github.go index 639d921..408f798 100644 --- a/pkg/notifier/github.go +++ b/pkg/notifier/github.go @@ -220,7 +220,7 @@ func (n *GitHubNotifier) Notify(ctx context.Context, payload *types.WebhookPaylo } currentState := issue.GetState() - canUpdateState := desiredState != "closed" || n.AutoCloseResolvedIssues + canUpdateState := desiredState == "open" || n.shouldAutoCloseIssue(payload) if desiredState != currentState && canUpdateState { req = &github.IssueRequest{ @@ -300,6 +300,14 @@ func (n *GitHubNotifier) getAlertID(payload *types.WebhookPayload) (string, erro return fmt.Sprintf("%x", sha256.Sum256([]byte(id))), nil } +func (n *GitHubNotifier) shouldAutoCloseIssue(payload *types.WebhookPayload) bool { + if !n.AutoCloseResolvedIssues { + return false + } + + return !payload.HasSkipAutoCloseAnnotation() +} + func checkSearchResponse(response *github.Response) error { if response.StatusCode < 200 || 300 <= response.StatusCode { return fmt.Errorf("issue search returned %d", response.StatusCode) diff --git a/pkg/types/payload.go b/pkg/types/payload.go index ddbdade..768799b 100644 --- a/pkg/types/payload.go +++ b/pkg/types/payload.go @@ -10,6 +10,9 @@ type AlertStatus string const ( AlertStatusResolved AlertStatus = "resolved" AlertStatusFiring AlertStatus = "firing" + + skipAutoCloseAnnotationKey = "atg-skip-auto-close" + skipAutoCloseAnnotationValue = "true" ) type WebhookPayload struct { @@ -77,3 +80,18 @@ func (p *WebhookPayload) AnnotationKeysExceptCommon() []string { return keys } + +func (p *WebhookPayload) HasSkipAutoCloseAnnotation() bool { + for _, alert := range p.Alerts { + if alert.Annotations == nil { + continue + } + + val, ok := alert.Annotations[skipAutoCloseAnnotationKey] + if ok && val == skipAutoCloseAnnotationValue { + return true + } + } + + return false +} diff --git a/pkg/types/payload_test.go b/pkg/types/payload_test.go new file mode 100644 index 0000000..634cf15 --- /dev/null +++ b/pkg/types/payload_test.go @@ -0,0 +1,92 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWebhookPayloadHasSkipAutoCloseAnnotation(t *testing.T) { + tests := []struct { + name string + payload *WebhookPayload + expected bool + }{ + { + name: "no annotations", + payload: &WebhookPayload{ + Alerts: []WebhookAlert{{ + Labels: map[string]string{ + "job": "example", + }, + }}, + }, + expected: false, + }, + { + name: "has the annotation", + payload: &WebhookPayload{ + Alerts: []WebhookAlert{{ + Annotations: map[string]string{ + "atg-skip-auto-close": "true", + }, + }}, + }, + expected: true, + }, + { + name: "don't has the annotation", + payload: &WebhookPayload{ + Alerts: []WebhookAlert{{ + Annotations: map[string]string{ + "description": "example", + }, + }}, + }, + expected: false, + }, + { + name: "no alerts has the annotation", + payload: &WebhookPayload{ + Alerts: []WebhookAlert{ + { + Labels: map[string]string{ + "job": "example", + }, + }, + { + Labels: map[string]string{ + "job": "example", + }, + }, + }, + }, + expected: false, + }, + { + name: "some alerts have the annotation", + payload: &WebhookPayload{ + Alerts: []WebhookAlert{ + { + Annotations: map[string]string{ + "atg-skip-auto-close": "true", + }, + }, + { + Labels: map[string]string{ + "job": "example", + }, + }, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := tt.payload.HasSkipAutoCloseAnnotation() + assert.Equal(t, tt.expected, actual) + }) + } +}