Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
5 changes: 5 additions & 0 deletions pkg/cli/templates/body.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,9 @@ Previous Issue: {{ $previousIssue.HTMLURL }}
{{end -}}
</table>

{{- if $payload.HasSkipAutoCloseAnnotation }}

*This issue will not be auto-closed because the alerts have `atg-skip-auto-close=true` annotation.*
{{- end }}

<!-- alert data: {{json $payload}} -->
10 changes: 9 additions & 1 deletion pkg/notifier/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions pkg/types/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ type AlertStatus string
const (
AlertStatusResolved AlertStatus = "resolved"
AlertStatusFiring AlertStatus = "firing"

skipAutoCloseAnnotationKey = "atg-skip-auto-close"
skipAutoCloseAnnotationValue = "true"
)

type WebhookPayload struct {
Expand Down Expand Up @@ -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
}
92 changes: 92 additions & 0 deletions pkg/types/payload_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}