Skip to content

Commit 001999c

Browse files
committed
[exporter/elasticsearch] Add span event to traces OTel mapping mode
1 parent 0f63b5a commit 001999c

File tree

6 files changed

+124
-9
lines changed

6 files changed

+124
-9
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: elasticsearchexporter
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add span event support to traces OTel mapping mode
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [34831]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
Span events are now supported in OTel mapping mode.
20+
They will be routed to `logs-${data_stream.dataset}-${data_stream.namespace}` if `traces_dynamic_index::enabled` is `true`.
21+
22+
# If your change doesn't affect end users or the exported elements of any package,
23+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
24+
# Optional: The change log or logs in which this entry should be included.
25+
# e.g. '[user]' or '[user, api]'
26+
# Include 'user' if the change is relevant to end users.
27+
# Include 'api' if there is a change to a library API.
28+
# Default: '[user]'
29+
change_logs: [user]

exporter/elasticsearchexporter/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ This can be customised through the following settings:
135135

136136
- `traces_dynamic_index` (optional): uses resource, scope, or span attributes to dynamically construct index name.
137137
- `enabled`(default=false): Enable/Disable dynamic index for trace spans. If `data_stream.dataset` or `data_stream.namespace` exist in attributes (precedence: span attribute > scope attribute > resource attribute), they will be used to dynamically construct index name in the form `traces-${data_stream.dataset}-${data_stream.namespace}`. Otherwise, if
138-
`elasticsearch.index.prefix` or `elasticsearch.index.suffix` exist in attributes (precedence: resource attribute > scope attribute > span attribute), they will be used to dynamically construct index name in the form `${elasticsearch.index.prefix}${traces_index}${elasticsearch.index.suffix}`. Otherwise, the index name falls back to `traces-generic-default`, and `traces_index` config will be ignored. Except for prefix/suffix attribute presence, the resulting docs will contain the corresponding `data_stream.*` fields.
138+
`elasticsearch.index.prefix` or `elasticsearch.index.suffix` exist in attributes (precedence: resource attribute > scope attribute > span attribute), they will be used to dynamically construct index name in the form `${elasticsearch.index.prefix}${traces_index}${elasticsearch.index.suffix}`. Otherwise, the index name falls back to `traces-generic-default`, and `traces_index` config will be ignored. Except for prefix/suffix attribute presence, the resulting docs will contain the corresponding `data_stream.*` fields. There is an exception for span events under OTel mapping mode (`mapping::mode: otel`), where span event attributes instead of span attributes are considered, and `data_stream.type` is always `logs` instead of `traces` such that documents are routed to `logs-${data_stream.dataset}-${data_stream.namespace}`.
139139

140140
- `logstash_format` (optional): Logstash format compatibility. Logs, metrics and traces can be written into an index in Logstash format.
141141
- `enabled`(default=false): Enable/disable Logstash format compatibility. When `logstash_format.enabled` is `true`, the index name is composed using `(logs|metrics|traces)_index` or `(logs|metrics|traces)_dynamic_index` as prefix and the date as suffix,
@@ -155,8 +155,10 @@ behaviours, which may be configured through the following settings:
155155
- `none`: Use original fields and event structure from the OTLP event.
156156
- `ecs`: Try to map fields to [Elastic Common Schema (ECS)][ECS]
157157
- `otel`: Elastic's preferred "OTel-native" mapping mode. Uses original fields and event structure from the OTLP event.
158-
:warning: This mode's behavior is unstable, it is currently is experimental and undergoing changes.
159-
There's a special treatment for the following attributes: `data_stream.type`, `data_stream.dataset`, `data_stream.namespace`. Instead of serializing these values under the `*attributes.*` namespace, they're put at the root of the document, to conform with the conventions of the data stream naming scheme that maps these as `constant_keyword` fields.
158+
- :warning: This mode's behavior is unstable, it is currently is experimental and undergoing changes.
159+
- There's a special treatment for the following attributes: `data_stream.type`, `data_stream.dataset`, `data_stream.namespace`. Instead of serializing these values under the `*attributes.*` namespace, they're put at the root of the document, to conform with the conventions of the data stream naming scheme that maps these as `constant_keyword` fields.
160+
- `data_stream.dataset` will always be appended with `.otel`. It is recommended to use with `*_dynamic_index.enabled: true` to route documents to data stream `${data_stream.type}-${data_stream.dataset}-${data_stream.namespace}`.
161+
- Span events are stored in separate documents. They will be routed with `data_stream.type` set to `logs` if `traces_dynamic_index::enabled` is `true`.
160162

161163
- `raw`: Omit the `Attributes.` string prefixed to field names for log and
162164
span attributes as well as omit the `Events.` string prefixed to

exporter/elasticsearchexporter/data_stream_router.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"go.opentelemetry.io/collector/pdata/ptrace"
1212
)
1313

