From 98f629335f246b1a4e23e140432ab38c71ed1078 Mon Sep 17 00:00:00 2001 From: naari3 Date: Wed, 5 Aug 2026 15:30:29 +0900 Subject: [PATCH] [processor/coldstart] pair cold start spans using faas.invocation_id The processor identifies execution spans by the faas.execution attribute, which semantic conventions v1.19.0 renamed to faas.invocation_id. Current instrumentations set only the new name, so no execution span is ever recognized. When that happens the cold start span is held indefinitely: it is removed from the batch while waiting for an execution span to pair with, and once the batch is empty the processor returns ErrSkipProcessingData, dropping the data. The result is that adding the coldstart processor to a pipeline silently loses the platform.initRuntimeDone span rather than enriching it. Look up both attribute names so current and older instrumentations are handled. Keeping the old name working avoids breaking instrumentations that still emit it, which is why this does not simply rename the attribute. Relates to #2099 and #1652. --- .../processor/coldstartprocessor/README.md | 2 +- collector/processor/coldstartprocessor/doc.go | 3 +- collector/processor/coldstartprocessor/go.mod | 3 +- collector/processor/coldstartprocessor/go.sum | 2 - .../processor/coldstartprocessor/processor.go | 20 +++++- .../coldstartprocessor/processor_test.go | 61 ++++++++++++++++--- 6 files changed, 73 insertions(+), 18 deletions(-) diff --git a/collector/processor/coldstartprocessor/README.md b/collector/processor/coldstartprocessor/README.md index 4ef1124c61..994c2edcc1 100644 --- a/collector/processor/coldstartprocessor/README.md +++ b/collector/processor/coldstartprocessor/README.md @@ -7,7 +7,7 @@ | Distributions | [extension] | This processor associates cold start information generated by the [telemetryapireceiver](../../receiver/telemetryapireceiver) with incoming span data processed by -the Collector extension. It reads the following of incoming Lambda execution spans identified by the `faas.execution` attribute: +the Collector extension. It reads the following of incoming Lambda execution spans, identified by the `faas.invocation_id` attribute (or `faas.execution`, the name it replaced in semantic conventions v1.19.0): - trace ID - parent span ID diff --git a/collector/processor/coldstartprocessor/doc.go b/collector/processor/coldstartprocessor/doc.go index 445abb573b..25f983d436 100644 --- a/collector/processor/coldstartprocessor/doc.go +++ b/collector/processor/coldstartprocessor/doc.go @@ -15,7 +15,8 @@ // Package coldstartprocessor correlates cold start information generated by the telemetryapireceiver // with incoming span data. // -// It reads the following of incoming Lambda execution spans identified by the faas.execution attribute: +// It reads the following of incoming Lambda execution spans, identified by the faas.invocation_id +// attribute (or faas.execution, the name it replaced in semantic conventions v1.19.0): // // - trace ID // - parent span ID diff --git a/collector/processor/coldstartprocessor/go.mod b/collector/processor/coldstartprocessor/go.mod index bd18192590..a6faa8760a 100644 --- a/collector/processor/coldstartprocessor/go.mod +++ b/collector/processor/coldstartprocessor/go.mod @@ -12,7 +12,7 @@ require ( go.opentelemetry.io/collector/processor v1.63.0 go.opentelemetry.io/collector/processor/processorhelper v0.157.0 go.opentelemetry.io/collector/processor/processortest v0.157.0 - go.opentelemetry.io/collector/semconv v0.128.0 + go.opentelemetry.io/otel v1.44.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.28.0 ) @@ -37,7 +37,6 @@ require ( go.opentelemetry.io/collector/pdata/testdata v0.157.0 // indirect go.opentelemetry.io/collector/pipeline v1.63.0 // indirect go.opentelemetry.io/collector/processor/xprocessor v0.157.0 // indirect - go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/sdk v1.44.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect diff --git a/collector/processor/coldstartprocessor/go.sum b/collector/processor/coldstartprocessor/go.sum index 8584eb295e..7f379bd72c 100644 --- a/collector/processor/coldstartprocessor/go.sum +++ b/collector/processor/coldstartprocessor/go.sum @@ -71,8 +71,6 @@ go.opentelemetry.io/collector/processor/processortest v0.157.0 h1:dtpyoIvbB3VLqZ go.opentelemetry.io/collector/processor/processortest v0.157.0/go.mod h1:gkRzdmNYfKJAmGCk/x8qDsIavVbNI0wTs+uhN7rY7B0= go.opentelemetry.io/collector/processor/xprocessor v0.157.0 h1:+WyLGrHwcPk3TK+qt7T8CH+oADEMY4r+U5fIMTBvrAc= go.opentelemetry.io/collector/processor/xprocessor v0.157.0/go.mod h1:DPmoWlks+CihSRjTGKvPKK6a8bl9mHk8hxthrJ+lvBI= -go.opentelemetry.io/collector/semconv v0.128.0 h1:MzYOz7Vgb3Kf5D7b49pqqgeUhEmOCuT10bIXb/Cc+k4= -go.opentelemetry.io/collector/semconv v0.128.0/go.mod h1:OPXer4l43X23cnjLXIZnRj/qQOjSuq4TgBLI76P9hns= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= diff --git a/collector/processor/coldstartprocessor/processor.go b/collector/processor/coldstartprocessor/processor.go index 805169079d..4242440ba3 100644 --- a/collector/processor/coldstartprocessor/processor.go +++ b/collector/processor/coldstartprocessor/processor.go @@ -22,10 +22,24 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/processor" "go.opentelemetry.io/collector/processor/processorhelper" - semconv "go.opentelemetry.io/collector/semconv/v1.5.0" + semconvlegacy "go.opentelemetry.io/otel/semconv/v1.18.0" + semconv "go.opentelemetry.io/otel/semconv/v1.25.0" "go.uber.org/zap" ) +// faasInvocationID reports the invocation identifier of an execution span, looking up both the +// current attribute and the one it replaced. +// +// faas.execution was renamed to faas.invocation_id in semantic conventions v1.19.0. Current +// instrumentations set only the new name, while older ones set only the old name, so both have +// to be accepted for the cold start span to be paired with its execution span. +func faasInvocationID(span ptrace.Span) (pcommon.Value, bool) { + if attr, ok := span.Attributes().Get(string(semconv.FaaSInvocationIDKey)); ok { + return attr, true + } + return span.Attributes().Get(string(semconvlegacy.FaaSExecutionKey)) +} + type faasExecution struct { span ptrace.Span scope pcommon.InstrumentationScope @@ -52,7 +66,7 @@ func (p *coldstartProcessor) processTraces(ctx context.Context, td ptrace.Traces if p.reported { return false } - if attr, ok := span.Attributes().Get(semconv.AttributeFaaSColdstart); ok && attr.Bool() { + if attr, ok := span.Attributes().Get(string(semconv.FaaSColdstartKey)); ok && attr.Bool() { if p.faasExecution == nil { sp := ptrace.NewSpan() p.coldstartSpan = &sp @@ -67,7 +81,7 @@ func (p *coldstartProcessor) processTraces(ctx context.Context, td ptrace.Traces return false } } - if _, ok := span.Attributes().Get(semconv.AttributeFaaSExecution); ok { + if _, ok := faasInvocationID(span); ok { if p.coldstartSpan == nil { p.faasExecution = &faasExecution{ span: ptrace.NewSpan(), diff --git a/collector/processor/coldstartprocessor/processor_test.go b/collector/processor/coldstartprocessor/processor_test.go index a815153242..69ad872f20 100644 --- a/collector/processor/coldstartprocessor/processor_test.go +++ b/collector/processor/coldstartprocessor/processor_test.go @@ -33,7 +33,8 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/processor/processorhelper" "go.opentelemetry.io/collector/processor/processortest" - semconv "go.opentelemetry.io/collector/semconv/v1.5.0" + semconvlegacy "go.opentelemetry.io/otel/semconv/v1.18.0" + semconv "go.opentelemetry.io/otel/semconv/v1.25.0" "go.uber.org/multierr" ) @@ -57,7 +58,7 @@ func TestProcessor(t *testing.T) { input: func() ptrace.Traces { td := ptrace.NewTraces() span := td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() - span.Attributes().PutBool(semconv.AttributeFaaSColdstart, true) + span.Attributes().PutBool(string(semconv.FaaSColdstartKey), true) return td }(), expected: ptrace.NewTraces(), @@ -81,7 +82,7 @@ func TestProcessor(t *testing.T) { input: func() ptrace.Traces { td := ptrace.NewTraces() span := td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() - span.Attributes().PutBool(semconv.AttributeFaaSColdstart, true) + span.Attributes().PutBool(string(semconv.FaaSColdstartKey), true) span.Attributes().PutBool("faas.initialization", true) addExecutionSpan(td, executionTraceID) return td @@ -105,7 +106,7 @@ func TestProcessor(t *testing.T) { td := ptrace.NewTraces() addExecutionSpan(td, executionTraceID) span := td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() - span.Attributes().PutBool(semconv.AttributeFaaSColdstart, true) + span.Attributes().PutBool(string(semconv.FaaSColdstartKey), true) span.Attributes().PutBool("faas.initialization", true) return td }(), @@ -144,6 +145,48 @@ func TestProcessor(t *testing.T) { } } +// TestPairingByInvocationID covers the current semantic convention attribute. faas.execution was +// renamed to faas.invocation_id in v1.19.0 and current instrumentations set only the new name, so a +// cold start span must still be paired when the execution span carries it. +func TestPairingByInvocationID(t *testing.T) { + c, err := newColdstartProcessor( + nil, + nil, + processortest.NewNopSettings(Type), + ) + require.NoError(t, err) + + // The cold start span arrives first and is held back until its execution span shows up. + input := ptrace.NewTraces() + span := input.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() + span.Attributes().PutBool(string(semconv.FaaSColdstartKey), true) + output, err := c.processTraces(context.Background(), input) + require.ErrorIs(t, err, processorhelper.ErrSkipProcessingData) + require.Equal(t, 0, output.SpanCount()) + require.False(t, c.reported) + + executionTraceID := getTraceID() + input = ptrace.NewTraces() + rs := input.ResourceSpans().AppendEmpty() + rs.Resource().Attributes().PutStr("resource-attr", "faas-execution") + ss := rs.ScopeSpans().AppendEmpty() + ss.Scope().SetName("app/execution") + execSpan := ss.Spans().AppendEmpty() + execSpan.SetTraceID(executionTraceID) + execSpan.Attributes().PutStr(string(semconv.FaaSInvocationIDKey), "af9d5aa4-a685-4c5f-a22b-444f80b3cc28") + + output, err = c.processTraces(context.Background(), input) + require.NoError(t, err) + + // The held cold start span is released alongside the execution span and joins its trace. + require.Equal(t, 2, output.SpanCount()) + require.True(t, c.reported) + spans := output.ResourceSpans().At(0).ScopeSpans().At(0).Spans() + for i := 0; i < spans.Len(); i++ { + require.Equal(t, executionTraceID, spans.At(i).TraceID()) + } +} + func TestMultipleProcessTraces(t *testing.T) { c, err := newColdstartProcessor( nil, @@ -164,7 +207,7 @@ func TestMultipleProcessTraces(t *testing.T) { input = ptrace.NewTraces() expected = ptrace.NewTraces() span := input.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() - span.Attributes().PutBool(semconv.AttributeFaaSColdstart, true) + span.Attributes().PutBool(string(semconv.FaaSColdstartKey), true) span.Attributes().PutBool("faas.initialization", true) input.CopyTo(expected) output, err = c.processTraces(context.Background(), input) @@ -186,7 +229,7 @@ func TestMultipleProcessTraces(t *testing.T) { input = ptrace.NewTraces() expected = ptrace.NewTraces() span = input.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty() - span.Attributes().PutBool(semconv.AttributeFaaSColdstart, true) + span.Attributes().PutBool(string(semconv.FaaSColdstartKey), true) span.Attributes().PutBool("faas.initialization", true) input.CopyTo(expected) output, err = c.processTraces(context.Background(), input) @@ -225,17 +268,17 @@ func addExecutionSpan(td ptrace.Traces, id pcommon.TraceID) { ss.Scope().SetName("app/execution") span := ss.Spans().AppendEmpty() span.SetTraceID(id) - span.Attributes().PutStr(semconv.AttributeFaaSExecution, "af9d5aa4-a685-4c5f-a22b-444f80b3cc28") + span.Attributes().PutStr(string(semconvlegacy.FaaSExecutionKey), "af9d5aa4-a685-4c5f-a22b-444f80b3cc28") } func executionSpan(span ptrace.Span, id pcommon.TraceID) { span.SetTraceID(id) - span.Attributes().PutStr(semconv.AttributeFaaSExecution, "af9d5aa4-a685-4c5f-a22b-444f80b3cc28") + span.Attributes().PutStr(string(semconvlegacy.FaaSExecutionKey), "af9d5aa4-a685-4c5f-a22b-444f80b3cc28") } func initializationSpan(span ptrace.Span, id pcommon.TraceID) { span.SetTraceID(id) - span.Attributes().PutBool(semconv.AttributeFaaSColdstart, true) + span.Attributes().PutBool(string(semconv.FaaSColdstartKey), true) span.Attributes().PutBool("faas.initialization", true) }