Skip to content

Commit b9bb769

Browse files
authored
chore: polish span profiler (#2811)
1 parent f262b24 commit b9bb769

File tree

6 files changed

+119
-66
lines changed

6 files changed

+119
-66
lines changed

examples/golang-push/rideshare/go.work.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
1616
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
1717
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
1818
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
19+
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
1920
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
2021
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
2122
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=

pkg/util/spanprofiler/README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ You can wrap the global tracer using `spanprofiler.NewTracer`:
2121
```go
2222
func main() {
2323
// Initialize your OpenTracing tracer
24-
tracer := // Your opentracing tracer initialization
25-
wrappedTracer := spanprofiler.NewTracer(tracer) // Wrap the global tracer
26-
24+
tracer := opentracing.GlobalTracer()
25+
// Wrap it with the tracer-profiler
26+
wrappedTracer := spanprofiler.NewTracer(tracer)
2727
// Use the wrapped tracer in your application
2828
opentracing.SetGlobalTracer(wrappedTracer)
2929

30+
// Or, as an oneliner:
31+
// opentracing.SetGlobalTracer(spanprofiler.NewTracer(opentracing.GlobalTracer()))
32+
3033
// Your application logic here
3134
}
3235
```
@@ -49,7 +52,7 @@ The `spanprofiler.StartSpanFromContext` function allows you to granularly contro
4952

5053
func main() {
5154
// Start a span and enable profiling for it
52-
span, ctx := spanprofiler.StartSpanFromContext(context.Background(), "YourSpanOperation", tracer)
55+
span, ctx := spanprofiler.StartSpanFromContext(context.Background(), "YourOperationName", tracer)
5356
defer span.Finish() // Finish the span when done
5457

5558
// Use the span in your application logic
@@ -63,7 +66,7 @@ seamless integration, reserving explicit span profiling only for cases where spa
6366

6467
## Implementation details
6568

66-
When a new trace span is created, and is eligible for profiling, the tracer sets a `span_id` and `span_name` [pprof labels](https://github.com/google/pprof/blob/master/doc/README.md#tag-filtering)
69+
When a new trace span is created, and is eligible for profiling, the tracer sets `span_id` and `span_name` [pprof labels](https://github.com/google/pprof/blob/master/doc/README.md#tag-filtering)
6770
that point to the respective span. These labels are stored in the goroutine's local storage and inherited by any
6871
subsequent child goroutines.
6972

@@ -79,8 +82,8 @@ This allows to find such spans in the trace view (in the screenshot) and fetch p
7982
It's important to note that the presence of this attribute does not guarantee profile availability; stack trace samples
8083
might not be collected if the CPU time utilized falls below the sample interval (10ms).
8184

82-
It is crucial to understand that this module doesn't directly control the pprof profiler; its initiation is still
83-
necessary for profile collection. This initiation can be achieved through the `runtime/pprof` package, or using the
85+
It is crucial to understand that this module doesn't directly control the pprof profiler; its initialization is still
86+
necessary for profile collection. This initialization can be achieved through the `runtime/pprof` package, or using the
8487
[Pyroscope client](https://github.com/grafana/pyroscope-go).
8588

8689
Limitations:

pkg/util/spanprofiler/spanprofiler.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import (
1212
// any Span found within `ctx` as a ChildOfRef. If no such parent could be
1313
// found, StartSpanFromContext creates a root (parentless) Span.
1414
//
15+
// The call sets `operationName` as `span_name` pprof label, and the new span
16+
// identifier as `span_id` pprof label, if the trace is sampled.
17+
//
1518
// The second return value is a context.Context object built around the
1619
// returned Span.
1720
//
@@ -31,6 +34,9 @@ func StartSpanFromContext(ctx context.Context, operationName string, opts ...ope
3134
// it creates a root span. It also returns a context.Context object built
3235
// around the returned span.
3336
//
37+
// The call sets `operationName` as `span_name` pprof label, and the new span
38+
// identifier as `span_id` pprof label, if the trace is sampled.
39+
//
3440
// It's behavior is identical to StartSpanFromContext except that it takes an explicit
3541
// tracer as opposed to using the global tracer.
3642
func StartSpanFromContextWithTracer(ctx context.Context, tracer opentracing.Tracer, operationName string, opts ...opentracing.StartSpanOption) (opentracing.Span, context.Context) {
@@ -52,26 +58,30 @@ func wrapJaegerSpanWithGoroutineLabels(
5258
// storage and are always copied to child goroutines. This way, stack
5359
// trace samples collected during execution of child spans will be taken
5460
// into account at the root.
55-
var labels []string
61+
var ctx context.Context
5662
if spanID != "" {
57-
labels = []string{spanNameLabelName, operationName, spanIDLabelName, spanID}
63+
ctx = pprof.WithLabels(parentCtx, pprof.Labels(
64+
spanNameLabelName, operationName,
65+
spanIDLabelName, spanID))
5866
} else {
5967
// Even if the trace has not been sampled, we still need to keep track
6068
// of samples that belong to the span (all spans with the given name).
61-
labels = []string{spanNameLabelName, operationName}
69+
ctx = pprof.WithLabels(parentCtx, pprof.Labels(
70+
spanNameLabelName, operationName))
6271
}
72+
// Goroutine labels should be set as early as possible,
73+
// in order to capture the overhead of the function call.
74+
pprof.SetGoroutineLabels(ctx)
6375
// We create a span wrapper to ensure we remove the newly attached pprof
6476
// labels when span finishes. The need of this wrapper is questioned:
6577
// as we do not have the original context, we could leave the goroutine
6678
// labels – normally, span is finished at the very end of the goroutine's
6779
// lifetime, so no significant side effects should take place.
6880
w := spanWrapper{
6981
parentPprofCtx: parentCtx,
70-
currentPprofCtx: pprof.WithLabels(parentCtx, pprof.Labels(labels...)),
71-
Span: span,
82+
currentPprofCtx: ctx,
7283
}
73-
pprof.SetGoroutineLabels(w.currentPprofCtx)
74-
opentracing.Tag{Key: profileIDTagKey, Value: spanID}.Set(span)
84+
w.Span = span.SetTag(profileIDTagKey, spanID)
7585
return &w
7686
}
7787

pkg/util/spanprofiler/spanprofiler_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,23 @@ func TestSpanProfiler_pprof_labels_propagation(t *testing.T) {
1313
defer func() { require.NoError(t, tt.Close()) }()
1414

1515
t.Run("pprof labels are not propagated to child spans", func(t *testing.T) {
16-
spanR, ctx := StartSpanFromContext(context.Background(), "RootSpan")
17-
defer spanR.Finish()
18-
rootLabels := spanPprofLabels(spanR)
16+
rootSpan, ctx := StartSpanFromContext(context.Background(), "RootSpan")
17+
defer rootSpan.Finish()
18+
rootLabels := spanPprofLabels(rootSpan)
1919
require.Equal(t, rootLabels["span_name"], "RootSpan")
2020
rootSpanID, err := jaeger.SpanIDFromString(rootLabels["span_id"])
2121
require.NoError(t, err)
2222

2323
// Regardless of anything, pprof labels are attached to the current
2424
// goroutine, and the "pyroscope.profile.id" tag is set.
25-
spanA, _ := StartSpanFromContext(ctx, "ChildSpan")
26-
defer spanA.Finish()
27-
childLabels := spanPprofLabels(spanA)
25+
childSpan, _ := StartSpanFromContext(ctx, "ChildSpan")
26+
defer childSpan.Finish()
27+
childLabels := spanPprofLabels(childSpan)
2828
require.Equal(t, childLabels["span_name"], "ChildSpan")
2929
childSpanID, err := jaeger.SpanIDFromString(childLabels["span_id"])
3030
require.NoError(t, err)
3131

3232
require.NotEqual(t, rootSpanID, childSpanID)
33-
require.Equal(t, childLabels["span_id"], spanTags(spanA)["pyroscope.profile.id"])
33+
require.Equal(t, childSpanID.String(), spanTags(childSpan)["pyroscope.profile.id"])
3434
})
3535
}

pkg/util/spanprofiler/tracer.go

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ const (
1717

1818
type tracer struct{ opentracing.Tracer }
1919

20+
// NewTracer creates a new opentracing.Tracer with the span profiler integrated.
21+
//
22+
// For efficiency, the tracer selectively records profiles for _root_ spans
23+
// — the initial _local_ span in a process — since a trace may encompass
24+
// thousands of spans. All stack trace samples accumulated during the execution
25+
// of their child spans contribute to the root span's profile. In practical
26+
// terms, this signifies that, for instance, an HTTP request results in a
27+
// singular profile, irrespective of the numerous spans within the trace. It's
28+
// important to note that these profiles don't extend beyond the boundaries of
29+
// a single process.
30+
//
31+
// The limitation of this approach is that only spans created within the same
32+
// goroutine, or its children, as the parent are taken into account.
33+
// Consequently, in scenarios involving asynchronous execution, where the parent
34+
// span context is passed to another goroutine, explicit profiling becomes
35+
// necessary using `spanprofiler.StartSpanFromContext`.
2036
func NewTracer(tr opentracing.Tracer) opentracing.Tracer { return &tracer{tr} }
2137

2238
func (t *tracer) StartSpan(operationName string, opts ...opentracing.StartSpanOption) opentracing.Span {
@@ -25,46 +41,69 @@ func (t *tracer) StartSpan(operationName string, opts ...opentracing.StartSpanOp
2541
if !ok {
2642
return span
2743
}
28-
parent, ok := parentSpanContextFromRef(opts...)
29-
if ok && !isRemoteSpan(parent) {
44+
// pprof labels are attached only once, at the span root level.
45+
if !isRootSpan(opts...) {
3046
return span
3147
}
3248
// The pprof label API assumes that pairs of labels are passed through the
3349
// context. Unfortunately, the opentracing Tracer API doesn't match this
34-
// concept. This makes it impossible to save an existing pprof context and
50+
// concept: this makes it impossible to save an existing pprof context and
3551
// all the original pprof labels associated with the goroutine.
36-
return wrapJaegerSpanWithGoroutineLabels(context.Background(), span, operationName, sampledSpanID(spanCtx))
52+
ctx := context.Background()
53+
return wrapJaegerSpanWithGoroutineLabels(ctx, span, operationName, sampledSpanID(spanCtx))
54+
}
55+
56+
// isRootSpan reports whether the span is a root span.
57+
//
58+
// There are only two valid cases: if the span is the first span in the trace,
59+
// or is the first _local_ span in the trace.
60+
//
61+
// An exception is made for FollowsFrom reference: spans without an explicit
62+
// parent are considered as root ones.
63+
func isRootSpan(opts ...opentracing.StartSpanOption) bool {
64+
parent, ok := parentSpanContextFromRef(opts...)
65+
return !ok || isRemoteSpan(parent)
3766
}
3867

39-
func parentSpanContextFromRef(options ...opentracing.StartSpanOption) (jaeger.SpanContext, bool) {
68+
// parentSpanContextFromRef returns the first parent reference.
69+
func parentSpanContextFromRef(options ...opentracing.StartSpanOption) (sc jaeger.SpanContext, ok bool) {
4070
var sso opentracing.StartSpanOptions
4171
for _, option := range options {
4272
option.Apply(&sso)
4373
}
4474
for _, ref := range sso.References {
4575
if ref.Type == opentracing.ChildOfRef && ref.ReferencedContext != nil {
46-
c, ok := ref.ReferencedContext.(jaeger.SpanContext)
47-
return c, ok
76+
sc, ok = ref.ReferencedContext.(jaeger.SpanContext)
77+
return sc, ok
4878
}
4979
}
50-
return jaeger.SpanContext{}, false
80+
return sc, ok
5181
}
5282

83+
// isRemoteSpan reports whether the span context represents a remote parent.
84+
//
85+
// NOTE(kolesnikovae): this is ugly, but the only reliable method I found.
86+
// The opentracing-go package and Jaeger client are not meant to change as
87+
// both are deprecated.
5388
func isRemoteSpan(c jaeger.SpanContext) bool {
54-
// This is ugly and unsafe, but is the only reliable way to get to know which
55-
// spans should be profiled. The opentracing-go package and Jaeger client
56-
// are not meant to change as both are deprecated.
5789
jaegerCtx := *(*jaegerSpanCtx)(unsafe.Pointer(&c))
5890
return jaegerCtx.remote
5991
}
6092

93+
// jaegerSpanCtx represents memory layout of the jaeger.SpanContext type.
6194
type jaegerSpanCtx struct {
62-
traceID [16]byte // TraceID
63-
spanID [8]byte // SpanID
64-
parentID [8]byte // SpanID
65-
baggage uintptr // map[string]string
66-
debugID [2]uintptr // string
95+
traceID [16]byte // TraceID
96+
spanID [8]byte // SpanID
97+
parentID [8]byte // SpanID
98+
baggage uintptr // map[string]string
99+
debugID [2]uintptr // string
100+
101+
// samplingState is a pointer to a struct that has "localRootSpan" member,
102+
// which we could probably use: that would allow omitting quite expensive
103+
// parentSpanContextFromRef call. However, interpreting the pointer and
104+
// the complex struct memory layout is more complicated and dangerous.
67105
samplingState uintptr
106+
68107
// remote indicates that span context represents a remote parent
69108
remote bool
70109
}

pkg/util/spanprofiler/tracer_test.go

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,63 +17,63 @@ func TestTracer_pprof_labels_propagation(t *testing.T) {
1717
defer func() { require.NoError(t, tt.Close()) }()
1818

1919
t.Run("root span name and ID are propagated as pprof labels", func(t *testing.T) {
20-
spanR, _ := opentracing.StartSpanFromContext(context.Background(), "RootSpan")
21-
defer spanR.Finish()
22-
pprofLabels := spanPprofLabels(spanR)
20+
rootSpan, _ := opentracing.StartSpanFromContext(context.Background(), "RootSpan")
21+
defer rootSpan.Finish()
22+
pprofLabels := spanPprofLabels(rootSpan)
2323
// Label / tag names should be specified explicitly.
2424
require.Equal(t, pprofLabels["span_name"], "RootSpan")
2525
_, err := jaeger.SpanIDFromString(pprofLabels["span_id"])
2626
require.NoError(t, err)
2727
// Make sure the root span has "pyroscope.profile.id" attribute,
2828
// and it matches the corresponding pprof label.
29-
require.Equal(t, pprofLabels["span_id"], spanTags(spanR)["pyroscope.profile.id"])
29+
require.Equal(t, pprofLabels["span_id"], spanTags(rootSpan)["pyroscope.profile.id"])
3030
})
3131

3232
t.Run("pprof labels are propagated to child spans", func(t *testing.T) {
33-
spanR, ctx := opentracing.StartSpanFromContext(context.Background(), "RootSpan")
34-
defer spanR.Finish()
35-
// Nested child span has the same labels, but
36-
spanA, _ := opentracing.StartSpanFromContext(ctx, "SpanA")
37-
defer spanA.Finish()
33+
rootSpan, ctx := opentracing.StartSpanFromContext(context.Background(), "RootSpan")
34+
defer rootSpan.Finish()
35+
childSpan, _ := opentracing.StartSpanFromContext(ctx, "ChildSpan")
36+
defer childSpan.Finish()
3837
// Goroutine labels are inherited from the parent,
3938
// we do not set them repeatedly for the child spans.
40-
require.Empty(t, spanPprofLabels(spanA))
39+
require.Empty(t, spanPprofLabels(childSpan))
4140
// Only the root span is annotated with the profile ID tag.
42-
require.Nil(t, spanTags(spanA)["pyroscope.profile.id"])
41+
require.Nil(t, spanTags(childSpan)["pyroscope.profile.id"])
4342
})
4443

4544
t.Run("pprof labels are not propagated to child spans after parent is finished", func(t *testing.T) {
46-
spanR, ctx := opentracing.StartSpanFromContext(context.Background(), "RootSpan")
47-
rootLabels := spanPprofLabels(spanR)
48-
// Finalize the span, which removes the labels from the goroutine's
49-
// storage. Note that we can't access them (Go runtime does not provide
50-
// public methods) but we rely on SetGoroutineLabels implementation:
45+
rootSpan, ctx := opentracing.StartSpanFromContext(context.Background(), "RootSpan")
46+
rootLabels := spanPprofLabels(rootSpan)
47+
require.NotEmpty(t, rootLabels)
48+
// This removes the labels from the goroutine's storage.
49+
// Note that we can't access them (Go runtime does not provide public
50+
// methods) but we rely on SetGoroutineLabels implementation:
5151
// tracer alters currentPprofCtx, so that it actually points to the
5252
// parentPprofCtx – the state prior StartSpanFromContext call.
53-
spanR.Finish()
54-
spanA, _ := opentracing.StartSpanFromContext(ctx, "SpanA")
55-
defer spanA.Finish()
56-
childLabels := spanPprofLabels(spanA)
57-
require.NotEqual(t, rootLabels, childLabels)
53+
rootSpan.Finish()
54+
childSpan, _ := opentracing.StartSpanFromContext(ctx, "ChildSpan")
55+
defer childSpan.Finish()
56+
childLabels := spanPprofLabels(childSpan)
57+
require.Empty(t, childLabels)
5858
})
5959

60-
t.Run("pprof labels are not propagated to child spans if those are spawn in a separate goroutine hierarchy", func(t *testing.T) {
60+
t.Run("pprof labels are not propagated to child spans if they are created in a separate goroutine hierarchy", func(t *testing.T) {
6161
c := make(chan opentracing.SpanContext)
6262
done := make(chan struct{})
6363
go func() {
6464
defer close(done)
6565
// Normally, we assume that pprof labels are propagated to child
6666
// goroutines, and we do not have to set pprof labels for each nested
67-
// span repeatedly. However, if the child span is created in e.g.,
68-
// sibling goroutine, no pprof labels will be attached.
69-
// The exact relation (child of / follows from) does not matter.
70-
span := opentracing.StartSpan("Detached", opentracing.ChildOf(<-c))
67+
// span repeatedly. However, if the child span is created in a sibling
68+
// goroutine, no pprof labels will be attached.
69+
// The exact span relation (child of / follows from) does not matter.
70+
span := opentracing.StartSpan("ChildSpan", opentracing.ChildOf(<-c))
7171
require.Empty(t, spanPprofLabels(span))
7272
span.Finish()
7373
}()
74-
spanR, _ := opentracing.StartSpanFromContext(context.Background(), "RootSpan")
75-
defer spanR.Finish()
76-
c <- spanR.Context()
74+
rootSpan, _ := opentracing.StartSpanFromContext(context.Background(), "RootSpan")
75+
defer rootSpan.Finish()
76+
c <- rootSpan.Context()
7777
<-done
7878
})
7979
}

0 commit comments

Comments
 (0)