-
Notifications
You must be signed in to change notification settings - Fork 246
feat: add OTLP integration support #1229
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
base: master
Are you sure you want to change the base?
Changes from all commits
81fc1b5
6dd527f
540da32
a6ec2d6
db0fe71
c5ba72a
332898d
580b42c
aea47e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| // This example demonstrates two ways to set up OpenTelemetry tracing with Sentry. | ||
| // | ||
| // setupTracerProviderWithSentry exports spans directly to Sentry using | ||
| // sentryotlp.NewTraceExporter. | ||
| // | ||
| // setupTracerProviderWithCollector exports spans to a standard OpenTelemetry | ||
| // Collector using otlptracehttp.New. | ||
| // | ||
| // To link Sentry errors, register sentryotel.NewErrorLinkingIntegration in | ||
| // sentry.Init. | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "log" | ||
| "net/http" | ||
| "os" | ||
| "time" | ||
|
|
||
| "github.com/getsentry/sentry-go" | ||
| sentryotel "github.com/getsentry/sentry-go/otel" | ||
| sentryotlp "github.com/getsentry/sentry-go/otel/otlp" | ||
| "go.opentelemetry.io/otel" | ||
| sdktrace "go.opentelemetry.io/otel/sdk/trace" | ||
| ) | ||
|
|
||
| func main() { | ||
| dsn := os.Getenv("SENTRY_DSN") | ||
| if err := sentry.Init(sentry.ClientOptions{ | ||
| Dsn: dsn, | ||
| EnableTracing: true, | ||
| TracesSampleRate: 1.0, | ||
| Integrations: func(integrations []sentry.Integration) []sentry.Integration { | ||
| return append(integrations, sentryotel.NewErrorLinkingIntegration()) | ||
| }, | ||
| }); err != nil { | ||
| log.Fatalf("sentry.Init: %v", err) | ||
| } | ||
| defer sentry.Flush(2 * time.Second) | ||
|
|
||
| ctx := context.Background() | ||
| // Direct-to-Sentry setup: | ||
| tp, err := setupTracerProviderWithSentry(ctx, dsn) | ||
| if err != nil { | ||
| log.Fatal(err) | ||
| } | ||
| // When exporting through a collector, keep the same Sentry initialization above | ||
| // and switch only the TracerProvider setup: | ||
| // | ||
| // tp, err := setupTracerProviderWithCollector(ctx) | ||
| // ... | ||
| defer func() { | ||
| if err := tp.Shutdown(ctx); err != nil { | ||
| log.Printf("TracerProvider.Shutdown: %v", err) | ||
| } | ||
| }() | ||
|
|
||
| otel.SetTracerProvider(tp) | ||
|
|
||
| mux := http.NewServeMux() | ||
| mux.HandleFunc("/demo", func(w http.ResponseWriter, r *http.Request) { | ||
| ctx, span := otel.Tracer("example-service").Start(r.Context(), "GET /demo") | ||
| defer span.End() | ||
|
|
||
| hub := sentry.GetHubFromContext(ctx) | ||
| if hub == nil { | ||
| hub = sentry.CurrentHub() | ||
| } | ||
| hub.Client().CaptureException( | ||
| errors.New("demo handler failure"), | ||
| &sentry.EventHint{Context: ctx}, | ||
| hub.Scope(), | ||
| ) | ||
|
|
||
| w.WriteHeader(http.StatusInternalServerError) | ||
| _, _ = w.Write([]byte("captured an error and linked it to the active trace\n")) | ||
| }) | ||
|
|
||
| fmt.Println("Send a request to http://localhost:8080/demo to generate one trace and one linked error.") | ||
| log.Fatal(http.ListenAndServe(":8080", mux)) | ||
| } | ||
|
|
||
| // setupTracerProviderWithSentry sends spans directly to Sentry's OTLP endpoint. | ||
| func setupTracerProviderWithSentry(ctx context.Context, dsn string) (*sdktrace.TracerProvider, error) { | ||
| exporter, err := sentryotlp.NewTraceExporter(ctx, dsn) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("sentryotlp.NewTraceExporter: %w", err) | ||
| } | ||
|
|
||
| return sdktrace.NewTracerProvider( | ||
| sdktrace.WithBatcher(exporter), | ||
| ), nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package sentryotel | ||
|
|
||
| import ( | ||
| "github.com/getsentry/sentry-go" | ||
| "github.com/getsentry/sentry-go/otel/internal/common" | ||
| ) | ||
|
|
||
| type errorLinkingIntegration struct{} | ||
|
|
||
| // NewErrorLinkingIntegration registers OpenTelemetry error linking with Sentry. | ||
| // | ||
| // It attaches the active OTel trace and span IDs to captured Sentry errors. | ||
| func NewErrorLinkingIntegration() sentry.Integration { | ||
| return errorLinkingIntegration{} | ||
| } | ||
|
|
||
| func (errorLinkingIntegration) Name() string { | ||
| return "OtelErrorLinking" | ||
| } | ||
|
|
||
| func (errorLinkingIntegration) SetupOnce(_ *sentry.Client) { | ||
| sentry.AddGlobalEventProcessor(common.NewEventProcessor()) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Integration uses global processor instead of per-client processorMedium Severity
|
||
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package common | ||
|
|
||
| import ( | ||
| "github.com/getsentry/sentry-go" | ||
| "go.opentelemetry.io/otel/trace" | ||
| ) | ||
|
|
||
| // NewEventProcessor creates a Sentry event processor that attaches OTel trace | ||
| // information from the active SpanContext to an error event. | ||
| func NewEventProcessor() sentry.EventProcessor { | ||
| return linkTraceContextToErrorEvent | ||
| } | ||
|
|
||
| func linkTraceContextToErrorEvent(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { | ||
| if hint == nil || hint.Context == nil { | ||
| return event | ||
| } | ||
| if event.Type == "transaction" { | ||
| return event | ||
| } | ||
|
|
||
| otelSpanContext := trace.SpanContextFromContext(hint.Context) | ||
| if !otelSpanContext.IsValid() { | ||
| return event | ||
| } | ||
|
|
||
| if event.Contexts == nil { | ||
| event.Contexts = make(map[string]sentry.Context) | ||
| } | ||
|
|
||
| traceContext, found := event.Contexts["trace"] | ||
| if !found { | ||
| event.Contexts["trace"] = make(map[string]any) | ||
| traceContext = event.Contexts["trace"] | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| traceContext["trace_id"] = otelSpanContext.TraceID().String() | ||
| traceContext["span_id"] = otelSpanContext.SpanID().String() | ||
| return event | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| package common | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/getsentry/sentry-go" | ||
| "github.com/stretchr/testify/assert" | ||
| "go.opentelemetry.io/otel/trace" | ||
| ) | ||
|
|
||
| func TestLinkTraceContextToErrorEventSetsOTelIDs(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| traceID := trace.TraceID{0xd4, 0xcd, 0xa9, 0x5b, 0x65, 0x2f, 0x4a, 0x15, 0x92, 0xb4, 0x49, 0xd5, 0x92, 0x9f, 0xda, 0x1b} | ||
| spanID := trace.SpanID{0x6e, 0x0c, 0x63, 0x25, 0x7d, 0xe3, 0x4c, 0x92} | ||
|
|
||
| event := &sentry.Event{} | ||
|
|
||
| ctx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{ | ||
| TraceID: traceID, | ||
| SpanID: spanID, | ||
| })) | ||
|
|
||
| got := linkTraceContextToErrorEvent(event, &sentry.EventHint{Context: ctx}) | ||
|
|
||
| assert.Equal(t, map[string]any{ | ||
| "trace_id": traceID.String(), | ||
| "span_id": spanID.String(), | ||
| }, got.Contexts["trace"]) | ||
| } | ||
|
|
||
| func TestLinkTraceContextToErrorEventPreservesExistingTraceContext(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| traceID := trace.TraceID{0xd4, 0xcd, 0xa9, 0x5b, 0x65, 0x2f, 0x4a, 0x15, 0x92, 0xb4, 0x49, 0xd5, 0x92, 0x9f, 0xda, 0x1b} | ||
| spanID := trace.SpanID{0x6e, 0x0c, 0x63, 0x25, 0x7d, 0xe3, 0x4c, 0x92} | ||
|
|
||
| event := &sentry.Event{ | ||
| Contexts: map[string]map[string]any{ | ||
| "trace": { | ||
| "trace_id": "123", | ||
| "span_id": "456", | ||
| "op": "http.server", | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| ctx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{ | ||
| TraceID: traceID, | ||
| SpanID: spanID, | ||
| })) | ||
|
|
||
| got := linkTraceContextToErrorEvent(event, &sentry.EventHint{Context: ctx}) | ||
|
|
||
| assert.Equal(t, map[string]any{ | ||
| "trace_id": traceID.String(), | ||
| "span_id": spanID.String(), | ||
| "op": "http.server", | ||
| }, got.Contexts["trace"]) | ||
| } | ||
|
|
||
| func TestLinkTraceContextToErrorEventSkipsInvalidSpanContext(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| event := &sentry.Event{} | ||
| got := linkTraceContextToErrorEvent(event, &sentry.EventHint{Context: context.Background()}) | ||
|
|
||
| _, found := got.Contexts["trace"] | ||
| assert.False(t, found) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| module github.com/getsentry/sentry-go/otel/otlp | ||
|
|
||
| go 1.24.0 | ||
|
|
||
| replace github.com/getsentry/sentry-go => ../../ | ||
|
|
||
| require ( | ||
| github.com/getsentry/sentry-go v0.43.0 | ||
| go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.11.0 | ||
| go.opentelemetry.io/otel/sdk v1.11.0 | ||
| ) | ||
|
|
||
| require ( | ||
| github.com/cenkalti/backoff/v4 v4.1.3 // indirect | ||
| github.com/go-logr/logr v1.2.3 // indirect | ||
| github.com/go-logr/stdr v1.2.2 // indirect | ||
| github.com/golang/protobuf v1.5.3 // indirect | ||
| github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect | ||
| go.opentelemetry.io/otel v1.11.0 // indirect | ||
| go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.0 // indirect | ||
| go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.0 // indirect | ||
| go.opentelemetry.io/otel/trace v1.11.0 // indirect | ||
| go.opentelemetry.io/proto/otlp v0.19.0 // indirect | ||
| golang.org/x/net v0.38.0 // indirect | ||
| golang.org/x/sys v0.31.0 // indirect | ||
| golang.org/x/text v0.23.0 // indirect | ||
| google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect | ||
| google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect | ||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect | ||
| google.golang.org/grpc v1.58.3 // indirect | ||
| google.golang.org/protobuf v1.31.0 // indirect | ||
| ) |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Initializing multiple Sentry clients with
NewErrorLinkingIntegration()registers the same global event processor multiple times, causing redundant event processing and performance overhead.Severity: MEDIUM
Suggested Fix
To prevent duplicate registration, the
SetupOncemethod should be made idempotent. This can be achieved by using a global flag, such as async.Onceor a boolean, to ensure thatsentry.AddGlobalEventProcessor()is only called the first timeSetupOnceis executed across all client initializations.Prompt for AI Agent