Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

alertmanager: Replace typed fields with plain string values #4083

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
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
Loading