From e3749fa0ce43fd1efc2b1b6fe807d602fb0f8434 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Tue, 15 Apr 2025 10:50:23 +0000 Subject: [PATCH 01/20] Add OTEL support to spanlogger & tracing packages Signed-off-by: Oleg Zaytsev --- go.mod | 10 +- go.sum | 9 +- spanlogger/opentracing_spanlogger_test.go | 269 +++++++++++++++ spanlogger/otel_spanlogger_test.go | 399 ++++++++++++++++++++++ spanlogger/spanlogger.go | 239 +++++++++++-- spanlogger/spanlogger_test.go | 296 +--------------- tracing/tracing.go | 38 ++- tracing/tracing_test.go | 87 ++++- 8 files changed, 1019 insertions(+), 328 deletions(-) create mode 100644 spanlogger/opentracing_spanlogger_test.go create mode 100644 spanlogger/otel_spanlogger_test.go diff --git a/go.mod b/go.mod index febde83a4..afe71c312 100644 --- a/go.mod +++ b/go.mod @@ -41,12 +41,15 @@ require ( github.com/prometheus/common v0.44.0 github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97 github.com/sercand/kuberesolver/v6 v6.0.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/uber/jaeger-client-go v2.28.0+incompatible github.com/uber/jaeger-lib v2.2.0+incompatible go.etcd.io/etcd/api/v3 v3.5.0 go.etcd.io/etcd/client/pkg/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 + go.opentelemetry.io/otel v1.34.0 + go.opentelemetry.io/otel/sdk v1.34.0 + go.opentelemetry.io/otel/trace v1.34.0 go.uber.org/atomic v1.10.0 go.uber.org/goleak v1.2.0 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 @@ -68,8 +71,11 @@ require ( github.com/fatih/color v1.13.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/gomodule/redigo v1.8.9 // indirect github.com/google/btree v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-hclog v0.14.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.0 // indirect @@ -91,6 +97,8 @@ require ( github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.17.0 // indirect golang.org/x/crypto v0.32.0 // indirect diff --git a/go.sum b/go.sum index df4066877..f2866beb6 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,7 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -306,8 +307,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= @@ -326,8 +327,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/uber/jaeger-client-go v2.28.0+incompatible h1:G4QSBfvPKvg5ZM2j9MrJFdfI5iSljY/WnJqOGFao6HI= github.com/uber/jaeger-client-go v2.28.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= diff --git a/spanlogger/opentracing_spanlogger_test.go b/spanlogger/opentracing_spanlogger_test.go new file mode 100644 index 000000000..077844888 --- /dev/null +++ b/spanlogger/opentracing_spanlogger_test.go @@ -0,0 +1,269 @@ +package spanlogger + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "path/filepath" + "runtime" + "slices" + "strings" + "testing" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/opentracing/opentracing-go" + otlog "github.com/opentracing/opentracing-go/log" + "github.com/opentracing/opentracing-go/mocktracer" + "github.com/stretchr/testify/require" + "github.com/uber/jaeger-client-go" + + dskit_log "github.com/grafana/dskit/log" + "github.com/grafana/dskit/tenant" + "github.com/grafana/dskit/user" +) + +var mockTracer = mocktracer.New() + +func init() { opentracing.SetGlobalTracer(mockTracer) } + +func TestOpentracingSpanLogger_Log(t *testing.T) { + logger := log.NewNopLogger() + resolver := tenant.NewMultiResolver() + span, ctx := New(context.Background(), logger, "test", resolver, "bar") + _ = span.Log("foo") + newSpan := FromContext(ctx, logger, resolver) + require.Equal(t, span.opentracingSpan, newSpan.opentracingSpan) + _ = newSpan.Log("bar") + noSpan := FromContext(context.Background(), logger, resolver) + _ = noSpan.Log("foo") + require.Error(t, noSpan.Error(errors.New("err"))) + require.NoError(t, noSpan.Error(nil)) +} + +func TestOpentracingSpanLogger_CustomLogger(t *testing.T) { + var logged [][]interface{} + var logger funcLogger = func(keyvals ...interface{}) error { + logged = append(logged, keyvals) + return nil + } + resolver := tenant.NewMultiResolver() + + span, ctx := New(context.Background(), logger, "test", resolver) + _ = span.Log("msg", "original spanlogger") + + span = FromContext(ctx, log.NewNopLogger(), resolver) + _ = span.Log("msg", "restored spanlogger") + + span = FromContext(context.Background(), logger, resolver) + _ = span.Log("msg", "fallback spanlogger") + + expect := [][]interface{}{ + {"method", "test", "msg", "original spanlogger"}, + {"msg", "restored spanlogger"}, + {"msg", "fallback spanlogger"}, + } + require.Equal(t, expect, logged) +} + +func TestOpentracingSpanLogger_SetSpanAndLogTag(t *testing.T) { + logMessages := [][]interface{}{} + var logger funcLogger = func(keyvals ...interface{}) error { + logMessages = append(logMessages, keyvals) + return nil + } + + spanLogger, _ := New(context.Background(), logger, "the_method", tenant.NewMultiResolver()) + require.NoError(t, spanLogger.Log("msg", "this is the first message")) + + spanLogger.SetSpanAndLogTag("id", "123") + require.NoError(t, spanLogger.Log("msg", "this is the second message")) + + spanLogger.SetSpanAndLogTag("more context", "abc") + require.NoError(t, spanLogger.Log("msg", "this is the third message")) + + span := spanLogger.opentracingSpan.(*mocktracer.MockSpan) + expectedTags := map[string]interface{}{ + "id": "123", + "more context": "abc", + } + require.Equal(t, expectedTags, span.Tags()) + + expectedLogMessages := [][]interface{}{ + { + "method", "the_method", + "msg", "this is the first message", + }, + { + "method", "the_method", + "id", "123", + "msg", "this is the second message", + }, + { + "method", "the_method", + "id", "123", + "more context", "abc", + "msg", "this is the third message", + }, + } + + require.Equal(t, expectedLogMessages, logMessages) +} + +func TestOpentracingSpanCreatedWithTenantTag(t *testing.T) { + mockSpan := createOpentracingMockSpan(user.InjectOrgID(context.Background(), "team-a")) + + require.Equal(t, []string{"team-a"}, mockSpan.Tag(TenantIDsTagName)) +} + +func TestOpentracingSpanCreatedWithoutTenantTag(t *testing.T) { + mockSpan := createOpentracingMockSpan(context.Background()) + + _, exists := mockSpan.Tags()[TenantIDsTagName] + require.False(t, exists) +} + +func createOpentracingMockSpan(ctx context.Context) *mocktracer.MockSpan { + logger, _ := New(ctx, log.NewNopLogger(), "name", tenant.NewMultiResolver()) + return logger.opentracingSpan.(*mocktracer.MockSpan) +} + +func TestOpentracingSpanLoggerAwareCaller(t *testing.T) { + testCases := map[string]func(w io.Writer) log.Logger{ + // This is based on Mimir's default logging configuration: https://github.com/grafana/mimir/blob/50d1c27b4ad82b265ff5a865345bec2d726f64ef/pkg/util/log/log.go#L45-L46 + "default logger": func(w io.Writer) log.Logger { + logger := dskit_log.NewGoKitWithWriter("logfmt", w) + logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", Caller(5)) + logger = level.NewFilter(logger, level.AllowAll()) + return logger + }, + + // This is based on Mimir's logging configuration with rate-limiting enabled: https://github.com/grafana/mimir/blob/50d1c27b4ad82b265ff5a865345bec2d726f64ef/pkg/util/log/log.go#L42-L43 + "rate-limited logger": func(w io.Writer) log.Logger { + logger := dskit_log.NewGoKitWithWriter("logfmt", w) + logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", Caller(6)) + logger = dskit_log.NewRateLimitedLogger(logger, 1000, 1000, nil) + logger = level.NewFilter(logger, level.AllowAll()) + return logger + }, + + "default logger that has been wrapped with further information": func(w io.Writer) log.Logger { + logger := dskit_log.NewGoKitWithWriter("logfmt", w) + logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", Caller(5)) + logger = level.NewFilter(logger, level.AllowAll()) + logger = log.With(logger, "user", "user-1") + return logger + }, + } + + resolver := tenant.NewMultiResolver() + + setupTest := func(t *testing.T, loggerFactory func(io.Writer) log.Logger) (*bytes.Buffer, *SpanLogger, *jaeger.Span) { + reporter := jaeger.NewInMemoryReporter() + tracer, closer := jaeger.NewTracer( + "test", + jaeger.NewConstSampler(true), + reporter, + ) + t.Cleanup(func() { _ = closer.Close() }) + + span, ctx := opentracing.StartSpanFromContextWithTracer(context.Background(), tracer, "test") + + buf := bytes.NewBuffer(nil) + logger := loggerFactory(buf) + spanLogger := FromContext(ctx, logger, resolver) + + return buf, spanLogger, span.(*jaeger.Span) + } + + requireSpanHasTwoLogLinesWithoutCaller := func(t *testing.T, span *jaeger.Span, extraFields ...otlog.Field) { + logs := span.Logs() + require.Len(t, logs, 2) + + expectedFields := append(slices.Clone(extraFields), otlog.String("msg", "this is a test")) + require.Equal(t, expectedFields, logs[0].Fields) + + expectedFields = append(slices.Clone(extraFields), otlog.String("msg", "this is another test")) + require.Equal(t, expectedFields, logs[1].Fields) + } + + toCallerInfo := func(path string, lineNumber int) string { + fileName := filepath.Base(path) + return fmt.Sprintf("%s:%v", fileName, lineNumber) + } + + for name, loggerFactory := range testCases { + t.Run(name, func(t *testing.T) { + t.Run("logging with Log()", func(t *testing.T) { + logs, spanLogger, span := setupTest(t, loggerFactory) + + _, thisFile, lineNumberTwoLinesBeforeFirstLogCall, ok := runtime.Caller(0) + require.True(t, ok) + _ = spanLogger.Log("msg", "this is a test") + + logged := logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeFirstLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + logs.Reset() + _, _, lineNumberTwoLinesBeforeSecondLogCall, ok := runtime.Caller(0) + require.True(t, ok) + _ = spanLogger.Log("msg", "this is another test") + + logged = logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeSecondLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + requireSpanHasTwoLogLinesWithoutCaller(t, span) + }) + + t.Run("logging with DebugLog()", func(t *testing.T) { + logs, spanLogger, span := setupTest(t, loggerFactory) + + _, thisFile, lineNumberTwoLinesBeforeLogCall, ok := runtime.Caller(0) + require.True(t, ok) + spanLogger.DebugLog("msg", "this is a test") + + logged := logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + logs.Reset() + _, _, lineNumberTwoLinesBeforeSecondLogCall, ok := runtime.Caller(0) + require.True(t, ok) + spanLogger.DebugLog("msg", "this is another test") + + logged = logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeSecondLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + requireSpanHasTwoLogLinesWithoutCaller(t, span) + }) + + t.Run("logging with SpanLogger wrapped in a level", func(t *testing.T) { + logs, spanLogger, span := setupTest(t, loggerFactory) + + _, thisFile, lineNumberTwoLinesBeforeFirstLogCall, ok := runtime.Caller(0) + require.True(t, ok) + _ = level.Info(spanLogger).Log("msg", "this is a test") + + logged := logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeFirstLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + logs.Reset() + _, _, lineNumberTwoLinesBeforeSecondLogCall, ok := runtime.Caller(0) + require.True(t, ok) + _ = level.Info(spanLogger).Log("msg", "this is another test") + + logged = logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeSecondLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + requireSpanHasTwoLogLinesWithoutCaller(t, span, otlog.String("level", "info")) + }) + }) + } +} diff --git a/spanlogger/otel_spanlogger_test.go b/spanlogger/otel_spanlogger_test.go new file mode 100644 index 000000000..f75c26d2f --- /dev/null +++ b/spanlogger/otel_spanlogger_test.go @@ -0,0 +1,399 @@ +package spanlogger + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "path/filepath" + "runtime" + "slices" + "strings" + "testing" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + tracesdk "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + "go.opentelemetry.io/otel/trace" + + dskit_log "github.com/grafana/dskit/log" + "github.com/grafana/dskit/tenant" + "github.com/grafana/dskit/user" +) + +var ( + tracer = otel.Tracer("dskit/spanlogger.test") + spanExporter = tracetest.NewInMemoryExporter() +) + +func init() { + otel.SetTracerProvider(tracesdk.NewTracerProvider(tracesdk.WithSpanProcessor(tracesdk.NewSimpleSpanProcessor(spanExporter)))) +} + +func TestOTelSpanLogger_New_FromContext_Finish(t *testing.T) { + t.Cleanup(spanExporter.Reset) + + logger := log.NewNopLogger() + resolver := tenant.NewMultiResolver() + + span, ctx := NewOTel(context.Background(), logger, tracer, "test", resolver, "bar") + require.Nil(t, span.opentracingSpan) + require.NotNil(t, span.otelSpan) + require.Len(t, spanExporter.GetSpans(), 0, "There should be no spans exported before the span is finished") + + spanFromContext := FromContext(ctx, logger, resolver) + require.Nil(t, spanFromContext.opentracingSpan) + require.NotNil(t, spanFromContext.otelSpan) + + require.Equal(t, span.otelSpan.SpanContext().TraceID(), spanFromContext.otelSpan.SpanContext().TraceID(), "Span from context should have the same Trace ID") + require.Len(t, spanExporter.GetSpans(), 0, "There should be no spans exported before the span is finished") + + span.Finish() + require.Len(t, spanExporter.GetSpans(), 1, "There should be exactly one span after the span is finished") +} + +func TestOTelSpanLogger_Log(t *testing.T) { + t.Cleanup(spanExporter.Reset) + + logger := log.NewNopLogger() + resolver := tenant.NewMultiResolver() + span, ctx := NewOTel(context.Background(), logger, tracer, "test", resolver, "bar") + _ = span.Log("foo") + + newSpan := FromContext(ctx, logger, resolver) + + require.Equal(t, span.opentracingSpan, newSpan.opentracingSpan) + _ = newSpan.Log("bar") + spanFromContext := FromContext(context.Background(), logger, resolver) + _ = spanFromContext.Log("foo") + require.Error(t, spanFromContext.Error(errors.New("err"))) + require.NoError(t, spanFromContext.Error(nil)) +} + +func TestOTelSpanLogger_Error(t *testing.T) { + t.Cleanup(spanExporter.Reset) + + logger := log.NewNopLogger() + resolver := tenant.NewMultiResolver() + span, _ := NewOTel(context.Background(), logger, tracer, "test", resolver, "bar") + + require.Error(t, span.Error(errors.New("err"))) + require.NoError(t, span.Error(nil)) + span.Finish() + + spans := spanExporter.GetSpans() + require.Len(t, spans, 1, "There should be exactly one span after the span is finished") + exportedSpan := spans[0] + require.Len(t, exportedSpan.Events, 2, "There should be one log and one exception event") + require.Equal(t, codes.Error, exportedSpan.Status.Code) + require.Equal(t, "log", exportedSpan.Events[0].Name) + require.Equal(t, "exception", exportedSpan.Events[1].Name) +} + +func TestOTelSpanLogger_SetError(t *testing.T) { + t.Cleanup(spanExporter.Reset) + + logger := log.NewNopLogger() + resolver := tenant.NewMultiResolver() + span, _ := NewOTel(context.Background(), logger, tracer, "test", resolver, "bar") + + span.SetError() + span.Finish() + + spans := spanExporter.GetSpans() + require.Len(t, spans, 1, "There should be exactly one span after the span is finished") + exportedSpan := spans[0] + require.Equal(t, codes.Error, exportedSpan.Status.Code) +} + +func TestOTelSpanLogger_CustomLogger(t *testing.T) { + t.Cleanup(spanExporter.Reset) + + var expectedTraceID string + var logged []map[string]string + // logger will store all non-"trace_id" keys in `logged` + // it will check that "trace_id" is equal to expectedTraceID. + var logger funcLogger = func(keyvals ...interface{}) error { + values := map[string]string{} + var loggedTraceID string + for i := 0; i < len(keyvals); i += 2 { + k := keyvals[i].(string) + v := keyvals[i+1].(string) // we only log strings. + if k == "trace_id" { + loggedTraceID = v + } else { + values[k] = v + } + } + require.Equal(t, expectedTraceID, loggedTraceID) + logged = append(logged, values) + return nil + } + resolver := tenant.NewMultiResolver() + + span, ctx := NewOTel(context.Background(), logger, tracer, "test", resolver) + expectedTraceID = trace.SpanFromContext(ctx).SpanContext().TraceID().String() + _ = span.Log("msg", "original spanlogger") + + span = FromContext(ctx, log.NewNopLogger(), resolver) + _ = span.Log("msg", "restored spanlogger") + + // No trace_id expected for the next one. + expectedTraceID = "" + span = FromContext(context.Background(), logger, resolver) + _ = span.Log("msg", "fallback spanlogger") + + expect := []map[string]string{ + {"method": "test", "msg": "original spanlogger"}, + {"msg": "restored spanlogger"}, + {"msg": "fallback spanlogger"}, + } + require.Equal(t, expect, logged) +} + +func TestOTelSpanLogger_SetSpanAndLogTag(t *testing.T) { + t.Cleanup(spanExporter.Reset) + + var expectedTraceID string + var logged []map[string]string + // logger will store all non-"trace_id" keys in `logged` + // it will check that "trace_id" is equal to expectedTraceID. + var logger funcLogger = func(keyvals ...interface{}) error { + values := map[string]string{} + var loggedTraceID string + for i := 0; i < len(keyvals); i += 2 { + k := keyvals[i].(string) + v := keyvals[i+1].(string) // we only log strings. + if k == "trace_id" { + loggedTraceID = v + } else { + values[k] = v + } + } + require.Equal(t, expectedTraceID, loggedTraceID) + logged = append(logged, values) + return nil + } + + spanLogger, ctx := NewOTel(context.Background(), logger, tracer, "the_method", tenant.NewMultiResolver()) + expectedTraceID = trace.SpanFromContext(ctx).SpanContext().TraceID().String() + require.NoError(t, spanLogger.Log("msg", "this is the first message")) + + spanLogger.SetSpanAndLogTag("id", "123") + require.NoError(t, spanLogger.Log("msg", "this is the second message")) + + spanLogger.SetSpanAndLogTag("more context", "abc") + require.NoError(t, spanLogger.Log("msg", "this is the third message")) + + spanLogger.Finish() + spans := spanExporter.GetSpans() + require.Len(t, spans, 1, "There should be exactly one span after the span is finished") + exportedSpan := spans[0] + + expectedAttributes := []attribute.KeyValue{ + attribute.String("id", "123"), + attribute.String("more context", "abc"), + } + require.Equal(t, expectedAttributes, exportedSpan.Attributes) + + expectedLogMessages := []map[string]string{ + { + "method": "the_method", + "msg": "this is the first message", + }, + { + "method": "the_method", + "id": "123", + "msg": "this is the second message", + }, + { + "method": "the_method", + "id": "123", + "more context": "abc", + "msg": "this is the third message", + }, + } + + require.Equal(t, expectedLogMessages, logged) +} + +func TestOTelSpanCreatedWithTenantTag(t *testing.T) { + t.Cleanup(spanExporter.Reset) + + ctx := user.InjectOrgID(context.Background(), "team-a") + sp, _ := NewOTel(ctx, log.NewNopLogger(), tracer, "name", tenant.NewMultiResolver()) + sp.Finish() + + spans := spanExporter.GetSpans() + require.Len(t, spans, 1, "There should be exactly one span after the span is finished") + exportedSpan := spans[0] + + require.Equal(t, []attribute.KeyValue{attribute.String(TenantIDsTagName, "team-a")}, exportedSpan.Attributes) +} + +func TestOTelSpanCreatedWithoutTenantTag(t *testing.T) { + t.Cleanup(spanExporter.Reset) + + sp, _ := NewOTel(context.Background(), log.NewNopLogger(), tracer, "name", tenant.NewMultiResolver()) + sp.Finish() + + spans := spanExporter.GetSpans() + require.Len(t, spans, 1, "There should be exactly one span after the span is finished") + exportedSpan := spans[0] + + require.Empty(t, exportedSpan.Attributes) +} + +func TestOTelSpanLoggerAwareCaller(t *testing.T) { + testCases := map[string]func(w io.Writer) log.Logger{ + // This is based on Mimir's default logging configuration: https://github.com/grafana/mimir/blob/50d1c27b4ad82b265ff5a865345bec2d726f64ef/pkg/util/log/log.go#L45-L46 + "default logger": func(w io.Writer) log.Logger { + logger := dskit_log.NewGoKitWithWriter("logfmt", w) + logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", Caller(5)) + logger = level.NewFilter(logger, level.AllowAll()) + return logger + }, + + //This is based on Mimir's logging configuration with rate-limiting enabled: https://github.com/grafana/mimir/blob/50d1c27b4ad82b265ff5a865345bec2d726f64ef/pkg/util/log/log.go#L42-L43 + "rate-limited logger": func(w io.Writer) log.Logger { + logger := dskit_log.NewGoKitWithWriter("logfmt", w) + logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", Caller(6)) + logger = dskit_log.NewRateLimitedLogger(logger, 1000, 1000, nil) + logger = level.NewFilter(logger, level.AllowAll()) + return logger + }, + + "default logger that has been wrapped with further information": func(w io.Writer) log.Logger { + logger := dskit_log.NewGoKitWithWriter("logfmt", w) + logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", Caller(5)) + logger = level.NewFilter(logger, level.AllowAll()) + logger = log.With(logger, "user", "user-1") + return logger + }, + } + + resolver := tenant.NewMultiResolver() + + setupTest := func(t *testing.T, loggerFactory func(io.Writer) log.Logger) (*bytes.Buffer, *SpanLogger) { + t.Cleanup(spanExporter.Reset) + + ctx, _ := tracer.Start(context.Background(), "test") + + buf := bytes.NewBuffer(nil) + logger := loggerFactory(buf) + spanLogger := FromContext(ctx, logger, resolver) + require.NotNil(t, spanLogger.otelSpan) + require.Nil(t, spanLogger.opentracingSpan) + + return buf, spanLogger + } + + requireSpanHasTwoLogLinesWithoutCaller := func(t *testing.T, span tracetest.SpanStub, extraAttributes ...attribute.KeyValue) { + evs := span.Events + require.Len(t, evs, 2) + + require.Equal(t, "log", evs[0].Name) + require.Equal(t, "log", evs[1].Name) + require.Equal(t, append(slices.Clone(extraAttributes), attribute.String("msg", "this is a test")), evs[0].Attributes) + require.Equal(t, append(slices.Clone(extraAttributes), attribute.String("msg", "this is another test")), evs[1].Attributes) + } + + toCallerInfo := func(path string, lineNumber int) string { + fileName := filepath.Base(path) + return fmt.Sprintf("%s:%v", fileName, lineNumber) + } + + for name, loggerFactory := range testCases { + t.Run(name, func(t *testing.T) { + t.Run("logging with Log()", func(t *testing.T) { + t.Cleanup(spanExporter.Reset) + + logs, spanLogger := setupTest(t, loggerFactory) + + _, thisFile, lineNumberTwoLinesBeforeFirstLogCall, ok := runtime.Caller(0) + require.True(t, ok) + _ = spanLogger.Log("msg", "this is a test") + + logged := logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeFirstLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + logs.Reset() + _, _, lineNumberTwoLinesBeforeSecondLogCall, ok := runtime.Caller(0) + require.True(t, ok) + _ = spanLogger.Log("msg", "this is another test") + + logged = logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeSecondLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + spanLogger.Finish() + exportedSpans := spanExporter.GetSpans() + require.Len(t, exportedSpans, 1, "There should be exactly one span after the span is finished") + requireSpanHasTwoLogLinesWithoutCaller(t, exportedSpans[0]) + }) + + t.Run("logging with DebugLog()", func(t *testing.T) { + t.Cleanup(spanExporter.Reset) + + logs, spanLogger := setupTest(t, loggerFactory) + + _, thisFile, lineNumberTwoLinesBeforeLogCall, ok := runtime.Caller(0) + require.True(t, ok) + spanLogger.DebugLog("msg", "this is a test") + + logged := logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + logs.Reset() + _, _, lineNumberTwoLinesBeforeSecondLogCall, ok := runtime.Caller(0) + require.True(t, ok) + spanLogger.DebugLog("msg", "this is another test") + + logged = logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeSecondLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + spanLogger.Finish() + exportedSpans := spanExporter.GetSpans() + require.Len(t, exportedSpans, 1, "There should be exactly one span after the span is finished") + requireSpanHasTwoLogLinesWithoutCaller(t, exportedSpans[0]) + }) + + t.Run("logging with SpanLogger wrapped in a level", func(t *testing.T) { + t.Cleanup(spanExporter.Reset) + + logs, spanLogger := setupTest(t, loggerFactory) + + _, thisFile, lineNumberTwoLinesBeforeFirstLogCall, ok := runtime.Caller(0) + require.True(t, ok) + _ = level.Info(spanLogger).Log("msg", "this is a test") + + logged := logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeFirstLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + logs.Reset() + _, _, lineNumberTwoLinesBeforeSecondLogCall, ok := runtime.Caller(0) + require.True(t, ok) + _ = level.Info(spanLogger).Log("msg", "this is another test") + + logged = logs.String() + require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeSecondLogCall+2)) + require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) + + spanLogger.Finish() + exportedSpans := spanExporter.GetSpans() + require.Len(t, exportedSpans, 1, "There should be exactly one span after the span is finished") + requireSpanHasTwoLogLinesWithoutCaller(t, exportedSpans[0], attribute.String("level", "info")) + }) + }) + } +} diff --git a/spanlogger/spanlogger.go b/spanlogger/spanlogger.go index c73ffb16c..867d0e3f6 100644 --- a/spanlogger/spanlogger.go +++ b/spanlogger/spanlogger.go @@ -6,11 +6,15 @@ package spanlogger import ( "context" + "fmt" + "math" "runtime" "slices" "strconv" "strings" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "go.uber.org/atomic" // Really just need sync/atomic but there is a lint rule preventing it. "github.com/go-kit/log" @@ -18,6 +22,7 @@ import ( opentracing "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" otlog "github.com/opentracing/opentracing-go/log" + "go.opentelemetry.io/otel/trace" "github.com/grafana/dskit/tracing" ) @@ -43,11 +48,14 @@ var ( // SpanLogger unifies tracing and logging, to reduce repetition. type SpanLogger struct { - ctx context.Context // context passed in, with logger - resolver TenantResolver // passed in - baseLogger log.Logger // passed in - logger atomic.Pointer[log.Logger] // initialized on first use - span opentracing.Span + ctx context.Context // context passed in, with logger + resolver TenantResolver // passed in + baseLogger log.Logger // passed in + logger atomic.Pointer[log.Logger] // initialized on first use + + opentracingSpan opentracing.Span + otelSpan trace.Span + sampled bool debugEnabled bool } @@ -61,10 +69,39 @@ func New(ctx context.Context, logger log.Logger, method string, resolver TenantR } _, sampled := tracing.ExtractSampledTraceID(ctx) l := &SpanLogger{ - ctx: ctx, - resolver: resolver, - baseLogger: log.With(logger, "method", method), - span: span, + ctx: ctx, + resolver: resolver, + baseLogger: log.With(logger, "method", method), + + opentracingSpan: span, + otelSpan: nil, + + sampled: sampled, + debugEnabled: debugEnabled(logger), + } + if len(kvps) > 0 { + l.DebugLog(kvps...) + } + + ctx = context.WithValue(ctx, loggerCtxKey, logger) + return l, ctx +} + +func NewOTel(ctx context.Context, logger log.Logger, tracer trace.Tracer, method string, resolver TenantResolver, kvps ...any) (*SpanLogger, context.Context) { + ctx, span := tracer.Start(ctx, method) + if ids, err := resolver.TenantIDs(ctx); err == nil && len(ids) > 0 { + span.SetAttributes(attribute.String(TenantIDsTagName, strings.Join(ids, ","))) + } + sampled := span.SpanContext().IsSampled() + + l := &SpanLogger{ + ctx: ctx, + resolver: resolver, + baseLogger: log.With(logger, "method", method), + + opentracingSpan: nil, + otelSpan: span, + sampled: sampled, debugEnabled: debugEnabled(logger), } @@ -85,23 +122,38 @@ func FromContext(ctx context.Context, fallback log.Logger, resolver TenantResolv if !ok { logger = fallback } - sampled := false - sp := opentracing.SpanFromContext(ctx) - if sp == nil { - sp = opentracing.NoopTracer{}.StartSpan("noop") - } else { - _, sampled = tracing.ExtractSampledTraceID(ctx) - } + otelSpan, opentracingSpan, sampled := spanFromContext(ctx) + return &SpanLogger{ - ctx: ctx, - baseLogger: logger, - resolver: resolver, - span: sp, + ctx: ctx, + baseLogger: logger, + resolver: resolver, + + otelSpan: otelSpan, + opentracingSpan: opentracingSpan, + sampled: sampled, debugEnabled: debugEnabled(logger), } } +func spanFromContext(ctx context.Context) (otelSpan trace.Span, opentracingSpan opentracing.Span, sampled bool) { + if opentracingSpan = opentracing.SpanFromContext(ctx); opentracingSpan != nil { + _, sampled = tracing.ExtractSampledTraceID(ctx) + return nil, opentracingSpan, sampled + } + + otelSpan = trace.SpanFromContext(ctx) + otelSpanContext := otelSpan.SpanContext() + if otelSpanContext.IsValid() { + return otelSpan, nil, otelSpanContext.IsSampled() + } + + // noop not sample span. + // TODO: we could also return the otelSpan here, but at this point it's probably more performant to not-call the opentracing span. + return nil, opentracing.NoopTracer{}.StartSpan("noop"), false +} + // Detect whether we should output debug logging. // false iff the logger says it's not enabled; true if the logger doesn't say. func debugEnabled(logger log.Logger) bool { @@ -134,6 +186,13 @@ func (s *SpanLogger) spanLog(kvps ...interface{}) error { if !s.sampled { return nil } + + if s.otelSpan != nil { + // LogKV is more efficient with OTel. + s.LogKV(kvps...) + return nil + } + fields, err := otlog.InterleavedKVToFields(kvps...) if err != nil { return err @@ -148,7 +207,11 @@ func (s *SpanLogger) Error(err error) error { return err } s.SetError() - s.LogFields(otlog.Error(err)) + if s.otelSpan != nil { + s.otelSpan.RecordError(err) + } else { + s.LogFields(otlog.Error(err)) + } return err } @@ -193,17 +256,31 @@ func (s *SpanLogger) SetSpanAndLogTag(key string, value interface{}) { // SetError will set the error flag on the span. func (s *SpanLogger) SetError() { - ext.Error.Set(s.span, true) + if s.otelSpan != nil { + s.otelSpan.SetStatus(codes.Error, "error") + return + } + + ext.Error.Set(s.opentracingSpan, true) } // SetTag will set a tag/attribute on the span. func (s *SpanLogger) SetTag(key string, value interface{}) { - s.span.SetTag(key, value) + if s.otelSpan != nil { + s.otelSpan.SetAttributes(kvToAttr(key, value)) + return + } + + s.opentracingSpan.SetTag(key, value) } // Finish will finish the span. func (s *SpanLogger) Finish() { - s.span.Finish() + if s.otelSpan != nil { + s.otelSpan.End() + return + } + s.opentracingSpan.Finish() } // LogFields will log the provided fields in the span, this is more performant that LogKV when using opentracing library. @@ -212,8 +289,14 @@ func (s *SpanLogger) LogFields(kvps ...otlog.Field) { return } + if s.otelSpan != nil { + attrs := opentracingFieldsToAttributes(kvps...) + s.otelSpan.AddEvent("log", trace.WithAttributes(attrs...)) + return + } + // Clone kvps to prevent it from escaping to heap even when it's not sampled. - s.span.LogFields(slices.Clone(kvps)...) + s.opentracingSpan.LogFields(slices.Clone(kvps)...) } // LogKV will log the provided key/value pairs in the span, this is less performant than LogFields when using opentracing library. @@ -222,8 +305,13 @@ func (s *SpanLogger) LogKV(kvps ...interface{}) { return } + if s.otelSpan != nil { + attrs := otelAttributesFromKVs(kvps) + s.otelSpan.AddEvent("log", trace.WithAttributes(attrs...)) + return + } // Clone kvps to prevent it from escaping to heap even when it's not sampled. - s.span.LogKV(slices.Clone(kvps)...) + s.opentracingSpan.LogKV(slices.Clone(kvps)...) } // Caller is like github.com/go-kit/log's Caller, but ensures that the caller information is @@ -287,3 +375,102 @@ func formatCallerInfoForLog(file string, line int) string { idx := strings.LastIndexByte(file, '/') return file[idx+1:] + ":" + strconv.Itoa(line) } + +func otelAttributesFromKVs(kvps []any) []attribute.KeyValue { + attrs := make([]attribute.KeyValue, 0, len(kvps)/2) + for i := 0; i < len(kvps); i += 2 { + if i+1 < len(kvps) { + attrs = append(attrs, kvToAttr(kvps[i].(string), kvps[i+1])) + } + } + return attrs +} + +func kvToAttr(key string, val any) attribute.KeyValue { + var attr attribute.KeyValue + switch v := val.(type) { + case string: + attr = attribute.String(key, v) + case int: + attr = attribute.Int(key, v) + case int64: + attr = attribute.Int64(key, v) + case float64: + attr = attribute.Float64(key, v) + case bool: + attr = attribute.Bool(key, v) + case []string: + attr = attribute.StringSlice(key, v) + case []int: + attr = attribute.IntSlice(key, v) + case []int64: + attr = attribute.Int64Slice(key, v) + case fmt.Stringer: + attr = attribute.Stringer(key, v) + case []byte: + attr = attribute.String(key, string(v)) + default: + // Fallback to string representation for unsupported types. + attr = attribute.String(key, fmt.Sprintf("%v", val)) + } + return attr +} + +type opentracingFieldsToAttributesMarshaler []attribute.KeyValue + +func (f *opentracingFieldsToAttributesMarshaler) EmitString(key, value string) { + *f = append(*f, attribute.String(key, value)) +} + +func (f *opentracingFieldsToAttributesMarshaler) EmitBool(key string, value bool) { + *f = append(*f, attribute.Bool(key, value)) +} + +func (f *opentracingFieldsToAttributesMarshaler) EmitInt(key string, value int) { + *f = append(*f, attribute.Int(key, value)) +} + +func (f *opentracingFieldsToAttributesMarshaler) EmitInt32(key string, value int32) { + *f = append(*f, attribute.Int(key, int(value))) +} + +func (f *opentracingFieldsToAttributesMarshaler) EmitInt64(key string, value int64) { + *f = append(*f, attribute.Int64(key, value)) +} + +func (f *opentracingFieldsToAttributesMarshaler) EmitUint32(key string, value uint32) { + *f = append(*f, attribute.Int(key, int(value))) +} + +func (f *opentracingFieldsToAttributesMarshaler) EmitUint64(key string, value uint64) { + if value > math.MaxInt64 { + // Append as string if it exceeds int64 range. + *f = append(*f, attribute.String(key, strconv.FormatUint(value, 10))) + return + } + *f = append(*f, attribute.Int64(key, int64(value))) +} + +func (f *opentracingFieldsToAttributesMarshaler) EmitFloat32(key string, value float32) { + *f = append(*f, attribute.Float64(key, float64(value))) +} + +func (f *opentracingFieldsToAttributesMarshaler) EmitFloat64(key string, value float64) { + *f = append(*f, attribute.Float64(key, value)) +} + +func (f *opentracingFieldsToAttributesMarshaler) EmitObject(key string, value interface{}) { + *f = append(*f, attribute.String(key, fmt.Sprintf("%v", value))) +} + +func (f *opentracingFieldsToAttributesMarshaler) EmitLazyLogger(value otlog.LazyLogger) { + value(f) // don't be lazy. +} + +func opentracingFieldsToAttributes(kvps ...otlog.Field) []attribute.KeyValue { + fields := make(opentracingFieldsToAttributesMarshaler, 0, len(kvps)) + for _, kvp := range kvps { + kvp.Marshal(&fields) + } + return fields +} diff --git a/spanlogger/spanlogger_test.go b/spanlogger/spanlogger_test.go index dbe63a0bd..da85502e0 100644 --- a/spanlogger/spanlogger_test.go +++ b/spanlogger/spanlogger_test.go @@ -3,173 +3,23 @@ package spanlogger import ( "bytes" "context" - "fmt" - "io" - "path/filepath" - "runtime" - "slices" - "strings" "testing" dskit_log "github.com/grafana/dskit/log" - "github.com/grafana/dskit/user" + "github.com/grafana/dskit/tenant" "github.com/go-kit/log" "github.com/go-kit/log/level" - "github.com/opentracing/opentracing-go" - otlog "github.com/opentracing/opentracing-go/log" - "github.com/opentracing/opentracing-go/mocktracer" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - "github.com/uber/jaeger-client-go" ) -func TestSpanLogger_Log(t *testing.T) { - logger := log.NewNopLogger() - resolver := fakeResolver{} - span, ctx := New(context.Background(), logger, "test", resolver, "bar") - _ = span.Log("foo") - newSpan := FromContext(ctx, logger, resolver) - require.Equal(t, span.span, newSpan.span) - _ = newSpan.Log("bar") - noSpan := FromContext(context.Background(), logger, resolver) - _ = noSpan.Log("foo") - require.Error(t, noSpan.Error(errors.New("err"))) - require.NoError(t, noSpan.Error(nil)) -} - -func TestSpanLogger_CustomLogger(t *testing.T) { - var logged [][]interface{} - var logger funcLogger = func(keyvals ...interface{}) error { - logged = append(logged, keyvals) - return nil - } - resolver := fakeResolver{} - - span, ctx := New(context.Background(), logger, "test", resolver) - _ = span.Log("msg", "original spanlogger") - - span = FromContext(ctx, log.NewNopLogger(), resolver) - _ = span.Log("msg", "restored spanlogger") - - span = FromContext(context.Background(), logger, resolver) - _ = span.Log("msg", "fallback spanlogger") - - expect := [][]interface{}{ - {"method", "test", "msg", "original spanlogger"}, - {"msg", "restored spanlogger"}, - {"msg", "fallback spanlogger"}, - } - require.Equal(t, expect, logged) -} - -func TestSpanCreatedWithTenantTag(t *testing.T) { - mockSpan := createSpan(user.InjectOrgID(context.Background(), "team-a")) - - require.Equal(t, []string{"team-a"}, mockSpan.Tag(TenantIDsTagName)) -} - -func TestSpanCreatedWithoutTenantTag(t *testing.T) { - mockSpan := createSpan(context.Background()) - - _, exists := mockSpan.Tags()[TenantIDsTagName] - require.False(t, exists) -} - -func TestSpanLogger_SetSpanAndLogTag(t *testing.T) { - mockTracer := mocktracer.New() - opentracing.SetGlobalTracer(mockTracer) - - logMessages := [][]interface{}{} - var logger funcLogger = func(keyvals ...interface{}) error { - logMessages = append(logMessages, keyvals) - return nil - } - - spanLogger, _ := New(context.Background(), logger, "the_method", fakeResolver{}) - require.NoError(t, spanLogger.Log("msg", "this is the first message")) - - spanLogger.SetSpanAndLogTag("id", "123") - require.NoError(t, spanLogger.Log("msg", "this is the second message")) - - spanLogger.SetSpanAndLogTag("more context", "abc") - require.NoError(t, spanLogger.Log("msg", "this is the third message")) - - span := spanLogger.span.(*mocktracer.MockSpan) - expectedTags := map[string]interface{}{ - "id": "123", - "more context": "abc", - } - require.Equal(t, expectedTags, span.Tags()) - - expectedLogMessages := [][]interface{}{ - { - "method", "the_method", - "msg", "this is the first message", - }, - { - "method", "the_method", - "id", "123", - "msg", "this is the second message", - }, - { - "method", "the_method", - "id", "123", - "more context", "abc", - "msg", "this is the third message", - }, - } - - require.Equal(t, expectedLogMessages, logMessages) -} - -func createSpan(ctx context.Context) *mocktracer.MockSpan { - mockTracer := mocktracer.New() - opentracing.SetGlobalTracer(mockTracer) - - logger, _ := New(ctx, log.NewNopLogger(), "name", fakeResolver{}) - return logger.span.(*mocktracer.MockSpan) -} - type funcLogger func(keyvals ...interface{}) error -func (f funcLogger) Log(keyvals ...interface{}) error { - return f(keyvals...) -} - -type fakeResolver struct { -} - -func (fakeResolver) TenantID(ctx context.Context) (string, error) { - id, err := user.ExtractOrgID(ctx) - if err != nil { - return "", err - } - - // handle the relative reference to current and parent path. - if id == "." || id == ".." || strings.ContainsAny(id, `\/`) { - return "", nil - } - - return id, nil -} - -func (r fakeResolver) TenantIDs(ctx context.Context) ([]string, error) { - id, err := r.TenantID(ctx) - if err != nil { - return nil, err - } - if id == "" { - return nil, nil - } - - return []string{id}, nil -} +func (f funcLogger) Log(keyvals ...interface{}) error { return f(keyvals...) } // Using a no-op logger and no tracing provider, measure the overhead of a small log call. func BenchmarkSpanLogger(b *testing.B) { logger := noDebugNoopLogger{} - resolver := fakeResolver{} + resolver := tenant.NewMultiResolver() sl, _ := New(context.Background(), logger, "test", resolver, "bar") b.Run("log", func(b *testing.B) { for i := 0; i < b.N; i++ { @@ -211,7 +61,7 @@ func BenchmarkSpanLoggerWithRealLogger(b *testing.B) { debugEnabled: debugEnabled, } - resolver := fakeResolver{} + resolver := tenant.NewMultiResolver() sl, _ := New(context.Background(), logger, "test", resolver, "bar") b.Run("Log", func(b *testing.B) { @@ -282,141 +132,3 @@ type loggerWithDebugEnabled struct { } func (l loggerWithDebugEnabled) DebugEnabled() bool { return l.debugEnabled } - -func TestSpanLoggerAwareCaller(t *testing.T) { - testCases := map[string]func(w io.Writer) log.Logger{ - // This is based on Mimir's default logging configuration: https://github.com/grafana/mimir/blob/50d1c27b4ad82b265ff5a865345bec2d726f64ef/pkg/util/log/log.go#L45-L46 - "default logger": func(w io.Writer) log.Logger { - logger := dskit_log.NewGoKitWithWriter("logfmt", w) - logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", Caller(5)) - logger = level.NewFilter(logger, level.AllowAll()) - return logger - }, - - // This is based on Mimir's logging configuration with rate-limiting enabled: https://github.com/grafana/mimir/blob/50d1c27b4ad82b265ff5a865345bec2d726f64ef/pkg/util/log/log.go#L42-L43 - "rate-limited logger": func(w io.Writer) log.Logger { - logger := dskit_log.NewGoKitWithWriter("logfmt", w) - logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", Caller(6)) - logger = dskit_log.NewRateLimitedLogger(logger, 1000, 1000, nil) - logger = level.NewFilter(logger, level.AllowAll()) - return logger - }, - - "default logger that has been wrapped with further information": func(w io.Writer) log.Logger { - logger := dskit_log.NewGoKitWithWriter("logfmt", w) - logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", Caller(5)) - logger = level.NewFilter(logger, level.AllowAll()) - logger = log.With(logger, "user", "user-1") - return logger - }, - } - - resolver := fakeResolver{} - - setupTest := func(t *testing.T, loggerFactory func(io.Writer) log.Logger) (*bytes.Buffer, *SpanLogger, *jaeger.Span) { - reporter := jaeger.NewInMemoryReporter() - tracer, closer := jaeger.NewTracer( - "test", - jaeger.NewConstSampler(true), - reporter, - ) - t.Cleanup(func() { _ = closer.Close() }) - - span, ctx := opentracing.StartSpanFromContextWithTracer(context.Background(), tracer, "test") - - buf := bytes.NewBuffer(nil) - logger := loggerFactory(buf) - spanLogger := FromContext(ctx, logger, resolver) - - return buf, spanLogger, span.(*jaeger.Span) - } - - requireSpanHasTwoLogLinesWithoutCaller := func(t *testing.T, span *jaeger.Span, extraFields ...otlog.Field) { - logs := span.Logs() - require.Len(t, logs, 2) - - expectedFields := append(slices.Clone(extraFields), otlog.String("msg", "this is a test")) - require.Equal(t, expectedFields, logs[0].Fields) - - expectedFields = append(slices.Clone(extraFields), otlog.String("msg", "this is another test")) - require.Equal(t, expectedFields, logs[1].Fields) - } - - for name, loggerFactory := range testCases { - t.Run(name, func(t *testing.T) { - t.Run("logging with Log()", func(t *testing.T) { - logs, spanLogger, span := setupTest(t, loggerFactory) - - _, thisFile, lineNumberTwoLinesBeforeFirstLogCall, ok := runtime.Caller(0) - require.True(t, ok) - _ = spanLogger.Log("msg", "this is a test") - - logged := logs.String() - require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeFirstLogCall+2)) - require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) - - logs.Reset() - _, _, lineNumberTwoLinesBeforeSecondLogCall, ok := runtime.Caller(0) - require.True(t, ok) - _ = spanLogger.Log("msg", "this is another test") - - logged = logs.String() - require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeSecondLogCall+2)) - require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) - - requireSpanHasTwoLogLinesWithoutCaller(t, span) - }) - - t.Run("logging with DebugLog()", func(t *testing.T) { - logs, spanLogger, span := setupTest(t, loggerFactory) - _, thisFile, lineNumberTwoLinesBeforeLogCall, ok := runtime.Caller(0) - require.True(t, ok) - spanLogger.DebugLog("msg", "this is a test") - - logged := logs.String() - require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeLogCall+2)) - require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) - - logs.Reset() - _, _, lineNumberTwoLinesBeforeSecondLogCall, ok := runtime.Caller(0) - require.True(t, ok) - spanLogger.DebugLog("msg", "this is another test") - - logged = logs.String() - require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeSecondLogCall+2)) - require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) - - requireSpanHasTwoLogLinesWithoutCaller(t, span) - }) - - t.Run("logging with SpanLogger wrapped in a level", func(t *testing.T) { - logs, spanLogger, span := setupTest(t, loggerFactory) - - _, thisFile, lineNumberTwoLinesBeforeFirstLogCall, ok := runtime.Caller(0) - require.True(t, ok) - _ = level.Info(spanLogger).Log("msg", "this is a test") - - logged := logs.String() - require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeFirstLogCall+2)) - require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) - - logs.Reset() - _, _, lineNumberTwoLinesBeforeSecondLogCall, ok := runtime.Caller(0) - require.True(t, ok) - _ = level.Info(spanLogger).Log("msg", "this is another test") - - logged = logs.String() - require.Contains(t, logged, "caller="+toCallerInfo(thisFile, lineNumberTwoLinesBeforeSecondLogCall+2)) - require.Equalf(t, 1, strings.Count(logged, "caller="), "expected to only have one caller field, but got: %v", logged) - - requireSpanHasTwoLogLinesWithoutCaller(t, span, otlog.String("level", "info")) - }) - }) - } -} - -func toCallerInfo(path string, lineNumber int) string { - fileName := filepath.Base(path) - - return fmt.Sprintf("%s:%v", fileName, lineNumber) -} diff --git a/tracing/tracing.go b/tracing/tracing.go index 1882a081d..157c69829 100644 --- a/tracing/tracing.go +++ b/tracing/tracing.go @@ -13,6 +13,7 @@ import ( jaeger "github.com/uber/jaeger-client-go" jaegercfg "github.com/uber/jaeger-client-go/config" jaegerprom "github.com/uber/jaeger-lib/metrics/prometheus" + "go.opentelemetry.io/otel/trace" ) // ErrInvalidConfiguration is an error to notify client to provide valid trace report agent or config server @@ -58,6 +59,9 @@ func ExtractTraceID(ctx context.Context) (string, bool) { if tid, _, ok := extractJaegerContext(ctx); ok { return tid.String(), true } + if tid, _, ok := extractOTelContext(ctx); ok { + return tid.String(), true + } return "", false } @@ -66,6 +70,9 @@ func ExtractTraceSpanID(ctx context.Context) (string, string, bool) { if tid, sid, ok := extractJaegerContext(ctx); ok { return tid.String(), sid.String(), true } + if tid, sid, ok := extractOTelContext(ctx); ok { + return tid.String(), sid.String(), true + } return "", "", false } @@ -81,17 +88,42 @@ func extractJaegerContext(ctx context.Context) (tid jaeger.TraceID, sid jaeger.S return jsp.TraceID(), jsp.SpanID(), true } +func extractOTelContext(ctx context.Context) (tid trace.TraceID, sid trace.SpanID, success bool) { + sp := trace.SpanFromContext(ctx) + sc := sp.SpanContext() + if !sc.IsValid() { + return + } + return sc.TraceID(), sc.SpanID(), true +} + // ExtractSampledTraceID works like ExtractTraceID but the returned bool is only // true if the returned trace id is sampled. func ExtractSampledTraceID(ctx context.Context) (string, bool) { + if tid, ok := extractSampledJaegerTraceID(ctx); ok { + return tid.String(), true + } + if tid, ok := extractSampledOTelTraceID(ctx); ok { + return tid.String(), true + } + return "", false +} + +func extractSampledOTelTraceID(ctx context.Context) (traceID trace.TraceID, sampled bool) { + sp := trace.SpanFromContext(ctx) + sc := sp.SpanContext() + return sc.TraceID(), sc.IsValid() && sc.IsSampled() +} + +func extractSampledJaegerTraceID(ctx context.Context) (traceID jaeger.TraceID, sampled bool) { sp := opentracing.SpanFromContext(ctx) if sp == nil { - return "", false + return } sctx, ok := sp.Context().(jaeger.SpanContext) if !ok { - return "", false + return } - return sctx.TraceID().String(), sctx.IsSampled() + return sctx.TraceID(), sctx.IsSampled() } diff --git a/tracing/tracing_test.go b/tracing/tracing_test.go index c730366cc..d0e2d8266 100644 --- a/tracing/tracing_test.go +++ b/tracing/tracing_test.go @@ -9,9 +9,12 @@ import ( "github.com/opentracing/opentracing-go" "github.com/stretchr/testify/require" jaeger "github.com/uber/jaeger-client-go" + "go.opentelemetry.io/otel/trace" ) -func TestExtractTraceSpanID(t *testing.T) { +const expectedTraceID = "00000000000000010000000000000002" + +func TestExtractTraceSpanID_Opentracing(t *testing.T) { spanCtx := jaeger.NewSpanContext(jaeger.TraceID{High: 1, Low: 2}, jaeger.SpanID(3), 0, true, nil) tracer, closer := jaeger.NewTracer("test", jaeger.NewConstSampler(true), jaeger.NewNullReporter()) defer closer.Close() @@ -32,7 +35,7 @@ func TestExtractTraceSpanID(t *testing.T) { "trace": { ctx: opentracing.ContextWithSpan(context.Background(), span), expectedOk: true, - expectedTraceID: "00000000000000010000000000000002", + expectedTraceID: expectedTraceID, expectedSpanID: span.Context().(jaeger.SpanContext).SpanID().String(), }, } @@ -49,3 +52,83 @@ func TestExtractTraceSpanID(t *testing.T) { }) } } + +func TestExtractTraceSpanID_OTel(t *testing.T) { + const expectedSpanID = "0000000000000003" + + traceID, err := trace.TraceIDFromHex(expectedTraceID) + require.NoError(t, err) + spanID, err := trace.SpanIDFromHex(expectedSpanID) + require.NoError(t, err) + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }) + + testCases := map[string]struct { + ctx context.Context + expectedOk bool + expectedTraceID string + expectedSpanID string + }{ + "no trace": { + ctx: context.Background(), + expectedOk: false, + expectedTraceID: "", + expectedSpanID: "", + }, + "trace": { + ctx: trace.ContextWithSpanContext(context.Background(), sc), + expectedOk: true, + expectedTraceID: expectedTraceID, + expectedSpanID: expectedSpanID, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + traceID, spanID, ok := ExtractTraceSpanID(tc.ctx) + require.Equal(t, tc.expectedOk, ok) + require.Equal(t, tc.expectedTraceID, traceID) + require.Equal(t, tc.expectedSpanID, spanID) + + traceID, ok = ExtractTraceID(tc.ctx) + require.Equal(t, tc.expectedOk, ok) + require.Equal(t, tc.expectedTraceID, traceID) + }) + } +} + +func TestExtractSampledTraceID_OTel(t *testing.T) { + traceID, err := trace.TraceIDFromHex(expectedTraceID) + require.NoError(t, err) + spanID, err := trace.SpanIDFromHex("0000000000000003") + require.NoError(t, err) + + t.Run("sampled", func(t *testing.T) { + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }) + ctx := trace.ContextWithSpanContext(context.Background(), sc) + gotTraceID, sampled := ExtractSampledTraceID(ctx) + require.True(t, sampled) + require.Equal(t, expectedTraceID, gotTraceID) + }) + + t.Run("not sampled", func(t *testing.T) { + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + }) + ctx := trace.ContextWithSpanContext(context.Background(), sc) + _, sampled := ExtractSampledTraceID(ctx) + require.False(t, sampled) + }) + + t.Run("no span", func(t *testing.T) { + _, sampled := ExtractSampledTraceID(context.Background()) + require.False(t, sampled) + }) +} From f9ad9c1fd57ea1acfe67d03efc49a7c794aaa408 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 16 Apr 2025 09:27:56 +0000 Subject: [PATCH 02/20] Add OTel to instrument & httpgrpc/server Signed-off-by: Oleg Zaytsev --- go.mod | 26 +++---- go.sum | 54 ++++++++------- httpgrpc/server/server.go | 11 ++- instrument/instrument.go | 13 ++-- spanlogger/spanlogger.go | 57 ++-------------- tracing/span.go | 140 ++++++++++++++++++++++++++++++++++++++ tracing/span_option.go | 29 ++++++++ 7 files changed, 234 insertions(+), 96 deletions(-) create mode 100644 tracing/span.go create mode 100644 tracing/span_option.go diff --git a/go.mod b/go.mod index afe71c312..f67be0662 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/cristalhq/hedgedhttp v0.9.1 github.com/davecgh/go-spew v1.1.1 github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb - github.com/felixge/httpsnoop v1.0.3 + github.com/felixge/httpsnoop v1.0.4 github.com/go-kit/log v0.2.1 github.com/go-redis/redis/v8 v8.11.5 github.com/gogo/googleapis v1.1.0 @@ -20,7 +20,7 @@ require ( github.com/gogo/status v1.1.0 github.com/golang/protobuf v1.5.4 github.com/golang/snappy v0.0.4 - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/gorilla/mux v1.8.0 github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d github.com/grafana/pyroscope-go/godeltaprof v0.1.8 @@ -47,14 +47,16 @@ require ( go.etcd.io/etcd/api/v3 v3.5.0 go.etcd.io/etcd/client/pkg/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 - go.opentelemetry.io/otel v1.34.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 + go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/sdk v1.34.0 - go.opentelemetry.io/otel/trace v1.34.0 + go.opentelemetry.io/otel/trace v1.35.0 go.uber.org/atomic v1.10.0 go.uber.org/goleak v1.2.0 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 - golang.org/x/net v0.34.0 - golang.org/x/sync v0.10.0 + golang.org/x/net v0.35.0 + golang.org/x/sync v0.11.0 golang.org/x/time v0.1.0 google.golang.org/grpc v1.71.1 gopkg.in/yaml.v2 v2.4.0 @@ -98,18 +100,18 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.17.0 // indirect - golang.org/x/crypto v0.32.0 // indirect + golang.org/x/crypto v0.33.0 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/protobuf v1.36.5 // indirect ) // Replace memberlist with our fork which includes some fixes that haven't been diff --git a/go.sum b/go.sum index f2866beb6..9064025b9 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -131,8 +131,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -347,16 +347,22 @@ go.etcd.io/etcd/client/v3 v3.5.0 h1:62Eh0XOro+rDwkrypAGDfgmNh5Joq+z+W9HZdlXMzek= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -371,8 +377,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= @@ -406,8 +412,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -422,8 +428,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -457,16 +463,16 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -496,8 +502,8 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -519,8 +525,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/httpgrpc/server/server.go b/httpgrpc/server/server.go index 3acd6896a..2bceb1ddc 100644 --- a/httpgrpc/server/server.go +++ b/httpgrpc/server/server.go @@ -17,6 +17,9 @@ import ( otgrpc "github.com/opentracing-contrib/go-grpc" "github.com/opentracing/opentracing-go" "github.com/sercand/kuberesolver/v6" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -184,6 +187,7 @@ func NewClient(address string) (*Client, error) { otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer()), middleware.ClientUserHeaderInterceptor, ), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), } conn, err := grpc.NewClient(address, dialOptions...) @@ -199,13 +203,18 @@ func NewClient(address string) (*Client, error) { // ServeHTTP implements http.Handler func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if tracer := opentracing.GlobalTracer(); tracer != nil { + // Are we using OpenTracing? + if tracer := opentracing.GlobalTracer(); opentracing.IsGlobalTracerRegistered() && tracer != nil { if span := opentracing.SpanFromContext(r.Context()); span != nil { if err := tracer.Inject(span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)); err != nil { level.Warn(log.Global()).Log("msg", "failed to inject tracing headers into request", "err", err) } } } + // Are we using OpenTelemetry? + if span := trace.SpanFromContext(r.Context()); span.SpanContext().IsValid() { + otelhttptrace.Inject(r.Context(), r) + } req, err := httpgrpc.FromHTTPRequest(r) if err != nil { diff --git a/instrument/instrument.go b/instrument/instrument.go index f54e49def..66fd74451 100644 --- a/instrument/instrument.go +++ b/instrument/instrument.go @@ -10,9 +10,6 @@ import ( "context" "time" - "github.com/opentracing/opentracing-go" - "github.com/opentracing/opentracing-go/ext" - otlog "github.com/opentracing/opentracing-go/log" "github.com/prometheus/client_golang/prometheus" "github.com/grafana/dskit/grpcutil" @@ -158,8 +155,8 @@ func CollectedRequest(ctx context.Context, method string, col Collector, toStatu if toStatusCode == nil { toStatusCode = ErrorCode } - sp, newCtx := opentracing.StartSpanFromContext(ctx, method) - ext.SpanKindRPCClient.Set(sp) + sp, newCtx := tracing.StartSpanFromContext(ctx, method, tracing.SpanKindRPCClient{}) + defer sp.Finish() if userID, err := user.ExtractUserID(ctx); err == nil { sp.SetTag("user", userID) } @@ -174,12 +171,10 @@ func CollectedRequest(ctx context.Context, method string, col Collector, toStatu if err != nil { if !grpcutil.IsCanceled(err) { - ext.Error.Set(sp, true) + sp.SetError() } - sp.LogFields(otlog.Error(err)) + sp.LogError(err) } - sp.Finish() - return err } diff --git a/spanlogger/spanlogger.go b/spanlogger/spanlogger.go index 867d0e3f6..be937ab0c 100644 --- a/spanlogger/spanlogger.go +++ b/spanlogger/spanlogger.go @@ -122,7 +122,7 @@ func FromContext(ctx context.Context, fallback log.Logger, resolver TenantResolv if !ok { logger = fallback } - otelSpan, opentracingSpan, sampled := spanFromContext(ctx) + otelSpan, opentracingSpan, sampled := tracing.SpanFromContext(ctx) return &SpanLogger{ ctx: ctx, @@ -137,23 +137,6 @@ func FromContext(ctx context.Context, fallback log.Logger, resolver TenantResolv } } -func spanFromContext(ctx context.Context) (otelSpan trace.Span, opentracingSpan opentracing.Span, sampled bool) { - if opentracingSpan = opentracing.SpanFromContext(ctx); opentracingSpan != nil { - _, sampled = tracing.ExtractSampledTraceID(ctx) - return nil, opentracingSpan, sampled - } - - otelSpan = trace.SpanFromContext(ctx) - otelSpanContext := otelSpan.SpanContext() - if otelSpanContext.IsValid() { - return otelSpan, nil, otelSpanContext.IsSampled() - } - - // noop not sample span. - // TODO: we could also return the otelSpan here, but at this point it's probably more performant to not-call the opentracing span. - return nil, opentracing.NoopTracer{}.StartSpan("noop"), false -} - // Detect whether we should output debug logging. // false iff the logger says it's not enabled; true if the logger doesn't say. func debugEnabled(logger log.Logger) bool { @@ -267,7 +250,7 @@ func (s *SpanLogger) SetError() { // SetTag will set a tag/attribute on the span. func (s *SpanLogger) SetTag(key string, value interface{}) { if s.otelSpan != nil { - s.otelSpan.SetAttributes(kvToAttr(key, value)) + s.otelSpan.SetAttributes(tracing.KeyValueToOTelAttribute(key, value)) return } @@ -380,42 +363,16 @@ func otelAttributesFromKVs(kvps []any) []attribute.KeyValue { attrs := make([]attribute.KeyValue, 0, len(kvps)/2) for i := 0; i < len(kvps); i += 2 { if i+1 < len(kvps) { - attrs = append(attrs, kvToAttr(kvps[i].(string), kvps[i+1])) + key, ok := kvps[i].(string) + if !ok { + key = fmt.Sprintf("not_string_key:%v", kvps[i]) + } + attrs = append(attrs, tracing.KeyValueToOTelAttribute(key, kvps[i+1])) } } return attrs } -func kvToAttr(key string, val any) attribute.KeyValue { - var attr attribute.KeyValue - switch v := val.(type) { - case string: - attr = attribute.String(key, v) - case int: - attr = attribute.Int(key, v) - case int64: - attr = attribute.Int64(key, v) - case float64: - attr = attribute.Float64(key, v) - case bool: - attr = attribute.Bool(key, v) - case []string: - attr = attribute.StringSlice(key, v) - case []int: - attr = attribute.IntSlice(key, v) - case []int64: - attr = attribute.Int64Slice(key, v) - case fmt.Stringer: - attr = attribute.Stringer(key, v) - case []byte: - attr = attribute.String(key, string(v)) - default: - // Fallback to string representation for unsupported types. - attr = attribute.String(key, fmt.Sprintf("%v", val)) - } - return attr -} - type opentracingFieldsToAttributesMarshaler []attribute.KeyValue func (f *opentracingFieldsToAttributesMarshaler) EmitString(key, value string) { diff --git a/tracing/span.go b/tracing/span.go new file mode 100644 index 000000000..816d8a391 --- /dev/null +++ b/tracing/span.go @@ -0,0 +1,140 @@ +package tracing + +import ( + "context" + "fmt" + + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + otlog "github.com/opentracing/opentracing-go/log" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +// Span is either an OpenTracing span or an OpenTelemetry span. +// It could also be a noop span. +type Span struct { + opentracingSpan opentracing.Span + otelSpan trace.Span +} + +// StartSpanFromContext starts a new span from the context using the parent tracing library. +// If parent span is not sampled, it returns a noop span. +func StartSpanFromContext(ctx context.Context, operation string, options ...SpanOption) (*Span, context.Context) { + otelSpan, opentracingSpan, sampled := SpanFromContext(ctx) + if sampled { + if opentracingSpan != nil { + var opentracingOptions []opentracing.StartSpanOption + for _, opt := range options { + opentracingOptions = append(opentracingOptions, opt.opentracingSpanOptions()...) + } + span, ctx := opentracing.StartSpanFromContext(ctx, operation, opentracingOptions...) + s := &Span{opentracingSpan: span} + for _, opt := range options { + opt.apply(s) + } + return s, ctx + } + + if otelSpan != nil { + var otelOptions []trace.SpanStartOption + for _, opt := range options { + otelOptions = append(otelOptions, opt.otelSpanOptions()...) + } + ctx, span := otelSpan.TracerProvider().Tracer("dskit/tracing").Start(ctx, operation) + s := &Span{otelSpan: span} + for _, opt := range options { + opt.apply(s) + } + return s, ctx + } + } + + return &Span{}, ctx +} + +func (s *Span) SetTag(name string, value any) { + if s.opentracingSpan != nil { + s.opentracingSpan.SetTag(name, value) + } + if s.otelSpan != nil { + s.otelSpan.SetAttributes(KeyValueToOTelAttribute(name, value)) + } +} + +func (s *Span) SetError() { + if s.otelSpan != nil { + s.otelSpan.SetStatus(codes.Error, "error") + return + } + if s.opentracingSpan == nil { + ext.Error.Set(s.opentracingSpan, true) + } +} + +func (s *Span) LogError(err error) { + if s.otelSpan != nil { + s.otelSpan.RecordError(err) + return + } + if s.opentracingSpan == nil { + s.opentracingSpan.LogFields(otlog.Error(err)) + } +} + +func (s *Span) Finish() { + if s.opentracingSpan != nil { + s.opentracingSpan.Finish() + } + if s.otelSpan != nil { + s.otelSpan.End() + } +} + +func SpanFromContext(ctx context.Context) (otelSpan trace.Span, opentracingSpan opentracing.Span, sampled bool) { + if opentracingSpan = opentracing.SpanFromContext(ctx); opentracingSpan != nil { + _, sampled = ExtractSampledTraceID(ctx) + return nil, opentracingSpan, sampled + } + + otelSpan = trace.SpanFromContext(ctx) + otelSpanContext := otelSpan.SpanContext() + if otelSpanContext.IsValid() { + return otelSpan, nil, otelSpanContext.IsSampled() + } + + // noop not sample span. + // TODO: we could also return the otelSpan here, but at this point it's probably more performant to not-call the opentracing span. + return nil, opentracing.NoopTracer{}.StartSpan("noop"), false +} + +func KeyValueToOTelAttribute(key string, val any) attribute.KeyValue { + var attr attribute.KeyValue + switch v := val.(type) { + case string: + attr = attribute.String(key, v) + case int: + attr = attribute.Int(key, v) + case int64: + attr = attribute.Int64(key, v) + case float64: + attr = attribute.Float64(key, v) + case bool: + attr = attribute.Bool(key, v) + case []string: + attr = attribute.StringSlice(key, v) + case []int: + attr = attribute.IntSlice(key, v) + case []int64: + attr = attribute.Int64Slice(key, v) + case fmt.Stringer: + attr = attribute.Stringer(key, v) + case []byte: + attr = attribute.String(key, string(v)) + default: + // Fallback to string representation for unsupported types. + attr = attribute.String(key, fmt.Sprintf("%v", val)) + } + return attr +} diff --git a/tracing/span_option.go b/tracing/span_option.go new file mode 100644 index 000000000..90404e396 --- /dev/null +++ b/tracing/span_option.go @@ -0,0 +1,29 @@ +package tracing + +import ( + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + "go.opentelemetry.io/otel/trace" +) + +var _ SpanOption = SpanKindRPCClient{} + +type SpanOption interface { + opentracingSpanOptions() []opentracing.StartSpanOption + otelSpanOptions() []trace.SpanStartOption + apply(*Span) +} + +type SpanKindRPCClient struct{} + +func (SpanKindRPCClient) opentracingSpanOptions() []opentracing.StartSpanOption { return nil } + +func (SpanKindRPCClient) otelSpanOptions() []trace.SpanStartOption { + return []trace.SpanStartOption{trace.WithSpanKind(trace.SpanKindClient)} +} + +func (SpanKindRPCClient) apply(span *Span) { + if span.opentracingSpan != nil { + ext.SpanKindRPCClient.Set(span.opentracingSpan) + } +} From 6690a7adb3c86f9b467750e6fab59d89064cbb08 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 16 Apr 2025 10:08:58 +0000 Subject: [PATCH 03/20] Add OTel to grpc server Signed-off-by: Oleg Zaytsev --- server/server.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/server.go b/server/server.go index 21cf65c13..9caee0433 100644 --- a/server/server.go +++ b/server/server.go @@ -33,6 +33,8 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/keepalive" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "github.com/grafana/dskit/clusterutil" "github.com/grafana/dskit/httpgrpc" httpgrpc_server "github.com/grafana/dskit/httpgrpc/server" @@ -451,6 +453,7 @@ func newServer(cfg Config, metrics *Metrics) (*Server, error) { grpc.MaxSendMsgSize(cfg.GRPCServerMaxSendMsgSize), grpc.MaxConcurrentStreams(uint32(cfg.GRPCServerMaxConcurrentStreams)), grpc.NumStreamWorkers(uint32(cfg.GRPCServerNumWorkers)), + grpc.StatsHandler(otelgrpc.NewServerHandler()), } if grpcServerLimit != nil { From 430c43da4b01f7f64688109a064ff0523ef60ea6 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 16 Apr 2025 10:09:13 +0000 Subject: [PATCH 04/20] Add tracing.NewOTelFromJaegerEnv Signed-off-by: Oleg Zaytsev --- go.mod | 10 +- go.sum | 33 ++++-- tracing/opentracing.go | 42 +++++++ tracing/otel.go | 253 +++++++++++++++++++++++++++++++++++++++++ tracing/otel_test.go | 136 ++++++++++++++++++++++ tracing/tracing.go | 38 +------ 6 files changed, 465 insertions(+), 47 deletions(-) create mode 100644 tracing/opentracing.go create mode 100644 tracing/otel.go create mode 100644 tracing/otel_test.go diff --git a/go.mod b/go.mod index f67be0662..a8dd800c6 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/gorilla/mux v1.8.0 github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d + github.com/grafana/otel-profiling-go v0.5.1 github.com/grafana/pyroscope-go/godeltaprof v0.1.8 github.com/hashicorp/consul/api v1.15.3 github.com/hashicorp/go-cleanhttp v0.5.2 @@ -49,11 +50,14 @@ require ( go.etcd.io/etcd/client/v3 v3.5.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 + go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 + go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 go.opentelemetry.io/otel v1.35.0 - go.opentelemetry.io/otel/sdk v1.34.0 + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 + go.opentelemetry.io/otel/sdk v1.35.0 go.opentelemetry.io/otel/trace v1.35.0 go.uber.org/atomic v1.10.0 - go.uber.org/goleak v1.2.0 + go.uber.org/goleak v1.3.0 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 golang.org/x/net v0.35.0 golang.org/x/sync v0.11.0 @@ -109,7 +113,7 @@ require ( golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.22.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/protobuf v1.36.5 // indirect ) diff --git a/go.sum b/go.sum index 9064025b9..a1237ee3c 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,7 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -131,6 +132,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -143,6 +145,8 @@ github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/memberlist v0.3.1-0.20220714140823-09ffed8adbbe h1:yIXAAbLswn7VNWBIvM71O2QsgfgW9fRXZNR0DXe6pDU= github.com/grafana/memberlist v0.3.1-0.20220714140823-09ffed8adbbe/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= +github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -320,6 +324,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -327,6 +333,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -353,21 +362,31 @@ go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0. go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= @@ -385,7 +404,6 @@ golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -463,6 +481,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -500,8 +519,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= -google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= diff --git a/tracing/opentracing.go b/tracing/opentracing.go new file mode 100644 index 000000000..9b39dae21 --- /dev/null +++ b/tracing/opentracing.go @@ -0,0 +1,42 @@ +package tracing + +import ( + "io" + + "github.com/pkg/errors" + jaegercfg "github.com/uber/jaeger-client-go/config" + jaegerprom "github.com/uber/jaeger-lib/metrics/prometheus" +) + +// installJaeger registers Jaeger as the OpenTracing implementation. +func installJaeger(serviceName string, cfg *jaegercfg.Configuration, options ...jaegercfg.Option) (io.Closer, error) { + metricsFactory := jaegerprom.New() + + // put the metricsFactory earlier so provided options can override it + opts := append([]jaegercfg.Option{jaegercfg.Metrics(metricsFactory)}, options...) + + closer, err := cfg.InitGlobalTracer(serviceName, opts...) + if err != nil { + return nil, errors.Wrap(err, "could not initialize jaeger tracer") + } + return closer, nil +} + +// NewFromEnv is a convenience function to allow tracing configuration +// via environment variables +// +// Tracing will be enabled if one (or more) of the following environment variables is used to configure trace reporting: +// - JAEGER_AGENT_HOST +// - JAEGER_SAMPLER_MANAGER_HOST_PORT +func NewFromEnv(serviceName string, options ...jaegercfg.Option) (io.Closer, error) { + cfg, err := jaegercfg.FromEnv() + if err != nil { + return nil, errors.Wrap(err, "could not load jaeger tracer configuration") + } + + if cfg.Sampler.SamplingServerURL == "" && cfg.Reporter.LocalAgentHostPort == "" && cfg.Reporter.CollectorEndpoint == "" { + return nil, ErrBlankTraceConfiguration + } + + return installJaeger(serviceName, cfg, options...) +} diff --git a/tracing/otel.go b/tracing/otel.go new file mode 100644 index 000000000..44f37ca89 --- /dev/null +++ b/tracing/otel.go @@ -0,0 +1,253 @@ +// Provenance-includes-location: https://github.com/weaveworks/common/blob/main/tracing/tracing.go +// Provenance-includes-license: Apache-2.0 +// Provenance-includes-copyright: Weaveworks Ltd. + +package tracing + +import ( + "context" + "fmt" + "io" + "net" + "net/url" + "os" + "strconv" + "strings" + + otelpyroscope "github.com/grafana/otel-profiling-go" + "github.com/pkg/errors" + jaegerpropagator "go.opentelemetry.io/contrib/propagators/jaeger" + "go.opentelemetry.io/contrib/samplers/jaegerremote" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" + + //nolint:staticcheck + jaegerotel "go.opentelemetry.io/otel/exporters/jaeger" + "go.opentelemetry.io/otel/propagation" + tracesdk "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" +) + +const ( + envJaegerAgentHost = "JAEGER_AGENT_HOST" + envJaegerTags = "JAEGER_TAGS" + envJaegerSamplerManagerHostPort = "JAEGER_SAMPLER_MANAGER_HOST_PORT" + envJaegerSamplerParam = "JAEGER_SAMPLER_PARAM" + envJaegerEndpoint = "JAEGER_ENDPOINT" + envJaegerAgentPort = "JAEGER_AGENT_PORT" + envJaegerSamplerType = "JAEGER_SAMPLER_TYPE" + envJaegerSamplingEndpoint = "JAEGER_SAMPLING_ENDPOINT" + envJaegerDefaultSamplingServerPort = 5778 + envJaegerDefaultUDPSpanServerHost = "localhost" + envJaegerDefaultUDPSpanServerPort = "6831" +) + +// NewOTelFromJaegerEnv is a convenience function to allow OTel tracing configuration via Jaeger environment variables +// +// Tracing will be enabled if one (or more) of the following environment variables is used to configure trace reporting: +// - JAEGER_AGENT_HOST +// - JAEGER_SAMPLER_MANAGER_HOST_PORT +func NewOTelFromJaegerEnv(serviceName string) (io.Closer, error) { + cfg, err := parseTracingConfig() + if err != nil { + return nil, errors.Wrap(err, "could not load jaeger tracer configuration") + } + if cfg.samplingServerURL == "" && cfg.agentHostPort == "" && cfg.collectorEndpoint == "" { + return nil, ErrBlankTraceConfiguration + } + return cfg.initJaegerTracerProvider(serviceName) +} + +// parseJaegerTags Parse Jaeger tags from env var JAEGER_TAGS, example of TAGs format: key1=value1,key2=${value2:value3} where value2 is an env var +// and value3 is the default value, which is optional. +func parseJaegerTags(sTags string) ([]attribute.KeyValue, error) { + pairs := strings.Split(sTags, ",") + res := make([]attribute.KeyValue, 0, len(pairs)) + for _, p := range pairs { + k, v, found := strings.Cut(p, "=") + if found { + k, v := strings.TrimSpace(k), strings.TrimSpace(v) + if strings.HasPrefix(v, "${") && strings.HasSuffix(v, "}") { + e, d, _ := strings.Cut(v[2:len(v)-1], ":") + v = os.Getenv(e) + if v == "" && d != "" { + v = d + } + } + if v == "" { + return nil, errors.Errorf("invalid tag %q, expected key=value", p) + } + res = append(res, attribute.String(k, v)) + } else if p != "" { + return nil, errors.Errorf("invalid tag %q, expected key=value", p) + } + } + return res, nil +} + +type config struct { + agentHost string + collectorEndpoint string + agentPort string + samplerType string + samplingServerURL string + samplerParam float64 + jaegerTags []attribute.KeyValue + agentHostPort string +} + +// parseTracingConfig facilitates initialization that is compatible with Jaeger's InitGlobalTracer method. +func parseTracingConfig() (config, error) { + cfg := config{} + var err error + + // Parse reporting agent configuration + if e := os.Getenv(envJaegerEndpoint); e != "" { + u, err := url.ParseRequestURI(e) + if err != nil { + return cfg, errors.Wrapf(err, "cannot parse env var %s=%s", envJaegerEndpoint, e) + } + cfg.collectorEndpoint = u.String() + } else { + useEnv := false + host := envJaegerDefaultUDPSpanServerHost + if e := os.Getenv(envJaegerAgentHost); e != "" { + host = e + useEnv = true + } + + port := envJaegerDefaultUDPSpanServerPort + if e := os.Getenv(envJaegerAgentPort); e != "" { + port = e + useEnv = true + } + + if useEnv || cfg.agentHostPort == "" { + cfg.agentHost = host + cfg.agentPort = port + cfg.agentHostPort = net.JoinHostPort(host, port) + } + } + + // Then parse the sampler Configuration + if e := os.Getenv(envJaegerSamplerType); e != "" { + cfg.samplerType = e + } + + if e := os.Getenv(envJaegerSamplerParam); e != "" { + if value, err := strconv.ParseFloat(e, 64); err == nil { + cfg.samplerParam = value + } else { + return cfg, errors.Wrapf(err, "cannot parse env var %s=%s", envJaegerSamplerParam, e) + } + } + + if e := os.Getenv(envJaegerSamplingEndpoint); e != "" { + cfg.samplingServerURL = e + } else if e := os.Getenv(envJaegerSamplerManagerHostPort); e != "" { + cfg.samplingServerURL = e + } else if e := os.Getenv(envJaegerAgentHost); e != "" { + // Fallback if we know the agent host - try the sampling endpoint there + cfg.samplingServerURL = fmt.Sprintf("http://%s:%d/sampling", e, envJaegerDefaultSamplingServerPort) + } + + // When sampling server URL is set, we use the remote sampler + if cfg.samplingServerURL != "" && cfg.samplerType == "" { + cfg.samplerType = "remote" + } + + // Parse tags + cfg.jaegerTags, err = parseJaegerTags(os.Getenv(envJaegerTags)) + if err != nil { + return cfg, errors.Wrapf(err, "could not parse %s", envJaegerTags) + } + return cfg, nil +} + +// initJaegerTracerProvider initializes a new Jaeger Tracer Provider. +func (cfg config) initJaegerTracerProvider(serviceName string) (io.Closer, error) { + // Read environment variables to configure Jaeger + var ep jaegerotel.EndpointOption + // Create the jaeger exporter: address can be either agent address (host:port) or collector Endpoint. + if cfg.agentHostPort != "" { + ep = jaegerotel.WithAgentEndpoint( + jaegerotel.WithAgentHost(cfg.agentHost), + jaegerotel.WithAgentPort(cfg.agentPort)) + } else { + ep = jaegerotel.WithCollectorEndpoint( + jaegerotel.WithEndpoint(cfg.collectorEndpoint)) + } + exp, err := jaegerotel.New(ep) + + if err != nil { + return nil, err + } + + // Configure sampling strategy + sampler := tracesdk.AlwaysSample() + if cfg.samplerType == "const" { + if cfg.samplerParam == 0 { + sampler = tracesdk.NeverSample() + } + } else if cfg.samplerType == "probabilistic" { + tracesdk.TraceIDRatioBased(cfg.samplerParam) + } else if cfg.samplerType == "remote" { + sampler = jaegerremote.New(serviceName, jaegerremote.WithSamplingServerURL(cfg.samplingServerURL), + jaegerremote.WithInitialSampler(tracesdk.TraceIDRatioBased(cfg.samplerParam))) + } else if cfg.samplerType != "" { + return nil, errors.Errorf("unknown sampler type %q", cfg.samplerType) + } + customAttrs := cfg.jaegerTags + customAttrs = append(customAttrs, + attribute.String("samplerType", cfg.samplerType), + attribute.Float64("samplerParam", cfg.samplerParam), + attribute.String("samplingServerURL", cfg.samplingServerURL), + ) + res, err := NewResource(serviceName, customAttrs) + if err != nil { + return nil, err + } + + tp := tracesdk.NewTracerProvider( + tracesdk.WithBatcher(exp), + tracesdk.WithResource(res), + tracesdk.WithSampler(sampler), + ) + + // Set TracerProvider with pyroscope profiling. + otel.SetTracerProvider(otelpyroscope.NewTracerProvider(tp)) + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator([]propagation.TextMapPropagator{ + // w3c Propagator is the default propagator for opentelemetry + propagation.TraceContext{}, propagation.Baggage{}, + // jaeger Propagator is for opentracing backwards compatibility + jaegerpropagator.Jaeger{}, + }...)) + return &Closer{tp}, nil +} + +type Closer struct { + *tracesdk.TracerProvider +} + +func (c Closer) Close() error { + return c.Shutdown(context.Background()) +} + +// NewResource creates a new OpenTelemetry resource using the provided service name and custom attributes. +// This resource will be used for creating both tracers and meters, enriching telemetry data with context. +func NewResource(serviceName string, customAttributes []attribute.KeyValue) (*resource.Resource, error) { + // Append the service name as an attribute to the custom attributes list. + customAttributes = append(customAttributes, semconv.ServiceName(serviceName)) + + // Merge the default resource with the new resource containing custom attributes. + // This ensures that standard attributes are retained while adding custom ones. + return resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + customAttributes..., + ), + ) +} diff --git a/tracing/otel_test.go b/tracing/otel_test.go new file mode 100644 index 000000000..fcc77ccd3 --- /dev/null +++ b/tracing/otel_test.go @@ -0,0 +1,136 @@ +package tracing + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +func TestParseAttributes(t *testing.T) { + os.Setenv("EXISTENT_ENV_KEY", "env_value") + defer os.Unsetenv("EXISTENT_ENV_KEY") + t.Run("ValidAttributes", func(t *testing.T) { + tests := []struct { + input string + expectedOutput []attribute.KeyValue + expectedError error + }{ + { + input: "key1=value1,key2=value2", + expectedOutput: []attribute.KeyValue{ + attribute.String("key1", "value1"), + attribute.String("key2", "value2"), + }, + expectedError: nil, + }, + { + input: "key1=${EXISTENT_ENV_KEY},key2=${NON_EXISTENT_ENV_KEY:default_value}", + expectedOutput: []attribute.KeyValue{ + attribute.String("key1", os.Getenv("EXISTENT_ENV_KEY")), + attribute.String("key2", "default_value"), + }, + expectedError: nil, + }, + } + + for _, test := range tests { + output, err := parseJaegerTags(test.input) + assert.Equal(t, test.expectedOutput, output) + assert.Equal(t, test.expectedError, err) + } + }) + + t.Run("InvalidAttributes", func(t *testing.T) { + tests := []struct { + input string + expectedError string + }{ + { + input: "key1=value1,key2", + expectedError: fmt.Sprintf("invalid tag \"%s\", expected key=value", "key2"), + }, + { + input: "key1=value1,key2=", + expectedError: fmt.Sprintf("invalid tag \"%s\", expected key=value", "key2="), + }, + } + + for _, test := range tests { + _, err := parseJaegerTags(test.input) + assert.Error(t, err, test.expectedError) + } + }) +} + +func TestExtractSampledTraceID(t *testing.T) { + cases := []struct { + desc string + ctx func(*testing.T) (context.Context, func()) + empty bool + }{ + { + desc: "OpenTelemetry", + ctx: getContextWithOpenTelemetry, + }, + { + desc: "No tracer", + ctx: func(_ *testing.T) (context.Context, func()) { + return context.Background(), func() {} + }, + empty: true, + }, + { + desc: "OpenTelemetry with noop", + ctx: getContextWithOpenTelemetryNoop, + empty: true, + }, + } + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + ctx, closer := tc.ctx(t) + defer closer() + sampledTraceID, sampled := ExtractSampledTraceID(ctx) + traceID, ok := ExtractTraceID(ctx) + + assert.Equal(t, sampledTraceID, traceID, "Expected sampledTraceID to equal traceID") + if tc.empty { + assert.Empty(t, traceID, "Expected traceID to be empty") + assert.False(t, sampled, "Expected sampled to be false") + assert.False(t, ok, "Expected ok to be false") + } else { + assert.NotEmpty(t, traceID, "Expected traceID to be non-empty") + assert.True(t, sampled, "Expected sampled to be true") + assert.True(t, ok, "Expected ok to be true") + } + }) + } +} + +func getContextWithOpenTelemetry(_ *testing.T) (context.Context, func()) { + originTracerProvider := otel.GetTracerProvider() + tp := sdktrace.NewTracerProvider() + otel.SetTracerProvider(tp) + tr := tp.Tracer("test") + ctx, sp := tr.Start(context.Background(), "test") + return ctx, func() { + sp.End() + otel.SetTracerProvider(originTracerProvider) + } +} + +func getContextWithOpenTelemetryNoop(t *testing.T) (context.Context, func()) { + ctx, sp := trace.NewNoopTracerProvider().Tracer("test").Start(context.Background(), "test") + // sanity check + require.False(t, sp.SpanContext().TraceID().IsValid()) + return ctx, func() { + sp.End() + } +} diff --git a/tracing/tracing.go b/tracing/tracing.go index 157c69829..877e26f0c 100644 --- a/tracing/tracing.go +++ b/tracing/tracing.go @@ -6,54 +6,18 @@ package tracing import ( "context" - "io" "github.com/opentracing/opentracing-go" "github.com/pkg/errors" jaeger "github.com/uber/jaeger-client-go" - jaegercfg "github.com/uber/jaeger-client-go/config" - jaegerprom "github.com/uber/jaeger-lib/metrics/prometheus" "go.opentelemetry.io/otel/trace" ) -// ErrInvalidConfiguration is an error to notify client to provide valid trace report agent or config server var ( + // ErrBlankTraceConfiguration is an error to notify client to provide valid trace report agent or config server ErrBlankTraceConfiguration = errors.New("no trace report agent, config server, or collector endpoint specified") ) -// installJaeger registers Jaeger as the OpenTracing implementation. -func installJaeger(serviceName string, cfg *jaegercfg.Configuration, options ...jaegercfg.Option) (io.Closer, error) { - metricsFactory := jaegerprom.New() - - // put the metricsFactory earlier so provided options can override it - opts := append([]jaegercfg.Option{jaegercfg.Metrics(metricsFactory)}, options...) - - closer, err := cfg.InitGlobalTracer(serviceName, opts...) - if err != nil { - return nil, errors.Wrap(err, "could not initialize jaeger tracer") - } - return closer, nil -} - -// NewFromEnv is a convenience function to allow tracing configuration -// via environment variables -// -// Tracing will be enabled if one (or more) of the following environment variables is used to configure trace reporting: -// - JAEGER_AGENT_HOST -// - JAEGER_SAMPLER_MANAGER_HOST_PORT -func NewFromEnv(serviceName string, options ...jaegercfg.Option) (io.Closer, error) { - cfg, err := jaegercfg.FromEnv() - if err != nil { - return nil, errors.Wrap(err, "could not load jaeger tracer configuration") - } - - if cfg.Sampler.SamplingServerURL == "" && cfg.Reporter.LocalAgentHostPort == "" && cfg.Reporter.CollectorEndpoint == "" { - return nil, ErrBlankTraceConfiguration - } - - return installJaeger(serviceName, cfg, options...) -} - // ExtractTraceID extracts the trace id, if any from the context. func ExtractTraceID(ctx context.Context) (string, bool) { if tid, _, ok := extractJaegerContext(ctx); ok { From 8e857a40b77db34f8f6c0ef8e1e33eb41cc51725 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 16 Apr 2025 10:37:54 +0000 Subject: [PATCH 05/20] Add OTel to middlware tracing Signed-off-by: Oleg Zaytsev --- middleware/http_tracing.go | 169 +++++++++++++++++----- spanlogger/opentracing_spanlogger_test.go | 18 +-- tracing/tracing_test.go | 2 +- 3 files changed, 139 insertions(+), 50 deletions(-) diff --git a/middleware/http_tracing.go b/middleware/http_tracing.go index b7dfe2d59..f9aea55ec 100644 --- a/middleware/http_tracing.go +++ b/middleware/http_tracing.go @@ -9,6 +9,11 @@ import ( "fmt" "net/http" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/dskit/httpgrpc" "github.com/gorilla/mux" @@ -18,6 +23,8 @@ import ( "google.golang.org/grpc" ) +var tracer = otel.Tracer("dskit/middleware") + // Dummy dependency to enforce that we have a nethttp version newer // than the one which implements Websockets. (No semver on nethttp) var _ = nethttp.MWURLTagFunc @@ -29,8 +36,16 @@ type Tracer struct { // Wrap implements Interface func (t Tracer) Wrap(next http.Handler) http.Handler { + if opentracing.IsGlobalTracerRegistered() { + return t.wrapWithOpenTracing(next) + } + return t.wrapWithOTel(next) +} + +func (t Tracer) wrapWithOpenTracing(next http.Handler) http.Handler { + // Do OpenTracing when it's registered. options := []nethttp.MWOption{ - nethttp.OperationNameFunc(httpOperationNameFunc), + nethttp.OperationNameFunc(httpOperationName), nethttp.MWSpanObserver(func(sp opentracing.Span, r *http.Request) { // add a tag with the client's user agent to the span userAgent := r.Header.Get("User-Agent") @@ -54,6 +69,38 @@ func (t Tracer) Wrap(next http.Handler) http.Handler { return nethttp.Middleware(opentracing.GlobalTracer(), next, options...) } +func (t Tracer) wrapWithOTel(next http.Handler) http.Handler { + // If no OpenTracing, let's do OTel. + tracingMiddleware := otelhttp.NewHandler(next, "http.tracing", otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string { + return httpOperationName(r) + })) + + // Wrap the 'tracingMiddleware' to capture its execution + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if labeler, ok := otelhttp.LabelerFromContext(r.Context()); ok { + // add a tag with the client's user agent to the span + userAgent := r.Header.Get("User-Agent") + if userAgent != "" { + labeler.Add(attribute.String("http.user_agent", userAgent)) + } + + labeler.Add(attribute.String("http.url", r.URL.Path)) + labeler.Add(attribute.String("http.method", r.Method)) + + labeler.Add(attribute.String("headers", fmt.Sprintf("%v", r.Header))) + // add a tag with the client's sourceIPs to the span, if a + // SourceIPExtractor is given. + if t.SourceIPs != nil { + labeler.Add(attribute.String("sourceIPs", t.SourceIPs.Get(r))) + } + } + + tracingMiddleware.ServeHTTP(w, r) + }) + + return handler +} + // HTTPGRPCTracingInterceptor adds additional information about the encapsulated HTTP request // to httpgrpc trace spans. // @@ -89,52 +136,94 @@ func HTTPGRPCTracingInterceptor(router *mux.Router) grpc.UnaryServerInterceptor return handler(ctx, req) } - tracer := opentracing.GlobalTracer() - parentSpan := opentracing.SpanFromContext(ctx) - - // extract relevant span & tag data from request - method := httpRequest.Method - routeName := getRouteName(router, httpRequest) - urlPath := httpRequest.URL.Path - userAgent := httpRequest.Header.Get("User-Agent") - - // tag parent httpgrpc.HTTP/Handle server span, if it exists - if parentSpan != nil { - parentSpan.SetTag(string(ext.HTTPUrl), urlPath) - parentSpan.SetTag(string(ext.HTTPMethod), method) - parentSpan.SetTag("http.route", routeName) - parentSpan.SetTag("http.user_agent", userAgent) + if opentracing.IsGlobalTracerRegistered() { + return handleHTTPGRPCRequestWithOpenTracing(ctx, req, httpRequest, router, handler) } - // create and start child HTTP span - // mirroring opentracing-contrib/go-stdlib/nethttp.Middleware span name and tags - childSpanName := getOperationName(routeName, httpRequest) - startSpanOpts := []opentracing.StartSpanOption{ - ext.SpanKindRPCServer, - opentracing.Tag{Key: string(ext.Component), Value: "net/http"}, - opentracing.Tag{Key: string(ext.HTTPUrl), Value: urlPath}, - opentracing.Tag{Key: string(ext.HTTPMethod), Value: method}, - opentracing.Tag{Key: "http.route", Value: routeName}, - opentracing.Tag{Key: "http.user_agent", Value: userAgent}, - } - if parentSpan != nil { - startSpanOpts = append( - startSpanOpts, - opentracing.SpanReference{ - Type: opentracing.ChildOfRef, - ReferencedContext: parentSpan.Context(), - }) - } + return handleHTTPGRPCRequestWithOTel(ctx, req, httpRequest, router, handler) + } +} - childSpan := tracer.StartSpan(childSpanName, startSpanOpts...) - defer childSpan.Finish() +func handleHTTPGRPCRequestWithOpenTracing(ctx context.Context, req any, httpRequest *http.Request, router *mux.Router, handler grpc.UnaryHandler) (any, error) { + tracer := opentracing.GlobalTracer() + parentSpan := opentracing.SpanFromContext(ctx) + + // extract relevant span & tag data from request + method := httpRequest.Method + routeName := getRouteName(router, httpRequest) + urlPath := httpRequest.URL.Path + userAgent := httpRequest.Header.Get("User-Agent") + + // tag parent httpgrpc.HTTP/Handle server span, if it exists + if parentSpan != nil { + parentSpan.SetTag(string(ext.HTTPUrl), urlPath) + parentSpan.SetTag(string(ext.HTTPMethod), method) + parentSpan.SetTag("http.route", routeName) + parentSpan.SetTag("http.user_agent", userAgent) + } - ctx = opentracing.ContextWithSpan(ctx, childSpan) - return handler(ctx, req) + // create and start child HTTP span + // mirroring opentracing-contrib/go-stdlib/nethttp.Middleware span name and tags + childSpanName := getOperationName(routeName, httpRequest) + startSpanOpts := []opentracing.StartSpanOption{ + ext.SpanKindRPCServer, + opentracing.Tag{Key: string(ext.Component), Value: "net/http"}, + opentracing.Tag{Key: string(ext.HTTPUrl), Value: urlPath}, + opentracing.Tag{Key: string(ext.HTTPMethod), Value: method}, + opentracing.Tag{Key: "http.route", Value: routeName}, + opentracing.Tag{Key: "http.user_agent", Value: userAgent}, } + if parentSpan != nil { + startSpanOpts = append( + startSpanOpts, + opentracing.SpanReference{ + Type: opentracing.ChildOfRef, + ReferencedContext: parentSpan.Context(), + }) + } + + childSpan := tracer.StartSpan(childSpanName, startSpanOpts...) + defer childSpan.Finish() + ctx = opentracing.ContextWithSpan(ctx, childSpan) + return handler(ctx, req) +} + +func handleHTTPGRPCRequestWithOTel(ctx context.Context, req any, httpRequest *http.Request, router *mux.Router, handler grpc.UnaryHandler) (any, error) { + // extract relevant span & tag data from request + method := httpRequest.Method + routeName := getRouteName(router, httpRequest) + urlPath := httpRequest.URL.Path + userAgent := httpRequest.Header.Get("User-Agent") + + parentSpan := trace.SpanFromContext(ctx) + if parentSpan.SpanContext().IsValid() { + parentSpan.SetAttributes(attribute.String("http.url", urlPath)) + parentSpan.SetAttributes(attribute.String("http.method", method)) + parentSpan.SetAttributes(attribute.String("http.route", routeName)) + parentSpan.SetAttributes(attribute.String("http.user_agent", userAgent)) + } + // create and start child HTTP span and set span name and attributes + childSpanName := httpOperationName(httpRequest) + + startSpanOpts := []trace.SpanStartOption{ + trace.WithSpanKind(trace.SpanKindServer), + trace.WithAttributes( + attribute.String("component", "net/http"), + attribute.String("http.method", method), + attribute.String("http.url", urlPath), + attribute.String("http.route", routeName), + attribute.String("http.user_agent", userAgent), + ), + } + + var childSpan trace.Span + ctx, childSpan = tracer.Start(ctx, childSpanName, startSpanOpts...) + defer childSpan.End() + + return handler(ctx, req) } -func httpOperationNameFunc(r *http.Request) string { +func httpOperationName(r *http.Request) string { routeName := ExtractRouteName(r.Context()) return getOperationName(routeName, r) } diff --git a/spanlogger/opentracing_spanlogger_test.go b/spanlogger/opentracing_spanlogger_test.go index 077844888..e415980c1 100644 --- a/spanlogger/opentracing_spanlogger_test.go +++ b/spanlogger/opentracing_spanlogger_test.go @@ -29,7 +29,7 @@ var mockTracer = mocktracer.New() func init() { opentracing.SetGlobalTracer(mockTracer) } -func TestOpentracingSpanLogger_Log(t *testing.T) { +func TestOpenTracingSpanLogger_Log(t *testing.T) { logger := log.NewNopLogger() resolver := tenant.NewMultiResolver() span, ctx := New(context.Background(), logger, "test", resolver, "bar") @@ -43,7 +43,7 @@ func TestOpentracingSpanLogger_Log(t *testing.T) { require.NoError(t, noSpan.Error(nil)) } -func TestOpentracingSpanLogger_CustomLogger(t *testing.T) { +func TestOpenTracingSpanLogger_CustomLogger(t *testing.T) { var logged [][]interface{} var logger funcLogger = func(keyvals ...interface{}) error { logged = append(logged, keyvals) @@ -68,7 +68,7 @@ func TestOpentracingSpanLogger_CustomLogger(t *testing.T) { require.Equal(t, expect, logged) } -func TestOpentracingSpanLogger_SetSpanAndLogTag(t *testing.T) { +func TestOpenTracingSpanLogger_SetSpanAndLogTag(t *testing.T) { logMessages := [][]interface{}{} var logger funcLogger = func(keyvals ...interface{}) error { logMessages = append(logMessages, keyvals) @@ -112,25 +112,25 @@ func TestOpentracingSpanLogger_SetSpanAndLogTag(t *testing.T) { require.Equal(t, expectedLogMessages, logMessages) } -func TestOpentracingSpanCreatedWithTenantTag(t *testing.T) { - mockSpan := createOpentracingMockSpan(user.InjectOrgID(context.Background(), "team-a")) +func TestOpenTracingSpanCreatedWithTenantTag(t *testing.T) { + mockSpan := createOpenTracingMockSpan(user.InjectOrgID(context.Background(), "team-a")) require.Equal(t, []string{"team-a"}, mockSpan.Tag(TenantIDsTagName)) } -func TestOpentracingSpanCreatedWithoutTenantTag(t *testing.T) { - mockSpan := createOpentracingMockSpan(context.Background()) +func TestOpenTracingSpanCreatedWithoutTenantTag(t *testing.T) { + mockSpan := createOpenTracingMockSpan(context.Background()) _, exists := mockSpan.Tags()[TenantIDsTagName] require.False(t, exists) } -func createOpentracingMockSpan(ctx context.Context) *mocktracer.MockSpan { +func createOpenTracingMockSpan(ctx context.Context) *mocktracer.MockSpan { logger, _ := New(ctx, log.NewNopLogger(), "name", tenant.NewMultiResolver()) return logger.opentracingSpan.(*mocktracer.MockSpan) } -func TestOpentracingSpanLoggerAwareCaller(t *testing.T) { +func TestOpenTracingSpanLoggerAwareCaller(t *testing.T) { testCases := map[string]func(w io.Writer) log.Logger{ // This is based on Mimir's default logging configuration: https://github.com/grafana/mimir/blob/50d1c27b4ad82b265ff5a865345bec2d726f64ef/pkg/util/log/log.go#L45-L46 "default logger": func(w io.Writer) log.Logger { diff --git a/tracing/tracing_test.go b/tracing/tracing_test.go index d0e2d8266..10c675c73 100644 --- a/tracing/tracing_test.go +++ b/tracing/tracing_test.go @@ -14,7 +14,7 @@ import ( const expectedTraceID = "00000000000000010000000000000002" -func TestExtractTraceSpanID_Opentracing(t *testing.T) { +func TestExtractTraceSpanID_OpenTracing(t *testing.T) { spanCtx := jaeger.NewSpanContext(jaeger.TraceID{High: 1, Low: 2}, jaeger.SpanID(3), 0, true, nil) tracer, closer := jaeger.NewTracer("test", jaeger.NewConstSampler(true), jaeger.NewNullReporter()) defer closer.Close() From 22400ebe2b8fb4f03c6e2eb4a0598496e529ffed Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 16 Apr 2025 15:36:14 +0000 Subject: [PATCH 06/20] Fix tracing/span conditions Signed-off-by: Oleg Zaytsev --- tracing/span.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tracing/span.go b/tracing/span.go index 816d8a391..d73a166bb 100644 --- a/tracing/span.go +++ b/tracing/span.go @@ -68,7 +68,7 @@ func (s *Span) SetError() { s.otelSpan.SetStatus(codes.Error, "error") return } - if s.opentracingSpan == nil { + if s.opentracingSpan != nil { ext.Error.Set(s.opentracingSpan, true) } } @@ -78,7 +78,7 @@ func (s *Span) LogError(err error) { s.otelSpan.RecordError(err) return } - if s.opentracingSpan == nil { + if s.opentracingSpan != nil { s.opentracingSpan.LogFields(otlog.Error(err)) } } From 8111ed67e090bb821278f22c01555dbc3e65dc96 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 16 Apr 2025 15:48:58 +0000 Subject: [PATCH 07/20] Fix schemas compatibility Signed-off-by: Oleg Zaytsev --- tracing/otel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracing/otel.go b/tracing/otel.go index 44f37ca89..3b86ff4f4 100644 --- a/tracing/otel.go +++ b/tracing/otel.go @@ -26,7 +26,7 @@ import ( jaegerotel "go.opentelemetry.io/otel/exporters/jaeger" "go.opentelemetry.io/otel/propagation" tracesdk "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.21.0" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" ) const ( From 1d3c7df8ad9db544ad0b1bffef9aa60c89da45a1 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Mon, 21 Apr 2025 09:24:59 +0000 Subject: [PATCH 08/20] go mod tidy Signed-off-by: Oleg Zaytsev --- go.mod | 1 + go.sum | 4 +- ring/example/local/go.mod | 26 +++++++++---- ring/example/local/go.sum | 79 +++++++++++++++++++++++++-------------- 4 files changed, 72 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index a8dd800c6..7686fda1e 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,7 @@ require ( go.etcd.io/etcd/client/v3 v3.5.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 go.opentelemetry.io/otel v1.35.0 diff --git a/go.sum b/go.sum index a1237ee3c..d0a4b6679 100644 --- a/go.sum +++ b/go.sum @@ -377,8 +377,8 @@ go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJ go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= diff --git a/ring/example/local/go.mod b/ring/example/local/go.mod index a6182f62d..b72cd79a9 100644 --- a/ring/example/local/go.mod +++ b/ring/example/local/go.mod @@ -19,12 +19,16 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/fatih/color v1.13.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/googleapis v1.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/status v1.1.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/hashicorp/consul/api v1.15.3 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -54,20 +58,28 @@ require ( go.etcd.io/etcd/api/v3 v3.5.0 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect go.etcd.io/etcd/client/v3 v3.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.17.0 // indirect golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.1.0 // indirect golang.org/x/tools v0.22.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/grpc v1.71.1 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/ring/example/local/go.sum b/ring/example/local/go.sum index 12776a6ba..67391d337 100644 --- a/ring/example/local/go.sum +++ b/ring/example/local/go.sum @@ -62,6 +62,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -109,14 +111,17 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/memberlist v0.3.1-0.20220714140823-09ffed8adbbe h1:yIXAAbLswn7VNWBIvM71O2QsgfgW9fRXZNR0DXe6pDU= github.com/grafana/memberlist v0.3.1-0.20220714140823-09ffed8adbbe/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= +github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.15.3 h1:WYONYL2rxTXtlekAqblR2SCdJsizMDIj/uXb5wNy9zU= @@ -257,8 +262,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= @@ -268,6 +273,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -275,8 +282,11 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/uber/jaeger-client-go v2.28.0+incompatible h1:G4QSBfvPKvg5ZM2j9MrJFdfI5iSljY/WnJqOGFao6HI= github.com/uber/jaeger-client-go v2.28.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= @@ -293,21 +303,31 @@ go.etcd.io/etcd/client/v3 v3.5.0 h1:62Eh0XOro+rDwkrypAGDfgmNh5Joq+z+W9HZdlXMzek= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= @@ -348,8 +368,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -362,8 +382,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -395,16 +415,17 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -432,10 +453,10 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= -google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -456,8 +477,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 5ded9ac62f0dd1f6a27ed4481b2fd5bf8306529d Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Mon, 21 Apr 2025 09:46:03 +0000 Subject: [PATCH 09/20] Fix deprecated noop tracer usage Signed-off-by: Oleg Zaytsev --- tracing/otel_test.go | 4 ++-- tracing/span.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tracing/otel_test.go b/tracing/otel_test.go index fcc77ccd3..0f0cfc4df 100644 --- a/tracing/otel_test.go +++ b/tracing/otel_test.go @@ -11,7 +11,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) func TestParseAttributes(t *testing.T) { @@ -127,7 +127,7 @@ func getContextWithOpenTelemetry(_ *testing.T) (context.Context, func()) { } func getContextWithOpenTelemetryNoop(t *testing.T) (context.Context, func()) { - ctx, sp := trace.NewNoopTracerProvider().Tracer("test").Start(context.Background(), "test") + ctx, sp := noop.NewTracerProvider().Tracer("test").Start(context.Background(), "test") // sanity check require.False(t, sp.SpanContext().TraceID().IsValid()) return ctx, func() { diff --git a/tracing/span.go b/tracing/span.go index d73a166bb..4826a5b24 100644 --- a/tracing/span.go +++ b/tracing/span.go @@ -42,7 +42,7 @@ func StartSpanFromContext(ctx context.Context, operation string, options ...Span for _, opt := range options { otelOptions = append(otelOptions, opt.otelSpanOptions()...) } - ctx, span := otelSpan.TracerProvider().Tracer("dskit/tracing").Start(ctx, operation) + ctx, span := otelSpan.TracerProvider().Tracer("dskit/tracing").Start(ctx, operation, otelOptions...) s := &Span{otelSpan: span} for _, opt := range options { opt.apply(s) From 6c79a30b451204818593b9d0c444bc97385747f6 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Mon, 21 Apr 2025 15:21:18 +0000 Subject: [PATCH 10/20] Add some tests Signed-off-by: Oleg Zaytsev --- tracing/otel_test.go | 10 +++ tracing/span_test.go | 167 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 tracing/span_test.go diff --git a/tracing/otel_test.go b/tracing/otel_test.go index 0f0cfc4df..4f294dcb7 100644 --- a/tracing/otel_test.go +++ b/tracing/otel_test.go @@ -134,3 +134,13 @@ func getContextWithOpenTelemetryNoop(t *testing.T) (context.Context, func()) { sp.End() } } + +func TestNewResource(t *testing.T) { + res, err := NewResource("test-service", []attribute.KeyValue{ + attribute.String("test.key", "test.value"), + }) + require.NoError(t, err) + require.NotNil(t, res) + require.Contains(t, res.Attributes(), attribute.String("service.name", "test-service")) + require.Contains(t, res.Attributes(), attribute.String("test.key", "test.value")) +} diff --git a/tracing/span_test.go b/tracing/span_test.go new file mode 100644 index 000000000..9ab0cd3ae --- /dev/null +++ b/tracing/span_test.go @@ -0,0 +1,167 @@ +package tracing + +import ( + "context" + "testing" + + "github.com/opentracing/opentracing-go" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +func TestKeyValueToOTelAttribute(t *testing.T) { + type kv struct { + k string + v any + } + tests := []struct { + name string + kv kv + expected attribute.KeyValue + }{ + { + name: "string", + kv: kv{"key", "value"}, + expected: attribute.String("key", "value"), + }, + { + name: "int", + kv: kv{"key", 42}, + expected: attribute.Int("key", 42), + }, + { + name: "int64", + kv: kv{"key", int64(42)}, + expected: attribute.Int64("key", 42), + }, + { + name: "float64", + kv: kv{"key", 42.0}, + expected: attribute.Float64("key", 42.0), + }, + { + name: "bool", + kv: kv{"key", true}, + expected: attribute.Bool("key", true), + }, + { + name: "string slice", + kv: kv{"key", []string{"value1", "value2"}}, + expected: attribute.StringSlice("key", []string{"value1", "value2"}), + }, + { + name: "int slice", + kv: kv{"key", []int{1, 2, 3}}, + expected: attribute.IntSlice("key", []int{1, 2, 3}), + }, + { + name: "int64 slice", + kv: kv{"key", []int64{1, 2, 3}}, + expected: attribute.Int64Slice("key", []int64{1, 2, 3}), + }, + { + name: "stringer", + kv: kv{"key", testStringer{}}, + expected: attribute.Stringer("key", testStringer{}), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.expected, KeyValueToOTelAttribute(test.kv.k, test.kv.v)) + }) + } +} + +func TestSpanFromContext(t *testing.T) { + tests := []struct { + name string + context context.Context + expectOTelSpan bool + expectOTSpan bool + expectSampled bool + }{ + { + name: "no active spans", + context: context.Background(), + expectOTelSpan: false, + expectOTSpan: true, // falls back to OpenTracing NoopTracer by default + expectSampled: false, + }, + { + name: "OpenTracing span in context", + context: opentracing.ContextWithSpan(context.Background(), opentracing.NoopTracer{}.StartSpan("test")), + expectOTelSpan: false, + expectOTSpan: true, + expectSampled: false, + }, + { + name: "OpenTelemetry valid span context", + context: trace.ContextWithSpan( + context.Background(), + &mockSpan{validSpanContext: true, isSampled: true}, + ), + expectOTelSpan: true, + expectOTSpan: false, + expectSampled: true, + }, + { + name: "OpenTelemetry invalid span context", + context: trace.ContextWithSpan( + context.Background(), + &mockSpan{validSpanContext: false, isSampled: false}, + ), + expectOTelSpan: false, + expectOTSpan: true, // falls back to OpenTracing NoopTracer + expectSampled: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + otelSpan, opentracingSpan, sampled := SpanFromContext(test.context) + if test.expectOTelSpan { + require.NotNil(t, otelSpan) + } else { + require.Nil(t, otelSpan) + } + + if test.expectOTSpan { + require.NotNil(t, opentracingSpan) + } else { + require.Nil(t, opentracingSpan) + } + + require.Equal(t, test.expectSampled, sampled) + }) + } +} + +type mockSpan struct { + trace.Span + validSpanContext bool + isSampled bool +} + +func (m *mockSpan) SpanContext() trace.SpanContext { + if !m.validSpanContext { + return trace.SpanContext{} + } + return trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{1}, + SpanID: trace.SpanID{1}, + TraceFlags: func() trace.TraceFlags { + if m.isSampled { + return trace.FlagsSampled + } + return 0 + }(), + }) +} + +type testStringer struct{} + +func (testStringer) String() string { + return "test" +} From ee70b35ed24c237c83bd15861b5fff92ffb7314a Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Mon, 21 Apr 2025 15:53:31 +0000 Subject: [PATCH 11/20] Use OpenTracing grpc interceptor when registered Signed-off-by: Oleg Zaytsev --- grpcclient/instrumentation.go | 17 ++++++++++++----- httpgrpc/server/server.go | 11 +++++++---- server/server.go | 26 +++++++++++++++++++++----- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/grpcclient/instrumentation.go b/grpcclient/instrumentation.go index 280f02180..211740cda 100644 --- a/grpcclient/instrumentation.go +++ b/grpcclient/instrumentation.go @@ -10,13 +10,20 @@ import ( ) func Instrument(requestDuration *prometheus.HistogramVec, instrumentationLabelOptions ...middleware.InstrumentationOption) ([]grpc.UnaryClientInterceptor, []grpc.StreamClientInterceptor) { - return []grpc.UnaryClientInterceptor{ - otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer()), + var ( + unary []grpc.UnaryClientInterceptor + stream []grpc.StreamClientInterceptor + ) + if opentracing.IsGlobalTracerRegistered() { + unary = append(unary, otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer())) + stream = append(stream, otgrpc.OpenTracingStreamClientInterceptor(opentracing.GlobalTracer())) + } + return append(unary, middleware.ClientUserHeaderInterceptor, middleware.UnaryClientInstrumentInterceptor(requestDuration, instrumentationLabelOptions...), - }, []grpc.StreamClientInterceptor{ - otgrpc.OpenTracingStreamClientInterceptor(opentracing.GlobalTracer()), + ), + append(stream, middleware.StreamClientUserHeaderInterceptor, middleware.StreamClientInstrumentInterceptor(requestDuration, instrumentationLabelOptions...), - } + ) } diff --git a/httpgrpc/server/server.go b/httpgrpc/server/server.go index 2bceb1ddc..58a41c23c 100644 --- a/httpgrpc/server/server.go +++ b/httpgrpc/server/server.go @@ -180,13 +180,16 @@ func NewClient(address string) (*Client, error) { } const grpcServiceConfig = `{"loadBalancingPolicy":"round_robin"}` + var unaryInterceptors []grpc.UnaryClientInterceptor + if opentracing.IsGlobalTracerRegistered() { + unaryInterceptors = append(unaryInterceptors, otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer())) + } + unaryInterceptors = append(unaryInterceptors, middleware.ClientUserHeaderInterceptor) + dialOptions := []grpc.DialOption{ grpc.WithDefaultServiceConfig(grpcServiceConfig), grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithChainUnaryInterceptor( - otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer()), - middleware.ClientUserHeaderInterceptor, - ), + grpc.WithChainUnaryInterceptor(unaryInterceptors...), grpc.WithStatsHandler(otelgrpc.NewClientHandler()), } diff --git a/server/server.go b/server/server.go index 9caee0433..dbfd02f48 100644 --- a/server/server.go +++ b/server/server.go @@ -405,10 +405,18 @@ func newServer(cfg Config, metrics *Metrics) (*Server, error) { } grpcMiddleware := []grpc.UnaryServerInterceptor{ serverLog.UnaryServerInterceptor, - otgrpc.OpenTracingServerInterceptor(opentracing.GlobalTracer()), - middleware.HTTPGRPCTracingInterceptor(router), // This must appear after the OpenTracingServerInterceptor. - middleware.UnaryServerInstrumentInterceptor(metrics.RequestDuration, grpcInstrumentationOptions...), } + + if opentracing.IsGlobalTracerRegistered() { + grpcMiddleware = append(grpcMiddleware, + otgrpc.OpenTracingServerInterceptor(opentracing.GlobalTracer()), + ) + } + + grpcMiddleware = append(grpcMiddleware, + middleware.HTTPGRPCTracingInterceptor(router), // This must appear after the OpenTracingServerInterceptor, if that's configured. + middleware.UnaryServerInstrumentInterceptor(metrics.RequestDuration, grpcInstrumentationOptions...), + ) grpcMiddleware = append(grpcMiddleware, cfg.GRPCMiddleware...) if cfg.ClusterValidation.GRPC.Enabled { grpcMiddleware = append(grpcMiddleware, middleware.ClusterUnaryServerInterceptor( @@ -419,9 +427,17 @@ func newServer(cfg Config, metrics *Metrics) (*Server, error) { grpcStreamMiddleware := []grpc.StreamServerInterceptor{ serverLog.StreamServerInterceptor, - otgrpc.OpenTracingStreamServerInterceptor(opentracing.GlobalTracer()), - middleware.StreamServerInstrumentInterceptor(metrics.RequestDuration, grpcInstrumentationOptions...), } + + if opentracing.IsGlobalTracerRegistered() { + grpcStreamMiddleware = append(grpcStreamMiddleware, + otgrpc.OpenTracingStreamServerInterceptor(opentracing.GlobalTracer()), + ) + } + + grpcStreamMiddleware = append(grpcStreamMiddleware, + middleware.StreamServerInstrumentInterceptor(metrics.RequestDuration, grpcInstrumentationOptions...), + ) grpcStreamMiddleware = append(grpcStreamMiddleware, cfg.GRPCStreamMiddleware...) grpcKeepAliveOptions := keepalive.ServerParameters{ From c59312f51d3b0a7243ca848857f448538bead045 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 23 Apr 2025 09:44:43 +0000 Subject: [PATCH 12/20] Server: add otel stats handler when no opentracing Signed-off-by: Oleg Zaytsev --- httpgrpc/server/server.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/httpgrpc/server/server.go b/httpgrpc/server/server.go index 58a41c23c..e368f7ecd 100644 --- a/httpgrpc/server/server.go +++ b/httpgrpc/server/server.go @@ -190,7 +190,9 @@ func NewClient(address string) (*Client, error) { grpc.WithDefaultServiceConfig(grpcServiceConfig), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithChainUnaryInterceptor(unaryInterceptors...), - grpc.WithStatsHandler(otelgrpc.NewClientHandler()), + } + if !opentracing.IsGlobalTracerRegistered() { // Note: I'm not sure whether this condition is required, feel free to question it. + dialOptions = append(dialOptions, grpc.WithStatsHandler(otelgrpc.NewClientHandler())) } conn, err := grpc.NewClient(address, dialOptions...) From cf8aa951952fe61e8ddebdd5d7ba8da6a01d2dfa Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 23 Apr 2025 09:54:14 +0000 Subject: [PATCH 13/20] Move misplaced comment Signed-off-by: Oleg Zaytsev --- middleware/http_tracing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/http_tracing.go b/middleware/http_tracing.go index f9aea55ec..b59990c13 100644 --- a/middleware/http_tracing.go +++ b/middleware/http_tracing.go @@ -39,6 +39,7 @@ func (t Tracer) Wrap(next http.Handler) http.Handler { if opentracing.IsGlobalTracerRegistered() { return t.wrapWithOpenTracing(next) } + // If no OpenTracing, let's do OTel. return t.wrapWithOTel(next) } @@ -70,7 +71,6 @@ func (t Tracer) wrapWithOpenTracing(next http.Handler) http.Handler { } func (t Tracer) wrapWithOTel(next http.Handler) http.Handler { - // If no OpenTracing, let's do OTel. tracingMiddleware := otelhttp.NewHandler(next, "http.tracing", otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string { return httpOperationName(r) })) From 586d82e6188cf57d65023863fe79428c34b495c3 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 23 Apr 2025 09:55:56 +0000 Subject: [PATCH 14/20] Add content type attribute Signed-off-by: Oleg Zaytsev --- middleware/http_tracing.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/middleware/http_tracing.go b/middleware/http_tracing.go index b59990c13..a47183acc 100644 --- a/middleware/http_tracing.go +++ b/middleware/http_tracing.go @@ -87,6 +87,11 @@ func (t Tracer) wrapWithOTel(next http.Handler) http.Handler { labeler.Add(attribute.String("http.url", r.URL.Path)) labeler.Add(attribute.String("http.method", r.Method)) + // add the content type, useful when query requests are sent as POST + if ct := r.Header.Get("Content-Type"); ct != "" { + labeler.Add(attribute.String("http.content_type", ct)) + } + labeler.Add(attribute.String("headers", fmt.Sprintf("%v", r.Header))) // add a tag with the client's sourceIPs to the span, if a // SourceIPExtractor is given. From 06e1c24bf8be05cc442f4c1bef6c06e9e28e3bf5 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 23 Apr 2025 10:38:45 +0000 Subject: [PATCH 15/20] s/tracing.config/tracing.otelConfig/ Signed-off-by: Oleg Zaytsev --- tracing/otel.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tracing/otel.go b/tracing/otel.go index 3b86ff4f4..90fc87394 100644 --- a/tracing/otel.go +++ b/tracing/otel.go @@ -49,7 +49,7 @@ const ( // - JAEGER_AGENT_HOST // - JAEGER_SAMPLER_MANAGER_HOST_PORT func NewOTelFromJaegerEnv(serviceName string) (io.Closer, error) { - cfg, err := parseTracingConfig() + cfg, err := parseOTelConfig() if err != nil { return nil, errors.Wrap(err, "could not load jaeger tracer configuration") } @@ -86,7 +86,7 @@ func parseJaegerTags(sTags string) ([]attribute.KeyValue, error) { return res, nil } -type config struct { +type otelConfig struct { agentHost string collectorEndpoint string agentPort string @@ -97,9 +97,9 @@ type config struct { agentHostPort string } -// parseTracingConfig facilitates initialization that is compatible with Jaeger's InitGlobalTracer method. -func parseTracingConfig() (config, error) { - cfg := config{} +// parseOTelConfig facilitates initialization that is compatible with Jaeger's InitGlobalTracer method. +func parseOTelConfig() (otelConfig, error) { + cfg := otelConfig{} var err error // Parse reporting agent configuration @@ -166,7 +166,7 @@ func parseTracingConfig() (config, error) { } // initJaegerTracerProvider initializes a new Jaeger Tracer Provider. -func (cfg config) initJaegerTracerProvider(serviceName string) (io.Closer, error) { +func (cfg otelConfig) initJaegerTracerProvider(serviceName string) (io.Closer, error) { // Read environment variables to configure Jaeger var ep jaegerotel.EndpointOption // Create the jaeger exporter: address can be either agent address (host:port) or collector Endpoint. From 4243e6da74529685c531cb27c80ae411b1f77f1a Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 23 Apr 2025 10:39:30 +0000 Subject: [PATCH 16/20] s/collectorEndpoint/jaegerEndpoint/ Signed-off-by: Oleg Zaytsev --- tracing/otel.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tracing/otel.go b/tracing/otel.go index 90fc87394..aba985c19 100644 --- a/tracing/otel.go +++ b/tracing/otel.go @@ -53,7 +53,7 @@ func NewOTelFromJaegerEnv(serviceName string) (io.Closer, error) { if err != nil { return nil, errors.Wrap(err, "could not load jaeger tracer configuration") } - if cfg.samplingServerURL == "" && cfg.agentHostPort == "" && cfg.collectorEndpoint == "" { + if cfg.samplingServerURL == "" && cfg.agentHostPort == "" && cfg.jaegerEndpoint == "" { return nil, ErrBlankTraceConfiguration } return cfg.initJaegerTracerProvider(serviceName) @@ -88,7 +88,7 @@ func parseJaegerTags(sTags string) ([]attribute.KeyValue, error) { type otelConfig struct { agentHost string - collectorEndpoint string + jaegerEndpoint string agentPort string samplerType string samplingServerURL string @@ -108,7 +108,7 @@ func parseOTelConfig() (otelConfig, error) { if err != nil { return cfg, errors.Wrapf(err, "cannot parse env var %s=%s", envJaegerEndpoint, e) } - cfg.collectorEndpoint = u.String() + cfg.jaegerEndpoint = u.String() } else { useEnv := false host := envJaegerDefaultUDPSpanServerHost @@ -176,7 +176,7 @@ func (cfg otelConfig) initJaegerTracerProvider(serviceName string) (io.Closer, e jaegerotel.WithAgentPort(cfg.agentPort)) } else { ep = jaegerotel.WithCollectorEndpoint( - jaegerotel.WithEndpoint(cfg.collectorEndpoint)) + jaegerotel.WithEndpoint(cfg.jaegerEndpoint)) } exp, err := jaegerotel.New(ep) From b8e1ea2bcc3e176d9517bcb861fd1bba2b0a5e99 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 23 Apr 2025 10:52:30 +0000 Subject: [PATCH 17/20] Hardcode the expected value Signed-off-by: Oleg Zaytsev --- tracing/otel_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracing/otel_test.go b/tracing/otel_test.go index 4f294dcb7..2d2342cdf 100644 --- a/tracing/otel_test.go +++ b/tracing/otel_test.go @@ -34,7 +34,7 @@ func TestParseAttributes(t *testing.T) { { input: "key1=${EXISTENT_ENV_KEY},key2=${NON_EXISTENT_ENV_KEY:default_value}", expectedOutput: []attribute.KeyValue{ - attribute.String("key1", os.Getenv("EXISTENT_ENV_KEY")), + attribute.String("key1", "env_value"), attribute.String("key2", "default_value"), }, expectedError: nil, From 80751c9791753d3adf368830cfa28c8bf8d72067 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 23 Apr 2025 10:55:20 +0000 Subject: [PATCH 18/20] Reduce nesting by checking !sampled Signed-off-by: Oleg Zaytsev --- tracing/span.go | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/tracing/span.go b/tracing/span.go index 4826a5b24..58fe87de5 100644 --- a/tracing/span.go +++ b/tracing/span.go @@ -23,34 +23,35 @@ type Span struct { // If parent span is not sampled, it returns a noop span. func StartSpanFromContext(ctx context.Context, operation string, options ...SpanOption) (*Span, context.Context) { otelSpan, opentracingSpan, sampled := SpanFromContext(ctx) - if sampled { - if opentracingSpan != nil { - var opentracingOptions []opentracing.StartSpanOption - for _, opt := range options { - opentracingOptions = append(opentracingOptions, opt.opentracingSpanOptions()...) - } - span, ctx := opentracing.StartSpanFromContext(ctx, operation, opentracingOptions...) - s := &Span{opentracingSpan: span} - for _, opt := range options { - opt.apply(s) - } - return s, ctx - } + if !sampled { + return &Span{}, ctx + } - if otelSpan != nil { - var otelOptions []trace.SpanStartOption - for _, opt := range options { - otelOptions = append(otelOptions, opt.otelSpanOptions()...) - } - ctx, span := otelSpan.TracerProvider().Tracer("dskit/tracing").Start(ctx, operation, otelOptions...) - s := &Span{otelSpan: span} - for _, opt := range options { - opt.apply(s) - } - return s, ctx + if opentracingSpan != nil { + var opentracingOptions []opentracing.StartSpanOption + for _, opt := range options { + opentracingOptions = append(opentracingOptions, opt.opentracingSpanOptions()...) + } + span, ctx := opentracing.StartSpanFromContext(ctx, operation, opentracingOptions...) + s := &Span{opentracingSpan: span} + for _, opt := range options { + opt.apply(s) } + return s, ctx } + if otelSpan != nil { + var otelOptions []trace.SpanStartOption + for _, opt := range options { + otelOptions = append(otelOptions, opt.otelSpanOptions()...) + } + ctx, span := otelSpan.TracerProvider().Tracer("dskit/tracing").Start(ctx, operation, otelOptions...) + s := &Span{otelSpan: span} + for _, opt := range options { + opt.apply(s) + } + return s, ctx + } return &Span{}, ctx } From 0b10eb1182aaa1ec3b1edb3bcfcdeb24018d3d41 Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 23 Apr 2025 10:57:54 +0000 Subject: [PATCH 19/20] Update TestKeyValueToOTelAttribute Signed-off-by: Oleg Zaytsev --- tracing/span_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tracing/span_test.go b/tracing/span_test.go index 9ab0cd3ae..9b99c104e 100644 --- a/tracing/span_test.go +++ b/tracing/span_test.go @@ -65,6 +65,16 @@ func TestKeyValueToOTelAttribute(t *testing.T) { kv: kv{"key", testStringer{}}, expected: attribute.Stringer("key", testStringer{}), }, + { + name: "[]byte", + kv: kv{"key", []byte("value")}, + expected: attribute.String("key", "value"), + }, + { + name: "fallback", + kv: kv{"key", map[string]string{"key": "value"}}, + expected: attribute.String("key", "map[key:value]"), + }, } for _, test := range tests { From 51ca0c0e327048a78dcd78bb56ae54b6cbfe054c Mon Sep 17 00:00:00 2001 From: Oleg Zaytsev Date: Wed, 23 Apr 2025 13:39:29 +0000 Subject: [PATCH 20/20] tracing.StartSpanFromContext: always start span This changes behaviour of tracing.StartSpanFromContext to always start a span, as we did previously, and we decide based on whether opentracing is registered or not. Also added tests, which I had to move to other packages as tracing installation is a global state. Signed-off-by: Oleg Zaytsev --- tracing/internal/opentracingtest/span_test.go | 57 +++++++++++++++++++ tracing/internal/oteltest/span_test.go | 48 ++++++++++++++++ tracing/otel.go | 2 + tracing/span.go | 31 ++++------ 4 files changed, 118 insertions(+), 20 deletions(-) create mode 100644 tracing/internal/opentracingtest/span_test.go create mode 100644 tracing/internal/oteltest/span_test.go diff --git a/tracing/internal/opentracingtest/span_test.go b/tracing/internal/opentracingtest/span_test.go new file mode 100644 index 000000000..502a4dcba --- /dev/null +++ b/tracing/internal/opentracingtest/span_test.go @@ -0,0 +1,57 @@ +// Package opentracingtest tests tracing with a global opentracing.TracerProvider registered. +package opentracingtest + +import ( + "context" + "testing" + + "github.com/opentracing/opentracing-go" + "github.com/stretchr/testify/require" + "github.com/uber/jaeger-client-go" + + "github.com/grafana/dskit/tracing" +) + +func init() { + // Install opentracing. + _, err := tracing.NewFromEnv("test") + if err != nil { + panic(err) + } +} + +func TestStartSpanFromContext(t *testing.T) { + t.Run("without parent", func(t *testing.T) { + // Start a new span from the context + newSpan, ctx := tracing.StartSpanFromContext(context.Background(), "test-span") + defer newSpan.Finish() + + spanFromContext := opentracing.SpanFromContext(ctx) + _, ok := spanFromContext.Context().(jaeger.SpanContext) + require.True(t, ok, "Expected span context to be of type jaeger.SpanContext") + }) + + t.Run("with parent", func(t *testing.T) { + // Create a new context with a span + ctx := context.Background() + tracer := opentracing.GlobalTracer() + span := tracer.StartSpan("test-span") + defer span.Finish() + + jaegerSpanContext, ok := span.Context().(jaeger.SpanContext) + require.True(t, ok, "Expected span context to be of type jaeger.SpanContext") + require.NotZero(t, jaegerSpanContext.TraceID(), "Expected non-zero trace ID") + + // Set the span in the context + ctx = opentracing.ContextWithSpan(ctx, span) + + // Start a new span from the context + newSpan, ctx := tracing.StartSpanFromContext(ctx, "child-span") + defer newSpan.Finish() + + spanFromContext := opentracing.SpanFromContext(ctx) + jaegerSpanFromContextContext, ok := spanFromContext.Context().(jaeger.SpanContext) + require.True(t, ok, "Expected span context to be of type jaeger.SpanContext") + require.Equal(t, jaegerSpanContext.TraceID().String(), jaegerSpanFromContextContext.TraceID().String(), "Expected trace ID to match parent span") + }) +} diff --git a/tracing/internal/oteltest/span_test.go b/tracing/internal/oteltest/span_test.go new file mode 100644 index 000000000..ec7e83e2e --- /dev/null +++ b/tracing/internal/oteltest/span_test.go @@ -0,0 +1,48 @@ +// Package opentracingtest tests tracing with OTel installed. +package oteltest + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" + + "github.com/grafana/dskit/tracing" +) + +var tracer = otel.Tracer("dskit.test") + +func init() { + // Install OTel tracing. + _, err := tracing.NewOTelFromJaegerEnv("test") + if err != nil { + panic(err) + } +} + +func TestStartSpanFromContext(t *testing.T) { + t.Run("without parent", func(t *testing.T) { + // Start a new span from the context + newSpan, ctx := tracing.StartSpanFromContext(context.Background(), "test-span") + defer newSpan.Finish() + + spanFromContext := trace.SpanFromContext(ctx) + require.True(t, spanFromContext.SpanContext().IsValid()) + }) + + t.Run("with parent", func(t *testing.T) { + // Create a new context with a span + ctx, span := tracer.Start(context.Background(), "test-span") + defer span.End() + require.NotZero(t, span.SpanContext().TraceID()) + + // Start a new span from the context + newSpan, ctx := tracing.StartSpanFromContext(ctx, "child-span") + defer newSpan.Finish() + + spanFromContext := trace.SpanFromContext(ctx) + require.Equal(t, span.SpanContext().TraceID().String(), spanFromContext.SpanContext().TraceID().String(), "Expected trace ID to match parent span") + }) +} diff --git a/tracing/otel.go b/tracing/otel.go index aba985c19..71423a933 100644 --- a/tracing/otel.go +++ b/tracing/otel.go @@ -29,6 +29,8 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.26.0" ) +var tracer = otel.Tracer("dskit/tracing") + const ( envJaegerAgentHost = "JAEGER_AGENT_HOST" envJaegerTags = "JAEGER_TAGS" diff --git a/tracing/span.go b/tracing/span.go index 58fe87de5..470f0e6a5 100644 --- a/tracing/span.go +++ b/tracing/span.go @@ -19,15 +19,9 @@ type Span struct { otelSpan trace.Span } -// StartSpanFromContext starts a new span from the context using the parent tracing library. -// If parent span is not sampled, it returns a noop span. +// StartSpanFromContext starts a new opentracing span if opentracing is registered, otherwise it starts a new otel span. func StartSpanFromContext(ctx context.Context, operation string, options ...SpanOption) (*Span, context.Context) { - otelSpan, opentracingSpan, sampled := SpanFromContext(ctx) - if !sampled { - return &Span{}, ctx - } - - if opentracingSpan != nil { + if opentracing.IsGlobalTracerRegistered() { var opentracingOptions []opentracing.StartSpanOption for _, opt := range options { opentracingOptions = append(opentracingOptions, opt.opentracingSpanOptions()...) @@ -40,19 +34,16 @@ func StartSpanFromContext(ctx context.Context, operation string, options ...Span return s, ctx } - if otelSpan != nil { - var otelOptions []trace.SpanStartOption - for _, opt := range options { - otelOptions = append(otelOptions, opt.otelSpanOptions()...) - } - ctx, span := otelSpan.TracerProvider().Tracer("dskit/tracing").Start(ctx, operation, otelOptions...) - s := &Span{otelSpan: span} - for _, opt := range options { - opt.apply(s) - } - return s, ctx + var otelOptions []trace.SpanStartOption + for _, opt := range options { + otelOptions = append(otelOptions, opt.otelSpanOptions()...) + } + ctx, span := tracer.Start(ctx, operation, otelOptions...) + s := &Span{otelSpan: span} + for _, opt := range options { + opt.apply(s) } - return &Span{}, ctx + return s, ctx } func (s *Span) SetTag(name string, value any) {