Skip to content

Commit 2c29787

Browse files
committed
alertmanager: Replace typed fields with plain string values
Signed-off-by: Jan-Otto Kröpke <[email protected]>
1 parent d04ef60 commit 2c29787

File tree

6 files changed

+123
-35
lines changed

6 files changed

+123
-35
lines changed

Diff for: config/notifiers.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@ type VictorOpsConfig struct {
676676
StateMessage string `yaml:"state_message" json:"state_message"`
677677
EntityDisplayName string `yaml:"entity_display_name" json:"entity_display_name"`
678678
MonitoringTool string `yaml:"monitoring_tool" json:"monitoring_tool"`
679-
CustomFields map[string]string `yaml:"custom_fields,omitempty" json:"custom_fields,omitempty"`
679+
CustomFields map[string]string `yaml:"fields,omitempty" json:"fields,omitempty"`
680680
}
681681

682682
// UnmarshalYAML implements the yaml.Unmarshaler interface.
@@ -906,7 +906,7 @@ type JiraConfig struct {
906906
WontFixResolution string `yaml:"wont_fix_resolution,omitempty" json:"wont_fix_resolution,omitempty"`
907907
ReopenDuration model.Duration `yaml:"reopen_duration,omitempty" json:"reopen_duration,omitempty"`
908908

909-
Fields map[string]any `yaml:"fields,omitempty" json:"custom_fields,omitempty"`
909+
Fields map[string]string `yaml:"fields,omitempty" json:"fields,omitempty"`
910910
}
911911

