Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dynamic Sampling] Head of trace #492

Merged
merged 6 commits into from Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
cleptric marked this conversation as resolved.
Show resolved Hide resolved
}

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
// }))
cleptric marked this conversation as resolved.
Show resolved Hide resolved
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