From 0150b4855e5773eb9017d729584cff42bc4c44c7 Mon Sep 17 00:00:00 2001 From: Carson Ip Date: Fri, 6 Sep 2024 11:26:47 +0100 Subject: [PATCH] [exporter/elasticsearch] Workaround TSDB array dimension limitation for metrics OTel mode (#35009) **Description:** Elasticsearch TSDB does not support array dimensions. Workaround it by stringifying attribute array values in OTel mapping mode for metrics. Refactor to improve test code reuse. **Link to tracking Issue:** Fixes #35004 **Testing:** Added exporter test for otel mode logs, metrics and traces **Documentation:** --- ...orter_workaround-tsdb-array-dimension.yaml | 28 +++ .../elasticsearchexporter/exporter_test.go | 193 ++++++++++++++---- exporter/elasticsearchexporter/model.go | 52 +++-- exporter/elasticsearchexporter/utils_test.go | 58 +++--- 4 files changed, 245 insertions(+), 86 deletions(-) create mode 100644 .chloggen/elasticsearchexporter_workaround-tsdb-array-dimension.yaml diff --git a/.chloggen/elasticsearchexporter_workaround-tsdb-array-dimension.yaml b/.chloggen/elasticsearchexporter_workaround-tsdb-array-dimension.yaml new file mode 100644 index 000000000000..c248d5274ef5 --- /dev/null +++ b/.chloggen/elasticsearchexporter_workaround-tsdb-array-dimension.yaml @@ -0,0 +1,28 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: bug_fix + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: elasticsearchexporter + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Stringify attribute array values in metrics OTel mode + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [35004] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + Elasticsearch TSDB does not support array dimensions. + Workaround it by stringifying attribute array values in OTel mapping mode for metrics. +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/exporter/elasticsearchexporter/exporter_test.go b/exporter/elasticsearchexporter/exporter_test.go index c53f97a872b4..02ca4f512244 100644 --- a/exporter/elasticsearchexporter/exporter_test.go +++ b/exporter/elasticsearchexporter/exporter_test.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componenttest" "go.opentelemetry.io/collector/config/configauth" @@ -61,15 +62,16 @@ func TestExporterLogs(t *testing.T) { exporter := newTestLogsExporter(t, server.URL, func(cfg *Config) { cfg.Mapping.Mode = "ecs" }) - logs := newLogsWithAttributeAndResourceMap( + logs := newLogsWithAttributes( // record attrs - map[string]string{ + map[string]any{ "application": "myapp", "service.name": "myservice", "exception.stacktrace": "no no no no", }, + nil, // resource attrs - map[string]string{ + map[string]any{ "attrKey1": "abc", "attrKey2": "def", }, @@ -94,8 +96,9 @@ func TestExporterLogs(t *testing.T) { cfg.Mapping.Mode = "ecs" cfg.Mapping.Dedot = true }) - logs := newLogsWithAttributeAndResourceMap( - map[string]string{"attr.key": "value"}, + logs := newLogsWithAttributes( + map[string]any{"attr.key": "value"}, + nil, nil, ) mustSendLogs(t, exporter, logs) @@ -114,10 +117,11 @@ func TestExporterLogs(t *testing.T) { cfg.Mapping.Mode = "raw" // dedup is the default }) - logs := newLogsWithAttributeAndResourceMap( + logs := newLogsWithAttributes( // Scope collides with the top-level "Scope" field, // so will be removed during deduplication. - map[string]string{"Scope": "value"}, + map[string]any{"Scope": "value"}, + nil, nil, ) mustSendLogs(t, exporter, logs) @@ -190,12 +194,13 @@ func TestExporterLogs(t *testing.T) { cfg.LogsIndex = index cfg.LogsDynamicIndex.Enabled = true }) - logs := newLogsWithAttributeAndResourceMap( - map[string]string{ + logs := newLogsWithAttributes( + map[string]any{ indexPrefix: "attrprefix-", indexSuffix: suffix, }, - map[string]string{ + nil, + map[string]any{ indexPrefix: prefix, }, ) @@ -218,11 +223,12 @@ func TestExporterLogs(t *testing.T) { exporter := newTestLogsExporter(t, server.URL, func(cfg *Config) { cfg.LogsDynamicIndex.Enabled = true }) - logs := newLogsWithAttributeAndResourceMap( - map[string]string{ + logs := newLogsWithAttributes( + map[string]any{ dataStreamDataset: "record.dataset", }, - map[string]string{ + nil, + map[string]any{ dataStreamDataset: "resource.dataset", dataStreamNamespace: "resource.namespace", }, @@ -247,7 +253,7 @@ func TestExporterLogs(t *testing.T) { cfg.LogstashFormat.Enabled = true cfg.LogsIndex = "not-used-index" }) - mustSendLogs(t, exporter, newLogsWithAttributeAndResourceMap(nil, nil)) + mustSendLogs(t, exporter, newLogsWithAttributes(nil, nil, nil)) rec.WaitItems(1) }) @@ -273,12 +279,13 @@ func TestExporterLogs(t *testing.T) { cfg.LogsDynamicIndex.Enabled = true cfg.LogstashFormat.Enabled = true }) - mustSendLogs(t, exporter, newLogsWithAttributeAndResourceMap( - map[string]string{ + mustSendLogs(t, exporter, newLogsWithAttributes( + map[string]any{ indexPrefix: "attrprefix-", indexSuffix: suffix, }, - map[string]string{ + nil, + map[string]any{ indexPrefix: prefix, }, )) @@ -296,12 +303,13 @@ func TestExporterLogs(t *testing.T) { cfg.LogsDynamicIndex.Enabled = true cfg.Mapping.Mode = "otel" }) - mustSendLogs(t, exporter, newLogsWithAttributeAndResourceMap( - map[string]string{ + mustSendLogs(t, exporter, newLogsWithAttributes( + map[string]any{ "data_stream.dataset": "attr.dataset", "attr.foo": "attr.foo.value", }, - map[string]string{ + nil, + map[string]any{ "data_stream.dataset": "resource.attribute.dataset", "data_stream.namespace": "resource.attribute.namespace", "resource.attr.foo": "resource.attr.foo.value", @@ -486,6 +494,34 @@ func TestExporterLogs(t *testing.T) { assert.Equal(t, [3]int{1, 2, 1}, attempts) }) + + t.Run("otel mode attribute array value", func(t *testing.T) { + rec := newBulkRecorder() + server := newESTestServer(t, func(docs []itemRequest) ([]itemResponse, error) { + rec.Record(docs) + return itemsAllOK(docs) + }) + + exporter := newTestLogsExporter(t, server.URL, func(cfg *Config) { + cfg.Mapping.Mode = "otel" + }) + + mustSendLogs(t, exporter, newLogsWithAttributes(map[string]any{ + "some.record.attribute": []string{"foo", "bar"}, + }, map[string]any{ + "some.scope.attribute": []string{"foo", "bar"}, + }, map[string]any{ + "some.resource.attribute": []string{"foo", "bar"}, + })) + + rec.WaitItems(1) + + assert.Len(t, rec.Items(), 1) + doc := rec.Items()[0].Document + assert.Equal(t, `{"some.record.attribute":["foo","bar"]}`, gjson.GetBytes(doc, `attributes`).Raw) + assert.Equal(t, `{"some.scope.attribute":["foo","bar"]}`, gjson.GetBytes(doc, `scope.attributes`).Raw) + assert.Equal(t, `{"some.resource.attribute":["foo","bar"]}`, gjson.GetBytes(doc, `resource.attributes`).Raw) + }) } func TestExporterMetrics(t *testing.T) { @@ -523,11 +559,12 @@ func TestExporterMetrics(t *testing.T) { cfg.MetricsIndex = "metrics.index" cfg.Mapping.Mode = "ecs" }) - metrics := newMetricsWithAttributeAndResourceMap( - map[string]string{ + metrics := newMetricsWithAttributes( + map[string]any{ indexSuffix: "-data.point.suffix", }, - map[string]string{ + nil, + map[string]any{ indexPrefix: "resource.prefix-", indexSuffix: "-resource.suffix", }, @@ -554,11 +591,12 @@ func TestExporterMetrics(t *testing.T) { cfg.MetricsIndex = "metrics.index" cfg.Mapping.Mode = "ecs" }) - metrics := newMetricsWithAttributeAndResourceMap( - map[string]string{ + metrics := newMetricsWithAttributes( + map[string]any{ dataStreamNamespace: "data.point.namespace", }, - map[string]string{ + nil, + map[string]any{ dataStreamDataset: "resource.dataset", dataStreamNamespace: "resource.namespace", }, @@ -588,7 +626,7 @@ func TestExporterMetrics(t *testing.T) { fooDp := fooDps.AppendEmpty() fooDp.SetIntValue(1) fooOtherDp := fooDps.AppendEmpty() - fillResourceAttributeMap(fooOtherDp.Attributes(), map[string]string{ + fillAttributeMap(fooOtherDp.Attributes(), map[string]any{ "dp.attribute": "dp.attribute.value", }) fooOtherDp.SetDoubleValue(1.0) @@ -599,12 +637,12 @@ func TestExporterMetrics(t *testing.T) { barDp := barDps.AppendEmpty() barDp.SetDoubleValue(1.0) barOtherDp := barDps.AppendEmpty() - fillResourceAttributeMap(barOtherDp.Attributes(), map[string]string{ + fillAttributeMap(barOtherDp.Attributes(), map[string]any{ "dp.attribute": "dp.attribute.value", }) barOtherDp.SetDoubleValue(1.0) barOtherIndexDp := barDps.AppendEmpty() - fillResourceAttributeMap(barOtherIndexDp.Attributes(), map[string]string{ + fillAttributeMap(barOtherIndexDp.Attributes(), map[string]any{ "dp.attribute": "dp.attribute.value", dataStreamNamespace: "bar", }) @@ -620,14 +658,14 @@ func TestExporterMetrics(t *testing.T) { metrics := pmetric.NewMetrics() resourceMetrics := metrics.ResourceMetrics().AppendEmpty() - fillResourceAttributeMap(resourceMetrics.Resource().Attributes(), map[string]string{ + fillAttributeMap(resourceMetrics.Resource().Attributes(), map[string]any{ dataStreamNamespace: "resource.namespace", }) scopeA := resourceMetrics.ScopeMetrics().AppendEmpty() addToMetricSlice(scopeA.Metrics()) scopeB := resourceMetrics.ScopeMetrics().AppendEmpty() - fillResourceAttributeMap(scopeB.Scope().Attributes(), map[string]string{ + fillAttributeMap(scopeB.Scope().Attributes(), map[string]any{ dataStreamDataset: "scope.b", }) addToMetricSlice(scopeB.Metrics()) @@ -880,6 +918,35 @@ func TestExporterMetrics(t *testing.T) { assertItemsEqual(t, expected, rec.Items(), false) }) + t.Run("otel mode attribute array value", func(t *testing.T) { + rec := newBulkRecorder() + server := newESTestServer(t, func(docs []itemRequest) ([]itemResponse, error) { + rec.Record(docs) + return itemsAllOK(docs) + }) + + exporter := newTestMetricsExporter(t, server.URL, func(cfg *Config) { + cfg.Mapping.Mode = "otel" + }) + + mustSendMetrics(t, exporter, newMetricsWithAttributes(map[string]any{ + "some.record.attribute": []string{"foo", "bar"}, + }, map[string]any{ + "some.scope.attribute": []string{"foo", "bar"}, + }, map[string]any{ + "some.resource.attribute": []string{"foo", "bar"}, + })) + + rec.WaitItems(1) + + assert.Len(t, rec.Items(), 1) + doc := rec.Items()[0].Document + // Workaround TSDB limitation by stringifying array values + assert.Equal(t, `{"some.record.attribute":"[\"foo\",\"bar\"]"}`, gjson.GetBytes(doc, `attributes`).Raw) + assert.Equal(t, `{"some.scope.attribute":"[\"foo\",\"bar\"]"}`, gjson.GetBytes(doc, `scope.attributes`).Raw) + assert.Equal(t, `{"some.resource.attribute":"[\"foo\",\"bar\"]"}`, gjson.GetBytes(doc, `resource.attributes`).Raw) + }) + t.Run("publish summary", func(t *testing.T) { rec := newBulkRecorder() server := newESTestServer(t, func(docs []itemRequest) ([]itemResponse, error) { @@ -972,12 +1039,13 @@ func TestExporterTraces(t *testing.T) { cfg.TracesDynamicIndex.Enabled = true }) - mustSendTraces(t, exporter, newTracesWithAttributeAndResourceMap( - map[string]string{ + mustSendTraces(t, exporter, newTracesWithAttributes( + map[string]any{ indexPrefix: "attrprefix-", indexSuffix: suffix, }, - map[string]string{ + nil, + map[string]any{ indexPrefix: prefix, }, )) @@ -1002,11 +1070,12 @@ func TestExporterTraces(t *testing.T) { cfg.TracesDynamicIndex.Enabled = true }) - mustSendTraces(t, exporter, newTracesWithAttributeAndResourceMap( - map[string]string{ + mustSendTraces(t, exporter, newTracesWithAttributes( + map[string]any{ dataStreamDataset: "span.dataset", }, - map[string]string{ + nil, + map[string]any{ dataStreamDataset: "resource.dataset", }, )) @@ -1032,7 +1101,7 @@ func TestExporterTraces(t *testing.T) { defaultCfg = *cfg }) - mustSendTraces(t, exporter, newTracesWithAttributeAndResourceMap(nil, nil)) + mustSendTraces(t, exporter, newTracesWithAttributes(nil, nil, nil)) rec.WaitItems(1) }) @@ -1060,12 +1129,13 @@ func TestExporterTraces(t *testing.T) { cfg.LogstashFormat.Enabled = true }) - mustSendTraces(t, exporter, newTracesWithAttributeAndResourceMap( - map[string]string{ + mustSendTraces(t, exporter, newTracesWithAttributes( + map[string]any{ indexPrefix: "attrprefix-", indexSuffix: suffix, }, - map[string]string{ + nil, + map[string]any{ indexPrefix: prefix, }, )) @@ -1106,12 +1176,12 @@ func TestExporterTraces(t *testing.T) { event.SetDroppedAttributesCount(1) scopeAttr := span.Attributes() - fillResourceAttributeMap(scopeAttr, map[string]string{ + fillAttributeMap(scopeAttr, map[string]any{ "attr.foo": "attr.bar", }) resAttr := rs.Resource().Attributes() - fillResourceAttributeMap(resAttr, map[string]string{ + fillAttributeMap(resAttr, map[string]any{ "resource.foo": "resource.bar", }) @@ -1121,7 +1191,7 @@ func TestExporterTraces(t *testing.T) { spanLink.SetFlags(10) spanLink.SetDroppedAttributesCount(11) spanLink.TraceState().FromRaw("bar") - fillResourceAttributeMap(spanLink.Attributes(), map[string]string{ + fillAttributeMap(spanLink.Attributes(), map[string]any{ "link.attr.foo": "link.attr.bar", }) @@ -1142,6 +1212,41 @@ func TestExporterTraces(t *testing.T) { assertItemsEqual(t, expected, rec.Items(), false) }) + + t.Run("otel mode attribute array value", func(t *testing.T) { + rec := newBulkRecorder() + server := newESTestServer(t, func(docs []itemRequest) ([]itemResponse, error) { + rec.Record(docs) + return itemsAllOK(docs) + }) + + exporter := newTestTracesExporter(t, server.URL, func(cfg *Config) { + cfg.Mapping.Mode = "otel" + }) + + traces := newTracesWithAttributes(map[string]any{ + "some.record.attribute": []string{"foo", "bar"}, + }, map[string]any{ + "some.scope.attribute": []string{"foo", "bar"}, + }, map[string]any{ + "some.resource.attribute": []string{"foo", "bar"}, + }) + spanEventAttrs := traces.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0).Events().AppendEmpty().Attributes() + fillAttributeMap(spanEventAttrs, map[string]any{ + "some.record.attribute": []string{"foo", "bar"}, + }) + mustSendTraces(t, exporter, traces) + + rec.WaitItems(2) + + assert.Len(t, rec.Items(), 2) + for _, item := range rec.Items() { + doc := item.Document + assert.Equal(t, `{"some.record.attribute":["foo","bar"]}`, gjson.GetBytes(doc, `attributes`).Raw) + assert.Equal(t, `{"some.scope.attribute":["foo","bar"]}`, gjson.GetBytes(doc, `scope.attributes`).Raw) + assert.Equal(t, `{"some.resource.attribute":["foo","bar"]}`, gjson.GetBytes(doc, `resource.attributes`).Raw) + } + }) } // TestExporterAuth verifies that the Elasticsearch exporter supports diff --git a/exporter/elasticsearchexporter/model.go b/exporter/elasticsearchexporter/model.go index 55af4eb45db9..d5064764bcb2 100644 --- a/exporter/elasticsearchexporter/model.go +++ b/exporter/elasticsearchexporter/model.go @@ -150,9 +150,9 @@ func (m *encodeModel) encodeLogOTelMode(resource pcommon.Resource, resourceSchem document.AddInt("severity_number", int64(record.SeverityNumber())) document.AddInt("dropped_attributes_count", int64(record.DroppedAttributesCount())) - m.encodeAttributesOTelMode(&document, record.Attributes()) - m.encodeResourceOTelMode(&document, resource, resourceSchemaURL) - m.encodeScopeOTelMode(&document, scope, scopeSchemaURL) + m.encodeAttributesOTelMode(&document, record.Attributes(), false) + m.encodeResourceOTelMode(&document, resource, resourceSchemaURL, false) + m.encodeScopeOTelMode(&document, scope, scopeSchemaURL, false) // Body setOTelLogBody(&document, record.Body()) @@ -286,9 +286,9 @@ func (m *encodeModel) upsertMetricDataPointValueOTelMode(documents map[uint32]ob } document.AddString("unit", metric.Unit()) - m.encodeAttributesOTelMode(&document, dp.Attributes()) - m.encodeResourceOTelMode(&document, resource, resourceSchemaURL) - m.encodeScopeOTelMode(&document, scope, scopeSchemaURL) + m.encodeAttributesOTelMode(&document, dp.Attributes(), true) + m.encodeResourceOTelMode(&document, resource, resourceSchemaURL, true) + m.encodeScopeOTelMode(&document, scope, scopeSchemaURL, true) } switch value.Type() { @@ -438,7 +438,7 @@ func numberToValue(dp pmetric.NumberDataPoint) (pcommon.Value, error) { return pcommon.Value{}, errInvalidNumberDataPoint } -func (m *encodeModel) encodeResourceOTelMode(document *objmodel.Document, resource pcommon.Resource, resourceSchemaURL string) { +func (m *encodeModel) encodeResourceOTelMode(document *objmodel.Document, resource pcommon.Resource, resourceSchemaURL string, stringifyArrayValues bool) { resourceMapVal := pcommon.NewValueMap() resourceMap := resourceMapVal.Map() if resourceSchemaURL != "" { @@ -454,11 +454,13 @@ func (m *encodeModel) encodeResourceOTelMode(document *objmodel.Document, resour } return false }) - + if stringifyArrayValues { + mapStringifyArrayValues(resourceAttrMap) + } document.Add("resource", objmodel.ValueFromAttribute(resourceMapVal)) } -func (m *encodeModel) encodeScopeOTelMode(document *objmodel.Document, scope pcommon.InstrumentationScope, scopeSchemaURL string) { +func (m *encodeModel) encodeScopeOTelMode(document *objmodel.Document, scope pcommon.InstrumentationScope, scopeSchemaURL string, stringifyArrayValues bool) { scopeMapVal := pcommon.NewValueMap() scopeMap := scopeMapVal.Map() if scope.Name() != "" { @@ -480,10 +482,13 @@ func (m *encodeModel) encodeScopeOTelMode(document *objmodel.Document, scope pco } return false }) + if stringifyArrayValues { + mapStringifyArrayValues(scopeAttrMap) + } document.Add("scope", objmodel.ValueFromAttribute(scopeMapVal)) } -func (m *encodeModel) encodeAttributesOTelMode(document *objmodel.Document, attributeMap pcommon.Map) { +func (m *encodeModel) encodeAttributesOTelMode(document *objmodel.Document, attributeMap pcommon.Map, stringifyArrayValues bool) { attrsCopy := pcommon.NewMap() // Copy to avoid mutating original map attributeMap.CopyTo(attrsCopy) attrsCopy.RemoveIf(func(key string, val pcommon.Value) bool { @@ -497,9 +502,24 @@ func (m *encodeModel) encodeAttributesOTelMode(document *objmodel.Document, attr } return false }) + if stringifyArrayValues { + mapStringifyArrayValues(attrsCopy) + } document.AddAttributes("attributes", attrsCopy) } +// mapStringifyArrayValues replaces all slice values within an attribute map to their string representation. +// It is useful to workaround Elasticsearch TSDB not supporting arrays as dimensions. +// See https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/35004 +func mapStringifyArrayValues(m pcommon.Map) { + m.Range(func(_ string, v pcommon.Value) bool { + if v.Type() == pcommon.ValueTypeSlice { + v.SetStr(v.AsString()) + } + return true + }) +} + func (m *encodeModel) encodeSpan(resource pcommon.Resource, resourceSchemaURL string, span ptrace.Span, scope pcommon.InstrumentationScope, scopeSchemaURL string) ([]byte, error) { var document objmodel.Document switch m.mode { @@ -525,7 +545,7 @@ func (m *encodeModel) encodeSpanOTelMode(resource pcommon.Resource, resourceSche document.AddString("kind", span.Kind().String()) document.AddInt("duration", int64(span.EndTimestamp()-span.StartTimestamp())) - m.encodeAttributesOTelMode(&document, span.Attributes()) + m.encodeAttributesOTelMode(&document, span.Attributes(), false) document.AddInt("dropped_attributes_count", int64(span.DroppedAttributesCount())) document.AddInt("dropped_events_count", int64(span.DroppedEventsCount())) @@ -549,8 +569,8 @@ func (m *encodeModel) encodeSpanOTelMode(resource pcommon.Resource, resourceSche document.AddString("status.message", span.Status().Message()) document.AddString("status.code", span.Status().Code().String()) - m.encodeResourceOTelMode(&document, resource, resourceSchemaURL) - m.encodeScopeOTelMode(&document, scope, scopeSchemaURL) + m.encodeResourceOTelMode(&document, resource, resourceSchemaURL, false) + m.encodeScopeOTelMode(&document, scope, scopeSchemaURL, false) return document } @@ -588,9 +608,9 @@ func (m *encodeModel) encodeSpanEvent(resource pcommon.Resource, resourceSchemaU document.AddTraceID("trace_id", span.TraceID()) document.AddInt("dropped_attributes_count", int64(spanEvent.DroppedAttributesCount())) - m.encodeAttributesOTelMode(&document, spanEvent.Attributes()) - m.encodeResourceOTelMode(&document, resource, resourceSchemaURL) - m.encodeScopeOTelMode(&document, scope, scopeSchemaURL) + m.encodeAttributesOTelMode(&document, spanEvent.Attributes(), false) + m.encodeResourceOTelMode(&document, resource, resourceSchemaURL, false) + m.encodeScopeOTelMode(&document, scope, scopeSchemaURL, false) return &document } diff --git a/exporter/elasticsearchexporter/utils_test.go b/exporter/elasticsearchexporter/utils_test.go index 09403a24271b..d53d31f9f2fe 100644 --- a/exporter/elasticsearchexporter/utils_test.go +++ b/exporter/elasticsearchexporter/utils_test.go @@ -247,50 +247,56 @@ func itemsHasError(resp []itemResponse) bool { return false } -func newLogsWithAttributeAndResourceMap(attrMp map[string]string, resMp map[string]string) plog.Logs { +func newLogsWithAttributes(recordAttrs, scopeAttrs, resourceAttrs map[string]any) plog.Logs { logs := plog.NewLogs() - resourceSpans := logs.ResourceLogs() - rs := resourceSpans.AppendEmpty() - - scopeAttr := rs.ScopeLogs().AppendEmpty().LogRecords().AppendEmpty().Attributes() - fillResourceAttributeMap(scopeAttr, attrMp) - - resAttr := rs.Resource().Attributes() - fillResourceAttributeMap(resAttr, resMp) + resourceLog := logs.ResourceLogs().AppendEmpty() + scopeLog := resourceLog.ScopeLogs().AppendEmpty() + fillAttributeMap(resourceLog.Resource().Attributes(), resourceAttrs) + fillAttributeMap(scopeLog.Scope().Attributes(), scopeAttrs) + fillAttributeMap(scopeLog.LogRecords().AppendEmpty().Attributes(), recordAttrs) return logs } -func newMetricsWithAttributeAndResourceMap(attrMp map[string]string, resMp map[string]string) pmetric.Metrics { +func newMetricsWithAttributes(recordAttrs, scopeAttrs, resourceAttrs map[string]any) pmetric.Metrics { metrics := pmetric.NewMetrics() - resourceMetrics := metrics.ResourceMetrics().AppendEmpty() + resourceMetric := metrics.ResourceMetrics().AppendEmpty() + scopeMetric := resourceMetric.ScopeMetrics().AppendEmpty() - fillResourceAttributeMap(resourceMetrics.Resource().Attributes(), resMp) - dp := resourceMetrics.ScopeMetrics().AppendEmpty().Metrics().AppendEmpty().SetEmptySum().DataPoints().AppendEmpty() + fillAttributeMap(resourceMetric.Resource().Attributes(), resourceAttrs) + fillAttributeMap(scopeMetric.Scope().Attributes(), scopeAttrs) + dp := scopeMetric.Metrics().AppendEmpty().SetEmptySum().DataPoints().AppendEmpty() dp.SetIntValue(0) - fillResourceAttributeMap(dp.Attributes(), attrMp) + fillAttributeMap(dp.Attributes(), recordAttrs) return metrics } -func newTracesWithAttributeAndResourceMap(attrMp map[string]string, resMp map[string]string) ptrace.Traces { +func newTracesWithAttributes(recordAttrs, scopeAttrs, resourceAttrs map[string]any) ptrace.Traces { traces := ptrace.NewTraces() - resourceSpans := traces.ResourceSpans() - rs := resourceSpans.AppendEmpty() - - scopeAttr := rs.ScopeSpans().AppendEmpty().Spans().AppendEmpty().Attributes() - fillResourceAttributeMap(scopeAttr, attrMp) + resourceSpan := traces.ResourceSpans().AppendEmpty() + scopeSpan := resourceSpan.ScopeSpans().AppendEmpty() - resAttr := rs.Resource().Attributes() - fillResourceAttributeMap(resAttr, resMp) + fillAttributeMap(resourceSpan.Resource().Attributes(), resourceAttrs) + fillAttributeMap(scopeSpan.Scope().Attributes(), scopeAttrs) + fillAttributeMap(scopeSpan.Spans().AppendEmpty().Attributes(), recordAttrs) return traces } -func fillResourceAttributeMap(attrs pcommon.Map, mp map[string]string) { - attrs.EnsureCapacity(len(mp)) - for k, v := range mp { - attrs.PutStr(k, v) +func fillAttributeMap(attrs pcommon.Map, m map[string]any) { + attrs.EnsureCapacity(len(m)) + for k, v := range m { + switch vv := v.(type) { + case string: + attrs.PutStr(k, vv) + case []string: + slice := attrs.PutEmptySlice(k) + slice.EnsureCapacity(len(vv)) + for _, s := range vv { + slice.AppendEmpty().SetStr(s) + } + } } }