Skip to content

Commit

Permalink
[Dynamic Sampling] Head of trace (#492)
Browse files Browse the repository at this point in the history
Co-authored-by: Abhijeet Prasad <devabhiprasad@gmail.com>
  • Loading branch information
cleptric and AbhiPrasad committed Nov 30, 2022
1 parent d01ce46 commit f219a81
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 17 deletions.
51 changes: 50 additions & 1 deletion dynamic_sampling_context.go
@@ -1,6 +1,7 @@
package sentry

import (
"strconv"
"strings"

"github.com/getsentry/sentry-go/internal/otel/baggage"
Expand All @@ -16,7 +17,7 @@ type DynamicSamplingContext struct {
Frozen bool
}

func NewDynamicSamplingContext(header []byte) (DynamicSamplingContext, error) {
func DynamicSamplingContextFromHeader(header []byte) (DynamicSamplingContext, error) {
bag, err := baggage.Parse(string(header))
if err != nil {
return DynamicSamplingContext{}, err
Expand All @@ -36,10 +37,58 @@ func NewDynamicSamplingContext(header []byte) (DynamicSamplingContext, error) {
}, nil
}

func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext {
entries := map[string]string{}

hub := hubFromContext(span.Context())
scope := hub.Scope()
client := hub.Client()
options := client.Options()

if traceID := span.TraceID.String(); traceID != "" {
entries["trace_id"] = traceID
}
if sampleRate := span.sampleRate; sampleRate != 0 {
entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64)
}

if dsn := client.dsn; dsn != nil {
if publicKey := dsn.publicKey; publicKey != "" {
entries["public_key"] = publicKey
}
}
if release := options.Release; release != "" {
entries["release"] = release
}
if environment := options.Environment; environment != "" {
entries["environment"] = environment
}

// Only include the transaction name if it's of good quality (not empty and not SourceURL)
if span.Source != "" && span.Source != SourceURL {
if transactionName := scope.Transaction(); transactionName != "" {
entries["transaction"] = transactionName
}
}

if userSegment := scope.user.Segment; userSegment != "" {
entries["user_segment"] = userSegment
}

return DynamicSamplingContext{
Entries: entries,
Frozen: true,
}
}

func (d DynamicSamplingContext) HasEntries() bool {
return len(d.Entries) > 0
}

func (d DynamicSamplingContext) IsFrozen() bool {
return d.Frozen
}

func (d DynamicSamplingContext) String() string {
// TODO implement me
return ""
Expand Down
84 changes: 82 additions & 2 deletions dynamic_sampling_context_test.go
Expand Up @@ -4,7 +4,7 @@ import (
"testing"
)

func TestNewDynamicSamplingContext(t *testing.T) {
func TestDynamicSamplingContextFromHeader(t *testing.T) {
tests := []struct {
input []byte
want DynamicSamplingContext
Expand Down Expand Up @@ -41,10 +41,90 @@ func TestNewDynamicSamplingContext(t *testing.T) {
}

for _, tc := range tests {
got, err := NewDynamicSamplingContext(tc.input)
got, err := DynamicSamplingContextFromHeader(tc.input)
if err != nil {
t.Fatal(err)
}
assertEqual(t, got, tc.want)
}
}

func TestDynamicSamplingContextFromTransaction(t *testing.T) {
tests := []struct {
input *Span
want DynamicSamplingContext
}{
// Normal flow
{
input: func() *Span {
ctx := NewTestContext(ClientOptions{
EnableTracing: true,
TracesSampleRate: 0.5,
Dsn: "http://public@example.com/sentry/1",
Release: "1.0.0",
Environment: "test",
})
hubFromContext(ctx).ConfigureScope(func(scope *Scope) {
scope.SetUser(User{Segment: "user_segment"})
})
txn := StartTransaction(ctx, "name", TransctionSource(SourceCustom))
txn.TraceID = TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03")
return txn
}(),
want: DynamicSamplingContext{
Frozen: true,
Entries: map[string]string{
"sample_rate": "0.5",
"trace_id": "d49d9bf66f13450b81f65bc51cf49c03",
"public_key": "public",
"release": "1.0.0",
"environment": "test",
"transaction": "name",
"user_segment": "user_segment",
},
},
},
// Transaction with source url, do not include in Dynamic Sampling context
{
input: func() *Span {
ctx := NewTestContext(ClientOptions{
EnableTracing: true,
TracesSampleRate: 0.5,
Dsn: "http://public@example.com/sentry/1",
Release: "1.0.0",
})
txn := StartTransaction(ctx, "name")
txn.TraceID = TraceIDFromHex("d49d9bf66f13450b81f65bc51cf49c03")
return txn
}(),
want: DynamicSamplingContext{
Frozen: true,
Entries: map[string]string{
"sample_rate": "0.5",
"trace_id": "d49d9bf66f13450b81f65bc51cf49c03",
"public_key": "public",
"release": "1.0.0",
},
},
},
}

for _, tc := range tests {
got := DynamicSamplingContextFromTransaction(tc.input)
assertEqual(t, got, tc.want)
}
}

func TestHasEntries(t *testing.T) {
var dsc DynamicSamplingContext

dsc = DynamicSamplingContext{}
assertEqual(t, dsc.HasEntries(), false)

dsc = DynamicSamplingContext{
Entries: map[string]string{
"foo": "bar",
},
}
assertEqual(t, dsc.HasEntries(), true)
}
7 changes: 4 additions & 3 deletions http/sentryhttp.go
Expand Up @@ -71,9 +71,9 @@ func (h *Handler) Handle(handler http.Handler) http.Handler {
// where that is convenient. In particular, use it to wrap a handler function
// literal.
//
// http.Handle(pattern, h.HandleFunc(func (w http.ResponseWriter, r *http.Request) {
// // handler code here
// }))
// http.Handle(pattern, h.HandleFunc(func (w http.ResponseWriter, r *http.Request) {
// // handler code here
// }))
func (h *Handler) HandleFunc(handler http.HandlerFunc) http.HandlerFunc {
return h.handle(handler)
}
Expand All @@ -89,6 +89,7 @@ func (h *Handler) handle(handler http.Handler) http.HandlerFunc {
options := []sentry.SpanOption{
sentry.OpName("http.server"),
sentry.ContinueFromRequest(r),
sentry.TransctionSource(sentry.SourceURL),
}
// We don't mind getting an existing transaction back so we don't need to
// check if it is.
Expand Down
3 changes: 2 additions & 1 deletion interfaces.go
Expand Up @@ -218,7 +218,8 @@ type Exception struct {
// SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline
// but which shouldn't get send to Sentry.
type SDKMetaData struct {
dsc DynamicSamplingContext
dsc DynamicSamplingContext
transactionSource TransactionSource
}

// EventID is a hexadecimal string representing a unique uuid4 for an Event.
Expand Down
58 changes: 48 additions & 10 deletions tracing.go
Expand Up @@ -28,25 +28,23 @@ type Span struct { //nolint: maligned // prefer readability over optimal memory
StartTime time.Time `json:"start_timestamp"`
EndTime time.Time `json:"timestamp"`
Data map[string]interface{} `json:"data,omitempty"`
Sampled Sampled `json:"-"`
Source TransactionSource `json:"-"`

Sampled Sampled `json:"-"`

// sample rate the span was sampled with.
sampleRate float64
// ctx is the context where the span was started. Always non-nil.
ctx context.Context

// Dynamic Sampling context
dynamicSamplingContext DynamicSamplingContext

// 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
}
Expand Down Expand Up @@ -141,7 +139,6 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp
option(&span)
}

// TODO only sample transactions?
span.Sampled = span.sample()

if hasParent {
Expand Down Expand Up @@ -273,7 +270,7 @@ func (s *Span) updateFromSentryTrace(header []byte) {

func (s *Span) updateFromBaggage(header []byte) {
if s.isTransaction {
dsc, err := NewDynamicSamplingContext(header)
dsc, err := DynamicSamplingContextFromHeader(header)
if err != nil {
return
}
Expand Down Expand Up @@ -311,12 +308,19 @@ func (s *Span) sample() Sampled {
// #1 tracing is not enabled.
if !clientOptions.EnableTracing {
Logger.Printf("Dropping transaction: EnableTracing is set to %t", clientOptions.EnableTracing)
s.sampleRate = 0.0
return SampledFalse
}

// #2 explicit sampling decision via StartSpan/StartTransaction options.
if s.Sampled != SampledUndefined {
Logger.Printf("Using explicit sampling decision from StartSpan/StartTransaction: %v", s.Sampled)
switch s.Sampled {
case SampledTrue:
s.sampleRate = 1.0
case SampledFalse:
s.sampleRate = 0.0
}
return s.Sampled
}

Expand All @@ -333,6 +337,7 @@ func (s *Span) sample() Sampled {
samplingContext := SamplingContext{Span: s, Parent: s.parent}
if sampler != nil {
tracesSamplerSampleRate := sampler.Sample(samplingContext)
s.sampleRate = tracesSamplerSampleRate
if tracesSamplerSampleRate < 0.0 || tracesSamplerSampleRate > 1.0 {
Logger.Printf("Dropping transaction: Returned TracesSampler rate is out of range [0.0, 1.0]: %f", tracesSamplerSampleRate)
return SampledFalse
Expand All @@ -351,11 +356,18 @@ func (s *Span) sample() Sampled {
// #4 inherit parent decision.
if s.parent != nil {
Logger.Printf("Using sampling decision from parent: %v", s.parent.Sampled)
switch s.parent.Sampled {
case SampledTrue:
s.sampleRate = 1.0
case SampledFalse:
s.sampleRate = 0.0
}
return s.parent.Sampled
}

// #5 use TracesSampleRate from ClientOptions.
sampleRate := clientOptions.TracesSampleRate
s.sampleRate = sampleRate
if sampleRate < 0.0 || sampleRate > 1.0 {
Logger.Printf("Dropping transaction: TracesSamplerRate out of range [0.0, 1.0]: %f", sampleRate)
return SampledFalse
Expand Down Expand Up @@ -388,6 +400,12 @@ func (s *Span) toEvent() *Event {
finished = append(finished, child)
}

// Create and attach a DynamicSamplingContext to the transaction.
// If the DynamicSamplingContext is not frozen at this point, we can assume being head of trace.
if !s.dynamicSamplingContext.IsFrozen() {
s.dynamicSamplingContext = DynamicSamplingContextFromTransaction(s)
}

return &Event{
Type: transactionType,
Transaction: hub.Scope().Transaction(),
Expand All @@ -400,7 +418,8 @@ func (s *Span) toEvent() *Event {
StartTime: s.StartTime,
Spans: finished,
sdkMetaData: SDKMetaData{
dsc: s.dynamicSamplingContext,
dsc: s.dynamicSamplingContext,
transactionSource: s.Source,
},
}
}
Expand Down Expand Up @@ -459,6 +478,18 @@ var (
zeroSpanID SpanID
)

// Contains information about how the name of the transaction was determined.
type TransactionSource string

const (
SourceCustom TransactionSource = "custom"
SourceURL TransactionSource = "url"
SourceRoute TransactionSource = "route"
SourceView TransactionSource = "view"
SourceComponent TransactionSource = "component"
SourceTask TransactionSource = "task"
)

// SpanStatus is the status of a span.
type SpanStatus uint8

Expand Down Expand Up @@ -639,13 +670,20 @@ func OpName(name string) SpanOption {
}
}

// TransctionSource sets the source of the transaction name.
func TransctionSource(source TransactionSource) SpanOption {
return func(s *Span) {
s.Source = source
}
}

// 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:
//
// ContinueFromHeaders(r.Header.Get("sentry-trace"), r.Header.Get("baggage"))
// ContinueFromHeaders(r.Header.Get("sentry-trace"), r.Header.Get("baggage")).
func ContinueFromRequest(r *http.Request) SpanOption {
return ContinueFromHeaders(r.Header.Get("sentry-trace"), r.Header.Get("baggage"))
}
Expand Down

0 comments on commit f219a81

Please sign in to comment.