-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
96c3953
commit aefcd7f
Showing
7 changed files
with
311 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ go 1.19 | |
|
||
use ( | ||
. | ||
./godeltaprof/ | ||
./godeltaprof/compat | ||
godeltaprof | ||
godeltaprof/compat | ||
otelpyroscope | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# OpenTelemetry tracing integration | ||
|
||
The package provides means to integrate tracing with profiling. More specifically, a `TracerProvider` implementation, | ||
that annotates profiling data with span IDs: when a new trace span emerges, the tracer adds a `span_id` [pprof tag](https://github.com/google/pprof/blob/master/doc/README.md#tag-filtering) | ||
that points to the span. This makes it possible to filter out a profile of a particular trace span in [Pyroscope](https://pyroscope.io). | ||
|
||
## Example | ||
|
||
You can find a complete example setup in the [Pyroscope repository](https://github.com/grafana/pyroscope/tree/main/examples/tracing/tempo). | ||
|
||
## Other Notes | ||
|
||
Note that the module does not control `pprof` profiler itself – it still needs to be started for profiles to be | ||
collected. This can be done either via `runtime/pprof` package, or using the [Pyroscope client](https://github.com/grafana/pyroscope-go). | ||
|
||
By default, only the root span gets labeled (the first span created locally): such spans are marked with the | ||
`pyroscope.profiling.enabled` attribute. Please note that presence of the attribute does not indicate that the | ||
span has a profile: stack trace samples might not be collected, if the actual utilized CPU time is less than the | ||
sample interval (10ms). | ||
|
||
Limitations: | ||
- Only CPU profiling is fully supported at the moment. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
module github.com/grafana/pyroscope-go/otelpyroscope | ||
|
||
go 1.19 | ||
|
||
require ( | ||
go.opentelemetry.io/otel v1.19.0 | ||
go.opentelemetry.io/otel/sdk v1.19.0 | ||
go.opentelemetry.io/otel/trace v1.19.0 | ||
) | ||
|
||
require ( | ||
github.com/go-logr/logr v1.2.4 // indirect | ||
github.com/go-logr/stdr v1.2.2 // indirect | ||
go.opentelemetry.io/otel/metric v1.19.0 // indirect | ||
golang.org/x/sys v0.12.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | ||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= | ||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | ||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= | ||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= | ||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | ||
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= | ||
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= | ||
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= | ||
go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= | ||
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= | ||
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= | ||
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= | ||
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= | ||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= | ||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
package otelpyroscope | ||
|
||
import ( | ||
"context" | ||
"runtime/pprof" | ||
|
||
"go.opentelemetry.io/otel/attribute" | ||
"go.opentelemetry.io/otel/trace" | ||
) | ||
|
||
const ( | ||
spanIDLabelName = "span_id" | ||
spanNameLabelName = "span_name" | ||
) | ||
|
||
var profilingEnabledSpanAttributeKey = attribute.Key("pyroscope.profiling.enabled") | ||
|
||
type Option func(*tracerProvider) | ||
|
||
// tracerProvider satisfies open telemetry TracerProvider interface. | ||
type tracerProvider struct { | ||
tp trace.TracerProvider | ||
config config | ||
} | ||
|
||
type config struct { | ||
spanNameScope scope | ||
spanIDScope scope | ||
} | ||
|
||
// NewTracerProvider creates a new tracer provider that annotates pprof | ||
// samples with span_id label. This allows to establish a relationship | ||
// between pprof profiles and reported tracing spans. | ||
func NewTracerProvider(tp trace.TracerProvider, options ...Option) trace.TracerProvider { | ||
p := tracerProvider{ | ||
tp: tp, | ||
config: config{ | ||
spanNameScope: scopeRootSpan, | ||
spanIDScope: scopeRootSpan, | ||
}, | ||
} | ||
for _, o := range options { | ||
o(&p) | ||
} | ||
return &p | ||
} | ||
|
||
func (w *tracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.Tracer { | ||
return &profileTracer{p: w, tr: w.tp.Tracer(name, opts...)} | ||
} | ||
|
||
type profileTracer struct { | ||
p *tracerProvider | ||
tr trace.Tracer | ||
} | ||
|
||
func (w *profileTracer) Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { | ||
ctx, span := w.tr.Start(ctx, spanName, opts...) | ||
spanCtx := span.SpanContext() | ||
addSpanIDLabel := w.p.config.spanIDScope != scopeNone && spanCtx.IsSampled() | ||
addSpanNameLabel := w.p.config.spanNameScope != scopeNone && spanName != "" | ||
if !(addSpanIDLabel || addSpanNameLabel) { | ||
return ctx, span | ||
} | ||
|
||
spanID := spanCtx.SpanID().String() | ||
s := spanWrapper{ | ||
Span: span, | ||
ctx: ctx, | ||
p: w.p, | ||
} | ||
|
||
rs, ok := rootSpanFromContext(ctx) | ||
if !ok { | ||
// This is the first local span. | ||
rs.id = spanID | ||
rs.name = spanName | ||
ctx = withRootSpan(ctx, rs) | ||
} | ||
|
||
// We can't skip labeling goroutines, even if we use the | ||
// parent's attributes, because the root span can finish | ||
// before all the descendants started (and inherited the | ||
// goroutine labels). | ||
labels := make([]string, 0, 4) | ||
if addSpanNameLabel { | ||
if w.p.config.spanNameScope == scopeRootSpan { | ||
spanName = rs.name | ||
} | ||
labels = append(labels, spanNameLabelName, spanName) | ||
} | ||
|
||
if addSpanIDLabel { | ||
if w.p.config.spanIDScope == scopeRootSpan { | ||
spanID = rs.id | ||
} | ||
labels = append(labels, spanIDLabelName, spanID) | ||
} | ||
|
||
// We mark spans with "pyroscope.profiling.enabled" attribute, | ||
// only if they can have profiles. Note that the presence | ||
// of the attribute does not indicate that we actually have | ||
// collected any samples for the span. | ||
if (w.p.config.spanIDScope == scopeRootSpan && spanID == rs.id) || | ||
w.p.config.spanIDScope == scopeAllSpans { | ||
span.SetAttributes(profilingEnabledSpanAttributeKey.Bool(true)) | ||
} | ||
|
||
ctx = pprof.WithLabels(ctx, pprof.Labels(labels...)) | ||
pprof.SetGoroutineLabels(ctx) | ||
return ctx, &s | ||
} | ||
|
||
type spanWrapper struct { | ||
trace.Span | ||
ctx context.Context | ||
p *tracerProvider | ||
} | ||
|
||
func (s spanWrapper) End(options ...trace.SpanEndOption) { | ||
s.Span.End(options...) | ||
pprof.SetGoroutineLabels(s.ctx) | ||
} | ||
|
||
type rootSpanCtxKey struct{} | ||
|
||
type rootSpan struct { | ||
id string | ||
name string | ||
} | ||
|
||
func withRootSpan(ctx context.Context, s rootSpan) context.Context { | ||
return context.WithValue(ctx, rootSpanCtxKey{}, s) | ||
} | ||
|
||
func rootSpanFromContext(ctx context.Context) (rootSpan, bool) { | ||
s, ok := ctx.Value(rootSpanCtxKey{}).(rootSpan) | ||
return s, ok | ||
} | ||
|
||
// TODO(kolesnikovae): Make options public. | ||
|
||
// withSpanNameLabelScope specifies whether the current span name should be | ||
// added to the profile labels. If the name is dynamic, i.e. includes | ||
// span-specific identifiers, such as URL or SQL query, this may significantly | ||
// deteriorate performance. | ||
// | ||
// By default, only the local root span name is recorded. Samples collected | ||
// during the child span execution will be included into the root span profile. | ||
func withSpanNameLabelScope(scope scope) Option { | ||
return func(tp *tracerProvider) { | ||
tp.config.spanNameScope = scope | ||
} | ||
} | ||
|
||
// withSpanIDScope specifies whether the current span ID should be added to | ||
// the profile labels. | ||
// | ||
// By default, only the local root span ID is recorded. Samples collected | ||
// during the child span execution will be included into the root span profile. | ||
func withSpanIDScope(scope scope) Option { | ||
return func(tp *tracerProvider) { | ||
tp.config.spanNameScope = scope | ||
} | ||
} | ||
|
||
type scope uint | ||
|
||
const ( | ||
scopeNone = iota | ||
scopeRootSpan | ||
scopeAllSpans | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package otelpyroscope | ||
|
||
import ( | ||
"context" | ||
"runtime/pprof" | ||
"testing" | ||
|
||
"go.opentelemetry.io/otel" | ||
"go.opentelemetry.io/otel/sdk/trace" | ||
) | ||
|
||
func Test_tracerProvider(t *testing.T) { | ||
otel.SetTracerProvider(NewTracerProvider(trace.NewTracerProvider())) | ||
|
||
tracer := otel.Tracer("") | ||
labels := make(map[string]string) | ||
|
||
ctx, spanR := tracer.Start(context.Background(), "RootSpan") | ||
pprof.ForLabels(ctx, func(key, value string) bool { | ||
labels[key] = value | ||
return true | ||
}) | ||
spanID, ok := labels[spanIDLabelName] | ||
if !ok { | ||
t.Fatal("span ID label not found") | ||
} | ||
if len(spanID) != 16 { | ||
t.Fatalf("invalid span ID: %q", spanID) | ||
} | ||
name, ok := labels[spanNameLabelName] | ||
if !ok { | ||
t.Fatal("span name label not found") | ||
} | ||
if name != "RootSpan" { | ||
t.Fatalf("invalid span name: %q", name) | ||
} | ||
|
||
// Nested child span has the same labels. | ||
ctx, spanA := tracer.Start(ctx, "SpanA") | ||
pprof.ForLabels(ctx, func(key, value string) bool { | ||
if v, ok := labels[key]; !ok || v != value { | ||
t.Fatalf("nested span labels mismatch: %q=%q", key, value) | ||
} | ||
return true | ||
}) | ||
|
||
spanA.End() | ||
spanR.End() | ||
|
||
// Child span created after the root span end using its context. | ||
ctx, spanB := tracer.Start(ctx, "SpanB") | ||
pprof.ForLabels(ctx, func(key, value string) bool { | ||
if v, ok := labels[key]; !ok || v != value { | ||
t.Fatalf("nested span labels mismatch: %q=%q", key, value) | ||
} | ||
return true | ||
}) | ||
spanB.End() | ||
|
||
// A new root span. | ||
ctx, spanC := tracer.Start(context.Background(), "SpanC") | ||
pprof.ForLabels(ctx, func(key, value string) bool { | ||
if v, ok := labels[key]; !ok || v == value { | ||
t.Fatalf("unexpected match: %q=%q", key, value) | ||
} | ||
return true | ||
}) | ||
spanC.End() | ||
} |