Skip to content

Commit

Permalink
feat(otel): add option to continue from otel
Browse files Browse the repository at this point in the history
  • Loading branch information
costela committed Aug 23, 2023
1 parent 82a00ab commit 8e878cd
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 8 deletions.
8 changes: 6 additions & 2 deletions http/sentryhttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Handler struct {
repanic bool
waitForDelivery bool
timeout time.Duration
spanOptions []sentry.SpanOption
}

// Options configure a Handler.
Expand Down Expand Up @@ -44,6 +45,8 @@ type Options struct {
// If the timeout is reached, the current goroutine is no longer blocked
// waiting, but the delivery is not canceled.
Timeout time.Duration
// SpanOptions are options to be passed to the span created by the handler.
SpanOptions []sentry.SpanOption
}

// New returns a new Handler. Use the Handle and HandleFunc methods to wrap
Expand All @@ -57,6 +60,7 @@ func New(options Options) *Handler {
repanic: options.Repanic,
timeout: timeout,
waitForDelivery: options.WaitForDelivery,
spanOptions: options.SpanOptions,
}
}

Expand Down Expand Up @@ -86,11 +90,11 @@ func (h *Handler) handle(handler http.Handler) http.HandlerFunc {
hub = sentry.CurrentHub().Clone()
ctx = sentry.SetHubOnContext(ctx, hub)
}
options := []sentry.SpanOption{
options := append([]sentry.SpanOption{
sentry.WithOpName("http.server"),
sentry.ContinueFromRequest(r),
sentry.WithTransactionSource(sentry.SourceURL),
}
}, h.spanOptions...)
// We don't mind getting an existing transaction back so we don't need to
// check if it is.
transaction := sentry.StartTransaction(ctx,
Expand Down
125 changes: 125 additions & 0 deletions otel/middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package sentryotel

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/getsentry/sentry-go"
sentryhttp "github.com/getsentry/sentry-go/http"
"go.opentelemetry.io/otel"
otelSdkTrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
)

func emptyContextWithSentryAndTracing(t *testing.T) (context.Context, <-chan *sentry.Event) {
t.Helper()

events := make(chan *sentry.Event, 1)

client, err := sentry.NewClient(sentry.ClientOptions{
Debug: true,
Dsn: "https://[email protected]/123",
Environment: "testing",
Release: "1.2.3",
EnableTracing: true,
BeforeSendTransaction: func(event *sentry.Event, _ *sentry.EventHint) *sentry.Event {
events <- event
return event
},
})
if err != nil {
t.Fatalf("failed to create sentry client: %v", err)
}

hub := sentry.NewHub(client, sentry.NewScope())
return sentry.SetHubOnContext(context.Background(), hub), events
}

func TestRespectOtelSampling(t *testing.T) {
spanProcessor := NewSentrySpanProcessor()

simulateOtelAndSentry := func(ctx context.Context) (root, inner trace.Span) {
handler := sentryhttp.New(sentryhttp.Options{
SpanOptions: []sentry.SpanOption{ContinueFromOtel()},
}).Handle(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, inner = otel.Tracer("").Start(ctx, "test-inner-span")
}))

tracer := otel.Tracer("")
// simulate an otel middleware creating the root span before sentry
ctx, root = tracer.Start(ctx, "test-root-span")

handler.ServeHTTP(
httptest.NewRecorder(),
httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx),
)

return root, inner
}

