forked from getsentry/sentry-go
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tracing.go
656 lines (587 loc) · 19.1 KB
/
tracing.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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
package sentry
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"time"
)
// A Span is the building block of a Sentry transaction. Spans build up a tree
// structure of timed operations. The span tree makes up a transaction event
// that is sent to Sentry when the root span is finished.
//
// Spans must be started with either StartSpan or Span.StartChild.
type Span struct { //nolint: maligned // prefer readability over optimal memory layout (see note below *)
TraceID TraceID `json:"trace_id"`
SpanID SpanID `json:"span_id"`
ParentSpanID SpanID `json:"parent_span_id"`
Op string `json:"op,omitempty"`
Description string `json:"description,omitempty"`
Status SpanStatus `json:"status,omitempty"`
Tags map[string]string `json:"tags,omitempty"`
StartTime time.Time `json:"start_timestamp"`
EndTime time.Time `json:"timestamp"`
Data map[string]interface{} `json:"data,omitempty"`
Sampled Sampled `json:"-"`
// ctx is the context where the span was started. Always non-nil.
ctx context.Context
// parent refers to the immediate local parent span. A remote parent span is
// only referenced by setting ParentSpanID.
parent *Span
// isTransaction is true only for the root span of a local span tree. The
// root span is the first span started in a context. Note that a local root
// span may have a remote parent belonging to the same trace, therefore
// isTransaction depends on ctx and not on parent.
isTransaction bool
// recorder stores all spans in a transaction. Guaranteed to be non-nil.
recorder *spanRecorder
}
// (*) Note on maligned:
//
// We prefer readability over optimal memory layout. If we ever decide to
// reorder fields, we can use a tool:
//
// go run honnef.co/go/tools/cmd/structlayout -json . Span | go run honnef.co/go/tools/cmd/structlayout-optimize
//
// Other structs would deserve reordering as well, for example Event.
// TODO: make Span.Tags and Span.Data opaque types (struct{unexported []slice}).
// An opaque type allows us to add methods and make it more convenient to use
// than maps, because maps require careful nil checks to use properly or rely on
// explicit initialization for every span, even when there might be no
// tags/data. For Span.Data, must gracefully handle values that cannot be
// marshaled into JSON (see transport.go:getRequestBodyFromEvent).
// StartSpan starts a new span to describe an operation. The new span will be a
// child of the last span stored in ctx, if any.
//
// One or more options can be used to modify the span properties. Typically one
// option as a function literal is enough. Combining multiple options can be
// useful to define and reuse specific properties with named functions.
//
// Caller should call the Finish method on the span to mark its end. Finishing a
// root span sends the span and all of its children, recursively, as a
// transaction to Sentry.
func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Span {
parent, hasParent := ctx.Value(spanContextKey{}).(*Span)
var span Span
span = Span{
// defaults
Op: operation,
StartTime: time.Now(),
ctx: context.WithValue(ctx, spanContextKey{}, &span),
parent: parent,
isTransaction: !hasParent,
}
if hasParent {
span.TraceID = parent.TraceID
} else {
// Implementation note:
//
// While math/rand is ~2x faster than crypto/rand (exact
// difference depends on hardware / OS), crypto/rand is probably
// fast enough and a safer choice.
//
// For reference, OpenTelemetry [1] uses crypto/rand to seed
// math/rand. AFAICT this approach does not preserve the
// properties from crypto/rand that make it suitable for
// cryptography. While it might be debatable whether those
// properties are important for us here, again, we're taking the
// safer path.
//
// See [2a] & [2b] for a discussion of some of the properties we
// obtain by using crypto/rand and [3a] & [3b] for why we avoid
// math/rand.
//
// Because the math/rand seed has only 64 bits (int64), if the
// first thing we do after seeding an RNG is to read in a random
// TraceID, there are only 2^64 possible values. Compared to
// UUID v4 that have 122 random bits, there is a much greater
// chance of collision [4a] & [4b].
//
// [1]: https://github.com/open-telemetry/opentelemetry-go/blob/958041ddf619a128/sdk/trace/trace.go#L25-L31
// [2a]: https://security.stackexchange.com/q/120352/246345
// [2b]: https://security.stackexchange.com/a/120365/246345
// [3a]: https://github.com/golang/go/issues/11871#issuecomment-126333686
// [3b]: https://github.com/golang/go/issues/11871#issuecomment-126357889
// [4a]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Collisions
// [4b]: https://www.wolframalpha.com/input/?i=sqrt%282*2%5E64*ln%281%2F%281-0.5%29%29%29
_, err := rand.Read(span.TraceID[:])
if err != nil {
panic(err)
}
}
_, err := rand.Read(span.SpanID[:])
if err != nil {
panic(err)
}
if hasParent {
span.ParentSpanID = parent.SpanID
}
// Apply options to override defaults.
for _, option := range options {
option(&span)
}
span.Sampled = span.sample()
if hasParent {
span.recorder = parent.spanRecorder()
} else {
span.recorder = &spanRecorder{}
}
span.recorder.record(&span)
// Update scope so that all events include a trace context, allowing
// Sentry to correlate errors to transactions/spans.
hubFromContext(ctx).Scope().SetContext("trace", span.traceContext().Map())
return &span
}
// Finish sets the span's end time, unless already set. If the span is the root
// of a span tree, Finish sends the span tree to Sentry as a transaction.
func (s *Span) Finish() {
// TODO(tracing): maybe make Finish run at most once, such that
// (incorrectly) calling it twice never double sends to Sentry.
if s.EndTime.IsZero() {
s.EndTime = monotonicTimeSince(s.StartTime)
}
if !s.Sampled.Bool() {
return
}
event := s.toEvent()
if event == nil {
return
}
// TODO(tracing): add breadcrumbs
// (see https://github.com/getsentry/sentry-python/blob/f6f3525f8812f609/sentry_sdk/tracing.py#L372)
hub := hubFromContext(s.ctx)
if hub.Scope().Transaction() == "" {
Logger.Printf("Missing transaction name for span with op = %q", s.Op)
}
hub.CaptureEvent(event)
}
// Context returns the context containing the span.
func (s *Span) Context() context.Context { return s.ctx }
// StartChild starts a new child span.
//
// The call span.StartChild(operation, options...) is a shortcut for
// StartSpan(span.Context(), operation, options...).
func (s *Span) StartChild(operation string, options ...SpanOption) *Span {
return StartSpan(s.Context(), operation, options...)
}
// SetTag sets a tag on the span. It is recommended to use SetTag instead of
// accessing the tags map directly as SetTag takes care of initializing the map
// when necessary.
func (s *Span) SetTag(name, value string) {
if s.Tags == nil {
s.Tags = make(map[string]string)
}
s.Tags[name] = value
}
// TODO(tracing): maybe add shortcuts to get/set transaction name. Right now the
// transaction name is in the Scope, as it has existed there historically, prior
// to tracing.
//
// See Scope.Transaction() and Scope.SetTransaction().
//
// func (s *Span) TransactionName() string
// func (s *Span) SetTransactionName(name string)
// ToSentryTrace returns the trace propagation value used with the sentry-trace
// HTTP header.
func (s *Span) ToSentryTrace() string {
// TODO(tracing): add instrumentation for outgoing HTTP requests using
// ToSentryTrace.
var b strings.Builder
fmt.Fprintf(&b, "%s-%s", s.TraceID.Hex(), s.SpanID.Hex())
switch s.Sampled {
case SampledTrue:
b.WriteString("-1")
case SampledFalse:
b.WriteString("-0")
}
return b.String()
}
// sentryTracePattern matches either
//
// TRACE_ID - SPAN_ID
// [[:xdigit:]]{32}-[[:xdigit:]]{16}
//
// or
//
// TRACE_ID - SPAN_ID - SAMPLED
// [[:xdigit:]]{32}-[[:xdigit:]]{16}-[01]
var sentryTracePattern = regexp.MustCompile(`^([[:xdigit:]]{32})-([[:xdigit:]]{16})(?:-([01]))?$`)
// updateFromSentryTrace parses a sentry-trace HTTP header (as returned by
// ToSentryTrace) and updates fields of the span. If the header cannot be
// recognized as valid, the span is left unchanged.
func (s *Span) updateFromSentryTrace(header []byte) {
m := sentryTracePattern.FindSubmatch(header)
if m == nil {
// no match
return
}
_, _ = hex.Decode(s.TraceID[:], m[1])
_, _ = hex.Decode(s.ParentSpanID[:], m[2])
if len(m[3]) != 0 {
switch m[3][0] {
case '0':
s.Sampled = SampledFalse
case '1':
s.Sampled = SampledTrue
}
}
}
func (s *Span) MarshalJSON() ([]byte, error) {
// span aliases Span to allow calling json.Marshal without an infinite loop.
// It preserves all fields while none of the attached methods.
type span Span
var parentSpanID string
if s.ParentSpanID != zeroSpanID {
parentSpanID = s.ParentSpanID.String()
}
return json.Marshal(struct {
*span
ParentSpanID string `json:"parent_span_id,omitempty"`
}{
span: (*span)(s),
ParentSpanID: parentSpanID,
})
}
func (s *Span) sample() Sampled {
// https://develop.sentry.dev/sdk/unified-api/tracing/#sampling
// #1 explicit sampling decision via StartSpan options.
if s.Sampled != SampledUndefined {
return s.Sampled
}
hub := hubFromContext(s.ctx)
var clientOptions ClientOptions
client := hub.Client()
if client != nil {
clientOptions = hub.Client().Options()
}
samplingContext := SamplingContext{Span: s, Parent: s.parent}
// Variant for non-transaction spans: they inherit the parent decision.
// TracesSampler only runs for the root span.
// Note: non-transaction should always have a parent, but we check both
// conditions anyway -- the first for semantic meaning, the second to
// avoid a nil pointer dereference.
if !s.isTransaction && s.parent != nil {
return s.parent.Sampled
}
// #2 use TracesSampler from ClientOptions.
sampler := clientOptions.TracesSampler
if sampler != nil {
return sampler.Sample(samplingContext)
}
// #3 inherit parent decision.
if s.parent != nil {
return s.parent.Sampled
}
// #4 uniform sampling using TracesSampleRate.
sampler = UniformTracesSampler(clientOptions.TracesSampleRate)
return sampler.Sample(samplingContext)
}
func (s *Span) toEvent() *Event {
if !s.isTransaction {
return nil // only transactions can be transformed into events
}
hub := hubFromContext(s.ctx)
children := s.recorder.children()
finished := make([]*Span, 0, len(children))
for _, child := range children {
if child.EndTime.IsZero() {
Logger.Printf("Dropped unfinished span: Op=%q TraceID=%s SpanID=%s", child.Op, child.TraceID, child.SpanID)
continue
}
finished = append(finished, child)
}
return &Event{
Type: transactionType,
Transaction: hub.Scope().Transaction(),
Contexts: map[string]Context{
"trace": s.traceContext().Map(),
},
Tags: s.Tags,
Extra: s.Data,
Timestamp: s.EndTime,
StartTime: s.StartTime,
Spans: finished,
}
}
func (s *Span) traceContext() *TraceContext {
return &TraceContext{
TraceID: s.TraceID,
SpanID: s.SpanID,
ParentSpanID: s.ParentSpanID,
Op: s.Op,
Description: s.Description,
Status: s.Status,
}
}
// spanRecorder stores the span tree. Guaranteed to be non-nil.
func (s *Span) spanRecorder() *spanRecorder { return s.recorder }
// TraceID identifies a trace.
type TraceID [16]byte
func (id TraceID) Hex() []byte {
b := make([]byte, hex.EncodedLen(len(id)))
hex.Encode(b, id[:])
return b
}
func (id TraceID) String() string {
return string(id.Hex())
}
func (id TraceID) MarshalText() ([]byte, error) {
return id.Hex(), nil
}
// SpanID identifies a span.
type SpanID [8]byte
func (id SpanID) Hex() []byte {
b := make([]byte, hex.EncodedLen(len(id)))
hex.Encode(b, id[:])
return b
}
func (id SpanID) String() string {
return string(id.Hex())
}
func (id SpanID) MarshalText() ([]byte, error) {
return id.Hex(), nil
}
// Zero values of TraceID and SpanID used for comparisons.
var (
zeroTraceID TraceID
zeroSpanID SpanID
)
// SpanStatus is the status of a span.
type SpanStatus uint8
// Implementation note:
//
// In Relay (ingestion), the SpanStatus type is an enum used as
// Annotated<SpanStatus> when embedded in structs, making it effectively
// Option<SpanStatus>. It means the status is either null or one of the known
// string values.
//
// In Snuba (search), the SpanStatus is stored as an uint8 and defaulted to 2
// ("unknown") when not set. It means that Discover searches for
// `transaction.status:unknown` return both transactions/spans with status
// `null` or `"unknown"`. Searches for `transaction.status:""` return nothing.
//
// With that in mind, the Go SDK default is SpanStatusUndefined, which is
// null/omitted when serializing to JSON, but integrations may update the status
// automatically based on contextual information.
const (
SpanStatusUndefined SpanStatus = iota
SpanStatusOK
SpanStatusCanceled
SpanStatusUnknown
SpanStatusInvalidArgument
SpanStatusDeadlineExceeded
SpanStatusNotFound
SpanStatusAlreadyExists
SpanStatusPermissionDenied
SpanStatusResourceExhausted
SpanStatusFailedPrecondition
SpanStatusAborted
SpanStatusOutOfRange
SpanStatusUnimplemented
SpanStatusInternalError
SpanStatusUnavailable
SpanStatusDataLoss
SpanStatusUnauthenticated
maxSpanStatus
)
func (ss SpanStatus) String() string {
if ss >= maxSpanStatus {
return ""
}
m := [maxSpanStatus]string{
"",
"ok",
"cancelled", // [sic]
"unknown",
"invalid_argument",
"deadline_exceeded",
"not_found",
"already_exists",
"permission_denied",
"resource_exhausted",
"failed_precondition",
"aborted",
"out_of_range",
"unimplemented",
"internal_error",
"unavailable",
"data_loss",
"unauthenticated",
}
return m[ss]
}
func (ss SpanStatus) MarshalJSON() ([]byte, error) {
s := ss.String()
if s == "" {
return []byte("null"), nil
}
return json.Marshal(s)
}
// A TraceContext carries information about an ongoing trace and is meant to be
// stored in Event.Contexts (as *TraceContext).
type TraceContext struct {
TraceID TraceID `json:"trace_id"`
SpanID SpanID `json:"span_id"`
ParentSpanID SpanID `json:"parent_span_id"`
Op string `json:"op,omitempty"`
Description string `json:"description,omitempty"`
Status SpanStatus `json:"status,omitempty"`
}
func (tc *TraceContext) MarshalJSON() ([]byte, error) {
// traceContext aliases TraceContext to allow calling json.Marshal without
// an infinite loop. It preserves all fields while none of the attached
// methods.
type traceContext TraceContext
var parentSpanID string
if tc.ParentSpanID != zeroSpanID {
parentSpanID = tc.ParentSpanID.String()
}
return json.Marshal(struct {
*traceContext
ParentSpanID string `json:"parent_span_id,omitempty"`
}{
traceContext: (*traceContext)(tc),
ParentSpanID: parentSpanID,
})
}
func (tc TraceContext) Map() map[string]interface{} {
m := map[string]interface{}{
"trace_id": tc.TraceID,
"span_id": tc.SpanID,
}
if tc.ParentSpanID != [8]byte{} {
m["parent_span_id"] = tc.ParentSpanID
}
if tc.Op != "" {
m["op"] = tc.Op
}
if tc.Description != "" {
m["description"] = tc.Description
}
if tc.Status > 0 && tc.Status < maxSpanStatus {
m["status"] = tc.Status
}
return m
}
// Sampled signifies a sampling decision.
type Sampled int8
// The possible trace sampling decisions are: SampledFalse, SampledUndefined
// (default) and SampledTrue.
const (
SampledFalse Sampled = -1 + iota
SampledUndefined
SampledTrue
)
func (s Sampled) String() string {
switch s {
case SampledFalse:
return "SampledFalse"
case SampledUndefined:
return "SampledUndefined"
case SampledTrue:
return "SampledTrue"
default:
return fmt.Sprintf("SampledInvalid(%d)", s)
}
}
// Bool returns true if the sample decision is SampledTrue, false otherwise.
func (s Sampled) Bool() bool {
return s == SampledTrue
}
// A SpanOption is a function that can modify the properties of a span.
type SpanOption func(s *Span)
// The TransactionName option sets the name of the current transaction.
//
// A span tree has a single transaction name, therefore using this option when
// starting a span affects the span tree as a whole, potentially overwriting a
// name set previously.
func TransactionName(name string) SpanOption {
return func(s *Span) {
hubFromContext(s.Context()).Scope().SetTransaction(name)
}
}
// OpName sets the operation name for a given span.
func OpName(name string) SpanOption {
return func(s *Span) {
s.Op = name
}
}
// ContinueFromRequest returns a span option that updates the span to continue
// an existing trace. If it cannot detect an existing trace in the request, the
// span will be left unchanged.
//
// ContinueFromRequest is an alias for:
//
// ContinueFromTrace(r.Header.Get("sentry-trace"))
func ContinueFromRequest(r *http.Request) SpanOption {
return ContinueFromTrace(r.Header.Get("sentry-trace"))
}
// ContinueFromTrace returns a span option that updates the span to continue
// an existing TraceID.
func ContinueFromTrace(trace string) SpanOption {
return func(s *Span) {
if trace == "" {
return
}
s.updateFromSentryTrace([]byte(trace))
}
}
// spanContextKey is used to store span values in contexts.
type spanContextKey struct{}
// TransactionFromContext returns the root span of the current transaction. It
// returns nil if no transaction is tracked in the context.
func TransactionFromContext(ctx context.Context) *Span {
if span, ok := ctx.Value(spanContextKey{}).(*Span); ok {
return span.recorder.root()
}
return nil
}
// spanFromContext returns the last span stored in the context or a dummy
// non-nil span.
//
// TODO(tracing): consider exporting this. Without this, users cannot retrieve a
// span from a context since spanContextKey is not exported.
//
// This can be added retroactively, and in the meantime think better whether it
// should return nil (like GetHubFromContext), always non-nil (like
// HubFromContext), or both: two exported functions.
//
// Note the equivalence:
//
// SpanFromContext(ctx).StartChild(...) === StartSpan(ctx, ...)
//
// So we don't aim spanFromContext at creating spans, but mutating existing
// spans that you'd have no access otherwise (because it was created in code you
// do not control, for example SDK auto-instrumentation).
//
// For now we provide TransactionFromContext, which solves the more common case
// of setting tags, etc, on the current transaction.
func spanFromContext(ctx context.Context) *Span {
if span, ok := ctx.Value(spanContextKey{}).(*Span); ok {
return span
}
return nil
}
// StartTransaction will create a transaction (root span) if there's no existing
// transaction in the context otherwise, it will return the existing transaction.
func StartTransaction(ctx context.Context, name string, options ...SpanOption) *Span {
currentTransaction, exists := ctx.Value(spanContextKey{}).(*Span)
if exists {
return currentTransaction
}
hub := GetHubFromContext(ctx)
if hub == nil {
hub = CurrentHub().Clone()
ctx = SetHubOnContext(ctx, hub)
}
options = append(options, TransactionName(name))
return StartSpan(
ctx,
"",
options...,
)
}