diff --git a/CHANGELOG.md b/CHANGELOG.md index 3430988ccf9..0e3bf6c555f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added + +- `WithSpanKind` option in `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc` to override the default span kind. (#8506) + ### Fixed - Enforce that `client_certificate_file` and `client_key_file` are provided together in `go.opentelemetry.io/contrib/otelconf`. (#8450) diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/config.go b/instrumentation/google.golang.org/grpc/otelgrpc/config.go index 0018e2d45fb..2a64fd330cc 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/config.go +++ b/instrumentation/google.golang.org/grpc/otelgrpc/config.go @@ -36,6 +36,7 @@ type config struct { Propagators propagation.TextMapPropagator TracerProvider trace.TracerProvider MeterProvider metric.MeterProvider + SpanKind trace.SpanKind SpanStartOptions []trace.SpanStartOption SpanAttributes []attribute.KeyValue MetricAttributes []attribute.KeyValue @@ -181,6 +182,18 @@ func WithSpanOptions(opts ...trace.SpanStartOption) Option { }) } +// WithSpanKind returns an Option to set the span kind for spans created by +// the handler. +// +// By default, [NewServerHandler] creates spans with +// [trace.SpanKindServer] and [NewClientHandler] creates spans with +// [trace.SpanKindClient]. +func WithSpanKind(sk trace.SpanKind) Option { + return optionFunc(func(c *config) { + c.SpanKind = sk + }) +} + // WithSpanAttributes returns an Option to add custom attributes to the spans. func WithSpanAttributes(a ...attribute.KeyValue) Option { return optionFunc(func(c *config) { diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler.go b/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler.go index 0340ca77668..3d85599c56d 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler.go +++ b/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler.go @@ -46,6 +46,10 @@ type serverHandler struct { // NewServerHandler creates a stats.Handler for a gRPC server. func NewServerHandler(opts ...Option) stats.Handler { c := newConfig(opts) + if c.SpanKind == trace.SpanKindUnspecified { + c.SpanKind = trace.SpanKindServer + } + h := &serverHandler{config: c} h.tracer = c.TracerProvider.Tracer( @@ -104,7 +108,7 @@ func (h *serverHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) cont spanAttributes := make([]attribute.KeyValue, 0, len(attrs)+len(h.SpanAttributes)) spanAttributes = append(append(spanAttributes, attrs...), h.SpanAttributes...) opts := []trace.SpanStartOption{ - trace.WithSpanKind(trace.SpanKindServer), + trace.WithSpanKind(h.SpanKind), trace.WithAttributes(spanAttributes...), } if h.PublicEndpoint || (h.PublicEndpointFn != nil && h.PublicEndpointFn(ctx, info)) { @@ -161,6 +165,10 @@ type clientHandler struct { // NewClientHandler creates a stats.Handler for a gRPC client. func NewClientHandler(opts ...Option) stats.Handler { c := newConfig(opts) + if c.SpanKind == trace.SpanKindUnspecified { + c.SpanKind = trace.SpanKindClient + } + h := &clientHandler{config: c} h.tracer = c.TracerProvider.Tracer( @@ -210,7 +218,7 @@ func (h *clientHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) cont ctx, _ = h.tracer.Start( ctx, name, - trace.WithSpanKind(trace.SpanKindClient), + trace.WithSpanKind(h.SpanKind), trace.WithAttributes(spanAttributes...), ) } diff --git a/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler_test.go b/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler_test.go index 5b4ce93d0a1..4c4380fd151 100644 --- a/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler_test.go +++ b/instrumentation/google.golang.org/grpc/otelgrpc/stats_handler_test.go @@ -165,6 +165,92 @@ func TestWithPublicEndpointFn(t *testing.T) { } } +func TestWithSpanKind(t *testing.T) { + tests := []struct { + name string + handler func(...Option) stats.Handler + opt Option + wantSpanKind trace.SpanKind + defaultKind trace.SpanKind + defaultKindStr string + }{ + { + name: "ServerHandler with default kind", + handler: NewServerHandler, + opt: nil, + wantSpanKind: trace.SpanKindServer, + defaultKind: trace.SpanKindServer, + defaultKindStr: "server", + }, + { + name: "ServerHandler with Internal kind", + handler: NewServerHandler, + opt: WithSpanKind(trace.SpanKindInternal), + wantSpanKind: trace.SpanKindInternal, + }, + { + name: "ServerHandler with Consumer kind", + handler: NewServerHandler, + opt: WithSpanKind(trace.SpanKindConsumer), + wantSpanKind: trace.SpanKindConsumer, + }, + { + name: "ClientHandler with default kind", + handler: NewClientHandler, + opt: nil, + wantSpanKind: trace.SpanKindClient, + defaultKind: trace.SpanKindClient, + defaultKindStr: "client", + }, + { + name: "ClientHandler with Internal kind", + handler: NewClientHandler, + opt: WithSpanKind(trace.SpanKindInternal), + wantSpanKind: trace.SpanKindInternal, + }, + { + name: "ClientHandler with Producer kind", + handler: NewClientHandler, + opt: WithSpanKind(trace.SpanKindProducer), + wantSpanKind: trace.SpanKindProducer, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + spanRecorder := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider( + sdktrace.WithSpanProcessor(spanRecorder), + ) + + opts := []Option{WithTracerProvider(provider)} + if tt.opt != nil { + opts = append(opts, tt.opt) + } + + h := tt.handler(opts...) + + ctx := h.TagRPC(t.Context(), &stats.RPCTagInfo{ + FullMethodName: "some.package/Method", + FailFast: true, + }) + + h.HandleRPC(ctx, &stats.End{ + Client: false, + BeginTime: time.Time{}, + EndTime: time.Time{}, + Trailer: metadata.MD{}, + Error: nil, + }) + + require.NoError(t, spanRecorder.ForceFlush(ctx)) + spans := spanRecorder.Ended() + require.Len(t, spans, 1) + assert.Equal(t, tt.wantSpanKind, spans[0].SpanKind()) + }) + } +} + func TestNilProviderOption(t *testing.T) { // Passing a nil TracerProvider or MeterProvider should not panic and // should use the global provider instead.