912912
func (c *JiraConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {

Diff for: docs/configuration.md

+15-3
Original file line numberDiff line numberDiff line change
@@ -1071,16 +1071,28 @@ Jira issue field can have multiple types.
10711071
Depends on the field type, the values must be provided differently.
10721072
See https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/#setting-custom-field-data-for-other-field-types for further examples.
10731073

1074+
All values must be declared as string. Quotes around the values are important.
1075+
10741076
```yaml
10751077
fields:
10761078
# Components
1077-
components: { name: "Monitoring" }
1079+
components: '{ name: "Monitoring" }'
10781080
# Custom Field TextField
10791081
customfield_10001: "Random text"
10801082
# Custom Field SelectList
1081-
customfield_10002: {"value": "red"}
1083+
customfield_10002: '{"value": "red"}'
10821084
# Custom Field MultiSelect
1083-
customfield_10003: [{"value": "red"}, {"value": "blue"}, {"value": "green"}]
1085+
customfield_10003: '[{"value": "red"}, {"value": "blue"}, {"value": "green"}]'
1086+
```
1087+
1088+
Alertmanager will always try to detect the correct type of the value.
1089+
This can be problematic if a numeric value has to be sent as a string type.
1090+
To explicitly declare a string, the value needs to be quoted again, for example:
1091+
1092+
```yaml
1093+
fields:
1094+
# Example: Numeric string values
1095+
customfield_10004: '"0"'
10841096
```
10851097

10861098
### `<opsgenie_config>`

Diff for: go.mod

-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ require (
3939
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749
4040
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546
4141
github.com/stretchr/testify v1.9.0
42-
github.com/trivago/tgo v1.0.7
4342
github.com/xlab/treeprint v1.2.0
4443
go.uber.org/atomic v1.11.0
4544
go.uber.org/automaxprocs v1.6.0

Diff for: go.sum

-2
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
508508
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
509509
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
510510
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
511-
github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM=
512-
github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=
513511
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
514512
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
515513
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=

Diff for: notify/jira/jira.go

+84-8
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
"github.com/go-kit/log/level"
2929
commoncfg "github.com/prometheus/common/config"
3030
"github.com/prometheus/common/model"
31-
"github.com/trivago/tgo/tcontainer"
3231

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

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

129-
// Recursively convert any maps to map[string]interface{}, filtering out all non-string keys, so the json encoder
130-
// doesn't blow up when marshaling JIRA requests.
131-
fieldsWithStringKeys, err := tcontainer.ConvertToMarshalMap(n.conf.Fields, func(v string) string { return v })
132-
if err != nil {
133-
return issue{}, fmt.Errorf("convertToMarshalMap: %w", err)
128+
fields := make(map[string]any)
129+
for name, field := range n.conf.Fields {
130+
templatedFieldValue, err := tmplTextFunc(field)
131+
if err != nil {
132+
return issue{}, fmt.Errorf("field %q template: %w", name, err)
133+
}
134+
135+
parsedValue, err := n.unmarshalField(templatedFieldValue)
136+
if err != nil {
137+
return issue{}, fmt.Errorf("field %q conversion: %w", name, err)
138+
}
139+
140+
fields[name] = parsedValue
134141
}
135142

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

149156
issueDescriptionString, err := tmplTextFunc(n.conf.Description)
@@ -172,6 +179,7 @@ func (n *Notifier) prepareIssueRequestBody(ctx context.Context, logger log.Logge
172179
}
173180
requestBody.Fields.Labels = append(requestBody.Fields.Labels, label)
174181
}
182+
175183
requestBody.Fields.Labels = append(requestBody.Fields.Labels, fmt.Sprintf("ALERT{%s}", groupID))
176184
sort.Strings(requestBody.Fields.Labels)
177185

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

198+
func (n *Notifier) unmarshalField(fieldValue any) (any, error) {
199+
// Handle type based on input directly without casting to string
200+
switch fieldValueTyped := fieldValue.(type) {
201+
case string:
202+
if len(fieldValueTyped) == 0 {
203+
return fieldValueTyped, nil
204+
}
205+
206+
// Try to parse as JSON. This includes the handling of strings, numbers, arrays, and maps.
207+
var parsedJSON any
208+
if err := json.Unmarshal([]byte(fieldValueTyped), &parsedJSON); err == nil {
209+
// if the JSON is a string, return it as a string.
210+
// Otherwise, numeric values inside strings are converted to float64.
211+
if parsedString, ok := parsedJSON.(string); ok {
212+
return parsedString, nil
213+
}
214+
215+
return n.unmarshalField(parsedJSON)
216+
}
217+
218+
// If no type conversion was possible, keep it as a string
219+
return fieldValueTyped, nil
220+
case []any:
221+
// Handle arrays by recursively parsing each element
222+
fieldValues := make([]any, len(fieldValueTyped))
223+
for i, elem := range fieldValueTyped {
224+
// if the JSON is a string, return it as a string.
225+
// Otherwise, numeric values inside strings are converted to float64.
226+
if v, ok := elem.(string); ok {
227+
fieldValues[i] = v
228+
continue
229+
}
230+
231+
parsedElem, err := n.unmarshalField(elem)
232+
if err != nil {
233+
return nil, err
234+
}
235+
236+
fieldValues[i] = parsedElem
237+
}
238+
239+
return fieldValues, nil
240+
case map[string]any:
241+
// Handle maps by recursively parsing each value
242+
for key, val := range fieldValueTyped {
243+
// if the JSON is a string, return it as a string.
244+
// Otherwise, numeric values inside strings are converted to float64.
245+
if stringVal, ok := val.(string); ok {
246+
fieldValueTyped[key] = stringVal
247+
248+
continue
249+
}
250+
251+
parsedVal, err := n.unmarshalField(val)
252+
if err != nil {
253+
return nil, err
254+
}
255+
256+
fieldValueTyped[key] = parsedVal
257+
}
258+
259+
return fieldValueTyped, nil
260+
default:
261+
// Return the value as-is if no specific handling is required
262+
return fieldValueTyped, nil
263+
}
264+
}
265+
190266
func (n *Notifier) searchExistingIssue(ctx context.Context, logger log.Logger, groupID string, firing bool) (*issue, bool, error) {
191267
jql := strings.Builder{}
192268

Diff for: notify/jira/jira_test.go

+22-19
Original file line numberDiff line numberDiff line change
@@ -218,16 +218,19 @@ func TestJiraNotify(t *testing.T) {
218218
Project: "OPS",
219219
Priority: `{{ template "jira.default.priority" . }}`,
220220
Labels: []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
221-
Fields: map[string]any{
222-
"components": map[any]any{"name": "Monitoring"},
223-
"customfield_10001": "value",
224-
"customfield_10002": 0,
225-
"customfield_10003": []any{0},
226-
"customfield_10004": map[any]any{"value": "red"},
227-
"customfield_10005": map[any]any{"value": 0},
228-
"customfield_10006": []map[any]any{{"value": "red"}, {"value": "blue"}, {"value": "green"}},
229-
"customfield_10007": []map[any]any{{"value": "red"}, {"value": "blue"}, {"value": 0}},
230-
"customfield_10008": []map[any]any{{"value": 0}, {"value": 1}, {"value": 2}},
221+
Fields: map[string]string{
222+
"components": `{"name": "Monitoring"}`,
223+
"customfield_10001": `value`,
224+
"customfield_10002": `0`,
225+
"customfield_10003": `"0"`,
226+
"customfield_10004": `[0]`,
227+
"customfield_10005": `["0"]`,
228+
"customfield_10006": `{"value": "red"}`,
229+
"customfield_10007": `{"value": 0}`,
230+
"customfield_10008": `[{"value": "red"}, {"value": "blue"}, {"value": "green"}]`,
231+
"customfield_10009": `[{"value": "red"}, {"value": "blue"}, {"value": 0}]`,
232+
"customfield_10010": `[{"value": 0}, {"value": 1}, {"value": 2}]`,
233+
"customfield_10011": `[{"value": {{ .Alerts.Firing | len }} }]`,
231234
},
232235
ReopenDuration: model.Duration(1 * time.Hour),
233236
ReopenTransition: "REOPEN",
@@ -261,12 +264,15 @@ func TestJiraNotify(t *testing.T) {
261264
customFieldAssetFn: func(t *testing.T, issue map[string]any) {
262265
require.Equal(t, "value", issue["customfield_10001"])
263266
require.Equal(t, float64(0), issue["customfield_10002"])
264-
require.Equal(t, []any{float64(0)}, issue["customfield_10003"])
265-
require.Equal(t, map[string]any{"value": "red"}, issue["customfield_10004"])
266-
require.Equal(t, map[string]any{"value": float64(0)}, issue["customfield_10005"])
267-
require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": "green"}}, issue["customfield_10006"])
268-
require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": float64(0)}}, issue["customfield_10007"])
269-
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"])
267+
require.Equal(t, "0", issue["customfield_10003"])
268+
require.Equal(t, []any{float64(0)}, issue["customfield_10004"])
269+
require.Equal(t, []any{"0"}, issue["customfield_10005"])
270+
require.Equal(t, map[string]any{"value": "red"}, issue["customfield_10006"])
271+
require.Equal(t, map[string]any{"value": float64(0)}, issue["customfield_10007"])
272+
require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": "green"}}, issue["customfield_10008"])
273+
require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": float64(0)}}, issue["customfield_10009"])
274+
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"])
275+
require.Equal(t, []any{map[string]any{"value": float64(1)}}, issue["customfield_10011"])
270276
},
271277
errMsg: "",
272278
},
@@ -563,9 +569,6 @@ func TestJiraNotify(t *testing.T) {
563569
}
564570

565571
w.WriteHeader(http.StatusCreated)
566-
567-
w.WriteHeader(http.StatusCreated)
568-
569572
default:
570573
t.Fatalf("unexpected path %s", r.URL.Path)
571574
}

0 commit comments

Comments
 (0)