From a3a5a6ffab68646a6e1466c8d66b2e0b3e7acafd Mon Sep 17 00:00:00 2001 From: big money stoffe Date: Mon, 19 Dec 2022 14:46:07 +0100 Subject: [PATCH 1/5] add support for additional labels --- pkg/config/config.go | 3 ++ pkg/notify/notify.go | 68 ++++++++++++++++++++++++++++++++------- pkg/notify/notify_test.go | 19 +++++++++++ 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index f72809c..4257666 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -148,6 +148,9 @@ type ReceiverConfig struct { // Label copy settings AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"` + // Additional labels + AdditionalLabels map[string]interface{} `yaml:"additional_labels" json:"additional_labels"` + // Flag to auto-resolve opened issue when the alert is resolved. AutoResolve *AutoResolve `yaml:"auto_resolve" json:"auto_resolve"` diff --git a/pkg/notify/notify.go b/pkg/notify/notify.go index 00a2d9e..77714fb 100644 --- a/pkg/notify/notify.go +++ b/pkg/notify/notify.go @@ -15,14 +15,16 @@ package notify import ( "bytes" + "crypto/sha1" "crypto/sha512" "fmt" - "github.com/andygrunwald/go-jira" "io" "reflect" "strings" "time" + "github.com/andygrunwald/go-jira" + "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/pkg/errors" @@ -32,6 +34,10 @@ import ( "github.com/trivago/tgo/tcontainer" ) +const ( + jiraHashLabel = "JIRALERT-HASH" +) + // TODO(bwplotka): Consider renaming this package to ticketer. type jiraIssueService interface { @@ -59,6 +65,44 @@ func NewReceiver(logger log.Logger, c *config.ReceiverConfig, t *template.Templa return &Receiver{logger: logger, conf: c, tmpl: t, client: client, timeNow: time.Now} } +func (r *Receiver) constructIssueLabel(groupLabels alertmanager.KV) ([]string, error) { + + labels := make([]string, 1) + labels[0] = toGroupTicketLabel(groupLabels, false) + + for k, v := range r.conf.AdditionalLabels { + label := fmt.Sprintf("%s=%q", k, v) + label = strings.Replace(label, " ", "", -1) + + labels = append(labels, label) + } + h := sha1.New() + + for _, label := range labels { + h.Write([]byte(label)) + } + + labels = append(labels, fmt.Sprintf("%s=%x", jiraHashLabel, h.Sum(nil))) + + return labels, nil +} + +// +// todo: +// +// add slice issueLabels and append issueGroupLabel +// add additional labels to issueLabels +// findIssueToReuse should search for issueLabels instead of issueGroupLabel. +// +// should be able to check if label exist on alert, then add it to additionalLabels +// if the label does not exist, then append empty string +// when done, remove all empty strings from issueLabels +// +// yaml example: +// additionalLabels: +// tenant: foo02 +// policy_level: + // Notify manages JIRA issues based on alertmanager webhook notify message. func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, error) { project, err := r.tmpl.Execute(r.conf.Project, data) @@ -66,9 +110,10 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er return false, errors.Wrap(err, "generate project from template") } - issueGroupLabel := toGroupTicketLabel(data.GroupLabels, hashJiraLabel) + issueLabel, err := r.constructIssueLabel(data.GroupLabels) + hashLabel := issueLabel[len(issueLabel)-1] - issue, retry, err := r.findIssueToReuse(project, issueGroupLabel) + issue, retry, err := r.findIssueToReuse(project, hashLabel) if err != nil { return retry, err } @@ -103,7 +148,7 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er if len(data.Alerts.Firing()) == 0 { if r.conf.AutoResolve != nil { - level.Debug(r.logger).Log("msg", "no firing alert; resolving issue", "key", issue.Key, "label", issueGroupLabel) + level.Debug(r.logger).Log("msg", "no firing alert; resolving issue", "key", issue.Key, "label", issueLabel) retry, err := r.resolveIssue(issue.Key) if err != nil { return retry, err @@ -111,32 +156,32 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er return false, nil } - level.Debug(r.logger).Log("msg", "no firing alert; summary checked, nothing else to do.", "key", issue.Key, "label", issueGroupLabel) + level.Debug(r.logger).Log("msg", "no firing alert; summary checked, nothing else to do.", "key", issue.Key, "label", issueLabel) return false, nil } // The set of JIRA status categories is fixed, this is a safe check to make. if issue.Fields.Status.StatusCategory.Key != "done" { - level.Debug(r.logger).Log("msg", "issue is unresolved, all is done", "key", issue.Key, "label", issueGroupLabel) + level.Debug(r.logger).Log("msg", "issue is unresolved, all is done", "key", issue.Key, "label", issueLabel) return false, nil } if r.conf.WontFixResolution != "" && issue.Fields.Resolution != nil && issue.Fields.Resolution.Name == r.conf.WontFixResolution { - level.Info(r.logger).Log("msg", "issue was resolved as won't fix, not reopening", "key", issue.Key, "label", issueGroupLabel, "resolution", issue.Fields.Resolution.Name) + level.Info(r.logger).Log("msg", "issue was resolved as won't fix, not reopening", "key", issue.Key, "label", issueLabel, "resolution", issue.Fields.Resolution.Name) return false, nil } - level.Info(r.logger).Log("msg", "issue was recently resolved, reopening", "key", issue.Key, "label", issueGroupLabel) + level.Info(r.logger).Log("msg", "issue was recently resolved, reopening", "key", issue.Key, "label", issueLabel) return r.reopen(issue.Key) } if len(data.Alerts.Firing()) == 0 { - level.Debug(r.logger).Log("msg", "no firing alert; nothing to do.", "label", issueGroupLabel) + level.Debug(r.logger).Log("msg", "no firing alert; nothing to do.", "label", issueLabel) return false, nil } - level.Info(r.logger).Log("msg", "no recent matching issue found, creating new issue", "label", issueGroupLabel) + level.Info(r.logger).Log("msg", "no recent matching issue found, creating new issue", "label", issueLabel) issueType, err := r.tmpl.Execute(r.conf.IssueType, data) if err != nil { @@ -149,7 +194,7 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er Type: jira.IssueType{Name: issueType}, Description: issueDesc, Summary: issueSummary, - Labels: []string{issueGroupLabel}, + Labels: issueLabel, Unknowns: tcontainer.NewMarshalMap(), }, } @@ -249,6 +294,7 @@ func deepCopyWithTemplate(value interface{}, tmpl *template.Template, data inter // if the combined length of all groupLabel key-value pairs would be // longer than 255 chars func toGroupTicketLabel(groupLabels alertmanager.KV, hashJiraLabel bool) string { + // new opt in behavior if hashJiraLabel { hash := sha512.New() diff --git a/pkg/notify/notify_test.go b/pkg/notify/notify_test.go index 78ef554..949a503 100644 --- a/pkg/notify/notify_test.go +++ b/pkg/notify/notify_test.go @@ -30,6 +30,25 @@ import ( "github.com/stretchr/testify/require" ) +func TestAdditionalLabels(t *testing.T) { + r := Receiver{ + conf: &config.ReceiverConfig{ + AdditionalLabels: map[string]interface{}{}, + }, + } + + r.conf.AdditionalLabels = map[string]interface{}{ + "foo": "bar", + } + + issueLabel, _ := r.constructIssueLabel(alertmanager.KV{"a": "b"}) + + require.Equal(t, `ALERT{a="b"}`, issueLabel[0]) + require.Equal(t, `foo="bar"`, issueLabel[1]) + require.Equal(t, `JIRALERT-HASH=6536991f7ce9c028fd657a21d258bc532670975f`, issueLabel[2]) + +} + func TestToGroupTicketLabel(t *testing.T) { require.Equal(t, `JIRALERT{9897cb21a3d1ba47d2aab501ce9bc60b74bf65e26658f8e34a7fc81705e6b6eadfe6ad8edfe7c68142b3fe10f2c89127bd85e5f3687fe6b9ff1eff4b3f71dd49}`, toGroupTicketLabel(alertmanager.KV{"a": "B", "C": "d"}, true)) require.Equal(t, `ALERT{C="d",a="B"}`, toGroupTicketLabel(alertmanager.KV{"a": "B", "C": "d"}, false)) From 72b45d0976042e770c217d121d69f674aa64f561 Mon Sep 17 00:00:00 2001 From: big money stoffe Date: Tue, 20 Dec 2022 13:37:33 +0100 Subject: [PATCH 2/5] add common labels --- pkg/config/config.go | 6 ++-- pkg/notify/notify.go | 65 ++++++++++++++------------------------- pkg/notify/notify_test.go | 65 +++++++++++++++++++++++++++------------ 3 files changed, 71 insertions(+), 65 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 4257666..48a8b36 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -146,10 +146,8 @@ type ReceiverConfig struct { Components []string `yaml:"components" json:"components"` // Label copy settings - AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"` - - // Additional labels - AdditionalLabels map[string]interface{} `yaml:"additional_labels" json:"additional_labels"` + AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"` + AddCommonLabels bool `yaml:"add_common_labels" json:"add_common_labels"` // Flag to auto-resolve opened issue when the alert is resolved. AutoResolve *AutoResolve `yaml:"auto_resolve" json:"auto_resolve"` diff --git a/pkg/notify/notify.go b/pkg/notify/notify.go index 77714fb..a813c24 100644 --- a/pkg/notify/notify.go +++ b/pkg/notify/notify.go @@ -35,7 +35,7 @@ import ( ) const ( - jiraHashLabel = "JIRALERT-HASH" + jiraHashLabel = "Jiralert-checksum" ) // TODO(bwplotka): Consider renaming this package to ticketer. @@ -65,44 +65,17 @@ func NewReceiver(logger log.Logger, c *config.ReceiverConfig, t *template.Templa return &Receiver{logger: logger, conf: c, tmpl: t, client: client, timeNow: time.Now} } -func (r *Receiver) constructIssueLabel(groupLabels alertmanager.KV) ([]string, error) { +func toChecksumTicketLabel(labels []string) string { - labels := make([]string, 1) - labels[0] = toGroupTicketLabel(groupLabels, false) - - for k, v := range r.conf.AdditionalLabels { - label := fmt.Sprintf("%s=%q", k, v) - label = strings.Replace(label, " ", "", -1) - - labels = append(labels, label) - } h := sha1.New() for _, label := range labels { - h.Write([]byte(label)) + _, _ = h.Write([]byte(label)) } - labels = append(labels, fmt.Sprintf("%s=%x", jiraHashLabel, h.Sum(nil))) - - return labels, nil + return fmt.Sprintf("%s=%x", jiraHashLabel, h.Sum(nil)) } -// -// todo: -// -// add slice issueLabels and append issueGroupLabel -// add additional labels to issueLabels -// findIssueToReuse should search for issueLabels instead of issueGroupLabel. -// -// should be able to check if label exist on alert, then add it to additionalLabels -// if the label does not exist, then append empty string -// when done, remove all empty strings from issueLabels -// -// yaml example: -// additionalLabels: -// tenant: foo02 -// policy_level: - // Notify manages JIRA issues based on alertmanager webhook notify message. func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, error) { project, err := r.tmpl.Execute(r.conf.Project, data) @@ -110,10 +83,18 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er return false, errors.Wrap(err, "generate project from template") } - issueLabel, err := r.constructIssueLabel(data.GroupLabels) - hashLabel := issueLabel[len(issueLabel)-1] + labels := make([]string, 0) + + if r.conf.AddCommonLabels { + for k, v := range data.CommonLabels { + labels = append(labels, fmt.Sprintf("%s=%q", k, v)) + } + labels = append(labels, toChecksumTicketLabel(labels)) + } else { + labels = append(labels, toGroupTicketLabel(data.GroupLabels, hashJiraLabel)) + } - issue, retry, err := r.findIssueToReuse(project, hashLabel) + issue, retry, err := r.findIssueToReuse(project, labels[len(labels)-1]) if err != nil { return retry, err } @@ -148,7 +129,7 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er if len(data.Alerts.Firing()) == 0 { if r.conf.AutoResolve != nil { - level.Debug(r.logger).Log("msg", "no firing alert; resolving issue", "key", issue.Key, "label", issueLabel) + level.Debug(r.logger).Log("msg", "no firing alert; resolving issue", "key", issue.Key, "label", labels) retry, err := r.resolveIssue(issue.Key) if err != nil { return retry, err @@ -156,32 +137,32 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er return false, nil } - level.Debug(r.logger).Log("msg", "no firing alert; summary checked, nothing else to do.", "key", issue.Key, "label", issueLabel) + level.Debug(r.logger).Log("msg", "no firing alert; summary checked, nothing else to do.", "key", issue.Key, "label", labels) return false, nil } // The set of JIRA status categories is fixed, this is a safe check to make. if issue.Fields.Status.StatusCategory.Key != "done" { - level.Debug(r.logger).Log("msg", "issue is unresolved, all is done", "key", issue.Key, "label", issueLabel) + level.Debug(r.logger).Log("msg", "issue is unresolved, all is done", "key", issue.Key, "label", labels) return false, nil } if r.conf.WontFixResolution != "" && issue.Fields.Resolution != nil && issue.Fields.Resolution.Name == r.conf.WontFixResolution { - level.Info(r.logger).Log("msg", "issue was resolved as won't fix, not reopening", "key", issue.Key, "label", issueLabel, "resolution", issue.Fields.Resolution.Name) + level.Info(r.logger).Log("msg", "issue was resolved as won't fix, not reopening", "key", issue.Key, "label", labels, "resolution", issue.Fields.Resolution.Name) return false, nil } - level.Info(r.logger).Log("msg", "issue was recently resolved, reopening", "key", issue.Key, "label", issueLabel) + level.Info(r.logger).Log("msg", "issue was recently resolved, reopening", "key", issue.Key, "label", labels) return r.reopen(issue.Key) } if len(data.Alerts.Firing()) == 0 { - level.Debug(r.logger).Log("msg", "no firing alert; nothing to do.", "label", issueLabel) + level.Debug(r.logger).Log("msg", "no firing alert; nothing to do.", "label", labels) return false, nil } - level.Info(r.logger).Log("msg", "no recent matching issue found, creating new issue", "label", issueLabel) + level.Info(r.logger).Log("msg", "no recent matching issue found, creating new issue", "label", labels) issueType, err := r.tmpl.Execute(r.conf.IssueType, data) if err != nil { @@ -194,7 +175,7 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er Type: jira.IssueType{Name: issueType}, Description: issueDesc, Summary: issueSummary, - Labels: issueLabel, + Labels: labels, Unknowns: tcontainer.NewMarshalMap(), }, } diff --git a/pkg/notify/notify_test.go b/pkg/notify/notify_test.go index 949a503..cdd5571 100644 --- a/pkg/notify/notify_test.go +++ b/pkg/notify/notify_test.go @@ -30,25 +30,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestAdditionalLabels(t *testing.T) { - r := Receiver{ - conf: &config.ReceiverConfig{ - AdditionalLabels: map[string]interface{}{}, - }, - } - - r.conf.AdditionalLabels = map[string]interface{}{ - "foo": "bar", - } - - issueLabel, _ := r.constructIssueLabel(alertmanager.KV{"a": "b"}) - - require.Equal(t, `ALERT{a="b"}`, issueLabel[0]) - require.Equal(t, `foo="bar"`, issueLabel[1]) - require.Equal(t, `JIRALERT-HASH=6536991f7ce9c028fd657a21d258bc532670975f`, issueLabel[2]) - -} - func TestToGroupTicketLabel(t *testing.T) { require.Equal(t, `JIRALERT{9897cb21a3d1ba47d2aab501ce9bc60b74bf65e26658f8e34a7fc81705e6b6eadfe6ad8edfe7c68142b3fe10f2c89127bd85e5f3687fe6b9ff1eff4b3f71dd49}`, toGroupTicketLabel(alertmanager.KV{"a": "B", "C": "d"}, true)) require.Equal(t, `ALERT{C="d",a="B"}`, toGroupTicketLabel(alertmanager.KV{"a": "B", "C": "d"}, false)) @@ -192,6 +173,18 @@ func testReceiverConfig2() *config.ReceiverConfig { WontFixResolution: "won't-fix", } } +func testReceiverConfig3() *config.ReceiverConfig { + reopen := config.Duration(1 * time.Hour) + return &config.ReceiverConfig{ + Project: "abc", + Summary: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}`, + ReopenDuration: &reopen, + ReopenState: "reopened", + Description: `{{ .Alerts.Firing | len }}`, + WontFixResolution: "won't-fix", + AddCommonLabels: true, + } +} func testReceiverConfigAutoResolve() *config.ReceiverConfig { reopen := config.Duration(1 * time.Hour) @@ -216,6 +209,40 @@ func TestNotify_JIRAInteraction(t *testing.T) { initJira func(t *testing.T) *fakeJira expectedJiraIssues map[string]*jira.Issue }{ + { + name: "empty jira, add common labels", + inputConfig: testReceiverConfig3(), + initJira: func(t *testing.T) *fakeJira { return newTestFakeJira() }, + inputAlert: &alertmanager.Data{ + CommonLabels: alertmanager.KV{"foo": "bar"}, + Alerts: alertmanager.Alerts{ + {Status: alertmanager.AlertFiring}, + {Status: "not firing"}, + {Status: alertmanager.AlertFiring}, + }, + Status: alertmanager.AlertFiring, + GroupLabels: alertmanager.KV{"a": "b", "c": "d"}, + }, + expectedJiraIssues: map[string]*jira.Issue{ + "1": { + ID: "1", + Key: "1", + Fields: &jira.IssueFields{ + Project: jira.Project{Key: testReceiverConfig1().Project}, + Labels: []string{ + `foo="bar"`, + "Jiralert-checksum=ab03a89430228717b59a73fe39f7b6a44aa79408", + }, + Description: "2", + Status: &jira.Status{ + StatusCategory: jira.StatusCategory{Key: "NotDone"}, + }, + Unknowns: tcontainer.MarshalMap{}, + Summary: "[FIRING:2] b d ", + }, + }, + }, + }, { name: "empty jira, new alert group", inputConfig: testReceiverConfig1(), From 64b8f120647aa9b32db385a7c6fabe41a416d6f4 Mon Sep 17 00:00:00 2001 From: Christopher Jansson Date: Tue, 20 Dec 2022 14:28:05 +0100 Subject: [PATCH 3/5] Create docker-image.yml Signed-off-by: Christopher Jansson --- .github/workflows/docker-image.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/docker-image.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..820fa12 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,30 @@ +name: Docker Image CI + +on: + workflow_dispatch: + inputs: + version: + required: true +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Docker Login + # You may pin to the exact commit or the version. + # uses: docker/login-action@49ed152c8eca782a232dede0303416e8f356c37b + uses: docker/login-action@v2.0.0 + with: + username: ${{secrets.DOCKERHUB_USERNAME}} + password: ${{secrets.DOCKERHUB_PASSWORD}} + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag crikke95/jiralert:${{github.event.inputs.version}} + - name: Docker push + run: docker push crikke95:jiralert:${{github.event.inputs.version}} + From 4d5ee49c22f304134b9b5b831e1ffaab6d5b0653 Mon Sep 17 00:00:00 2001 From: Christopher Jansson Date: Tue, 20 Dec 2022 14:33:21 +0100 Subject: [PATCH 4/5] Update docker-image.yml Signed-off-by: Christopher Jansson --- .github/workflows/docker-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 820fa12..beed79b 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -24,7 +24,7 @@ jobs: with: go-version: 1.19 - name: Build the Docker image - run: docker build . --file Dockerfile --tag crikke95/jiralert:${{github.event.inputs.version}} + run: docker build . --file Dockerfile --tag crikke95/jiralert:1.0.1 - name: Docker push run: docker push crikke95:jiralert:${{github.event.inputs.version}} From 5242dbfebec50c613514592ffa62dd30268a3f74 Mon Sep 17 00:00:00 2001 From: Christopher Jansson Date: Tue, 20 Dec 2022 14:37:17 +0100 Subject: [PATCH 5/5] Update docker-image.yml Signed-off-by: Christopher Jansson --- .github/workflows/docker-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index beed79b..14b8e5b 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -24,7 +24,7 @@ jobs: with: go-version: 1.19 - name: Build the Docker image - run: docker build . --file Dockerfile --tag crikke95/jiralert:1.0.1 + run: docker build . --file Dockerfile --tag crikke95/jiralert:${{github.event.inputs.version}} - name: Docker push - run: docker push crikke95:jiralert:${{github.event.inputs.version}} + run: docker push crikke95/jiralert:${{github.event.inputs.version}}