From a5f7ea6c34bd4e5f6b026847d21ab24d0fba1ebc Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Thu, 23 Jul 2026 09:25:58 +0200 Subject: [PATCH] feat(controlplane): add OTel trace/span IDs to Sentry request context Surface the OpenTelemetry trace and span IDs on control-plane Sentry error events so an event can be cross-referenced with its distributed trace in the OTLP backend (Tempo/Jaeger). The IDs are read from the request context (populated by the otelgrpc server handler) and added to the Sentry Request context as otel-trace-id and otel-span-id when a valid span context is present. They are informational fields and do not alter Sentry's canonical trace_id. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez Chainloop-Trace-Sessions: e2467003-af35-4aaa-b332-c9255f3d6171 --- .../internal/sentrycontext/sentry_context.go | 16 +++++++++++-- .../sentrycontext/sentry_context_test.go | 23 ++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/controlplane/internal/sentrycontext/sentry_context.go b/app/controlplane/internal/sentrycontext/sentry_context.go index 4253faa04..29554366e 100644 --- a/app/controlplane/internal/sentrycontext/sentry_context.go +++ b/app/controlplane/internal/sentrycontext/sentry_context.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import ( "github.com/go-kratos/kratos/v2/middleware" "github.com/go-kratos/kratos/v2/middleware/logging" "github.com/go-kratos/kratos/v2/transport" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc/metadata" ) @@ -89,12 +90,23 @@ func buildRequestContext(ctx context.Context, req interface{}) map[string]interf operation = info.Operation() } - return map[string]interface{}{ + requestContext := map[string]interface{}{ "protocol": protocol, "operation": operation, "args": extractArgs(req), "request-id": extractTracingIDFromMetadata(ctx), } + + // Surface the OpenTelemetry trace/span IDs so a Sentry event can be cross-referenced + // with its distributed trace in the OTLP backend (Tempo/Jaeger). The gRPC server is + // instrumented via otelgrpc, so a valid span context is present on ctx when tracing is + // enabled. These are informational fields and do not alter Sentry's canonical trace_id. + if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { + requestContext["otel-trace-id"] = sc.TraceID().String() + requestContext["otel-span-id"] = sc.SpanID().String() + } + + return requestContext } // extractTracingIDFromMetadata extracts the tracing ID from the metadata. diff --git a/app/controlplane/internal/sentrycontext/sentry_context_test.go b/app/controlplane/internal/sentrycontext/sentry_context_test.go index 190299833..15b9cb4a4 100644 --- a/app/controlplane/internal/sentrycontext/sentry_context_test.go +++ b/app/controlplane/internal/sentrycontext/sentry_context_test.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import ( "github.com/go-kratos/kratos/v2/transport" "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc/metadata" ) @@ -122,6 +123,26 @@ func TestBuildRequestContext(t *testing.T) { assert.Equal(t, "", requestContext["protocol"]) assert.Equal(t, "", requestContext["operation"]) }) + + t.Run("with a valid otel span context", func(t *testing.T) { + traceID, _ := trace.TraceIDFromHex("36b9df80c3e0c37e920caef35fa6eca0") + spanID, _ := trace.SpanIDFromHex("a1b2c3d4e5f60718") + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + }) + ctx := trace.ContextWithSpanContext(context.Background(), sc) + + requestContext := buildRequestContext(ctx, req) + assert.Equal(t, "36b9df80c3e0c37e920caef35fa6eca0", requestContext["otel-trace-id"]) + assert.Equal(t, "a1b2c3d4e5f60718", requestContext["otel-span-id"]) + }) + + t.Run("without a valid otel span context", func(t *testing.T) { + requestContext := buildRequestContext(context.Background(), req) + assert.NotContains(t, requestContext, "otel-trace-id") + assert.NotContains(t, requestContext, "otel-span-id") + }) } func TestExtractTracingIDFromMetadata(t *testing.T) {