14-
func routeWithDefaults(defaultDSType, defaultDSDataset, defaultDSNamespace string) func(
14+
func routeWithDefaults(defaultDSType, defaultDSDataset, defaultDSNamespace string) func( // nolint:unparam
1515
pcommon.Map,
1616
pcommon.Map,
1717
pcommon.Map,
@@ -91,3 +91,17 @@ func routeSpan(
9191
route := routeWithDefaults(defaultDataStreamTypeTraces, defaultDataStreamDataset, defaultDataStreamNamespace)
9292
return route(span.Attributes(), scope.Attributes(), resource.Attributes(), fIndex, otel)
9393
}
94+
95+
// routeSpanEvent returns the name of the index to send the span event to according to data stream routing attributes.
96+
// This function may mutate record attributes.
97+
func routeSpanEvent(
98+
spanEvent ptrace.SpanEvent,
99+
scope pcommon.InstrumentationScope,
100+
resource pcommon.Resource,
101+
fIndex string,
102+
otel bool,
103+
) string {
104+
// span events are sent to logs-*, not traces-*
105+
route := routeWithDefaults(defaultDataStreamTypeLogs, defaultDataStreamDataset, defaultDataStreamNamespace)
106+
return route(spanEvent.Attributes(), scope.Attributes(), resource.Attributes(), fIndex, otel)
107+
}

exporter/elasticsearchexporter/exporter.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,12 @@ func (e *elasticsearchExporter) pushTraceData(
361361
}
362362
errs = append(errs, err)
363363
}
364+
for ii := 0; ii < span.Events().Len(); ii++ {
365+
spanEvent := span.Events().At(ii)
366+
if err := e.pushSpanEvent(ctx, resource, il.SchemaUrl(), span, spanEvent, scope, scopeSpan.SchemaUrl(), session); err != nil {
367+
errs = append(errs, err)
368+
}
369+
}
364370
}
365371
}
366372
}
@@ -402,3 +408,37 @@ func (e *elasticsearchExporter) pushTraceRecord(
402408
}
403409
return bulkIndexerSession.Add(ctx, fIndex, bytes.NewReader(document), nil)
404410
}
411+
412+
func (e *elasticsearchExporter) pushSpanEvent(
413+
ctx context.Context,
414+
resource pcommon.Resource,
415+
resourceSchemaURL string,
416+
span ptrace.Span,
417+
spanEvent ptrace.SpanEvent,
418+
scope pcommon.InstrumentationScope,
419+
scopeSchemaURL string,
420+
bulkIndexerSession bulkIndexerSession,
421+
) error {
422+
fIndex := e.index
423+
if e.dynamicIndex {
424+
fIndex = routeSpanEvent(spanEvent, scope, resource, fIndex, e.otel)
425+
}
426+
427+
if e.logstashFormat.Enabled {
428+
formattedIndex, err := generateIndexWithLogstashFormat(fIndex, &e.logstashFormat, time.Now())
429+
if err != nil {
430+
return err
431+
}
432+
fIndex = formattedIndex
433+
}
434+
435+
document := e.model.encodeSpanEvent(resource, resourceSchemaURL, span, spanEvent, scope, scopeSchemaURL)
436+
if document == nil {
437+
return nil
438+
}
439+
docBytes, err := e.model.encodeDocument(*document)
440+
if err != nil {
441+
return err
442+
}
443+
return bulkIndexerSession.Add(ctx, fIndex, bytes.NewReader(docBytes), nil)
444+
}

exporter/elasticsearchexporter/exporter_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,11 @@ func TestExporterTraces(t *testing.T) {
10601060
span.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Unix(3600, 0)))
10611061
span.SetEndTimestamp(pcommon.NewTimestampFromTime(time.Unix(7200, 0)))
10621062

