Skip to content

Commit 06ebaba

Browse files
committed
Add SpanFromWorkflowContext function for OTel
1 parent 02ca26c commit 06ebaba

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

contrib/opentelemetry/tracing_interceptor.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"go.temporal.io/sdk/interceptor"
1616
"go.temporal.io/sdk/log"
1717
"go.temporal.io/sdk/temporal"
18+
"go.temporal.io/sdk/workflow"
1819
)
1920

2021
// DefaultTextMapPropagator is the default OpenTelemetry TextMapPropagator used
@@ -174,6 +175,21 @@ func (t *tracer) ContextWithSpan(ctx context.Context, span interceptor.TracerSpa
174175
return trace.ContextWithSpan(ctx, span.(*tracerSpan).Span)
175176
}
176177

178+
// SpanFromWorkflowContext extracts an OpenTelemetry span from the given
179+
// workflow context. If no span is found, a no-op span is returned.
180+
func SpanFromWorkflowContext(ctx workflow.Context) (trace.Span, bool) {
181+
val := ctx.Value(spanContextKey{})
182+
183+
if val != nil {
184+
if span, ok := val.(*tracerSpan); ok {
185+
return span.Span, true
186+
}
187+
}
188+
189+
// Fallback to OpenTelemetry span extraction behavior
190+
return trace.SpanFromContext(nil), true
191+
}
192+
177193
func (t *tracer) StartSpan(opts *interceptor.TracerStartSpanOptions) (interceptor.TracerSpan, error) {
178194
// Create context with parent
179195
var parent trace.SpanContext

contrib/opentelemetry/tracing_interceptor_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package opentelemetry_test
22

33
import (
4+
"context"
5+
"errors"
46
"testing"
57
"time"
68

79
"github.com/stretchr/testify/assert"
810
"github.com/stretchr/testify/require"
11+
"go.opentelemetry.io/otel/attribute"
912
"go.opentelemetry.io/otel/codes"
1013
sdktrace "go.opentelemetry.io/otel/sdk/trace"
1114
"go.opentelemetry.io/otel/sdk/trace/tracetest"
@@ -15,6 +18,9 @@ import (
1518
"go.temporal.io/sdk/interceptor"
1619
"go.temporal.io/sdk/internal/interceptortest"
1720
"go.temporal.io/sdk/temporal"
21+
"go.temporal.io/sdk/testsuite"
22+
"go.temporal.io/sdk/worker"
23+
"go.temporal.io/sdk/workflow"
1824
)
1925

2026
func TestSpanPropagation(t *testing.T) {
@@ -152,3 +158,80 @@ func TestBenignErrorSpanStatus(t *testing.T) {
152158
})
153159
}
154160
}
161+
162+
func setCustomSpanAttrWorkflow(ctx workflow.Context) error {
163+
span, ok := opentelemetry.SpanFromWorkflowContext(ctx)
164+
if !ok {
165+
return errors.New("Did not find span in workflow context")
166+
}
167+
168+
span.SetAttributes(attribute.String("testTag", "testValue"))
169+
return nil
170+
}
171+
172+
func TestSpanFromWorkflowContext(t *testing.T) {
173+
rec := tracetest.NewSpanRecorder()
174+
tracer, err := opentelemetry.NewTracer(opentelemetry.TracerOptions{
175+
Tracer: sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(rec)).Tracer(""),
176+
})
177+
require.NoError(t, err)
178+
179+
var suite testsuite.WorkflowTestSuite
180+
env := suite.NewTestWorkflowEnvironment()
181+
env.RegisterWorkflow(setCustomSpanAttrWorkflow)
182+
183+
// Set tracer interceptor
184+
env.SetWorkerOptions(worker.Options{
185+
Interceptors: []interceptor.WorkerInterceptor{interceptor.NewTracingInterceptor(tracer)},
186+
})
187+
188+
env.ExecuteWorkflow(setCustomSpanAttrWorkflow)
189+
190+
require.True(t, env.IsWorkflowCompleted())
191+
192+
// Verify span was recorded with added attribute
193+
spans := rec.Ended()
194+
require.GreaterOrEqual(t, len(spans), 1)
195+
196+
found := false
197+
for _, s := range spans {
198+
for _, kv := range s.Attributes() {
199+
if string(kv.Key) == "testTag" && kv.Value.AsString() == "testValue" {
200+
found = true
201+
break
202+
}
203+
}
204+
if found {
205+
break
206+
}
207+
}
208+
209+
require.True(t, found, "expected to find attribute 'testTag=testValue' on recorded spans")
210+
}
211+
212+
func TestSpanFromWorkflowContextNoOpSpan(t *testing.T) {
213+
var suite testsuite.WorkflowTestSuite
214+
env := suite.NewTestWorkflowEnvironment()
215+
216+
nilValueWorkflow := func(ctx workflow.Context) error {
217+
span, ok := opentelemetry.SpanFromWorkflowContext(ctx)
218+
219+
if !ok {
220+
return errors.New("Expected ok to be true")
221+
}
222+
223+
// Make sure we retain behavior of returning no-op span when no span is present in context
224+
noopSpan := trace.SpanFromContext(context.TODO())
225+
if span != noopSpan {
226+
return errors.New("Expected span to be no-op span")
227+
}
228+
229+
return nil
230+
}
231+
232+
env.RegisterWorkflow(nilValueWorkflow)
233+
env.ExecuteWorkflow(nilValueWorkflow)
234+
235+
require.True(t, env.IsWorkflowCompleted())
236+
require.NoError(t, env.GetWorkflowError())
237+
}

0 commit comments

Comments
 (0)