From d84fb6b35b5485ce4c1e0908bcd85463bf5ce9d9 Mon Sep 17 00:00:00 2001 From: ansonlee Date: Tue, 9 Jun 2026 11:39:52 +0800 Subject: [PATCH 1/7] chore: restore grpc_with_error_reporting preset (Sentry-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit (535c442) deleted grpc_with_error_reporting.go alongside the newrelic deprecation. The file's build tag required newrelic (`grpc && newrelic && sentry && otel`), so its removal was technically defensible — but the *name* carries architectural meaning beyond the newrelic dependency: it is the role-named fan-out extension point for error reporters, distinct from the vendor-named grpc_with_sentry.go. Restore it as a Sentry-only preset (build tag `grpc && sentry && otel`, no newrelic) so that: - The vendor-named preset (DefaultGrpcServerWithSentry) stays as-is. - The role-named preset (DefaultGrpcServerWithErrorReporting) exists as the designated extension point for adding additional error reporters (Datadog, Rollbar, Bugsnag, etc.) to the interceptor chain in the future, without forcing every caller to migrate off the vendor-named preset. Today the two presets have identical interceptor chains; the distinction is the file's role as a fan-out point. This matches the pre-deletion design intent of the file (commit 090654f: 'double write to both newrelic and sentry'), generalized beyond the newrelic-specific double-write. Co-Authored-By: Claude Opus 4.8 --- .../grpc/presets/grpc_with_error_reporting.go | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 plugins/grpc/presets/grpc_with_error_reporting.go diff --git a/plugins/grpc/presets/grpc_with_error_reporting.go b/plugins/grpc/presets/grpc_with_error_reporting.go new file mode 100644 index 0000000..399b182 --- /dev/null +++ b/plugins/grpc/presets/grpc_with_error_reporting.go @@ -0,0 +1,89 @@ +//go:build grpc && sentry && otel +// +build grpc,sentry,otel + +package presets + +import ( + "context" + + "github.com/shoplineapp/go-app/plugins" + "github.com/shoplineapp/go-app/plugins/env" + grpc_plugin "github.com/shoplineapp/go-app/plugins/grpc" + "github.com/shoplineapp/go-app/plugins/grpc/interceptors" + "github.com/shoplineapp/go-app/plugins/logger" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/health" + healthgrpc "google.golang.org/grpc/health/grpc_health_v1" +) + +func init() { + plugins.Registry = append(plugins.Registry, NewDefaultGrpcServerWithErrorReporting) +} + +// DefaultGrpcServerWithErrorReporting is the "fan-out" error-reporting +// preset. Its name reflects a role, not a specific vendor: today it wires +// Sentry as the sole error reporter (so this preset is functionally a +// duplicate of DefaultGrpcServerWithSentry), but it is the designated +// extension point for adding additional error reporters (Datadog, Rollbar, +// Bugsnag, etc.) to the interceptor chain without forcing every caller to +// migrate off a vendor-named preset. +// +// Callers that only need Sentry should use DefaultGrpcServerWithSentry. +// Callers that want multiple error reporters should use +// DefaultGrpcServerWithErrorReporting. +type DefaultGrpcServerWithErrorReporting struct { + grpc_plugin.GrpcServer +} + +func NewDefaultGrpcServerWithErrorReporting( + lc fx.Lifecycle, + logger *logger.Logger, + env *env.Env, + grpcServer *grpc_plugin.GrpcServer, + deadline *interceptors.DeadlineInterceptor, + trace_id *interceptors.TraceIdInterceptor, + locale *interceptors.LocaleInterceptor, + requestLog *interceptors.RequestLogInterceptor, + recovery *interceptors.RecoveryInterceptor, + sentry *interceptors.SentryInterceptor, + otlp *interceptors.OtelInterceptor, +) *DefaultGrpcServerWithErrorReporting { + s := *grpcServer + plugin := &DefaultGrpcServerWithErrorReporting{ + GrpcServer: s, + } + + handles := []grpc.UnaryServerInterceptor{ + trace_id.Handler(), + locale.Handler(), + requestLog.Handler(), + sentry.Handler(), + deadline.Handler(), + recovery.Handler(), + otlp.Handler(), + } + + grpc_plugin.SetGlobalServerOptions( + grpc.StatsHandler(otelgrpc.NewServerHandler()), + ) + plugin.Configure( + grpc.ChainUnaryInterceptor( + handles..., + ), + ) + healthgrpc.RegisterHealthServer(plugin.Server(), health.NewServer()) + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + plugin.Serve() + return nil + }, + OnStop: func(ctx context.Context) error { + plugin.Shutdown() + return nil + }, + }) + + return plugin +} From 3a37620d9cdd6ed505fe0885f789d5f5b9c574a3 Mon Sep 17 00:00:00 2001 From: ansonlee Date: Tue, 9 Jun 2026 09:25:42 +0800 Subject: [PATCH 2/7] feat(otel): replace legacy trace_id interceptors with W3C OTel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Built on top of chore/deprecate-newrelic — the newrelic files are already gone, so this commit only touches the sentry path and Kitex. gRPC server: - Delete plugins/grpc/interceptors/trace_id.go (replaced by otelgrpc.NewServerHandler() StatsHandler, already wired in the grpc_with_sentry preset) - Delete plugins/grpc/interceptors/opentelemetry.go (custom interceptor duplicating the StatsHandler) - Migrate consumers (recovery, request_log) to read OTel SpanContextFromContext instead of ctx.Value("trace_id") - Drop trace_id and otlp from the grpc_with_sentry preset chain Kitex server: - Add github.com/kitex-contrib/obs-opentelemetry v0.3.0 - Add suites field + SetSuites method to KitexServer; install tracing.NewServerSuite() by default in NewKitexServer - Delete plugins/kitex/middlewares/trace_id.go (replaced by the suite) - Migrate KitexRequestLogMiddleware to read OTel SpanContext - Drop traceIDMiddleware from NewKitexServer's default chain Trace_id format changes from UUID to 32-char lowercase hex (W3C traceparent). Clients must read traceparent from gRPC response trailers / Kitex response metainfo instead of x-trace-id header. Co-Authored-By: Claude Opus 4.8 --- go.mod | 11 ++- go.sum | 26 +++-- plugins/grpc/interceptors/opentelemetry.go | 94 ------------------- plugins/grpc/interceptors/recovery.go | 6 +- plugins/grpc/interceptors/request_log.go | 8 +- plugins/grpc/interceptors/trace_id.go | 45 --------- .../grpc/presets/grpc_with_error_reporting.go | 4 - plugins/grpc/presets/grpc_with_sentry.go | 4 - plugins/kitex/kitex.go | 19 +++- plugins/kitex/middlewares/request_log.go | 7 +- plugins/kitex/middlewares/trace_id.go | 38 -------- 11 files changed, 61 insertions(+), 201 deletions(-) delete mode 100644 plugins/grpc/interceptors/opentelemetry.go delete mode 100644 plugins/grpc/interceptors/trace_id.go delete mode 100644 plugins/kitex/middlewares/trace_id.go diff --git a/go.mod b/go.mod index 8c88604..48578eb 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/joho/godotenv v1.4.0 github.com/joonix/log v0.0.0-20200409080653-9c1d2ceb5f1d github.com/kamva/mgm/v3 v3.4.1 + github.com/kitex-contrib/obs-opentelemetry v0.3.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.2 github.com/stoewer/go-strcase v1.3.0 @@ -39,8 +40,8 @@ require ( github.com/ardielle/ardielle-go v1.5.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic v1.14.1 // indirect - github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/bytedance/sonic v1.14.2 // indirect + github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect @@ -101,7 +102,7 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/dig v1.14.0 // indirect - go.uber.org/multierr v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.21.0 // indirect golang.org/x/arch v0.14.0 // indirect golang.org/x/crypto v0.41.0 // indirect @@ -110,7 +111,9 @@ require ( golang.org/x/sys v0.35.0 // indirect golang.org/x/term v0.34.0 // indirect golang.org/x/text v0.28.0 // indirect - google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect + google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f1daa58..a3280b8 100644 --- a/go.sum +++ b/go.sum @@ -77,10 +77,10 @@ github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= -github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -346,6 +346,8 @@ github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaR github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kitex-contrib/obs-opentelemetry v0.3.0 h1:STAuMGRhmtZP1zHKZVl9vj7sxMXpu7nU3IqrslShzbo= +github.com/kitex-contrib/obs-opentelemetry v0.3.0/go.mod h1:OReZqYd24Q5djEtkRU2kMQEMq4auWtxJNk4FTKPlGHE= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= @@ -505,6 +507,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -551,6 +554,10 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/propagators/b3 v1.20.0 h1:Yty9Vs4F3D6/liF1o6FNt0PvN85h/BJJ6DQKJ3nrcM0= +go.opentelemetry.io/contrib/propagators/b3 v1.20.0/go.mod h1:On4VgbkqYL18kbJlWsa18+cMNe6rYpBnPi1ARI/BrsU= +go.opentelemetry.io/contrib/propagators/ot v1.25.0 h1:9+54ye9caWA5XplhJoN6E8ECDKGeEsw/mqR4BIuZUfg= +go.opentelemetry.io/contrib/propagators/ot v1.25.0/go.mod h1:Fn0a9xFTClSSwNLpS1l0l55PkLHzr70RYlu+gUsPhHo= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -580,8 +587,9 @@ go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ 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.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= @@ -963,8 +971,12 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/plugins/grpc/interceptors/opentelemetry.go b/plugins/grpc/interceptors/opentelemetry.go deleted file mode 100644 index 2c2465d..0000000 --- a/plugins/grpc/interceptors/opentelemetry.go +++ /dev/null @@ -1,94 +0,0 @@ -//go:build grpc && otel -// +build grpc,otel - -package interceptors - -import ( - "context" - "path" - "strings" - - grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" - "github.com/shoplineapp/go-app/plugins" - "github.com/shoplineapp/go-app/plugins/opentelemetry" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" -) - -func init() { - plugins.Registry = append(plugins.Registry, NewOtelInterceptor) -} - -type OtelInterceptor struct { - agent *opentelemetry.OtelAgent -} - -func (i OtelInterceptor) Handler() grpc.UnaryServerInterceptor { - customNewrelicInterceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { - tracer := opentelemetry.GetTracer() - service := path.Dir(info.FullMethod)[1:] - if tracer == nil || service == "grpc.health.v1.Health" { - return handler(ctx, req) - } - - if v, ok := ctx.Value("trace_id").(string); ok && v != "" { - traceIDHex := strings.ReplaceAll(v, "-", "") - if tid, err := trace.TraceIDFromHex(traceIDHex); err == nil && tid.IsValid() { - spanContext := trace.SpanContextFromContext(ctx) - if !spanContext.IsValid() || spanContext.TraceID().String() != tid.String() { - sc := trace.NewSpanContext(trace.SpanContextConfig{ - TraceID: tid, - }) - ctx = trace.ContextWithSpanContext(ctx, sc) - } - } - } - - newCtx, span := tracer.Start(ctx, info.FullMethod) - - defer span.End() - - var attrs []attribute.KeyValue - - resp, err = handler(newCtx, req) - - if err != nil { - st, _ := status.FromError(err) - attrs = append(attrs, attribute.KeyValue{ - Key: "GrpcStatusMessage", - Value: attribute.StringValue(st.Message()), - }) - attrs = append(attrs, attribute.KeyValue{ - Key: "GrpcStatusCode", - Value: attribute.StringValue(st.Code().String()), - }) - span.RecordError(err) - } - - if md, ok := metadata.FromIncomingContext(ctx); ok { - for key, value := range md { - attrs = append(attrs, attribute.KeyValue{ - Key: attribute.Key(key), - Value: attribute.StringSliceValue(value), - }) - } - } - - span.SetAttributes(attrs...) - - return resp, err - } - - return grpc_middleware.ChainUnaryServer( - customNewrelicInterceptor, - ) -} - -func NewOtelInterceptor(agent *opentelemetry.OtelAgent) *OtelInterceptor { - return &OtelInterceptor{ - agent: agent, - } -} diff --git a/plugins/grpc/interceptors/recovery.go b/plugins/grpc/interceptors/recovery.go index eec988e..fd9f607 100644 --- a/plugins/grpc/interceptors/recovery.go +++ b/plugins/grpc/interceptors/recovery.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/shoplineapp/go-app/plugins" app_grpc "github.com/shoplineapp/go-app/plugins/grpc" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "google.golang.org/grpc/codes" ) @@ -41,7 +42,10 @@ func (i RecoveryInterceptor) Handler() grpc.UnaryServerInterceptor { } // trace_id is captured into the ApplicationError for downstream // reporters (Sentry, structured logs) to attribute the panic. - traceID, _ := ctx.Value("trace_id").(string) + var traceID string + if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { + traceID = sc.TraceID().String() + } err = app_grpc.NewApplicationError(traceID, err, codes.Internal, false, "panic recovered from RecoveryInterceptor") } }() diff --git a/plugins/grpc/interceptors/request_log.go b/plugins/grpc/interceptors/request_log.go index 47d79ba..6f37ca4 100644 --- a/plugins/grpc/interceptors/request_log.go +++ b/plugins/grpc/interceptors/request_log.go @@ -16,6 +16,7 @@ import ( "github.com/shoplineapp/go-app/plugins/env" "github.com/shoplineapp/go-app/plugins/logger" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" ) @@ -123,8 +124,13 @@ func (i RequestLogInterceptor) Handler() grpc.UnaryServerInterceptor { service := path.Dir(info.FullMethod)[1:] + var traceID string + if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { + traceID = sc.TraceID().String() + } + log := i.logger.WithFields(logrus.Fields{ - "trace_id": ctx.Value("trace_id"), + "trace_id": traceID, "service": service, "method": path.Base(info.FullMethod), }) diff --git a/plugins/grpc/interceptors/trace_id.go b/plugins/grpc/interceptors/trace_id.go deleted file mode 100644 index 44325ea..0000000 --- a/plugins/grpc/interceptors/trace_id.go +++ /dev/null @@ -1,45 +0,0 @@ -//go:build grpc -// +build grpc - -package interceptors - -import ( - "context" - - "github.com/google/uuid" - "github.com/shoplineapp/go-app/plugins" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" -) - -func init() { - plugins.Registry = append(plugins.Registry, NewTraceIdInterceptor) -} - -type TraceIdInterceptor struct { -} - -func (i TraceIdInterceptor) Handler() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { - traceId := uuid.New().String() - if md, ok := metadata.FromIncomingContext(ctx); ok { - if v := md.Get("x-trace-id"); len(v) > 0 { - traceId = v[0] - } else if v := md.Get("trace_id"); len(v) > 0 { - traceId = v[0] - } - } - - ctx = context.WithValue(ctx, "trace_id", traceId) - - grpc.SetHeader(ctx, metadata.Pairs("x-trace-id", traceId)) - - resp, err = handler(ctx, req) - - return resp, err - } -} - -func NewTraceIdInterceptor() *TraceIdInterceptor { - return &TraceIdInterceptor{} -} diff --git a/plugins/grpc/presets/grpc_with_error_reporting.go b/plugins/grpc/presets/grpc_with_error_reporting.go index 399b182..9e222e3 100644 --- a/plugins/grpc/presets/grpc_with_error_reporting.go +++ b/plugins/grpc/presets/grpc_with_error_reporting.go @@ -43,12 +43,10 @@ func NewDefaultGrpcServerWithErrorReporting( env *env.Env, grpcServer *grpc_plugin.GrpcServer, deadline *interceptors.DeadlineInterceptor, - trace_id *interceptors.TraceIdInterceptor, locale *interceptors.LocaleInterceptor, requestLog *interceptors.RequestLogInterceptor, recovery *interceptors.RecoveryInterceptor, sentry *interceptors.SentryInterceptor, - otlp *interceptors.OtelInterceptor, ) *DefaultGrpcServerWithErrorReporting { s := *grpcServer plugin := &DefaultGrpcServerWithErrorReporting{ @@ -56,13 +54,11 @@ func NewDefaultGrpcServerWithErrorReporting( } handles := []grpc.UnaryServerInterceptor{ - trace_id.Handler(), locale.Handler(), requestLog.Handler(), sentry.Handler(), deadline.Handler(), recovery.Handler(), - otlp.Handler(), } grpc_plugin.SetGlobalServerOptions( diff --git a/plugins/grpc/presets/grpc_with_sentry.go b/plugins/grpc/presets/grpc_with_sentry.go index 63398b0..08f83c9 100644 --- a/plugins/grpc/presets/grpc_with_sentry.go +++ b/plugins/grpc/presets/grpc_with_sentry.go @@ -32,12 +32,10 @@ func NewDefaultGrpcServerWithSentry( env *env.Env, grpcServer *grpc_plugin.GrpcServer, deadline *interceptors.DeadlineInterceptor, - trace_id *interceptors.TraceIdInterceptor, locale *interceptors.LocaleInterceptor, requestLog *interceptors.RequestLogInterceptor, recovery *interceptors.RecoveryInterceptor, sentry *interceptors.SentryInterceptor, - otlp *interceptors.OtelInterceptor, ) *DefaultGrpcServerWithSentry { s := *grpcServer plugin := &DefaultGrpcServerWithSentry{ @@ -45,13 +43,11 @@ func NewDefaultGrpcServerWithSentry( } handles := []grpc.UnaryServerInterceptor{ - trace_id.Handler(), locale.Handler(), requestLog.Handler(), sentry.Handler(), deadline.Handler(), recovery.Handler(), - otlp.Handler(), } grpc_plugin.SetGlobalServerOptions( diff --git a/plugins/kitex/kitex.go b/plugins/kitex/kitex.go index 86a5cb2..18175e3 100644 --- a/plugins/kitex/kitex.go +++ b/plugins/kitex/kitex.go @@ -14,6 +14,7 @@ import ( "github.com/cloudwego/kitex/pkg/endpoint" "github.com/cloudwego/kitex/server" kitex_server "github.com/cloudwego/kitex/server" + "github.com/kitex-contrib/obs-opentelemetry/tracing" "github.com/shoplineapp/go-app/plugins" "github.com/shoplineapp/go-app/plugins/env" "github.com/shoplineapp/go-app/plugins/kitex/middlewares" @@ -32,6 +33,7 @@ type KitexServer struct { wg *sync.WaitGroup server server.Server middlewares []endpoint.Middleware + suites []server.Suite kitexExit chan error } @@ -60,6 +62,12 @@ func (s *KitexServer) Configure(initializer func(opts ...kitex_server.Option) ki } } + if s.suites != nil { + for _, suite := range s.suites { + options = append(options, kitex_server.WithSuite(suite)) + } + } + s.server = initializer(options...) kitex_server.RegisterShutdownHook(func() { s.logger.Info("GRPC server gracefully shutting down...") @@ -70,6 +78,12 @@ func (s *KitexServer) SetMiddlewares(middlewares []endpoint.Middleware) { s.middlewares = middlewares } +// SetSuites attaches Kitex server.Suite options (e.g. OpenTelemetry tracing). +// The suites are applied in order after middlewares when Configure is called. +func (s *KitexServer) SetSuites(suites []server.Suite) { + s.suites = suites +} + func (s *KitexServer) RegisterGracefullyShutdown(lc fx.Lifecycle) { s.wg = &sync.WaitGroup{} @@ -108,7 +122,6 @@ func NewKitexServer( lc fx.Lifecycle, logger *logger.Logger, env *env.Env, - traceIDMiddleware *middlewares.KitexTraceIDMiddleware, requestLogMiddleware *middlewares.KitexRequestLogMiddleware, deadlineMiddleware *middlewares.KitexDeadlineMiddleware, ) *KitexServer { @@ -116,10 +129,12 @@ func NewKitexServer( logger: logger, env: env, middlewares: []endpoint.Middleware{ - traceIDMiddleware.Handler, requestLogMiddleware.Handler, deadlineMiddleware.Handler, }, + suites: []server.Suite{ + tracing.NewServerSuite(), + }, } plugin.RegisterGracefullyShutdown(lc) return plugin diff --git a/plugins/kitex/middlewares/request_log.go b/plugins/kitex/middlewares/request_log.go index 385b0ca..f0bd3e7 100644 --- a/plugins/kitex/middlewares/request_log.go +++ b/plugins/kitex/middlewares/request_log.go @@ -9,6 +9,7 @@ import ( "github.com/shoplineapp/go-app/plugins" "github.com/shoplineapp/go-app/plugins/logger" "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/trace" ) func init() { @@ -22,9 +23,13 @@ type KitexRequestLogMiddleware struct { func (m KitexRequestLogMiddleware) Handler(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request, response interface{}) error { ri := rpcinfo.GetRPCInfo(ctx) + var traceID string + if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { + traceID = sc.TraceID().String() + } logger := logrus.WithFields(logrus.Fields{ "Method": ri.To().Method(), - "trace_id": ctx.Value("trace_id"), + "trace_id": traceID, }) ctx = context.WithValue(ctx, "logger", logger) diff --git a/plugins/kitex/middlewares/trace_id.go b/plugins/kitex/middlewares/trace_id.go deleted file mode 100644 index 86e1660..0000000 --- a/plugins/kitex/middlewares/trace_id.go +++ /dev/null @@ -1,38 +0,0 @@ -package middlewares - -import ( - "context" - - "github.com/cloudwego/kitex/pkg/endpoint" - "github.com/google/uuid" - "github.com/shoplineapp/go-app/plugins" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" -) - -func init() { - plugins.Registry = append(plugins.Registry, NewKitexTraceIDMiddleware) -} - -type KitexTraceIDMiddleware struct { -} - -func (m KitexTraceIDMiddleware) Handler(next endpoint.Endpoint) endpoint.Endpoint { - return func(ctx context.Context, request, response interface{}) error { - traceId := uuid.New().String() - if md, ok := metadata.FromIncomingContext(ctx); ok { - if v := md.Get("x-trace-id"); len(v) > 0 { - traceId = v[0] - } - } - - ctx = context.WithValue(ctx, "trace_id", traceId) - grpc.SetHeader(ctx, metadata.Pairs("x-trace-id", traceId)) - err := next(ctx, request, response) - return err - } -} - -func NewKitexTraceIDMiddleware() *KitexTraceIDMiddleware { - return &KitexTraceIDMiddleware{} -} From 5c983bdaac245a50874dbb107a988e8c331f5edc Mon Sep 17 00:00:00 2001 From: ansonlee Date: Tue, 16 Jun 2026 10:10:23 +0800 Subject: [PATCH 3/7] chore: put trace_id into ctx --- common/context.go | 11 ++++-- plugins/grpc/interceptors/trace_id.go | 37 +++++++++++++++++++ .../grpc/presets/grpc_with_error_reporting.go | 2 + plugins/grpc/presets/grpc_with_sentry.go | 2 + plugins/kitex/kitex.go | 2 + plugins/kitex/middlewares/trace_id.go | 28 ++++++++++++++ plugins/kitex/middlewares/trace_id_otel.go | 37 +++++++++++++++++++ 7 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 plugins/grpc/interceptors/trace_id.go create mode 100644 plugins/kitex/middlewares/trace_id.go create mode 100644 plugins/kitex/middlewares/trace_id_otel.go diff --git a/common/context.go b/common/context.go index b471692..c223732 100644 --- a/common/context.go +++ b/common/context.go @@ -4,6 +4,7 @@ import ( "context" "github.com/google/uuid" + "go.opentelemetry.io/otel/trace" ) func NewContextWithTraceID(ctx context.Context, traceId string) context.Context { @@ -14,9 +15,11 @@ func NewContextWithTraceID(ctx context.Context, traceId string) context.Context } func GetTraceID(ctx context.Context) string { - traceId := ctx.Value("trace_id") - if traceId == nil { - return uuid.New().String() + if v, ok := ctx.Value("trace_id").(string); ok && v != "" { + return v } - return traceId.(string) + if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { + return sc.TraceID().String() + } + return "" } diff --git a/plugins/grpc/interceptors/trace_id.go b/plugins/grpc/interceptors/trace_id.go new file mode 100644 index 0000000..c7093d0 --- /dev/null +++ b/plugins/grpc/interceptors/trace_id.go @@ -0,0 +1,37 @@ +//go:build grpc && otel +// +build grpc,otel + +package interceptors + +import ( + "context" + + "github.com/shoplineapp/go-app/common" + "github.com/shoplineapp/go-app/plugins" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc" +) + +func init() { + plugins.Registry = append(plugins.Registry, NewTraceIdInterceptor) +} + +type TraceIdInterceptor struct { +} + +// Handler bridges the OTel SpanContext (populated by otelgrpc.NewServerHandler +// which is installed as a gRPC StatsHandler on the server) into the legacy +// ctx["trace_id"] string key that downstream consumers (e.g. common.GetTraceID, +// plugins/pulsar/producer.go) still rely on. +func (i TraceIdInterceptor) Handler() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { + ctx = common.NewContextWithTraceID(ctx, sc.TraceID().String()) + } + return handler(ctx, req) + } +} + +func NewTraceIdInterceptor() *TraceIdInterceptor { + return &TraceIdInterceptor{} +} diff --git a/plugins/grpc/presets/grpc_with_error_reporting.go b/plugins/grpc/presets/grpc_with_error_reporting.go index 4ebda3a..cfd0f64 100644 --- a/plugins/grpc/presets/grpc_with_error_reporting.go +++ b/plugins/grpc/presets/grpc_with_error_reporting.go @@ -32,6 +32,7 @@ func NewDefaultGrpcServerWithErrorReporting( env *env.Env, grpcServer *grpc_plugin.GrpcServer, deadline *interceptors.DeadlineInterceptor, + trace_id *interceptors.TraceIdInterceptor, locale *interceptors.LocaleInterceptor, requestLog *interceptors.RequestLogInterceptor, recovery *interceptors.RecoveryInterceptor, @@ -43,6 +44,7 @@ func NewDefaultGrpcServerWithErrorReporting( } handles := []grpc.UnaryServerInterceptor{ + trace_id.Handler(), locale.Handler(), requestLog.Handler(), sentry.Handler(), diff --git a/plugins/grpc/presets/grpc_with_sentry.go b/plugins/grpc/presets/grpc_with_sentry.go index 08f83c9..87c5e2c 100644 --- a/plugins/grpc/presets/grpc_with_sentry.go +++ b/plugins/grpc/presets/grpc_with_sentry.go @@ -32,6 +32,7 @@ func NewDefaultGrpcServerWithSentry( env *env.Env, grpcServer *grpc_plugin.GrpcServer, deadline *interceptors.DeadlineInterceptor, + trace_id *interceptors.TraceIdInterceptor, locale *interceptors.LocaleInterceptor, requestLog *interceptors.RequestLogInterceptor, recovery *interceptors.RecoveryInterceptor, @@ -43,6 +44,7 @@ func NewDefaultGrpcServerWithSentry( } handles := []grpc.UnaryServerInterceptor{ + trace_id.Handler(), locale.Handler(), requestLog.Handler(), sentry.Handler(), diff --git a/plugins/kitex/kitex.go b/plugins/kitex/kitex.go index 18175e3..d707c53 100644 --- a/plugins/kitex/kitex.go +++ b/plugins/kitex/kitex.go @@ -122,6 +122,7 @@ func NewKitexServer( lc fx.Lifecycle, logger *logger.Logger, env *env.Env, + traceIDMiddleware *middlewares.KitexTraceIDMiddleware, requestLogMiddleware *middlewares.KitexRequestLogMiddleware, deadlineMiddleware *middlewares.KitexDeadlineMiddleware, ) *KitexServer { @@ -129,6 +130,7 @@ func NewKitexServer( logger: logger, env: env, middlewares: []endpoint.Middleware{ + traceIDMiddleware.Handler, requestLogMiddleware.Handler, deadlineMiddleware.Handler, }, diff --git a/plugins/kitex/middlewares/trace_id.go b/plugins/kitex/middlewares/trace_id.go new file mode 100644 index 0000000..8fa4ab9 --- /dev/null +++ b/plugins/kitex/middlewares/trace_id.go @@ -0,0 +1,28 @@ +//go:build kitex && !otel +// +build kitex,!otel + +package middlewares + +import ( + "github.com/cloudwego/kitex/pkg/endpoint" + "github.com/shoplineapp/go-app/plugins" +) + +func init() { + plugins.Registry = append(plugins.Registry, NewKitexTraceIDMiddleware) +} + +type KitexTraceIDMiddleware struct { +} + +// Handler is a no-op when OpenTelemetry is not enabled. The OTel bridge lives +// in trace_id_otel.go; without OTel there is no SpanContext to copy into +// ctx["trace_id"], so the legacy key stays unset and common.GetTraceID +// returns "" for Kitex requests. +func (m KitexTraceIDMiddleware) Handler(next endpoint.Endpoint) endpoint.Endpoint { + return next +} + +func NewKitexTraceIDMiddleware() *KitexTraceIDMiddleware { + return &KitexTraceIDMiddleware{} +} diff --git a/plugins/kitex/middlewares/trace_id_otel.go b/plugins/kitex/middlewares/trace_id_otel.go new file mode 100644 index 0000000..0c30644 --- /dev/null +++ b/plugins/kitex/middlewares/trace_id_otel.go @@ -0,0 +1,37 @@ +//go:build kitex && otel +// +build kitex,otel + +package middlewares + +import ( + "context" + + "github.com/cloudwego/kitex/pkg/endpoint" + "github.com/shoplineapp/go-app/common" + "github.com/shoplineapp/go-app/plugins" + "go.opentelemetry.io/otel/trace" +) + +func init() { + plugins.Registry = append(plugins.Registry, NewKitexTraceIDMiddleware) +} + +type KitexTraceIDMiddleware struct { +} + +// Handler bridges the OTel SpanContext (populated by +// kitex-contrib/obs-opentelemetry's tracing.NewServerSuite) into the legacy +// ctx["trace_id"] string key that downstream consumers (e.g. common.GetTraceID, +// plugins/pulsar/producer.go) still rely on. +func (m KitexTraceIDMiddleware) Handler(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, request, response interface{}) error { + if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { + ctx = common.NewContextWithTraceID(ctx, sc.TraceID().String()) + } + return next(ctx, request, response) + } +} + +func NewKitexTraceIDMiddleware() *KitexTraceIDMiddleware { + return &KitexTraceIDMiddleware{} +} From 1f378179e4a65b93069126207801c6a3c02de49b Mon Sep 17 00:00:00 2001 From: ansonlee Date: Tue, 16 Jun 2026 10:57:25 +0800 Subject: [PATCH 4/7] fix(common): generate W3C trace_id when no span is active When neither the legacy ctx["trace_id"] key nor an OTel SpanContext is set, GetTraceID used to return "", breaking callers like plugins/pulsar/producer.go and downstream TraceIDClientInterceptor that rely on it for log/Sentry/message correlation. Fall back to a freshly generated 16-byte OTel TraceID instead, using the same pattern as the OTel SDK's own randomIDGenerator (math/rand seeded once from crypto/rand, mutex-guarded for goroutine safety). The returned value is a valid 32-char lowercase hex string, consistent with the W3C traceparent format produced elsewhere in the OTel pipeline. --- common/context.go | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/common/context.go b/common/context.go index c223732..de6c586 100644 --- a/common/context.go +++ b/common/context.go @@ -2,6 +2,10 @@ package common import ( "context" + crand "crypto/rand" + "encoding/binary" + "math/rand" + "sync" "github.com/google/uuid" "go.opentelemetry.io/otel/trace" @@ -14,6 +18,17 @@ func NewContextWithTraceID(ctx context.Context, traceId string) context.Context return context.WithValue(ctx, "trace_id", traceId) } +// GetTraceID returns the trace_id associated with ctx, looking in three places +// in order: +// +// 1. The legacy ctx["trace_id"] string key (seeded by the gRPC/Kitex bridge +// interceptors from the OTel SpanContext, or by NewContextWithTraceID). +// 2. The active OTel SpanContext on ctx (covers code paths that wrap a span +// without going through the bridge — e.g. background goroutines that +// inherit a parent span, or direct tracer.Start callers). +// 3. A freshly generated 16-byte W3C TraceID, so callers that invoke +// GetTraceID on a context.Background() / unset context still receive a +// valid W3C-format ID for log/Sentry/Pulsar correlation rather than "". func GetTraceID(ctx context.Context) string { if v, ok := ctx.Value("trace_id").(string); ok && v != "" { return v @@ -21,5 +36,27 @@ func GetTraceID(ctx context.Context) string { if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { return sc.TraceID().String() } - return "" + return newTraceID().String() +} + +// newTraceID returns a fresh 16-byte OTel TraceID sourced from math/rand, +// matching the OTel SDK's own randomIDGenerator pattern (see +// go.opentelemetry.io/otel/sdk/trace/id_generator.go). The rand source is +// seeded once at package init time from crypto/rand. Mutex-guarded since +// math/rand.Rand is not goroutine-safe. +var ( + traceIDRandMu sync.Mutex + traceIDRand = func() *rand.Rand { + var seed int64 + _ = binary.Read(crand.Reader, binary.LittleEndian, &seed) + return rand.New(rand.NewSource(seed)) + }() +) + +func newTraceID() trace.TraceID { + tid := trace.TraceID{} + traceIDRandMu.Lock() + _, _ = traceIDRand.Read(tid[:]) + traceIDRandMu.Unlock() + return tid } From 33ca03bdfe3a5976a50f65f9479ee28c0f23306b Mon Sep 17 00:00:00 2001 From: ansonlee Date: Tue, 16 Jun 2026 11:06:09 +0800 Subject: [PATCH 5/7] refactor(common): align newTraceID with OTel SDK v1.38 randomIDGenerator Switch from a self-managed *math/rand.Rand + sync.Mutex + crypto/rand seed to math/rand/v2's package-level functions, matching the OTel SDK's current pattern. The v2 functions are goroutine-safe so the mutex and crypto/rand seed are no longer needed. Also adopt the SDK's binary.NativeEndian.PutUint64 split (8 bytes per Uint64) and the IsValid() retry loop, which guarantees a non-zero trace ID. --- common/context.go | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/common/context.go b/common/context.go index de6c586..8750de9 100644 --- a/common/context.go +++ b/common/context.go @@ -2,10 +2,8 @@ package common import ( "context" - crand "crypto/rand" "encoding/binary" - "math/rand" - "sync" + "math/rand/v2" "github.com/google/uuid" "go.opentelemetry.io/otel/trace" @@ -39,24 +37,17 @@ func GetTraceID(ctx context.Context) string { return newTraceID().String() } -// newTraceID returns a fresh 16-byte OTel TraceID sourced from math/rand, -// matching the OTel SDK's own randomIDGenerator pattern (see -// go.opentelemetry.io/otel/sdk/trace/id_generator.go). The rand source is -// seeded once at package init time from crypto/rand. Mutex-guarded since -// math/rand.Rand is not goroutine-safe. -var ( - traceIDRandMu sync.Mutex - traceIDRand = func() *rand.Rand { - var seed int64 - _ = binary.Read(crand.Reader, binary.LittleEndian, &seed) - return rand.New(rand.NewSource(seed)) - }() -) - +// newTraceID returns a fresh non-zero 16-byte OTel TraceID, matching the +// OTel SDK's own randomIDGenerator.NewIDs pattern (see +// go.opentelemetry.io/otel/sdk/trace/id_generator.go). math/rand/v2's +// package-level functions are goroutine-safe, so no mutex is required. func newTraceID() trace.TraceID { - tid := trace.TraceID{} - traceIDRandMu.Lock() - _, _ = traceIDRand.Read(tid[:]) - traceIDRandMu.Unlock() - return tid + var tid trace.TraceID + for { + binary.NativeEndian.PutUint64(tid[:8], rand.Uint64()) + binary.NativeEndian.PutUint64(tid[8:], rand.Uint64()) + if tid.IsValid() { + return tid + } + } } From d9e3d8e9d2babfd8b0c1f410feeb5ff5b4da1689 Mon Sep 17 00:00:00 2001 From: ansonlee Date: Tue, 16 Jun 2026 15:30:30 +0800 Subject: [PATCH 6/7] chore: use existing trace --- Makefile | 2 +- plugins/kitex/kitex.go | 4 +-- plugins/kitex/middlewares/trace_id.go | 23 ++++++++++---- plugins/kitex/middlewares/trace_id_otel.go | 37 ---------------------- 4 files changed, 19 insertions(+), 47 deletions(-) delete mode 100644 plugins/kitex/middlewares/trace_id_otel.go diff --git a/Makefile b/Makefile index 072865f..c2ed369 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ .PHONY: all build test test: - @PROJECT_ROOT=$(PWD) APP_ENV=test go test -timeout 5s -tags grpc,pulsar,kitex ./... + @PROJECT_ROOT=$(PWD) APP_ENV=test go test -timeout 5s -tags grpc,pulsar,kitex,otel ./... diff --git a/plugins/kitex/kitex.go b/plugins/kitex/kitex.go index d707c53..188d1da 100644 --- a/plugins/kitex/kitex.go +++ b/plugins/kitex/kitex.go @@ -1,5 +1,5 @@ -//go:build kitex -// +build kitex +//go:build kitex && otel +// +build kitex,otel package kitex diff --git a/plugins/kitex/middlewares/trace_id.go b/plugins/kitex/middlewares/trace_id.go index 8fa4ab9..0c30644 100644 --- a/plugins/kitex/middlewares/trace_id.go +++ b/plugins/kitex/middlewares/trace_id.go @@ -1,11 +1,15 @@ -//go:build kitex && !otel -// +build kitex,!otel +//go:build kitex && otel +// +build kitex,otel package middlewares import ( + "context" + "github.com/cloudwego/kitex/pkg/endpoint" + "github.com/shoplineapp/go-app/common" "github.com/shoplineapp/go-app/plugins" + "go.opentelemetry.io/otel/trace" ) func init() { @@ -15,12 +19,17 @@ func init() { type KitexTraceIDMiddleware struct { } -// Handler is a no-op when OpenTelemetry is not enabled. The OTel bridge lives -// in trace_id_otel.go; without OTel there is no SpanContext to copy into -// ctx["trace_id"], so the legacy key stays unset and common.GetTraceID -// returns "" for Kitex requests. +// Handler bridges the OTel SpanContext (populated by +// kitex-contrib/obs-opentelemetry's tracing.NewServerSuite) into the legacy +// ctx["trace_id"] string key that downstream consumers (e.g. common.GetTraceID, +// plugins/pulsar/producer.go) still rely on. func (m KitexTraceIDMiddleware) Handler(next endpoint.Endpoint) endpoint.Endpoint { - return next + return func(ctx context.Context, request, response interface{}) error { + if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { + ctx = common.NewContextWithTraceID(ctx, sc.TraceID().String()) + } + return next(ctx, request, response) + } } func NewKitexTraceIDMiddleware() *KitexTraceIDMiddleware { diff --git a/plugins/kitex/middlewares/trace_id_otel.go b/plugins/kitex/middlewares/trace_id_otel.go deleted file mode 100644 index 0c30644..0000000 --- a/plugins/kitex/middlewares/trace_id_otel.go +++ /dev/null @@ -1,37 +0,0 @@ -//go:build kitex && otel -// +build kitex,otel - -package middlewares - -import ( - "context" - - "github.com/cloudwego/kitex/pkg/endpoint" - "github.com/shoplineapp/go-app/common" - "github.com/shoplineapp/go-app/plugins" - "go.opentelemetry.io/otel/trace" -) - -func init() { - plugins.Registry = append(plugins.Registry, NewKitexTraceIDMiddleware) -} - -type KitexTraceIDMiddleware struct { -} - -// Handler bridges the OTel SpanContext (populated by -// kitex-contrib/obs-opentelemetry's tracing.NewServerSuite) into the legacy -// ctx["trace_id"] string key that downstream consumers (e.g. common.GetTraceID, -// plugins/pulsar/producer.go) still rely on. -func (m KitexTraceIDMiddleware) Handler(next endpoint.Endpoint) endpoint.Endpoint { - return func(ctx context.Context, request, response interface{}) error { - if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { - ctx = common.NewContextWithTraceID(ctx, sc.TraceID().String()) - } - return next(ctx, request, response) - } -} - -func NewKitexTraceIDMiddleware() *KitexTraceIDMiddleware { - return &KitexTraceIDMiddleware{} -} From 591f15d1a5e16b3e24ec47d32fbf3b1ada4e8845 Mon Sep 17 00:00:00 2001 From: ansonlee Date: Tue, 16 Jun 2026 16:55:36 +0800 Subject: [PATCH 7/7] fix: handler order --- plugins/kitex/kitex.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/kitex/kitex.go b/plugins/kitex/kitex.go index 188d1da..5af3f35 100644 --- a/plugins/kitex/kitex.go +++ b/plugins/kitex/kitex.go @@ -56,18 +56,18 @@ func (s *KitexServer) Configure(initializer func(opts ...kitex_server.Option) ki }), } - if s.middlewares != nil { - for _, middleware := range s.middlewares { - options = append(options, kitex_server.WithMiddleware(middleware)) - } - } - if s.suites != nil { for _, suite := range s.suites { options = append(options, kitex_server.WithSuite(suite)) } } + if s.middlewares != nil { + for _, middleware := range s.middlewares { + options = append(options, kitex_server.WithMiddleware(middleware)) + } + } + s.server = initializer(options...) kitex_server.RegisterShutdownHook(func() { s.logger.Info("GRPC server gracefully shutting down...")