diff --git a/crosstest/go.mod b/crosstest/go.mod index 0bb595c4f..d8c225a2d 100644 --- a/crosstest/go.mod +++ b/crosstest/go.mod @@ -56,8 +56,6 @@ require ( github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect @@ -75,6 +73,7 @@ require ( github.com/kataras/tunnel v0.0.4 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailgun/raymond/v2 v2.0.48 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -86,6 +85,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect github.com/tdewolff/minify/v2 v2.20.19 // indirect @@ -97,10 +97,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yosssi/ace v0.0.5 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.49.0 // indirect diff --git a/crosstest/go.sum b/crosstest/go.sum index d3a2e347d..1476387d8 100644 --- a/crosstest/go.sum +++ b/crosstest/go.sum @@ -24,6 +24,7 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -43,11 +44,6 @@ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -136,6 +132,7 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -143,6 +140,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -210,16 +208,8 @@ github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FB github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/otel/context.go b/otel/context.go deleted file mode 100644 index b4906eb0b..000000000 --- a/otel/context.go +++ /dev/null @@ -1,7 +0,0 @@ -package sentryotel - -// Context keys to be used with context.WithValue(...) and ctx.Value(...) -type dynamicSamplingContextKey struct{} -type sentryTraceHeaderContextKey struct{} -type sentryTraceParentContextKey struct{} -type baggageContextKey struct{} diff --git a/otel/go.mod b/otel/go.mod index 0ee7f6f81..8c96f6059 100644 --- a/otel/go.mod +++ b/otel/go.mod @@ -6,7 +6,6 @@ replace github.com/getsentry/sentry-go => ../ require ( github.com/getsentry/sentry-go v0.46.2 - github.com/google/go-cmp v0.7.0 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 diff --git a/otel/helpers_test.go b/otel/helpers_test.go deleted file mode 100644 index 6b5fcd341..000000000 --- a/otel/helpers_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package sentryotel - -import ( - "encoding/hex" - "sort" - "testing" - - "github.com/getsentry/sentry-go" - "github.com/getsentry/sentry-go/internal/otel/baggage" - "github.com/getsentry/sentry-go/internal/testutils" - "github.com/google/go-cmp/cmp" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" -) - -var assertEqual = testutils.AssertEqual -var assertTrue = testutils.AssertTrue -var assertFalse = testutils.AssertFalse - -// assertMapCarrierEqual compares two values of type propagation.MapCarrier and raises an -// assertion error if the values differ. -// -// It is needed because some headers (e.g. "baggage") might contain the same set of values/attributes, -// (and therefore be semantically equal), but serialized in different order. -func assertMapCarrierEqual(t *testing.T, got, want propagation.MapCarrier, _ ...interface{}) { - t.Helper() - - // Make sure that keys are the same - gotKeysSorted := got.Keys() - sort.Strings(gotKeysSorted) - wantKeysSorted := want.Keys() - sort.Strings(wantKeysSorted) - - if diff := cmp.Diff(wantKeysSorted, gotKeysSorted); diff != "" { - t.Errorf("Comparing MapCarrier keys (-want +got):\n%s", diff) - } - - for _, key := range gotKeysSorted { - gotValue := got.Get(key) - wantValue := want.Get(key) - - // Ignore serialization order for baggage values - if key == sentry.SentryBaggageHeader { - gotBaggage, gotErr := baggage.Parse(gotValue) - wantBaggage, wantErr := baggage.Parse(wantValue) - - if diff := cmp.Diff(wantErr, gotErr); diff != "" { - t.Errorf("Comparing Baggage parsing errors (-want +got):\n%s", diff) - } - - if diff := cmp.Diff( - wantBaggage, - gotBaggage, - cmp.AllowUnexported(baggage.Member{}, baggage.Baggage{}), - ); diff != "" { - t.Errorf("Comparing Baggage values (-want +got):\n%s", diff) - } - continue - } - - // Everything else: do the exact comparison - if diff := cmp.Diff(wantValue, gotValue); diff != "" { - t.Errorf("Comparing MapCarrier values (-want +got):\n%s", diff) - } - } -} - -// FIXME: copied from tracing_test.go. -func TraceIDFromHex(s string) sentry.TraceID { - var id sentry.TraceID - _, err := hex.Decode(id[:], []byte(s)) - if err != nil { - panic(err) - } - return id -} - -func SpanIDFromHex(s string) sentry.SpanID { - var id sentry.SpanID - _, err := hex.Decode(id[:], []byte(s)) - if err != nil { - panic(err) - } - return id -} - -func stringPtr(s string) *string { - return &s -} - -func otelTraceIDFromHex(s string) trace.TraceID { - if s == "" { - return trace.TraceID{} - } - traceID, err := trace.TraceIDFromHex(s) - if err != nil { - panic(err) - } - return traceID -} - -func otelSpanIDFromHex(s string) trace.SpanID { - if s == "" { - return trace.SpanID{} - } - spanID, err := trace.SpanIDFromHex(s) - if err != nil { - panic(err) - } - return spanID -} diff --git a/otel/propagator.go b/otel/propagator.go deleted file mode 100644 index 3c368db96..000000000 --- a/otel/propagator.go +++ /dev/null @@ -1,131 +0,0 @@ -package sentryotel - -import ( - "context" - - "github.com/getsentry/sentry-go" - "github.com/getsentry/sentry-go/internal/otel/baggage" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" -) - -type sentryPropagator struct{} - -// NewSentryPropagator creates a propagator for Sentry trace headers. -// -// Deprecated: This propagator depends on the span-processor integration and does -// not work for OTLP-based setups. Prefer the standard OpenTelemetry propagators. -// Will be removed in 0.47.0. -// -// For more details: https://github.com/getsentry/sentry-go/issues/1221 -func NewSentryPropagator() propagation.TextMapPropagator { - return &sentryPropagator{} -} - -// Inject sets Sentry-related values from the Context into the carrier. -// -// https://opentelemetry.io/docs/reference/specification/context/api-propagators/#inject -func (p sentryPropagator) Inject(ctx context.Context, carrier propagation.TextMapCarrier) { - spanContext := trace.SpanContextFromContext(ctx) - - var sentrySpan *sentry.Span - - if spanContext.IsValid() { - sentrySpan, _ = sentrySpanMap.Get(spanContext.TraceID(), spanContext.SpanID()) - } else { - sentrySpan = nil - } - - // Propagate sentry-trace header - if sentrySpan == nil { - // No span => propagate the incoming sentry-trace header, if exists - sentryTraceHeader, _ := ctx.Value(sentryTraceHeaderContextKey{}).(string) - if sentryTraceHeader != "" { - carrier.Set(sentry.SentryTraceHeader, sentryTraceHeader) - } - } else { - // Sentry span exists => generate "sentry-trace" from it - carrier.Set(sentry.SentryTraceHeader, sentrySpan.ToSentryTrace()) - } - - // Propagate baggage header - sentryBaggageStr := "" - if sentrySpan != nil { - sentryBaggageStr = sentrySpan.GetTransaction().ToBaggage() - } - // FIXME(anton): We're basically reparsing the header again, because in sentry-go - // we currently don't expose a method to get only DSC or its baggage (only a string). - // This is not optimal and we should consider other approaches. - sentryBaggage, _ := baggage.Parse(sentryBaggageStr) - - // Merge the baggage values - finalBaggage, baggageOk := ctx.Value(baggageContextKey{}).(baggage.Baggage) - if !baggageOk { - finalBaggage = baggage.Baggage{} - } - for _, member := range sentryBaggage.Members() { - var err error - finalBaggage, err = finalBaggage.SetMember(member) - if err != nil { - continue - } - } - - if finalBaggage.Len() > 0 { - carrier.Set(sentry.SentryBaggageHeader, finalBaggage.String()) - } -} - -// Extract reads cross-cutting concerns from the carrier into a Context. -// -// https://opentelemetry.io/docs/reference/specification/context/api-propagators/#extract -func (p sentryPropagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context { - sentryTraceHeader := carrier.Get(sentry.SentryTraceHeader) - - if sentryTraceHeader != "" { - ctx = context.WithValue(ctx, sentryTraceHeaderContextKey{}, sentryTraceHeader) - if traceParentContext, valid := sentry.ParseTraceParentContext([]byte(sentryTraceHeader)); valid { - // Save traceParentContext because we'll at least need to know the original "sampled" - // value in the span processor. - ctx = context.WithValue(ctx, sentryTraceParentContextKey{}, traceParentContext) - - spanContextConfig := trace.SpanContextConfig{ - TraceID: trace.TraceID(traceParentContext.TraceID), - SpanID: trace.SpanID(traceParentContext.ParentSpanID), - TraceFlags: trace.FlagsSampled, - Remote: true, - } - ctx = trace.ContextWithSpanContext(ctx, trace.NewSpanContext(spanContextConfig)) - } - } - - baggageHeader := carrier.Get(sentry.SentryBaggageHeader) - if baggageHeader != "" { - // Preserve the original baggage - parsedBaggage, err := baggage.Parse(baggageHeader) - if err == nil { - ctx = context.WithValue(ctx, baggageContextKey{}, parsedBaggage) - } - } - - // The following cases should be already covered below: - // * We can extract a valid dynamic sampling context (DSC) from the baggage - // * No baggage header is present - // * No Sentry-related values are present - // * We cannot parse the baggage header for whatever reason - dynamicSamplingContext, err := sentry.DynamicSamplingContextFromHeader([]byte(baggageHeader)) - if err != nil { - // If there are any errors, create a new non-frozen one. - dynamicSamplingContext = sentry.DynamicSamplingContext{Frozen: false} - } - - ctx = context.WithValue(ctx, dynamicSamplingContextKey{}, dynamicSamplingContext) - return ctx -} - -// Fields returns a list of fields that will be used by the propagator. -// -// https://opentelemetry.io/docs/reference/specification/context/api-propagators/#fields -func (p sentryPropagator) Fields() []string { - return []string{sentry.SentryTraceHeader, sentry.SentryBaggageHeader} -} diff --git a/otel/propagator_test.go b/otel/propagator_test.go deleted file mode 100644 index 9e42e6ab7..000000000 --- a/otel/propagator_test.go +++ /dev/null @@ -1,345 +0,0 @@ -package sentryotel - -import ( - "context" - "testing" - - "github.com/getsentry/sentry-go" - "github.com/getsentry/sentry-go/internal/otel/baggage" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" -) - -func setupPropagatorTest() (propagation.TextMapPropagator, propagation.MapCarrier) { - // Make sure that the global span map is empty - sentrySpanMap.Clear() - - propagator := NewSentryPropagator() - carrier := propagation.MapCarrier{} - return propagator, carrier -} - -type transactionTestContext struct { - name string - traceID string - spanID string - sampled sentry.Sampled -} - -func createTransactionAndMaybeSpan(transactionContext transactionTestContext, withSpan bool) trace.SpanContextConfig { - transaction := sentry.StartTransaction( - emptyContextWithSentry(), - transactionContext.name, - sentry.WithSpanSampled(transactionContext.sampled), - ) - transaction.TraceID = TraceIDFromHex(transactionContext.traceID) - transaction.SpanID = SpanIDFromHex(transactionContext.spanID) - - otelTraceID := otelTraceIDFromHex(transactionContext.traceID) - otelSpanID := otelSpanIDFromHex(transactionContext.spanID) - if withSpan { - span := transaction.StartChild("op") - // We want the child to have the SpanID from transactionContext, so - // we "swap" span IDs from the transaction and the child span. - transaction.SpanID = span.SpanID - span.SpanID = SpanIDFromHex(transactionContext.spanID) - sentrySpanMap.Set(trace.SpanID(span.SpanID), span, otelTraceID) - } - sentrySpanMap.Set(trace.SpanID(transaction.SpanID), transaction, otelTraceID) - - otelContext := trace.SpanContextConfig{ - TraceID: otelTraceID, - SpanID: otelSpanID, - TraceFlags: trace.FlagsSampled, - } - return otelContext -} - -func TestNewSentryPropagator(t *testing.T) { - propagator := NewSentryPropagator() - - if _, valid := propagator.(*sentryPropagator); !valid { - t.Errorf( - "Invalid type returned by the propagator constructor: %#v\n", - propagator, - ) - } -} - -/// Fields - -func TestFieldsReturnsRightSet(t *testing.T) { - propagator, _ := setupPropagatorTest() - fields := propagator.Fields() - assertEqual(t, fields, []string{"sentry-trace", "baggage"}) -} - -/// Inject - -func TestInjectDoesNothingOnEmptyContext(t *testing.T) { - propagator, carrier := setupPropagatorTest() - - propagator.Inject(context.Background(), carrier) - - assertMapCarrierEqual(t, - carrier, - propagation.MapCarrier{}, - ) -} - -func TestInjectUsesSentryTraceOnEmptySpan(t *testing.T) { - propagator, carrier := setupPropagatorTest() - ctx := context.WithValue( - context.Background(), - sentryTraceHeaderContextKey{}, - "d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1", - ) - - propagator.Inject(ctx, carrier) - - assertMapCarrierEqual(t, - carrier, - propagation.MapCarrier{"sentry-trace": "d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1"}, - ) -} - -func TestInjectUsesBaggageOnEmptySpan(t *testing.T) { - propagator, carrier := setupPropagatorTest() - bag, _ := baggage.Parse("key1=value1;value2, key2=value2") - ctx := context.WithValue(context.Background(), baggageContextKey{}, bag) - - propagator.Inject(ctx, carrier) - - assertMapCarrierEqual(t, - carrier, - propagation.MapCarrier{"baggage": "key1=value1;value2,key2=value2"}, - ) -} - -func testInjectUsesSetsValidTrace(t *testing.T, withChildSpan bool) { - tests := []struct { - name string - sentryTransactionContext transactionTestContext - wantBaggage *string - wantSentryTrace *string - }{ - { - name: "should set baggage and sentry-trace when sampled", - sentryTransactionContext: transactionTestContext{ - name: "sampled-transaction", - traceID: "d4cda95b652f4a1592b449d5929fda1b", - spanID: "6e0c63257de34c92", - sampled: sentry.SampledTrue, - }, - wantBaggage: stringPtr("sentry-environment=testing,sentry-release=1.2.3,sentry-transaction=sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-sample_rate=1,sentry-sampled=true"), - wantSentryTrace: stringPtr("d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1"), - }, - { - name: "should set proper baggage and sentry-trace when not sampled", - sentryTransactionContext: transactionTestContext{ - name: "not-sampled-transaction", - traceID: "d4cda95b652f4a1592b449d5929fda1b", - spanID: "6e0c63257de34c92", - sampled: sentry.SampledFalse, - }, - wantBaggage: stringPtr("sentry-environment=testing,sentry-release=1.2.3,sentry-transaction=not-sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-sampled=false"), - wantSentryTrace: stringPtr("d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0"), - }, - { - name: "should NOT set headers when traceId is empty", - sentryTransactionContext: transactionTestContext{ - name: "transaction-name", - traceID: "", - spanID: "6e0c63257de34c92", - sampled: sentry.SampledTrue, - }, - wantBaggage: nil, - wantSentryTrace: nil, - }, - { - name: "should NOT set headers when spanId is empty", - sentryTransactionContext: transactionTestContext{ - name: "transaction-name", - traceID: "d4cda95b652f4a1592b449d5929fda1b", - spanID: "", - sampled: sentry.SampledTrue, - }, - wantBaggage: nil, - wantSentryTrace: nil, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - propagator, carrier := setupPropagatorTest() - testContextConfig := createTransactionAndMaybeSpan(tt.sentryTransactionContext, withChildSpan) - otelSpanContext := trace.NewSpanContext(testContextConfig) - ctx := trace.ContextWithSpanContext(context.Background(), otelSpanContext) - propagator.Inject(ctx, carrier) - - expectedCarrier := propagation.MapCarrier{} - if tt.wantBaggage != nil { - expectedCarrier["baggage"] = *tt.wantBaggage - } - if tt.wantSentryTrace != nil { - expectedCarrier["sentry-trace"] = *tt.wantSentryTrace - } - assertMapCarrierEqual(t, carrier, expectedCarrier) - }) - } -} - -func TestInjectUsesSetsValidTraceFromTransaction(t *testing.T) { - testInjectUsesSetsValidTrace(t, false) -} - -func TestInjectUsesSetsValidTraceFromChildSpan(t *testing.T) { - testInjectUsesSetsValidTrace(t, true) -} - -/// Extract - -// No sentry-trace header, no baggage header. -func TestExtractDoesNotChangeContextWithEmptyHeaders(t *testing.T) { - propagator, carrier := setupPropagatorTest() - - ctx := propagator.Extract(context.Background(), carrier) - - assertEqual(t, - ctx.Value(dynamicSamplingContextKey{}), - sentry.DynamicSamplingContext{Entries: map[string]string{}, Frozen: false}, - ) -} - -// No sentry-trace header, 3rd-party baggage header. -func TestExtractSetsUndefinedDynamicSamplingContext(t *testing.T) { - propagator, carrier := setupPropagatorTest() - carrier.Set(sentry.SentryBaggageHeader, "othervendor=bla") - - ctx := propagator.Extract(context.Background(), carrier) - - assertEqual(t, - ctx.Value(dynamicSamplingContextKey{}), - sentry.DynamicSamplingContext{Entries: map[string]string{}, Frozen: false}, - ) -} - -// With sentry-trace header, no baggage header. -func TestExtractSetsSentrySpanContext(t *testing.T) { - propagator, carrier := setupPropagatorTest() - carrier.Set( - sentry.SentryTraceHeader, - "d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1", - ) - - ctx := propagator.Extract(context.Background(), carrier) - - // Make sure that Extract added a proper span context to context - spanContext := trace.SpanContextFromContext(ctx) - spanID, _ := trace.SpanIDFromHex("6e0c63257de34c92") - traceID, _ := trace.TraceIDFromHex("d4cda95b652f4a1592b449d5929fda1b") - assertEqual(t, - spanContext, - trace.NewSpanContext(trace.SpanContextConfig{ - Remote: true, - SpanID: spanID, - TraceID: traceID, - TraceFlags: trace.FlagsSampled, - }), - ) -} - -// With sentry-trace header, no baggage header. -func TestExtractHandlesInvalidTraceHeader(t *testing.T) { - propagator, carrier := setupPropagatorTest() - carrier.Set( - sentry.SentryTraceHeader, - // Invalid trace value - "xxx", - ) - - ctx := propagator.Extract(context.Background(), carrier) - - // Span context should be invalid - spanContext := trace.SpanContextFromContext(ctx) - assertEqual(t, spanContext.IsValid(), false) -} - -// No sentry-trace header, with baggage header. -func TestExtractSetsDefinedDynamicSamplingContext(t *testing.T) { - propagator, carrier := setupPropagatorTest() - carrier.Set( - sentry.SentryBaggageHeader, - "othervendor=bla,sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dsc-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b", - ) - - ctx := propagator.Extract(context.Background(), carrier) - - assertEqual(t, - ctx.Value(dynamicSamplingContextKey{}), - sentry.DynamicSamplingContext{ - Entries: map[string]string{ - "environment": "production", - "public_key": "abc", - "release": "1.0.0", - "trace_id": "d4cda95b652f4a1592b449d5929fda1b", - "transaction": "dsc-transaction", - }, - Frozen: true}, - ) -} - -/// Integration tests - -func TestExtractAndInjectIntegration(t *testing.T) { - tests := []struct { - name string - inSentryTrace *string - inBaggage *string - }{ - { - name: "valid sentry-trace and baggage", - inSentryTrace: stringPtr("d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1"), - inBaggage: stringPtr("sentry-environment=production,sentry-release=1.0.0,othervendor=bla,sentry-transaction=dsc-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b"), - }, - { - name: "only sentry-trace, no baggage", - inSentryTrace: stringPtr("d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1"), - }, - { - name: "valid sentry-trace and mixed baggage with special characters", - inSentryTrace: stringPtr("d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1"), - inBaggage: stringPtr("sentry-transaction=GET%20POST,userId=Am%C3%A9lie, key1 = +++ , key2=%253B"), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - propagator, incomingCarrier := setupPropagatorTest() - - if tt.inBaggage != nil { - incomingCarrier.Set( - "baggage", - *tt.inBaggage, - ) - } - if tt.inSentryTrace != nil { - incomingCarrier.Set( - "sentry-trace", - *tt.inSentryTrace, - ) - } - outgoingCarrier := propagation.MapCarrier{} - - ctx := propagator.Extract(context.Background(), incomingCarrier) - propagator.Inject(ctx, outgoingCarrier) - - assertMapCarrierEqual(t, - outgoingCarrier, - incomingCarrier, - ) - }) - } -} diff --git a/otel/span_map.go b/otel/span_map.go deleted file mode 100644 index 5519551c9..000000000 --- a/otel/span_map.go +++ /dev/null @@ -1,113 +0,0 @@ -package sentryotel - -import ( - "sync/atomic" - - "github.com/getsentry/sentry-go" - "github.com/getsentry/sentry-go/internal/util" - otelTrace "go.opentelemetry.io/otel/trace" -) - -// TransactionEntry holds a reference to the root transaction span and -// tracks the number of active spans belonging to this trace. -// -// Deprecated: Prefer OTLP export via sentryotlp.NewTraceExporter. -// Will be removed in 0.47.0 alongside [NewSentrySpanProcessor]. -type TransactionEntry struct { - root *sentry.Span - activeCount atomic.Int64 - // spans holds active (not yet finished) spans for Get lookups. - spans util.SyncMap[otelTrace.SpanID, *sentry.Span] - // knownSpanIDs tracks all span IDs ever added to this transaction. - knownSpanIDs util.SyncMap[otelTrace.SpanID, struct{}] -} - -// HasSpan returns true if the given spanID was ever part of this transaction. -func (te *TransactionEntry) HasSpan(spanID otelTrace.SpanID) bool { - _, ok := te.knownSpanIDs.Load(spanID) - return ok -} - -// SentrySpanMap is a mapping between OpenTelemetry spans and Sentry spans. -// It stores spans per transaction for lookup by the propagator and event processor, -// and manages transaction entries for creating child spans via the shared spanRecorder. -// -// Deprecated: Prefer OTLP export via sentryotlp.NewTraceExporter. -// Will be removed in 0.47.0 alongside [NewSentrySpanProcessor]. -type SentrySpanMap struct { - transactions util.SyncMap[otelTrace.TraceID, *TransactionEntry] -} - -// Get returns the current sentry.Span associated with the given OTel traceID and spanID. -func (ssm *SentrySpanMap) Get(traceID otelTrace.TraceID, spanID otelTrace.SpanID) (*sentry.Span, bool) { - entry, ok := ssm.transactions.Load(traceID) - if !ok { - return nil, false - } - return entry.spans.Load(spanID) -} - -// GetTransaction returns the transaction information for the given OTel traceID. -func (ssm *SentrySpanMap) GetTransaction(traceID otelTrace.TraceID) (*TransactionEntry, bool) { - return ssm.transactions.Load(traceID) -} - -// Set stores the span and transaction information on the map. It handles both root and child spans automatically. -// -// If there is a cache miss on the given traceID, a transaction entry is created. Subsequent calls for the same traceID -// just increment the active span count and store the span in the entry. -func (ssm *SentrySpanMap) Set(spanID otelTrace.SpanID, span *sentry.Span, traceID otelTrace.TraceID) { - t := &TransactionEntry{root: span} - t.activeCount.Store(1) - t.spans.Store(spanID, span) - t.knownSpanIDs.Store(spanID, struct{}{}) - - if existing, loaded := ssm.transactions.LoadOrStore(traceID, t); loaded { - existing.activeCount.Add(1) - existing.spans.Store(spanID, span) - existing.knownSpanIDs.Store(spanID, struct{}{}) - } -} - -// MarkFinished removes a span from the active set and decrements the transaction's active count. -// When the count reaches zero, the transaction entry is removed. -// The span ID is kept in knownSpanIDs so that HasSpan continues to work for child span creation. -func (ssm *SentrySpanMap) MarkFinished(spanID otelTrace.SpanID, traceID otelTrace.TraceID) { - entry, ok := ssm.transactions.Load(traceID) - if !ok { - return - } - - entry.spans.Delete(spanID) - - if entry.activeCount.Add(-1) <= 0 { - // CompareAndSwap(CAS) is used to prevent a race between Set and MarkFinished. - // The race has two windows: - // 1. MarkFinished decremented activeCount to 0 but hasn't CAS'd yet -> Set Adds(1), and CAS fails keeping the - // transaction, since we just added a new span. - // 2. MarkFinished already CAS'd -> Set will store on the transaction marked for deletion (better than - // creating a new orphaned span). - if entry.activeCount.CompareAndSwap(0, -1) { - ssm.transactions.CompareAndDelete(traceID, entry) - } - } -} - -// Clear removes all spans stored on the map. -func (ssm *SentrySpanMap) Clear() { - ssm.transactions.Clear() -} - -// Len returns the number of spans on the map. -// -// This should only be used in tests, since computing the map length is fairly expensive. -func (ssm *SentrySpanMap) Len() int { - count := 0 - ssm.transactions.Range(func(_ otelTrace.TraceID, entry *TransactionEntry) bool { - count += int(entry.activeCount.Load()) - return true - }) - return count -} - -var sentrySpanMap = SentrySpanMap{} diff --git a/otel/span_map_test.go b/otel/span_map_test.go deleted file mode 100644 index 753bf74ea..000000000 --- a/otel/span_map_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package sentryotel - -import ( - "context" - "fmt" - "sync" - "testing" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" -) - -type noopExporter struct{} - -func (e *noopExporter) ExportSpans(_ context.Context, _ []sdktrace.ReadOnlySpan) error { - return nil -} -func (e *noopExporter) Shutdown(_ context.Context) error { return nil } - -func setupTracerProvider(useSentry bool) (*sdktrace.TracerProvider, func()) { - res, _ := resource.New(context.Background()) - - tp := sdktrace.NewTracerProvider( - sdktrace.WithResource(res), - sdktrace.WithSampler(sdktrace.AlwaysSample()), - ) - - if useSentry { - sentryProcessor := NewSentrySpanProcessor() - tp.RegisterSpanProcessor(sentryProcessor) - } - - tp.RegisterSpanProcessor(sdktrace.NewBatchSpanProcessor(&noopExporter{})) - - otel.SetTracerProvider(tp) - return tp, func() { _ = tp.Shutdown(context.Background()) } -} - -func simulateWorkflowBatch(ctx context.Context, tracer trace.Tracer, numInstances, dbSpansPerInstance int) { - ctx, rootSpan := tracer.Start(ctx, "job.workflow_runner") - - var wg sync.WaitGroup - for i := 0; i < numInstances; i++ { - wg.Add(1) - go func(idx int) { - defer wg.Done() - wfCtx, wfSpan := tracer.Start(ctx, fmt.Sprintf("workflow.debt_reminder_%d", idx)) - - for j := 0; j < dbSpansPerInstance; j++ { - _, dbSpan := tracer.Start(wfCtx, fmt.Sprintf("postgres.query_%d", j)) - dbSpan.End() - } - - wfSpan.End() - }(i) - } - wg.Wait() - rootSpan.End() -} - -// BenchmarkSpanMapContention measures how much the Sentry span processor slows down unrelated handler -// spans when a large workflow transaction is being created and cleaned up concurrently. -func BenchmarkSpanMapContention(b *testing.B) { - _, cleanup := setupTracerProvider(true) - defer cleanup() - tracer := otel.Tracer("bench") - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - iter := 0 - for pb.Next() { - if iter%500 == 0 { - // Every 500th iteration, simulate a large workflow batch. - // This creates 100×33 = 3300 child spans under a single root, - // then ends them all — exercising the cleanup path under contention. - simulateWorkflowBatch(context.Background(), tracer, 100, 33) - } else { - // This is the hot path that gets blocked from span cleanup. - ctx, span := tracer.Start(context.Background(), "GET /api/ping") - _, child := tracer.Start(ctx, "db.query") - child.End() - span.End() - } - iter++ - } - }) -} diff --git a/otel/span_processor.go b/otel/span_processor.go deleted file mode 100644 index 5b3fabbc7..000000000 --- a/otel/span_processor.go +++ /dev/null @@ -1,160 +0,0 @@ -package sentryotel - -import ( - "context" - "time" - - "github.com/getsentry/sentry-go" - "github.com/getsentry/sentry-go/otel/internal/common" - "github.com/getsentry/sentry-go/otel/internal/utils" - "go.opentelemetry.io/otel/attribute" - otelSdkTrace "go.opentelemetry.io/otel/sdk/trace" -) - -type sentrySpanProcessor struct{} - -// Singleton instance of the Sentry span processor. -// At the moment we do not support multiple instances. -var sentrySpanProcessorInstance *sentrySpanProcessor - -// NewSentrySpanProcessor creates an OpenTelemetry span processor that mirrors -// OTel spans into Sentry's native span model. -// -// Deprecated: Prefer OTLP export via sentryotlp.NewTraceExporter. -// For collector-based setups, use the standard OTel exporter and register -// sentryotel.NewOtelIntegration for linking. -// Will be removed in 0.47.0. -func NewSentrySpanProcessor() otelSdkTrace.SpanProcessor { - if sentrySpanProcessorInstance != nil { - return sentrySpanProcessorInstance - } - sentry.AddGlobalEventProcessor(common.NewEventProcessor()) - sentrySpanProcessorInstance = &sentrySpanProcessor{} - return sentrySpanProcessorInstance -} - -// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#onstart -func (ssp *sentrySpanProcessor) OnStart(parent context.Context, s otelSdkTrace.ReadWriteSpan) { - otelSpanContext := s.SpanContext() - otelSpanID := otelSpanContext.SpanID() - otelTraceID := otelSpanContext.TraceID() - otelParentSpanID := s.Parent().SpanID() - - txn, ok := sentrySpanMap.GetTransaction(otelTraceID) - if ok && txn.HasSpan(otelParentSpanID) { - span := txn.root.StartChild(s.Name()) - span.SpanID = sentry.SpanID(otelSpanID) - span.ParentSpanID = sentry.SpanID(otelParentSpanID) - span.StartTime = s.StartTime() - sentrySpanMap.Set(otelSpanID, span, otelTraceID) - return - } - traceParentContext := getTraceParentContext(parent) - transaction := sentry.StartTransaction( - parent, - s.Name(), - sentry.WithSpanSampled(traceParentContext.Sampled), - ) - transaction.SpanID = sentry.SpanID(otelSpanID) - transaction.TraceID = sentry.TraceID(otelTraceID) - transaction.ParentSpanID = sentry.SpanID(otelParentSpanID) - transaction.StartTime = s.StartTime() - if dsc, valid := parent.Value(dynamicSamplingContextKey{}).(sentry.DynamicSamplingContext); valid { - transaction.SetDynamicSamplingContext(dsc) - } - sentrySpanMap.Set(otelSpanID, transaction, otelTraceID) -} - -// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#onendspan -func (ssp *sentrySpanProcessor) OnEnd(s otelSdkTrace.ReadOnlySpan) { - otelSpanID := s.SpanContext().SpanID() - otelTraceID := s.SpanContext().TraceID() - sentrySpan, ok := sentrySpanMap.Get(otelTraceID, otelSpanID) - if !ok || sentrySpan == nil { - return - } - - if utils.IsSentryRequestSpan(sentrySpan.Context(), s) { - sentrySpanMap.MarkFinished(otelSpanID, otelTraceID) - return - } - - if sentrySpan.IsTransaction() { - updateTransactionWithOtelData(sentrySpan, s) - } else { - updateSpanWithOtelData(sentrySpan, s) - } - - sentrySpan.EndTime = s.EndTime() - sentrySpan.Finish() - - sentrySpanMap.MarkFinished(otelSpanID, otelTraceID) -} - -// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#shutdown-1 -func (ssp *sentrySpanProcessor) Shutdown(ctx context.Context) error { - sentrySpanMap.Clear() - // Note: according to the spec, "Shutdown MUST include the effects of ForceFlush". - return ssp.ForceFlush(ctx) -} - -// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#forceflush-1 -func (ssp *sentrySpanProcessor) ForceFlush(ctx context.Context) error { - return flushSpanProcessor(ctx) -} - -func flushSpanProcessor(ctx context.Context) error { - hub := sentry.GetHubFromContext(ctx) - if hub == nil { - hub = sentry.CurrentHub() - } - - // TODO(michi) should we make this configurable? - defer hub.Flush(2 * time.Second) - return nil -} - -func getTraceParentContext(ctx context.Context) sentry.TraceParentContext { - traceParentContext, ok := ctx.Value(sentryTraceParentContextKey{}).(sentry.TraceParentContext) - if !ok { - traceParentContext.Sampled = sentry.SampledUndefined - } - return traceParentContext -} - -func updateTransactionWithOtelData(transaction *sentry.Span, s otelSdkTrace.ReadOnlySpan) { - // TODO(michi) This is crazy inefficient - attributes := map[attribute.Key]interface{}{} - resource := map[attribute.Key]interface{}{} - - for _, kv := range s.Attributes() { - attributes[kv.Key] = kv.Value.AsInterface() - } - for _, kv := range s.Resource().Attributes() { - resource[kv.Key] = kv.Value.AsInterface() - } - - transaction.SetContext("otel", map[string]interface{}{ - "attributes": attributes, - "resource": resource, - }) - - spanAttributes := utils.ParseSpanAttributes(s) - - transaction.Status = utils.MapOtelStatus(s) - transaction.Name = spanAttributes.Description - transaction.Op = spanAttributes.Op - transaction.Source = spanAttributes.Source -} - -func updateSpanWithOtelData(span *sentry.Span, s otelSdkTrace.ReadOnlySpan) { - spanAttributes := utils.ParseSpanAttributes(s) - - span.Status = utils.MapOtelStatus(s) - span.Op = spanAttributes.Op - span.Description = spanAttributes.Description - span.SetData("otel.kind", s.SpanKind().String()) - for _, kv := range s.Attributes() { - span.SetData(string(kv.Key), kv.Value.AsInterface()) - } -} diff --git a/otel/span_processor_test.go b/otel/span_processor_test.go deleted file mode 100644 index cfa944b01..000000000 --- a/otel/span_processor_test.go +++ /dev/null @@ -1,519 +0,0 @@ -package sentryotel - -import ( - "context" - "errors" - "log" - "testing" - - "github.com/getsentry/sentry-go" - "github.com/getsentry/sentry-go/internal/testutils" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/sdk/resource" - otelSdkTrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.12.0" - "go.opentelemetry.io/otel/trace" -) - -func setupSpanProcessorTest() (otelSdkTrace.SpanProcessor, trace.Tracer) { - // Make sure that the global span map is empty - sentrySpanMap.Clear() - - spanProcessor := NewSentrySpanProcessor() - tp := otelSdkTrace.NewTracerProvider( - otelSdkTrace.WithSampler(otelSdkTrace.AlwaysSample()), - otelSdkTrace.WithResource( - resource.NewWithAttributes( - "", - semconv.ServiceNameKey.String("test-otel"), - ), - ), - ) - tp.RegisterSpanProcessor(spanProcessor) - tracer := tp.Tracer("test-tracer") - return spanProcessor, tracer -} - -func emptyContextWithSentry() context.Context { - client, _ := sentry.NewClient(sentry.ClientOptions{ - Dsn: "https://abc@example.com/123", - Environment: "testing", - Release: "1.2.3", - EnableTracing: true, - TracesSampleRate: 1.0, - Transport: &sentry.MockTransport{}, - }) - hub := sentry.NewHub(client, sentry.NewScope()) - return sentry.SetHubOnContext(context.Background(), hub) -} - -func getSentryTransportFromContext(ctx context.Context) *sentry.MockTransport { - hub := sentry.GetHubFromContext(ctx) - transport, ok := hub.Client().Transport.(*sentry.MockTransport) - if !ok { - log.Fatal( - "Cannot get mock transport from context", - ) - } - return transport -} - -func TestNewSentrySpanProcessor(t *testing.T) { - spanProcessor := NewSentrySpanProcessor() - - if _, valid := spanProcessor.(*sentrySpanProcessor); !valid { - t.Errorf( - "Invalid type returned by the span processor constructor: %#v\n", - spanProcessor, - ) - } -} - -func TestSpanProcessorShutdown(t *testing.T) { - spanProcessor, tracer := setupSpanProcessorTest() - ctx := context.Background() - tracer.Start(emptyContextWithSentry(), "spanName") - - assertEqual(t, sentrySpanMap.Len(), 1) - - _ = spanProcessor.Shutdown(ctx) - - // The span map should be empty - assertEqual(t, sentrySpanMap.Len(), 0) -} - -func TestSpanProcessorForceFlush(t *testing.T) { - // This test is pretty naive at the moment and just checks that - // ForceFlush() doesn't crash or return an error. Ideally we test it - // with a Sentry transport that can be checked to see if events were - // actually flushed. - spanProcessor, tracer := setupSpanProcessorTest() - ctx, span := tracer.Start(emptyContextWithSentry(), "spanName") - span.End() - - err := spanProcessor.ForceFlush(ctx) - if err != nil { - t.Errorf("Error from ForceFlush(): %v", err) - } -} - -func TestOnStartRootSpan(t *testing.T) { - _, tracer := setupSpanProcessorTest() - _, otelSpan := tracer.Start(emptyContextWithSentry(), "spanName") - - if sentrySpanMap.Len() != 1 { - t.Errorf("Span map size is %d, expected: 1", sentrySpanMap.Len()) - } - sentrySpan, ok := sentrySpanMap.Get(otelSpan.SpanContext().TraceID(), otelSpan.SpanContext().SpanID()) - if !ok { - t.Errorf("Sentry span not found in the map") - } - - otelTraceID := otelSpan.SpanContext().TraceID() - otelSpanID := otelSpan.SpanContext().SpanID() - // TODO(anton): use a simple "assert", not "assertEqual" - assertTrue(t, otelSpan.SpanContext().IsValid()) - assertEqual(t, sentrySpan.SpanID.String(), otelSpanID.String()) - assertEqual(t, sentrySpan.TraceID.String(), otelTraceID.String()) - assertEqual(t, sentrySpan.ParentSpanID, sentry.SpanID{}) - assertTrue(t, sentrySpan.IsTransaction()) - assertEqual(t, sentrySpan.Sampled, sentry.SampledTrue) - assertEqual(t, sentrySpan.Name, "spanName") - - testutils.AssertBaggageStringsEqual( - t, - sentrySpan.ToBaggage(), - "sentry-transaction=spanName,sentry-environment=testing,sentry-public_key=abc,sentry-release=1.2.3,sentry-sample_rate=1,sentry-sampled=true,sentry-trace_id="+otelTraceID.String(), - ) -} - -func TestOnStartWithTraceParentContext(t *testing.T) { - _, tracer := setupSpanProcessorTest() - - // Sentry context - ctx := context.WithValue( - emptyContextWithSentry(), - sentryTraceParentContextKey{}, - sentry.TraceParentContext{ - TraceID: TraceIDFromHex("d4cda95b652f4a1592b449d5929fda1b"), - ParentSpanID: SpanIDFromHex("6e0c63257de34c92"), - Sampled: sentry.SampledFalse, - }, - ) - dsc := sentry.DynamicSamplingContext{ - Frozen: true, - Entries: map[string]string{"environment": "dev"}, - } - ctx = context.WithValue(ctx, dynamicSamplingContextKey{}, dsc) - // Otel span context - ctx = trace.ContextWithSpanContext( - ctx, - trace.NewSpanContext(trace.SpanContextConfig{ - TraceID: otelTraceIDFromHex("bc6d53f15eb88f4320054569b8c553d4"), - SpanID: otelSpanIDFromHex("b72fa28504b07285"), - TraceFlags: trace.FlagsSampled, - }), - ) - _, otelSpan := tracer.Start(ctx, "spanName") - - if sentrySpanMap.Len() != 1 { - t.Errorf("Span map size is %d, expected: 1", sentrySpanMap.Len()) - } - sentrySpan, ok := sentrySpanMap.Get(otelSpan.SpanContext().TraceID(), otelSpan.SpanContext().SpanID()) - if !ok { - t.Errorf("Sentry span not found in the map") - } - - assertTrue(t, otelSpan.SpanContext().IsValid()) - assertEqual(t, sentrySpan.SpanID.String(), otelSpan.SpanContext().SpanID().String()) - // We're currently taking trace id and parent span id from the otel span context, - // (not sentry-trace header), mostly to be aligned with other SDKs. - assertEqual(t, sentrySpan.TraceID.String(), "bc6d53f15eb88f4320054569b8c553d4") - assertEqual(t, sentrySpan.ParentSpanID, SpanIDFromHex("b72fa28504b07285")) - assertTrue(t, sentrySpan.IsTransaction()) - assertEqual(t, sentrySpan.Sampled, sentry.SampledFalse) - assertEqual(t, sentrySpan.Name, "spanName") - assertEqual(t, sentrySpan.Status, sentry.SpanStatusUndefined) - assertEqual(t, sentrySpan.Source, sentry.SourceCustom) - - testutils.AssertBaggageStringsEqual( - t, - sentrySpan.ToBaggage(), - "sentry-environment=dev", - ) -} - -func TestOnStartWithExistingParentSpan(t *testing.T) { - _, tracer := setupSpanProcessorTest() - - // Otel span context - ctx := trace.ContextWithSpanContext( - emptyContextWithSentry(), - trace.NewSpanContext(trace.SpanContextConfig{ - TraceID: otelTraceIDFromHex("bc6d53f15eb88f4320054569b8c553d4"), - SpanID: otelSpanIDFromHex("b72fa28504b07285"), - TraceFlags: trace.FlagsSampled, - }), - ) - ctx, otelRootSpan := tracer.Start(ctx, "rootSpan") - _, otelChildSpan := tracer.Start(ctx, "childSpan") - - if sentrySpanMap.Len() != 2 { - t.Errorf("Span map size is %d, expected: 2", sentrySpanMap.Len()) - } - - sentryTransaction, ok1 := sentrySpanMap.Get(otelRootSpan.SpanContext().TraceID(), otelRootSpan.SpanContext().SpanID()) - if !ok1 { - t.Errorf("Sentry span not found in the map") - } - sentryChildSpan, ok2 := sentrySpanMap.Get(otelChildSpan.SpanContext().TraceID(), otelChildSpan.SpanContext().SpanID()) - if !ok2 { - t.Errorf("Sentry span not found in the map") - } - - assertTrue(t, otelChildSpan.SpanContext().IsValid()) - assertTrue(t, otelRootSpan.SpanContext().IsValid()) - assertEqual(t, sentryChildSpan.ParentSpanID, sentryTransaction.SpanID) - assertEqual(t, sentryChildSpan.SpanID.String(), otelChildSpan.SpanContext().SpanID().String()) - assertEqual(t, sentryChildSpan.TraceID.String(), "bc6d53f15eb88f4320054569b8c553d4") - assertFalse(t, sentryChildSpan.IsTransaction()) - assertEqual(t, sentryChildSpan.GetTransaction().Name, "rootSpan") - assertEqual(t, sentryChildSpan.Op, "childSpan") -} - -func TestOnEndWithTransaction(t *testing.T) { - _, tracer := setupSpanProcessorTest() - ctx, otelSpan := tracer.Start( - emptyContextWithSentry(), - "transactionName", - trace.WithAttributes( - attribute.String("key1", "value1"), - ), - ) - sentryTransaction, _ := sentrySpanMap.Get(otelSpan.SpanContext().TraceID(), otelSpan.SpanContext().SpanID()) - assertTrue(t, sentryTransaction.EndTime.IsZero()) - otelSpan.End() - - // The span map should be empty - assertEqual(t, sentrySpanMap.Len(), 0) - // EndTime should be populated - assertFalse(t, sentryTransaction.EndTime.IsZero()) - - assertEqual(t, sentryTransaction.Status, sentry.SpanStatusOK) - assertEqual(t, sentryTransaction.Source, sentry.TransactionSource("custom")) - assertEqual(t, sentryTransaction.Op, "") - assertEqual(t, sentryTransaction.Description, "") - - // One events should be captured by transport - sentryTransport := getSentryTransportFromContext(ctx) - events := sentryTransport.Events() - assertEqual(t, len(events), 1) - event := events[0] - assertEqual(t, event.StartTime, sentryTransaction.StartTime) - - otelContextGot := event.Contexts["otel"] - assertEqual( - t, - otelContextGot, - map[string]interface{}{ - "attributes": map[attribute.Key]interface{}{ - "key1": "value1", - }, - "resource": map[attribute.Key]interface{}{ - "service.name": "test-otel", - }, - }, - ) -} - -func TestOnEndWithChildSpan(t *testing.T) { - _, tracer := setupSpanProcessorTest() - ctx, otelRootSpan := tracer.Start(emptyContextWithSentry(), "rootSpan") - _, otelChildSpan := tracer.Start( - ctx, - "childSpan", - trace.WithAttributes( - attribute.String("childKey1", "value1"), - ), - ) - sentryTransaction, _ := sentrySpanMap.Get(otelRootSpan.SpanContext().TraceID(), otelRootSpan.SpanContext().SpanID()) - sentryChildSpan, _ := sentrySpanMap.Get(otelChildSpan.SpanContext().TraceID(), otelChildSpan.SpanContext().SpanID()) - otelChildSpan.End() - otelRootSpan.End() - - // The span map should be empty - assertEqual(t, sentrySpanMap.Len(), 0) - // EndTime should be populated - assertFalse(t, sentryTransaction.EndTime.IsZero()) - assertFalse(t, sentryChildSpan.EndTime.IsZero()) - - assertEqual(t, sentryChildSpan.Status, sentry.SpanStatusOK) - assertEqual(t, sentryChildSpan.Source, sentry.TransactionSource("")) - assertEqual(t, sentryChildSpan.Op, "") - assertEqual(t, sentryChildSpan.Description, "childSpan") - assertEqual( - t, - sentryChildSpan.Data, - map[string]interface{}{ - "childKey1": "value1", - "otel.kind": "internal", - }, - ) - // One events should be captured by transport - sentryTransport := getSentryTransportFromContext(ctx) - events := sentryTransport.Events() - assertEqual(t, len(events), 1) - assertEqual(t, events[0].StartTime, sentryTransaction.StartTime) -} - -func TestOnEndDoesNotFinishSentryRequests(t *testing.T) { - _, tracer := setupSpanProcessorTest() - ctx, otelSpan := tracer.Start( - emptyContextWithSentry(), - "POST to Sentry", - // Hostname is same as in Sentry DSN - trace.WithAttributes(attribute.String("http.url", "https://example.com/api/123/envelope/")), - ) - sentrySpan, _ := sentrySpanMap.Get(otelSpan.SpanContext().TraceID(), otelSpan.SpanContext().SpanID()) - otelSpan.End() - - // The span map should be empty - assertEqual(t, sentrySpanMap.Len(), 0) - // EndTime should NOT be populated - assertTrue(t, sentrySpan.EndTime.IsZero()) - // No events should be captured by transport - sentryTransport := getSentryTransportFromContext(ctx) - events := sentryTransport.Events() - assertEqual(t, len(events), 0) -} - -func TestParseSpanAttributesHttpClient(t *testing.T) { - _, tracer := setupSpanProcessorTest() - ctx, otelRootSpan := tracer.Start( - emptyContextWithSentry(), - "rootSpan", - trace.WithSpanKind(trace.SpanKindClient), - trace.WithAttributes(attribute.String("http.method", "GET")), - trace.WithAttributes(attribute.String("http.url", "http://localhost:1234/api/checkout1?q1=q2#fragment")), - ) - _, otelChildSpan := tracer.Start(ctx, - "childSpan", - trace.WithSpanKind(trace.SpanKindClient), - trace.WithAttributes(attribute.String("http.method", "POST")), - trace.WithAttributes(attribute.String("http.url", "http://localhost:2345/api/checkout2?q1=q2#fragment")), - ) - - sentryTransaction, _ := sentrySpanMap.Get(otelRootSpan.SpanContext().TraceID(), otelRootSpan.SpanContext().SpanID()) - sentrySpan, _ := sentrySpanMap.Get(otelChildSpan.SpanContext().TraceID(), otelChildSpan.SpanContext().SpanID()) - - otelChildSpan.End() - otelRootSpan.End() - - // Transaction - assertEqual(t, sentryTransaction.Name, "GET http://localhost:1234/api/checkout1") - assertEqual(t, sentryTransaction.Description, "") - assertEqual(t, sentryTransaction.Op, "http.client") - assertEqual(t, sentryTransaction.Source, sentry.TransactionSource("url")) - - // Span - assertEqual(t, sentrySpan.Name, "") - assertEqual(t, sentrySpan.Description, "POST http://localhost:2345/api/checkout2") - assertEqual(t, sentrySpan.Op, "http.client") - assertEqual(t, sentrySpan.Source, sentry.TransactionSource("")) -} - -func TestParseSpanAttributesHttpServer(t *testing.T) { - _, tracer := setupSpanProcessorTest() - ctx, otelRootSpan := tracer.Start( - emptyContextWithSentry(), - "rootSpan", - trace.WithSpanKind(trace.SpanKindServer), - trace.WithAttributes(attribute.String("http.method", "GET")), - trace.WithAttributes(attribute.String("http.target", "/api/checkout1?k=v")), - // We ignore "http.url" if "http.target" is present - trace.WithAttributes(attribute.String("http.url", "http://localhost:1234/api/checkout?q1=q2#fragment")), - ) - _, otelChildSpan := tracer.Start( - ctx, - "span name", - trace.WithSpanKind(trace.SpanKindServer), - trace.WithAttributes(attribute.String("http.method", "POST")), - trace.WithAttributes(attribute.String("http.target", "/api/checkout2?k=v")), - // We ignore "http.url" if "http.target" is present - trace.WithAttributes(attribute.String("http.url", "http://localhost:2345/api/checkout?q1=q2#fragment")), - ) - sentryTransaction, _ := sentrySpanMap.Get(otelRootSpan.SpanContext().TraceID(), otelRootSpan.SpanContext().SpanID()) - sentrySpan, _ := sentrySpanMap.Get(otelChildSpan.SpanContext().TraceID(), otelChildSpan.SpanContext().SpanID()) - - otelChildSpan.End() - otelRootSpan.End() - - // Transaction - assertEqual(t, sentryTransaction.Name, "GET /api/checkout1") - assertEqual(t, sentryTransaction.Description, "") - assertEqual(t, sentryTransaction.Op, "http.server") - assertEqual(t, sentryTransaction.Source, sentry.TransactionSource("url")) - - // Span - assertEqual(t, sentrySpan.Name, "") - assertEqual(t, sentrySpan.Description, "POST /api/checkout2") - assertEqual(t, sentrySpan.Op, "http.server") - assertEqual(t, sentrySpan.Source, sentry.TransactionSource("")) -} - -func TestSpanBecomesChildOfFinishedSpan(t *testing.T) { - _, tracer := setupSpanProcessorTest() - ctx, otelRootSpan := tracer.Start( - emptyContextWithSentry(), - "rootSpan", - ) - sentryTransaction, _ := sentrySpanMap.Get(otelRootSpan.SpanContext().TraceID(), otelRootSpan.SpanContext().SpanID()) - - ctx, childSpan1 := tracer.Start( - ctx, - "span name 1", - ) - sentrySpan1, _ := sentrySpanMap.Get(childSpan1.SpanContext().TraceID(), childSpan1.SpanContext().SpanID()) - childSpan1.End() - - _, childSpan2 := tracer.Start( - ctx, - "span name 2", - ) - sentrySpan2, _ := sentrySpanMap.Get(childSpan2.SpanContext().TraceID(), childSpan2.SpanContext().SpanID()) - childSpan2.End() - - otelRootSpan.End() - - assertEqual(t, sentryTransaction.IsTransaction(), true) - assertEqual(t, sentrySpan1.IsTransaction(), false) - assertEqual(t, sentrySpan1.ParentSpanID, sentryTransaction.SpanID) - assertEqual(t, sentrySpan2.IsTransaction(), false) - assertEqual(t, sentrySpan2.ParentSpanID, sentrySpan1.SpanID) -} - -func TestSpanWithFinishedParentShouldBeDeleted(t *testing.T) { - _, tracer := setupSpanProcessorTest() - - ctx, parent := tracer.Start(context.Background(), "parent") - traceID := parent.SpanContext().TraceID() - parentSpanID := parent.SpanContext().SpanID() - _, child := tracer.Start(ctx, "child") - childSpanID := child.SpanContext().SpanID() - - _, parentExists := sentrySpanMap.Get(traceID, parentSpanID) - _, childExists := sentrySpanMap.Get(traceID, childSpanID) - assertEqual(t, parentExists, true) - assertEqual(t, childExists, true) - - parent.End() - _, parentExists = sentrySpanMap.Get(traceID, parentSpanID) - assertEqual(t, parentExists, false) - _, childExists = sentrySpanMap.Get(traceID, childSpanID) - assertEqual(t, childExists, true) - - child.End() - _, childExists = sentrySpanMap.Get(traceID, childSpanID) - assertEqual(t, childExists, false) - assertEqual(t, sentrySpanMap.Len(), 0) -} - -func TestTransactionsStayIndependent(t *testing.T) { - _, tracer := setupSpanProcessorTest() - sharedTraceID := "bc6d53f15eb88f4320054569b8c553d4" - - ctx1 := trace.ContextWithSpanContext( - emptyContextWithSentry(), - trace.NewSpanContext(trace.SpanContextConfig{ - TraceID: otelTraceIDFromHex(sharedTraceID), - SpanID: otelSpanIDFromHex("1111111111111111"), - TraceFlags: trace.FlagsSampled, - }), - ) - ctx2 := trace.ContextWithSpanContext( - emptyContextWithSentry(), - trace.NewSpanContext(trace.SpanContextConfig{ - TraceID: otelTraceIDFromHex(sharedTraceID), - SpanID: otelSpanIDFromHex("2222222222222222"), - TraceFlags: trace.FlagsSampled, - }), - ) - - _, otelRoot1 := tracer.Start(ctx1, "request1") - sentryTxn1, _ := sentrySpanMap.Get(otelRoot1.SpanContext().TraceID(), otelRoot1.SpanContext().SpanID()) - - _, otelRoot2 := tracer.Start(ctx2, "request2") - sentryTxn2, _ := sentrySpanMap.Get(otelRoot2.SpanContext().TraceID(), otelRoot2.SpanContext().SpanID()) - - assertEqual(t, sentryTxn1.IsTransaction(), true) - assertEqual(t, sentryTxn2.IsTransaction(), true) - - assertTrue(t, sentryTxn1.SpanID != sentryTxn2.SpanID) - - otelRoot1.End() - otelRoot2.End() -} - -func TestLinkTraceContextToErrorEventWithSpanProcessor(t *testing.T) { - _, tracer := setupSpanProcessorTest() - - ctx, otelSpan := tracer.Start(emptyContextWithSentry(), "spanName") - hub := sentry.GetHubFromContext(ctx) - client, scope := hub.Client(), hub.Scope() - - client.CaptureException( - errors.New("span processor linked error"), - &sentry.EventHint{Context: ctx}, - scope, - ) - - transport := client.Transport.(*sentry.MockTransport) - events := transport.Events() - assertEqual(t, len(events), 1) - - errEvent := events[0] - assertEqual(t, errEvent.Contexts["trace"], map[string]any{ - "trace_id": otelSpan.SpanContext().TraceID().String(), - "span_id": otelSpan.SpanContext().SpanID().String(), - }) -}