Skip to content

Commit 94e7ff1

Browse files
pxawspmatyjasek-sumo
authored andcommitted
Add a config to decode json-encoded strings in attribute values (#2827)
* decoding json-encoded attribute values * update config unit tests * cover exception cases
1 parent bef4d3a commit 94e7ff1

File tree

8 files changed

+92
-46
lines changed

8 files changed

+92
-46
lines changed

exporter/awsemfexporter/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ The following exporter configuration parameters are supported.
2525
| `max_retries` | Maximum number of retries before abandoning an attempt to post data. | 1 |
2626
| `dimension_rollup_option`| DimensionRollupOption is the option for metrics dimension rollup. Three options are available. |"ZeroAndSingleDimensionRollup" (Enable both zero dimension rollup and single dimension rollup)|
2727
| `resource_to_telemetry_conversion` | "resource_to_telemetry_conversion" is the option for converting resource attributes to telemetry attributes. It has only one config onption- `enabled`. For metrics, if `enabled=true`, all the resource attributes will be converted to metric labels by default. See `Resource Attributes to Metric Labels` section below for examples. | `enabled=false` |
28+
| `parse_json_encoded_attr_values` | List of attribute keys whose corresponding values are JSON-encoded strings and will be converted to JSON structures in emf logs. For example, the attribute string value "{\\"x\\":5,\\"y\\":6}" will be converted to a json object: ```{"x": 5, "y": 6}```| [ ] |
2829
| [`metric_declarations`](#metric_declaration) | List of rules for filtering exported metrics and their dimensions. | [ ] |
2930
| [`metric_descriptors`](#metric_descriptor) | List of rules for inserting or updating metric descriptors.| [ ]|
3031

exporter/awsemfexporter/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ type Config struct {
6161
// "SingleDimensionRollupOnly" - Enable single dimension rollup
6262
// "NoDimensionRollup" - No dimension rollup (only keep original metrics which contain all dimensions)
6363
DimensionRollupOption string `mapstructure:"dimension_rollup_option"`
64+
// ParseJSONEncodedAttributeValues is an array of attribute keys whose corresponding values are JSON-encoded as strings.
65+
// Those strings will be decoded to its original json structure.
66+
ParseJSONEncodedAttributeValues []string `mapstructure:"parse_json_encoded_attr_values"`
67+
6468
// MetricDeclarations is the list of rules to be used to set dimensions for exported metrics.
6569
MetricDeclarations []*MetricDeclaration `mapstructure:"metric_declarations"`
6670

exporter/awsemfexporter/config_test.go

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -49,39 +49,41 @@ func TestLoadConfig(t *testing.T) {
4949
r1.Validate()
5050
assert.Equal(t,
5151
&Config{
52-
ExporterSettings: configmodels.ExporterSettings{TypeVal: configmodels.Type(typeStr), NameVal: "awsemf/1"},
53-
LogGroupName: "",
54-
LogStreamName: "",
55-
Endpoint: "",
56-
RequestTimeoutSeconds: 30,
57-
MaxRetries: 1,
58-
NoVerifySSL: false,
59-
ProxyAddress: "",
60-
Region: "us-west-2",
61-
RoleARN: "arn:aws:iam::123456789:role/monitoring-EKS-NodeInstanceRole",
62-
DimensionRollupOption: "ZeroAndSingleDimensionRollup",
63-
MetricDeclarations: []*MetricDeclaration{},
64-
MetricDescriptors: []MetricDescriptor{},
52+
ExporterSettings: configmodels.ExporterSettings{TypeVal: configmodels.Type(typeStr), NameVal: "awsemf/1"},
53+
LogGroupName: "",
54+
LogStreamName: "",
55+
Endpoint: "",
56+
RequestTimeoutSeconds: 30,
57+
MaxRetries: 1,
58+
NoVerifySSL: false,
59+
ProxyAddress: "",
60+
Region: "us-west-2",
61+
RoleARN: "arn:aws:iam::123456789:role/monitoring-EKS-NodeInstanceRole",
62+
DimensionRollupOption: "ZeroAndSingleDimensionRollup",
63+
ParseJSONEncodedAttributeValues: make([]string, 0),
64+
MetricDeclarations: []*MetricDeclaration{},
65+
MetricDescriptors: []MetricDescriptor{},
6566
}, r1)
6667

6768
r2 := cfg.Exporters["awsemf/resource_attr_to_label"].(*Config)
6869
r2.Validate()
6970
assert.Equal(t, r2,
7071
&Config{
71-
ExporterSettings: configmodels.ExporterSettings{TypeVal: configmodels.Type(typeStr), NameVal: "awsemf/resource_attr_to_label"},
72-
LogGroupName: "",
73-
LogStreamName: "",
74-
Endpoint: "",
75-
RequestTimeoutSeconds: 30,
76-
MaxRetries: 1,
77-
NoVerifySSL: false,
78-
ProxyAddress: "",
79-
Region: "",
80-
RoleARN: "",
81-
DimensionRollupOption: "ZeroAndSingleDimensionRollup",
82-
ResourceToTelemetrySettings: exporterhelper.ResourceToTelemetrySettings{Enabled: true},
83-
MetricDeclarations: []*MetricDeclaration{},
84-
MetricDescriptors: []MetricDescriptor{},
72+
ExporterSettings: configmodels.ExporterSettings{TypeVal: configmodels.Type(typeStr), NameVal: "awsemf/resource_attr_to_label"},
73+
LogGroupName: "",
74+
LogStreamName: "",
75+
Endpoint: "",
76+
RequestTimeoutSeconds: 30,
77+
MaxRetries: 1,
78+
NoVerifySSL: false,
79+
ProxyAddress: "",
80+
Region: "",
81+
RoleARN: "",
82+
DimensionRollupOption: "ZeroAndSingleDimensionRollup",
83+
ResourceToTelemetrySettings: exporterhelper.ResourceToTelemetrySettings{Enabled: true},
84+
ParseJSONEncodedAttributeValues: make([]string, 0),
85+
MetricDeclarations: []*MetricDeclaration{},
86+
MetricDescriptors: []MetricDescriptor{},
8587
})
8688
}
8789

exporter/awsemfexporter/emf_exporter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func (emf *emfExporter) pushMetricsData(_ context.Context, md pdata.Metrics) err
127127

128128
for _, groupedMetric := range groupedMetrics {
129129
cWMetric := translateGroupedMetricToCWMetric(groupedMetric, expConfig)
130-
putLogEvent := translateCWMetricToEMF(cWMetric)
130+
putLogEvent := translateCWMetricToEMF(cWMetric, expConfig)
131131

132132
logGroup := groupedMetric.Metadata.LogGroup
133133
logStream := groupedMetric.Metadata.LogStream

exporter/awsemfexporter/factory.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,20 @@ func createDefaultConfig() configmodels.Exporter {
4242
TypeVal: configmodels.Type(typeStr),
4343
NameVal: typeStr,
4444
},
45-
LogGroupName: "",
46-
LogStreamName: "",
47-
Namespace: "",
48-
Endpoint: "",
49-
RequestTimeoutSeconds: 30,
50-
MaxRetries: 1,
51-
NoVerifySSL: false,
52-
ProxyAddress: "",
53-
Region: "",
54-
RoleARN: "",
55-
DimensionRollupOption: "ZeroAndSingleDimensionRollup",
56-
MetricDeclarations: make([]*MetricDeclaration, 0),
57-
logger: nil,
45+
LogGroupName: "",
46+
LogStreamName: "",
47+
Namespace: "",
48+
Endpoint: "",
49+
RequestTimeoutSeconds: 30,
50+
MaxRetries: 1,
51+
NoVerifySSL: false,
52+
ProxyAddress: "",
53+
Region: "",
54+
RoleARN: "",
55+
DimensionRollupOption: "ZeroAndSingleDimensionRollup",
56+
ParseJSONEncodedAttributeValues: make([]string, 0),
57+
MetricDeclarations: make([]*MetricDeclaration, 0),
58+
logger: nil,
5859
}
5960
}
6061

exporter/awsemfexporter/metric_translator.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package awsemfexporter
1717
import (
1818
"encoding/json"
1919
"fmt"
20+
"reflect"
2021
"time"
2122

2223
"go.opentelemetry.io/collector/consumer/pdata"
@@ -332,11 +333,39 @@ func groupedMetricToCWMeasurementsWithFilters(groupedMetric *GroupedMetric, conf
332333
}
333334

334335
// translateCWMetricToEMF converts CloudWatch Metric format to EMF.
335-
func translateCWMetricToEMF(cWMetric *CWMetrics) *LogEvent {
336+
func translateCWMetricToEMF(cWMetric *CWMetrics, config *Config) *LogEvent {
336337
// convert CWMetric into map format for compatible with PLE input
337338
cWMetricMap := make(map[string]interface{})
338339
fieldMap := cWMetric.Fields
339340

341+
//restore the json objects that are stored as string in attributes
342+
for _, key := range config.ParseJSONEncodedAttributeValues {
343+
if fieldMap[key] == nil {
344+
continue
345+
}
346+
347+
if val, ok := fieldMap[key].(string); ok {
348+
var f interface{}
349+
err := json.Unmarshal([]byte(val), &f)
350+
if err != nil {
351+
config.logger.Debug(
352+
"Failed to parse json-encoded string",
353+
zap.String("label key", key),
354+
zap.String("label value", val),
355+
zap.Error(err),
356+
)
357+
continue
358+
}
359+
fieldMap[key] = f
360+
} else {
361+
config.logger.Debug(
362+
"Invalid json-encoded data. A string is expected",
363+
zap.Any("type", reflect.TypeOf(fieldMap[key])),
364+
zap.Any("value", reflect.ValueOf(fieldMap[key])),
365+
)
366+
}
367+
}
368+
340369
// Create `_aws` section only if there are measurements
341370
if len(cWMetric.Measurements) > 0 {
342371
// Create `_aws` section only if there are measurements

exporter/awsemfexporter/metric_translator_test.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -535,13 +535,22 @@ func TestTranslateCWMetricToEMF(t *testing.T) {
535535
fields[oTellibDimensionKey] = "cloudwatch-otel"
536536
fields["spanName"] = "test"
537537
fields["spanCounter"] = 0
538+
//add stringified json as attribute values
539+
fields["kubernetes"] = "{\"container_name\":\"cloudwatch-agent\",\"docker\":{\"container_id\":\"fc1b0a4c3faaa1808e187486a3a90cbea883dccaf2e2c46d4069d663b032a1ca\"},\"host\":\"ip-192-168-58-245.ec2.internal\",\"labels\":{\"controller-revision-hash\":\"5bdbf497dc\",\"name\":\"cloudwatch-agent\",\"pod-template-generation\":\"1\"},\"namespace_name\":\"amazon-cloudwatch\",\"pod_id\":\"e23f3413-af2e-4a98-89e0-5df2251e7f05\",\"pod_name\":\"cloudwatch-agent-26bl6\",\"pod_owners\":[{\"owner_kind\":\"DaemonSet\",\"owner_name\":\"cloudwatch-agent\"}]}"
540+
fields["Sources"] = "[\"cadvisor\",\"pod\",\"calculated\"]"
541+
542+
config := &Config{
543+
//include valid json string, a non-existing key, and keys whose value are not json/string
544+
ParseJSONEncodedAttributeValues: []string{"kubernetes", "Sources", "NonExistingAttributeKey", "spanName", "spanCounter"},
545+
logger: zap.NewNop(),
546+
}
538547

539548
met := &CWMetrics{
540549
TimestampMs: timestamp,
541550
Fields: fields,
542551
Measurements: []CWMeasurement{cwMeasurement},
543552
}
544-
inputLogEvent := translateCWMetricToEMF(met)
553+
inputLogEvent := translateCWMetricToEMF(met, config)
545554

546555
assert.Equal(t, readFromFile("testdata/testTranslateCWMetricToEMF.json"), *inputLogEvent.InputLogEvent.Message, "Expect to be equal")
547556
}
@@ -1994,7 +2003,7 @@ func TestTranslateCWMetricToEMFNoMeasurements(t *testing.T) {
19942003
Fields: fields,
19952004
Measurements: nil,
19962005
}
1997-
inputLogEvent := translateCWMetricToEMF(met)
2006+
inputLogEvent := translateCWMetricToEMF(met, &Config{})
19982007
expected := "{\"OTelLib\":\"cloudwatch-otel\",\"spanCounter\":0,\"spanName\":\"test\"}"
19992008

20002009
assert.Equal(t, expected, *inputLogEvent.InputLogEvent.Message)
@@ -2132,6 +2141,6 @@ func BenchmarkTranslateCWMetricToEMF(b *testing.B) {
21322141

21332142
b.ResetTimer()
21342143
for n := 0; n < b.N; n++ {
2135-
translateCWMetricToEMF(met)
2144+
translateCWMetricToEMF(met, &Config{})
21362145
}
21372146
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"OTelLib":"cloudwatch-otel","_aws":{"CloudWatchMetrics":[{"Namespace":"test-emf","Dimensions":[["OTelLib"],["OTelLib","spanName"]],"Metrics":[{"Name":"spanCounter","Unit":"Count"}]}],"Timestamp":1596151098037},"spanCounter":0,"spanName":"test"}
1+
{"OTelLib":"cloudwatch-otel","Sources":["cadvisor","pod","calculated"],"_aws":{"CloudWatchMetrics":[{"Namespace":"test-emf","Dimensions":[["OTelLib"],["OTelLib","spanName"]],"Metrics":[{"Name":"spanCounter","Unit":"Count"}]}],"Timestamp":1596151098037},"kubernetes":{"container_name":"cloudwatch-agent","docker":{"container_id":"fc1b0a4c3faaa1808e187486a3a90cbea883dccaf2e2c46d4069d663b032a1ca"},"host":"ip-192-168-58-245.ec2.internal","labels":{"controller-revision-hash":"5bdbf497dc","name":"cloudwatch-agent","pod-template-generation":"1"},"namespace_name":"amazon-cloudwatch","pod_id":"e23f3413-af2e-4a98-89e0-5df2251e7f05","pod_name":"cloudwatch-agent-26bl6","pod_owners":[{"owner_kind":"DaemonSet","owner_name":"cloudwatch-agent"}]},"spanCounter":0,"spanName":"test"}

0 commit comments

Comments
 (0)