1063+
event := span.Events().AppendEmpty()
1064+
event.SetName("exception")
1065+
event.Attributes().PutStr("event.attr.foo", "event.attr.bar")
1066+
event.SetDroppedAttributesCount(1)
1067+
10631068
scopeAttr := span.Attributes()
10641069
fillResourceAttributeMap(scopeAttr, map[string]string{
10651070
"attr.foo": "attr.bar",
@@ -1082,13 +1087,17 @@ func TestExporterTraces(t *testing.T) {
10821087

10831088
mustSendTraces(t, exporter, traces)
10841089

1085-
rec.WaitItems(1)
1090+
rec.WaitItems(2)
10861091

10871092
expected := []itemRequest{
10881093
{
10891094
Action: []byte(`{"create":{"_index":"traces-generic.otel-default"}}`),
10901095
Document: []byte(`{"@timestamp":"1970-01-01T01:00:00.000000000Z","attributes":{"attr.foo":"attr.bar"},"data_stream":{"dataset":"generic.otel","namespace":"default","type":"traces"},"dropped_attributes_count":2,"dropped_events_count":3,"dropped_links_count":4,"duration":3600000000000,"kind":"Unspecified","links":[{"attributes":{"link.attr.foo":"link.attr.bar"},"dropped_attributes_count":11,"span_id":"","trace_id":"","trace_state":"bar"}],"name":"name","resource":{"attributes":{"resource.foo":"resource.bar"},"dropped_attributes_count":0},"scope":{"dropped_attributes_count":0},"status":{"code":"Unset"},"trace_state":"foo"}`),
10911096
},
1097+
{
1098+
Action: []byte(`{"create":{"_index":"logs-generic.otel-default"}}`),
1099+
Document: []byte(`{"@timestamp":"1970-01-01T00:00:00.000000000Z","attributes":{"event.attr.foo":"event.attr.bar","event.name":"exception"},"data_stream":{"dataset":"generic.otel","namespace":"default","type":"logs"},"dropped_attributes_count":1,"resource":{"attributes":{"resource.foo":"resource.bar"},"dropped_attributes_count":0},"scope":{"dropped_attributes_count":0}}`),
1100+
},
10921101
}
10931102

10941103
assertItemsEqual(t, expected, rec.Items(), false)

exporter/elasticsearchexporter/model.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ var resourceAttrsToPreserve = map[string]bool{
6666
type mappingModel interface {
6767
encodeLog(pcommon.Resource, string, plog.LogRecord, pcommon.InstrumentationScope, string) ([]byte, error)
6868
encodeSpan(pcommon.Resource, string, ptrace.Span, pcommon.InstrumentationScope, string) ([]byte, error)
69+
encodeSpanEvent(resource pcommon.Resource, resourceSchemaURL string, span ptrace.Span, spanEvent ptrace.SpanEvent, scope pcommon.InstrumentationScope, scopeSchemaURL string) *objmodel.Document
6970
upsertMetricDataPointValue(map[uint32]objmodel.Document, pcommon.Resource, string, pcommon.InstrumentationScope, string, pmetric.Metric, dataPoint, pcommon.Value) error
7071
encodeDocument(objmodel.Document) ([]byte, error)
7172
}
@@ -463,7 +464,9 @@ func (m *encodeModel) encodeScopeOTelMode(document *objmodel.Document, scope pco
463464
}
464465

465466
func (m *encodeModel) encodeAttributesOTelMode(document *objmodel.Document, attributeMap pcommon.Map) {
466-
attributeMap.RemoveIf(func(key string, val pcommon.Value) bool {
467+
attrsCopy := pcommon.NewMap() // Copy to avoid mutating original map
468+
attributeMap.CopyTo(attrsCopy)
469+
attrsCopy.RemoveIf(func(key string, val pcommon.Value) bool {
467470
switch key {
468471
case dataStreamType, dataStreamDataset, dataStreamNamespace:
469472
// At this point the data_stream attributes are expected to be in the record attributes,
@@ -474,7 +477,7 @@ func (m *encodeModel) encodeAttributesOTelMode(document *objmodel.Document, attr
474477
}
475478
return false
476479
})
477-
document.AddAttributes("attributes", attributeMap)
480+
document.AddAttributes("attributes", attrsCopy)
478481
}
479482

480483
func (m *encodeModel) encodeSpan(resource pcommon.Resource, resourceSchemaURL string, span ptrace.Span, scope pcommon.InstrumentationScope, scopeSchemaURL string) ([]byte, error) {
@@ -529,8 +532,6 @@ func (m *encodeModel) encodeSpanOTelMode(resource pcommon.Resource, resourceSche
529532
m.encodeResourceOTelMode(&document, resource, resourceSchemaURL)
530533
m.encodeScopeOTelMode(&document, scope, scopeSchemaURL)
531534

532-
// TODO: add span events to log data streams
533-
534535
return document
535536
}
536537

@@ -554,6 +555,26 @@ func (m *encodeModel) encodeSpanDefaultMode(resource pcommon.Resource, span ptra
554555
return document
555556
}
556557

558+
func (m *encodeModel) encodeSpanEvent(resource pcommon.Resource, resourceSchemaURL string, span ptrace.Span, spanEvent ptrace.SpanEvent, scope pcommon.InstrumentationScope, scopeSchemaURL string) *objmodel.Document {
559+
if m.mode != MappingOTel {
560+
// Currently span events are stored separately only in OTel mapping mode.
561+
// In other modes, they are stored within the span document.
562+
return nil
563+
}
564+
var document objmodel.Document
565+
document.AddTimestamp("@timestamp", spanEvent.Timestamp())
566+
document.AddString("attributes.event.name", spanEvent.Name())
567+
document.AddSpanID("span_id", span.SpanID())
568+
document.AddTraceID("trace_id", span.TraceID())
569+
document.AddInt("dropped_attributes_count", int64(spanEvent.DroppedAttributesCount()))
570+
571+
m.encodeAttributesOTelMode(&document, spanEvent.Attributes())
572+
m.encodeResourceOTelMode(&document, resource, resourceSchemaURL)
573+
m.encodeScopeOTelMode(&document, scope, scopeSchemaURL)
574+
575+
return &document
576+
}
577+
557578
func (m *encodeModel) encodeAttributes(document *objmodel.Document, attributes pcommon.Map) {
558579
key := "Attributes"
559580
if m.mode == MappingRaw {

0 commit comments

Comments
 (0)