From 591b8e9d4519f9f409ba265c3521b9c815cca056 Mon Sep 17 00:00:00 2001 From: Stve Hb Date: Thu, 16 Jan 2025 23:55:35 +0100 Subject: [PATCH] feat(otelcol): allow event extraction from spans in spanlogs (#2427) --- CHANGELOG.md | 2 + .../otelcol/otelcol.connector.spanlogs.md | 2 + .../otelcol/connector/spanlogs/consumer.go | 49 +++++++++- .../otelcol/connector/spanlogs/spanlogs.go | 2 + .../connector/spanlogs/spanlogs_test.go | 96 ++++++++++++++++++- 5 files changed, 149 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 827e29b14e..b29974a3b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,8 @@ v1.6.0-rc.1 - Add a new `/-/healthy` endpoint which returns HTTP 500 if one or more components are unhealthy. (@ptodev) +- Add `otelcol.connector.spanlogs` allowing to export span events as logs. (@steve-hb) + ### Enhancements - Update `prometheus.write.queue` to support v2 for cpu performance. (@mattdurham) diff --git a/docs/sources/reference/components/otelcol/otelcol.connector.spanlogs.md b/docs/sources/reference/components/otelcol/otelcol.connector.spanlogs.md index 6afb4ba0e4..0b0f2d1d2e 100644 --- a/docs/sources/reference/components/otelcol/otelcol.connector.spanlogs.md +++ b/docs/sources/reference/components/otelcol/otelcol.connector.spanlogs.md @@ -40,8 +40,10 @@ otelcol.connector.spanlogs "LABEL" { | `spans` | `bool` | Log one line per span. | `false` | no | | `roots` | `bool` | Log one line for every root span of a trace. | `false` | no | | `processes` | `bool` | Log one line for every process. | `false` | no | +| `events` | `bool` | Log one line for every span event. | `false` | no | | `span_attributes` | `list(string)` | Additional span attributes to log. | `[]` | no | | `process_attributes` | `list(string)` | Additional process attributes to log. | `[]` | no | +| `event_attributes` | `list(string)` | Additional event attributes to log. | `[]` | no | | `labels` | `list(string)` | A list of keys that will be logged as labels. | `[]` | no | The values listed in `labels` should be the values of either span or process attributes. diff --git a/internal/component/otelcol/connector/spanlogs/consumer.go b/internal/component/otelcol/connector/spanlogs/consumer.go index 27d22b800f..90161a0f9d 100644 --- a/internal/component/otelcol/connector/spanlogs/consumer.go +++ b/internal/component/otelcol/connector/spanlogs/consumer.go @@ -19,6 +19,7 @@ const ( typeSpan = "span" typeRoot = "root" typeProcess = "process" + typeEvent = "event" ) type consumer struct { @@ -30,8 +31,10 @@ type options struct { spans bool roots bool processes bool + events bool spanAttributes []string processAttributes []string + eventAttributes []string overrides OverrideConfig labels map[string]struct{} nextConsumer otelconsumer.Logs @@ -67,8 +70,10 @@ func (c *consumer) UpdateOptions(args Arguments, nextConsumer otelconsumer.Logs) spans: args.Spans, roots: args.Roots, processes: args.Processes, + events: args.Events, spanAttributes: args.SpanAttributes, processAttributes: args.ProcessAttributes, + eventAttributes: args.EventAttributes, overrides: args.Overrides, labels: labels, nextConsumer: nextConsumer, @@ -126,11 +131,12 @@ func (c *consumer) consumeSpans(serviceName string, ss ptrace.ScopeSpans, rs pco span := ss.Spans().At(k) traceID := span.TraceID().String() + logEvents := c.opts.events logSpans := c.opts.spans logRoots := c.opts.roots && span.ParentSpanID().IsEmpty() logProcesses := c.opts.processes && lastTraceID != traceID - if !logSpans && !logRoots && !logProcesses { + if !logSpans && !logRoots && !logProcesses && !logEvents { return nil } @@ -175,7 +181,36 @@ func (c *consumer) consumeSpans(serviceName string, ss ptrace.ScopeSpans, rs pco return err } } + + if logEvents { + err := c.consumeEvents(keyValues, span.Events(), logRecords) + if err != nil { + return err + } + } + } + return nil +} + +func (c *consumer) consumeEvents(output pcommon.Map, events ptrace.SpanEventSlice, logRecords plog.LogRecordSlice) error { + eventsLen := events.Len() + for i := 0; i < eventsLen; i++ { + event := events.At(i) + + // Can we find a solution without relying on more memory allocation? + // Clone output map due to having multiple events in one span otherwise leading to continuous use + // of the previous set event keyVals. + eventOutput := pcommon.NewMap() + output.CopyTo(eventOutput) + + c.eventKeyVals(eventOutput, event) + + err := c.appendLogRecord(typeEvent, eventOutput, logRecords) + if err != nil { + return err + } } + return nil } @@ -242,6 +277,18 @@ func (c *consumer) createLogRecord(kind string, keyValues pcommon.Map) (*plog.Lo return &res, nil } +func (c *consumer) eventKeyVals(output pcommon.Map, event ptrace.SpanEvent) { + etAtts := event.Attributes() + + for _, name := range c.opts.eventAttributes { + att, ok := etAtts.Get(name) + if ok { + val := output.PutEmpty(name) + att.CopyTo(val) + } + } +} + func (c *consumer) processKeyVals(output pcommon.Map, resource pcommon.Resource, svc string) { rsAtts := resource.Attributes() diff --git a/internal/component/otelcol/connector/spanlogs/spanlogs.go b/internal/component/otelcol/connector/spanlogs/spanlogs.go index 72e00d70fd..4676737dc3 100644 --- a/internal/component/otelcol/connector/spanlogs/spanlogs.go +++ b/internal/component/otelcol/connector/spanlogs/spanlogs.go @@ -32,8 +32,10 @@ type Arguments struct { Spans bool `alloy:"spans,attr,optional"` Roots bool `alloy:"roots,attr,optional"` Processes bool `alloy:"processes,attr,optional"` + Events bool `alloy:"events,attr,optional"` SpanAttributes []string `alloy:"span_attributes,attr,optional"` ProcessAttributes []string `alloy:"process_attributes,attr,optional"` + EventAttributes []string `alloy:"event_attributes,attr,optional"` Overrides OverrideConfig `alloy:"overrides,block,optional"` Labels []string `alloy:"labels,attr,optional"` diff --git a/internal/component/otelcol/connector/spanlogs/spanlogs_test.go b/internal/component/otelcol/connector/spanlogs/spanlogs_test.go index f4a95522e1..9e39e4a7fc 100644 --- a/internal/component/otelcol/connector/spanlogs/spanlogs_test.go +++ b/internal/component/otelcol/connector/spanlogs/spanlogs_test.go @@ -86,12 +86,33 @@ func Test_ComponentIO(t *testing.T) { { "key": "account_id", "value": { "intValue": "2245" } + }], + "events": [{ + "name": "log", + "attributes": [{ + "key": "log.severity", + "value": { "stringValue": "INFO" } + }, + { + "key": "log.message", + "value": { "stringValue": "TestLogMessage" } + }] + }, + { + "name": "test_event", + "attributes": [{ + "key": "cause", + "value": { "stringValue": "call" } + }, + { + "key": "ignore", + "value": { "stringValue": "ignore" } + }] }] }] }] }] }` - defaultOverrides := spanlogs.OverrideConfig{ LogsTag: "traces", ServiceKey: "svc", @@ -695,6 +716,79 @@ func Test_ComponentIO(t *testing.T) { }] }`, }, + { + testName: "Events", + cfg: ` + events = true + span_attributes = ["attribute1", "redact_trace", "account_id"] + event_attributes = ["log.severity", "log.message"] + labels = ["attribute1", "redact_trace", "account_id", "log.severity", "log.message"] + + output { + // no-op: will be overridden by test code. + }`, + expectedUnmarshaledCfg: spanlogs.Arguments{ + Events: true, + EventAttributes: []string{"log.severity", "log.message"}, + SpanAttributes: []string{"attribute1", "redact_trace", "account_id"}, + Overrides: defaultOverrides, + Labels: []string{"attribute1", "redact_trace", "account_id", "log.severity", "log.message"}, + Output: &otelcol.ConsumerArguments{}, + }, + inputTraceJson: defaultInputTrace, + expectedOutputLogJson: `{ + "resourceLogs": [{ + "scopeLogs": [{ + "log_records": [{ + "body": { "stringValue": "span=TestSpan dur=0ns attribute1=78 redact_trace=true account_id=2245 svc=TestSvcName tid=7bba9f33312b3dbb8b2c2c62bb7abe2d log.severity=INFO log.message=TestLogMessage" }, + "attributes": [{ + "key": "traces", + "value": { "stringValue": "event" } + }, + { + "key": "attribute1", + "value": { "intValue": "78" } + }, + { + "key": "redact_trace", + "value": { "boolValue": true } + }, + { + "key": "account_id", + "value": { "intValue": "2245" } + }, + { + "key": "log.severity", + "value": { "stringValue": "INFO" } + }, + { + "key": "log.message", + "value": { "stringValue": "TestLogMessage" } + }] + }, + { + "body": { "stringValue": "span=TestSpan dur=0ns attribute1=78 redact_trace=true account_id=2245 svc=TestSvcName tid=7bba9f33312b3dbb8b2c2c62bb7abe2d" }, + "attributes": [{ + "key": "traces", + "value": { "stringValue": "event" } + }, + { + "key": "attribute1", + "value": { "intValue": "78" } + }, + { + "key": "redact_trace", + "value": { "boolValue": true } + }, + { + "key": "account_id", + "value": { "intValue": "2245" } + }] + }] + }] + }] + }`, + }, } for _, tt := range tests {