-
Notifications
You must be signed in to change notification settings - Fork 213
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add initial OpenTelemetry support (#537)
This PR adds two main integration components (along with all the main supporting functions to enrich Sentry spans with OTel data): * Span Processor * Propagator Co-authored-by: Anton Ovchinnikov <[email protected]>
- Loading branch information
Showing
15 changed files
with
1,634 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
//go:build go1.18 | ||
|
||
package sentryotel | ||
|
||
// Context keys to be used with context.WithValue(...) and ctx.Value(...) | ||
type dynamicSamplingContextKey struct{} | ||
type sentryTraceHeaderContextKey struct{} | ||
type sentryTraceParentContextKey struct{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
//go:build go1.18 | ||
|
||
package sentryotel | ||
|
||
import ( | ||
"github.com/getsentry/sentry-go" | ||
"go.opentelemetry.io/otel/trace" | ||
) | ||
|
||
// linkTraceContextToErrorEvent is a Sentry event processor that attaches trace information | ||
// to the error event. | ||
// | ||
// Caveat: hint.Context should contain a valid context populated by OpenTelemetry's span context. | ||
func linkTraceContextToErrorEvent(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { | ||
if hint == nil || hint.Context == nil { | ||
return event | ||
} | ||
otelSpanContext := trace.SpanContextFromContext(hint.Context) | ||
var sentrySpan *sentry.Span | ||
if otelSpanContext.IsValid() { | ||
sentrySpan, _ = sentrySpanMap.Get(otelSpanContext.SpanID()) | ||
} | ||
if sentrySpan == nil { | ||
return event | ||
} | ||
|
||
traceContext := event.Contexts["trace"] | ||
if len(traceContext) > 0 { | ||
// trace context is already set, not touching it | ||
return event | ||
} | ||
event.Contexts["trace"] = map[string]interface{}{ | ||
"trace_id": sentrySpan.TraceID.String(), | ||
"span_id": sentrySpan.SpanID.String(), | ||
"parent_span_id": sentrySpan.ParentSpanID.String(), | ||
} | ||
return event | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
//go:build go1.18 | ||
|
||
package sentryotel | ||
|
||
import ( | ||
"errors" | ||
"testing" | ||
|
||
"github.com/getsentry/sentry-go" | ||
) | ||
|
||
func TestLinkTraceContextToErrorEventWithEmptyTraceContext(t *testing.T) { | ||
_, _, tracer := setupSpanProcessorTest() | ||
ctx, otelSpan := tracer.Start(emptyContextWithSentry(), "spanName") | ||
sentrySpan, _ := sentrySpanMap.Get(otelSpan.SpanContext().SpanID()) | ||
|
||
hub := sentry.GetHubFromContext(ctx) | ||
client := hub.Client() | ||
client.CaptureException( | ||
errors.New("new sentry exception"), | ||
&sentry.EventHint{Context: ctx}, | ||
nil, | ||
) | ||
|
||
transport := client.Transport.(*TransportMock) | ||
events := transport.Events() | ||
assertEqual(t, len(events), 1) | ||
err := events[0] | ||
exception := err.Exception[0] | ||
assertEqual(t, exception.Type, "*errors.errorString") | ||
assertEqual(t, exception.Value, "new sentry exception") | ||
assertEqual(t, err.Type, "") | ||
assertEqual(t, | ||
err.Contexts["trace"], | ||
map[string]interface{}{ | ||
"trace_id": sentrySpan.TraceID.String(), | ||
"span_id": sentrySpan.SpanID.String(), | ||
"parent_span_id": sentrySpan.ParentSpanID.String(), | ||
}, | ||
) | ||
} | ||
|
||
func TestLinkTraceContextToErrorEventDoesNotTouchExistingTraceContext(t *testing.T) { | ||
_, _, tracer := setupSpanProcessorTest() | ||
ctx, _ := tracer.Start(emptyContextWithSentry(), "spanName") | ||
|
||
hub := sentry.GetHubFromContext(ctx) | ||
hub.Scope().SetContext("trace", map[string]interface{}{"trace_id": "123"}) | ||
client := hub.Client() | ||
client.CaptureException( | ||
errors.New("new sentry exception with existing trace context"), | ||
&sentry.EventHint{Context: ctx}, | ||
hub.Scope(), | ||
) | ||
|
||
transport := client.Transport.(*TransportMock) | ||
events := transport.Events() | ||
assertEqual(t, len(events), 1) | ||
err := events[0] | ||
exception := err.Exception[0] | ||
assertEqual(t, exception.Type, "*errors.errorString") | ||
assertEqual(t, exception.Value, "new sentry exception with existing trace context") | ||
assertEqual(t, err.Type, "") | ||
assertEqual(t, | ||
err.Contexts["trace"], | ||
map[string]interface{}{"trace_id": "123"}, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
module github.com/getsentry/sentry-go/otel | ||
|
||
go 1.19 | ||
|
||
require ( | ||
github.com/getsentry/sentry-go v0.17.0 | ||
github.com/google/go-cmp v0.5.9 | ||
go.opentelemetry.io/otel v1.11.2 | ||
go.opentelemetry.io/otel/sdk v1.11.2 | ||
go.opentelemetry.io/otel/trace v1.11.2 | ||
) | ||
|
||
replace github.com/getsentry/sentry-go => ../ | ||
|
||
require ( | ||
github.com/go-logr/logr v1.2.3 // indirect | ||
github.com/go-logr/stdr v1.2.2 // indirect | ||
golang.org/x/sys v0.4.0 // indirect | ||
golang.org/x/text v0.6.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= | ||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | ||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= | ||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | ||
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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= | ||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | ||
go.opentelemetry.io/otel v1.11.2 h1:YBZcQlsVekzFsFbjygXMOXSs6pialIZxcjfO/mBDmR0= | ||
go.opentelemetry.io/otel v1.11.2/go.mod h1:7p4EUV+AqgdlNV9gL97IgUZiVR3yrFXYo53f9BM3tRI= | ||
go.opentelemetry.io/otel/sdk v1.11.2 h1:GF4JoaEx7iihdMFu30sOyRx52HDHOkl9xQ8SMqNXUiU= | ||
go.opentelemetry.io/otel/sdk v1.11.2/go.mod h1:wZ1WxImwpq+lVRo4vsmSOxdd+xwoUJ6rqyLc3SyX9aU= | ||
go.opentelemetry.io/otel/trace v1.11.2 h1:Xf7hWSF2Glv0DE3MH7fBHvtpSBsjcBUe5MYAmZM/+y0= | ||
go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA= | ||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= | ||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= | ||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
//go:build go1.18 | ||
|
||
package sentryotel | ||
|
||
// TODO(anton): This is a copy of helpers_test.go in the repo root. | ||
// We should figure out how to share testing helpers. | ||
|
||
import ( | ||
"encoding/hex" | ||
"fmt" | ||
"log" | ||
"reflect" | ||
"sort" | ||
"sync" | ||
"testing" | ||
"time" | ||
|
||
"github.com/getsentry/sentry-go" | ||
"github.com/google/go-cmp/cmp" | ||
"go.opentelemetry.io/otel/baggage" | ||
"go.opentelemetry.io/otel/propagation" | ||
"go.opentelemetry.io/otel/trace" | ||
) | ||
|
||
func assertEqual(t *testing.T, got, want interface{}, userMessage ...interface{}) { | ||
t.Helper() | ||
|
||
if !reflect.DeepEqual(got, want) { | ||
logFailedAssertion(t, formatUnequalValues(got, want), userMessage...) | ||
} | ||
} | ||
|
||
func assertNotEqual(t *testing.T, got, want interface{}, userMessage ...interface{}) { | ||
t.Helper() | ||
|
||
if reflect.DeepEqual(got, want) { | ||
logFailedAssertion(t, formatUnequalValues(got, want), userMessage...) | ||
} | ||
} | ||
|
||
func logFailedAssertion(t *testing.T, summary string, userMessage ...interface{}) { | ||
t.Helper() | ||
text := summary | ||
|
||
if len(userMessage) > 0 { | ||
if message, ok := userMessage[0].(string); ok { | ||
if message != "" && len(userMessage) > 1 { | ||
text = fmt.Sprintf(message, userMessage[1:]...) + text | ||
} else if message != "" { | ||
text = fmt.Sprint(message) + text | ||
} | ||
} | ||
} | ||
|
||
t.Error(text) | ||
} | ||
|
||
func formatUnequalValues(got, want interface{}) string { | ||
var a, b string | ||
|
||
if reflect.TypeOf(got) != reflect.TypeOf(want) { | ||
a, b = fmt.Sprintf("%T(%#v)", got, got), fmt.Sprintf("%T(%#v)", want, want) | ||
} else { | ||
a, b = fmt.Sprintf("%#v", got), fmt.Sprintf("%#v", want) | ||
} | ||
|
||
return fmt.Sprintf("\ngot: %s\nwant: %s", a, b) | ||
} | ||
|
||
// 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, userMessage ...interface{}) { | ||
// 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) | ||
} | ||
|
||
// sortedBaggage = gotBaggage.Members() | ||
|
||
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 { | ||
traceID, err := trace.TraceIDFromHex(s) | ||
if err != nil { | ||
log.Fatalf("Cannot make a TraceID from the hex string: '%s'", s) | ||
} | ||
return traceID | ||
} | ||
|
||
func otelSpanIDFromHex(s string) trace.SpanID { | ||
spanID, err := trace.SpanIDFromHex(s) | ||
if err != nil { | ||
log.Fatalf("Cannot make a SPanID from the hex string: '%s'", s) | ||
} | ||
return spanID | ||
} | ||
|
||
// FIXME(anton): copie from mocks_test.go | ||
|
||
type TransportMock struct { | ||
mu sync.Mutex | ||
events []*sentry.Event | ||
lastEvent *sentry.Event | ||
} | ||
|
||
func (t *TransportMock) Configure(options sentry.ClientOptions) {} | ||
func (t *TransportMock) SendEvent(event *sentry.Event) { | ||
t.mu.Lock() | ||
defer t.mu.Unlock() | ||
t.events = append(t.events, event) | ||
t.lastEvent = event | ||
} | ||
func (t *TransportMock) Flush(timeout time.Duration) bool { | ||
return true | ||
} | ||
func (t *TransportMock) Events() []*sentry.Event { | ||
t.mu.Lock() | ||
defer t.mu.Unlock() | ||
return t.events | ||
} | ||
|
||
// |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package dummy | ||
|
||
// This package is intentionally left empty. | ||
// Reason: the otel module currenty requires go>=1.18. All files in the module have '//go:build go1.18' guards, so | ||
// with go1.17 "go test" might fail with the error: "go: warning: "./..." matched no packages; no packages to test". | ||
// As a workaround, we added this empty "dummy" package, which is the only package without the compiler version restrictions, | ||
// so at least the compiler doesn't complain that there are no packages to test. | ||
// | ||
// This file and package can be removed when we drop support for 1.17. |
Oops, something went wrong.