t.Run("always sample", func(t *testing.T) {
sentrySpanMap.Clear()

tp := otelSdkTrace.NewTracerProvider(
otelSdkTrace.WithSpanProcessor(spanProcessor),
otelSdkTrace.WithSampler(otelSdkTrace.AlwaysSample()),
)
otel.SetTracerProvider(tp)

ctx, events := emptyContextWithSentryAndTracing(t)

root, inner := simulateOtelAndSentry(ctx)

if root.SpanContext().TraceID() != inner.SpanContext().TraceID() {
t.Errorf("otel root span and inner span should have the same trace id")
}

if len(events) != 1 {
t.Errorf("got unexpected number of events sent to sentry: %d != 1", len(events))
}

for _, span := range []trace.Span{root, inner} {
if !span.SpanContext().IsSampled() {
t.Errorf("otel span should be sampled")
}

spanID := span.SpanContext().SpanID()

sentrySpan, ok := sentrySpanMap.Get(spanID)
if !ok {
t.Fatalf("sentry span could not be found from otel span %d", spanID)
}

if sentrySpan.Sampled != sentry.SampledTrue {
t.Errorf("sentry span should be sampled, not %v", sentrySpan.Sampled)
}
}

})

t.Run("never sample", func(t *testing.T) {
sentrySpanMap.Clear()

tp := otelSdkTrace.NewTracerProvider(
otelSdkTrace.WithSpanProcessor(spanProcessor),
otelSdkTrace.WithSampler(otelSdkTrace.NeverSample()),
)
otel.SetTracerProvider(tp)

ctx, events := emptyContextWithSentryAndTracing(t)

if len(events) != 0 {
t.Fatalf("sentry span should not have been sent to sentry")
}

root, inner := simulateOtelAndSentry(ctx)

for _, span := range []trace.Span{root, inner} {
if span.SpanContext().IsSampled() {
t.Errorf("otel span should not be sampled")
}
}
})
}
28 changes: 28 additions & 0 deletions otel/mittleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package sentryotel

import (
"github.com/getsentry/sentry-go"
"go.opentelemetry.io/otel/trace"
)

// ContinueFromOtel is a [sentry.SpanOption] that can be used with [sentryhttp.New] to ensure sentry uses any
// existing OpenTelemetry trace in the context as the parent of the sentry span.
//
// NOTE: be sure to start the OpenTelemetry span before the sentry one by, e.g. keeping any OpenTelemetry middlewares
// higher in the call chain.
func ContinueFromOtel() sentry.SpanOption {
return func(currentSpan *sentry.Span) {
otelTrace := trace.SpanFromContext(currentSpan.Context())
if otelTrace == nil {
return
}
transaction, ok := sentrySpanMap.Get(otelTrace.SpanContext().SpanID())
if !ok {
return
}
currentSpan.ParentSpanID = transaction.SpanID
// setting this directly because currentSpan.parent is not exported and this currently short-circuits
// span.sample() in the right place.
currentSpan.Sampled = transaction.Sampled
}
}
17 changes: 11 additions & 6 deletions otel/span_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ func (ssp *sentrySpanProcessor) OnStart(parent context.Context, s otelSdkTrace.R

sentrySpanMap.Set(otelSpanID, span)
} else {
traceParentContext := getTraceParentContext(parent)
sampled := getSampled(parent, s)
transaction := sentry.StartTransaction(
parent,
s.Name(),
sentry.WithSpanSampled(traceParentContext.Sampled),
sentry.WithSpanSampled(sampled),
)
transaction.SpanID = sentry.SpanID(otelSpanID)
transaction.TraceID = sentry.TraceID(otelTraceID)
Expand Down Expand Up @@ -108,12 +108,17 @@ func flushSpanProcessor(ctx context.Context) error {
return nil
}

func getTraceParentContext(ctx context.Context) sentry.TraceParentContext {
func getSampled(ctx context.Context, s otelSdkTrace.ReadWriteSpan) sentry.Sampled {
traceParentContext, ok := ctx.Value(sentryTraceParentContextKey{}).(sentry.TraceParentContext)
if !ok {
traceParentContext.Sampled = sentry.SampledUndefined
if ok {
return traceParentContext.Sampled
}
return traceParentContext

if s.SpanContext().IsSampled() {
return sentry.SampledTrue
}

return sentry.SampledFalse
}

func updateTransactionWithOtelData(transaction *sentry.Span, s otelSdkTrace.ReadOnlySpan) {
Expand Down

0 comments on commit 8e878cd

Please sign in to comment.