Skip to content

Commit

Permalink
alertmanager: Replace typed fields with plain string values
Browse files Browse the repository at this point in the history
Signed-off-by: Jan-Otto Kröpke <joe@cloudeteer.de>
  • Loading branch information
jkroepke committed Oct 25, 2024
1 parent d04ef60 commit c917ca6
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 34 deletions.
2 changes: 1 addition & 1 deletion config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,7 @@ type JiraConfig struct {
WontFixResolution string `yaml:"wont_fix_resolution,omitempty" json:"wont_fix_resolution,omitempty"`
ReopenDuration model.Duration `yaml:"reopen_duration,omitempty" json:"reopen_duration,omitempty"`

Fields map[string]any `yaml:"fields,omitempty" json:"custom_fields,omitempty"`
Fields map[string]string `yaml:"fields,omitempty" json:"fields,omitempty"`
}

func (c *JiraConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
Expand Down
18 changes: 15 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1071,16 +1071,28 @@ Jira issue field can have multiple types.
Depends on the field type, the values must be provided differently.
See https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/#setting-custom-field-data-for-other-field-types for further examples.

All values must be declared as string. Quotes around the values are important.

```yaml
fields:
# Components
components: { name: "Monitoring" }
components: '{ name: "Monitoring" }'
# Custom Field TextField
customfield_10001: "Random text"
# Custom Field SelectList
customfield_10002: {"value": "red"}
customfield_10002: '{"value": "red"}'
# Custom Field MultiSelect
customfield_10003: [{"value": "red"}, {"value": "blue"}, {"value": "green"}]
customfield_10003: '[{"value": "red"}, {"value": "blue"}, {"value": "green"}]'
```

Alertmanager will always try to detect the correct type of the value.
This can be problematic if a numeric value has to be sent as a string type.
To explicitly declare a string, the value needs to be quoted again, for example:

```yaml
fields:
# Example: Numeric string values
customfield_10004: '"0"'
```

### `<opsgenie_config>`
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ require (
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546
github.com/stretchr/testify v1.9.0
github.com/trivago/tgo v1.0.7
github.com/xlab/treeprint v1.2.0
go.uber.org/atomic v1.11.0
go.uber.org/automaxprocs v1.6.0
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -508,8 +508,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM=
github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
Expand Down
92 changes: 84 additions & 8 deletions notify/jira/jira.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
"github.com/go-kit/log/level"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/trivago/tgo/tcontainer"

"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
Expand Down Expand Up @@ -120,17 +119,25 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
return n.transitionIssue(ctx, logger, existingIssue, alerts.HasFiring())
}

func (n *Notifier) prepareIssueRequestBody(ctx context.Context, logger log.Logger, groupID string, tmplTextFunc templateFunc) (issue, error) {
func (n *Notifier) prepareIssueRequestBody(_ context.Context, logger log.Logger, groupID string, tmplTextFunc templateFunc) (issue, error) {
summary, err := tmplTextFunc(n.conf.Summary)
if err != nil {
return issue{}, fmt.Errorf("summary template: %w", err)
}

// Recursively convert any maps to map[string]interface{}, filtering out all non-string keys, so the json encoder
// doesn't blow up when marshaling JIRA requests.
fieldsWithStringKeys, err := tcontainer.ConvertToMarshalMap(n.conf.Fields, func(v string) string { return v })
if err != nil {
return issue{}, fmt.Errorf("convertToMarshalMap: %w", err)
fields := make(map[string]any)
for name, field := range n.conf.Fields {
templatedFieldValue, err := tmplTextFunc(field)
if err != nil {
return issue{}, fmt.Errorf("field %q template: %w", name, err)
}

parsedValue, err := n.unmarshalField(templatedFieldValue)
if err != nil {
return issue{}, fmt.Errorf("field %q conversion: %w", name, err)
}

fields[name] = parsedValue
}

summary, truncated := notify.TruncateInRunes(summary, maxSummaryLenRunes)
Expand All @@ -143,7 +150,7 @@ func (n *Notifier) prepareIssueRequestBody(ctx context.Context, logger log.Logge
Issuetype: &idNameValue{Name: n.conf.IssueType},
Summary: summary,
Labels: make([]string, 0, len(n.conf.Labels)+1),
Fields: fieldsWithStringKeys,
Fields: fields,
}}

issueDescriptionString, err := tmplTextFunc(n.conf.Description)
Expand Down Expand Up @@ -172,6 +179,7 @@ func (n *Notifier) prepareIssueRequestBody(ctx context.Context, logger log.Logge
}
requestBody.Fields.Labels = append(requestBody.Fields.Labels, label)
}

requestBody.Fields.Labels = append(requestBody.Fields.Labels, fmt.Sprintf("ALERT{%s}", groupID))
sort.Strings(requestBody.Fields.Labels)

Expand All @@ -187,6 +195,74 @@ func (n *Notifier) prepareIssueRequestBody(ctx context.Context, logger log.Logge
return requestBody, nil
}

func (n *Notifier) unmarshalField(fieldValue any) (any, error) {
// Handle type based on input directly without casting to string
switch fieldValueTyped := fieldValue.(type) {
case string:
if len(fieldValueTyped) == 0 {
return fieldValueTyped, nil
}

// Try to parse as JSON. This includes the handling of strings, numbers, arrays, and maps.
var parsedJSON any
if err := json.Unmarshal([]byte(fieldValueTyped), &parsedJSON); err == nil {
// if the JSON is a string, return it as a string.
// Otherwise, numeric values inside strings are converted to float64.
if parsedString, ok := parsedJSON.(string); ok {
return parsedString, nil
}

return n.unmarshalField(parsedJSON)
}

// If no type conversion was possible, keep it as a string
return fieldValueTyped, nil
case []any:
// Handle arrays by recursively parsing each element
fieldValues := make([]any, len(fieldValueTyped))
for i, elem := range fieldValueTyped {
// if the JSON is a string, return it as a string.
// Otherwise, numeric values inside strings are converted to float64.
if v, ok := elem.(string); ok {
fieldValues[i] = v
continue
}

parsedElem, err := n.unmarshalField(elem)
if err != nil {
return nil, err
}

fieldValues[i] = parsedElem
}

return fieldValues, nil
case map[string]any:
// Handle maps by recursively parsing each value
for key, val := range fieldValueTyped {
// if the JSON is a string, return it as a string.
// Otherwise, numeric values inside strings are converted to float64.
if stringVal, ok := val.(string); ok {
fieldValueTyped[key] = stringVal

continue
}

parsedVal, err := n.unmarshalField(val)
if err != nil {
return nil, err
}

fieldValueTyped[key] = parsedVal
}

return fieldValueTyped, nil
default:
// Return the value as-is if no specific handling is required
return fieldValueTyped, nil
}
}

func (n *Notifier) searchExistingIssue(ctx context.Context, logger log.Logger, groupID string, firing bool) (*issue, bool, error) {
jql := strings.Builder{}

Expand Down
41 changes: 22 additions & 19 deletions notify/jira/jira_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,16 +218,19 @@ func TestJiraNotify(t *testing.T) {
Project: "OPS",
Priority: `{{ template "jira.default.priority" . }}`,
Labels: []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
Fields: map[string]any{
"components": map[any]any{"name": "Monitoring"},
"customfield_10001": "value",
"customfield_10002": 0,
"customfield_10003": []any{0},
"customfield_10004": map[any]any{"value": "red"},
"customfield_10005": map[any]any{"value": 0},
"customfield_10006": []map[any]any{{"value": "red"}, {"value": "blue"}, {"value": "green"}},
"customfield_10007": []map[any]any{{"value": "red"}, {"value": "blue"}, {"value": 0}},
"customfield_10008": []map[any]any{{"value": 0}, {"value": 1}, {"value": 2}},
Fields: map[string]string{
"components": `{"name": "Monitoring"}`,
"customfield_10001": `value`,
"customfield_10002": `0`,
"customfield_10003": `"0"`,
"customfield_10004": `[0]`,
"customfield_10005": `["0"]`,
"customfield_10006": `{"value": "red"}`,
"customfield_10007": `{"value": 0}`,
"customfield_10008": `[{"value": "red"}, {"value": "blue"}, {"value": "green"}]`,
"customfield_10009": `[{"value": "red"}, {"value": "blue"}, {"value": 0}]`,
"customfield_10010": `[{"value": 0}, {"value": 1}, {"value": 2}]`,
"customfield_10011": `[{"value": {{ .Alerts.Firing | len }} }]`,
},
ReopenDuration: model.Duration(1 * time.Hour),
ReopenTransition: "REOPEN",
Expand Down Expand Up @@ -261,12 +264,15 @@ func TestJiraNotify(t *testing.T) {
customFieldAssetFn: func(t *testing.T, issue map[string]any) {
require.Equal(t, "value", issue["customfield_10001"])
require.Equal(t, float64(0), issue["customfield_10002"])
require.Equal(t, []any{float64(0)}, issue["customfield_10003"])
require.Equal(t, map[string]any{"value": "red"}, issue["customfield_10004"])
require.Equal(t, map[string]any{"value": float64(0)}, issue["customfield_10005"])
require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": "green"}}, issue["customfield_10006"])
require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": float64(0)}}, issue["customfield_10007"])
require.Equal(t, []any{map[string]any{"value": float64(0)}, map[string]any{"value": float64(1)}, map[string]any{"value": float64(2)}}, issue["customfield_10008"])
require.Equal(t, "0", issue["customfield_10003"])
require.Equal(t, []any{float64(0)}, issue["customfield_10004"])
require.Equal(t, []any{"0"}, issue["customfield_10005"])
require.Equal(t, map[string]any{"value": "red"}, issue["customfield_10006"])
require.Equal(t, map[string]any{"value": float64(0)}, issue["customfield_10007"])
require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": "green"}}, issue["customfield_10008"])
require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": float64(0)}}, issue["customfield_10009"])
require.Equal(t, []any{map[string]any{"value": float64(0)}, map[string]any{"value": float64(1)}, map[string]any{"value": float64(2)}}, issue["customfield_10010"])
require.Equal(t, []any{map[string]any{"value": float64(1)}}, issue["customfield_10011"])
},
errMsg: "",
},
Expand Down Expand Up @@ -563,9 +569,6 @@ func TestJiraNotify(t *testing.T) {
}

w.WriteHeader(http.StatusCreated)

w.WriteHeader(http.StatusCreated)

default:
t.Fatalf("unexpected path %s", r.URL.Path)
}
Expand Down

0 comments on commit c917ca6

Please sign in to comment.