-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathotelpyroscope.go
170 lines (144 loc) · 4.27 KB
/
otelpyroscope.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package otelpyroscope
import (
"context"
"runtime/pprof"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
)
const (
spanIDLabelName = "span_id"
spanNameLabelName = "span_name"
)
var profileIDSpanAttributeKey = attribute.Key("pyroscope.profile.id")
// tracerProvider satisfies open telemetry TracerProvider interface.
type tracerProvider struct {
noop.TracerProvider
tp trace.TracerProvider
config config
}
type config struct {
spanNameScope scope
spanIDScope scope
}
type Option func(*tracerProvider)
// 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 {
noop.Tracer
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 mark spans with "pyroscope.profile.id" attribute,
// only if they _can_ have profiles. Presence of the attribute
// does not indicate the fact 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(profileIDSpanAttributeKey.String(spanID))
}
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)
}
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
)