Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions filter/otel/trace/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ package trace

import (
"context"
"strconv"
)

import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/baggage"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/propagation"
Expand All @@ -31,13 +33,46 @@ import (
)

import (
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/common/extension"
"dubbo.apache.org/dubbo-go/v3/filter"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
"dubbo.apache.org/dubbo-go/v3/protocol/result"
)

// buildSpanName returns the standardized span name for a Dubbo invocation,
// e.g. "dubbo.consumer <service>/<method>" or "dubbo.provider <service>/<method>".
func buildSpanName(side string, url *common.URL, invocation base.Invocation) string {
return "dubbo." + side + " " + url.ServiceKey() + "/" + invocation.MethodName()
}

// buildSpanAttributes collects the semantic attributes for a Dubbo span.
func buildSpanAttributes(side string, url *common.URL, invocation base.Invocation) []attribute.KeyValue {
attrs := []attribute.KeyValue{
semconv.RPCSystemApacheDubbo,
semconv.RPCService(url.ServiceKey()),
semconv.RPCMethod(invocation.MethodName()),
DubboSideKey.String(side),
}
if url.Protocol != "" {
attrs = append(attrs, DubboProtocolKey.String(url.Protocol))
}
if group := url.Group(); group != "" {
attrs = append(attrs, DubboGroupKey.String(group))
}
if version := url.Version(); version != "" {
attrs = append(attrs, DubboVersionKey.String(version))
}
if url.Ip != "" {
attrs = append(attrs, semconv.ServerAddress(url.Ip))
}
if port, err := strconv.Atoi(url.Port); err == nil {
attrs = append(attrs, semconv.ServerPort(port))
}
return attrs
}

func init() {
// TODO: use single filter to simplify filter field in configuration
extension.SetFilter(constant.OTELServerTraceKey, func() filter.Filter {
Expand Down Expand Up @@ -78,15 +113,12 @@ func (f *otelServerFilter) Invoke(ctx context.Context, invoker base.Invoker, inv
trace.WithInstrumentationVersion(constant.Version),
)

url := invoker.GetURL()
ctx, span := tracer.Start(
trace.ContextWithRemoteSpanContext(ctx, spanCtx),
invocation.ActualMethodName(),
buildSpanName(sideProvider, url, invocation),
trace.WithSpanKind(trace.SpanKindServer),
trace.WithAttributes(
semconv.RPCSystemApacheDubbo,
semconv.RPCService(invoker.GetURL().ServiceKey()),
semconv.RPCMethod(invocation.MethodName()),
),
trace.WithAttributes(buildSpanAttributes(sideProvider, url, invocation)...),
)
defer span.End()

Expand Down Expand Up @@ -120,16 +152,13 @@ func (f *otelClientFilter) Invoke(ctx context.Context, invoker base.Invoker, inv
trace.WithInstrumentationVersion(constant.Version),
)

url := invoker.GetURL()
var span trace.Span
ctx, span = tracer.Start(
ctx,
invocation.ActualMethodName(),
buildSpanName(sideConsumer, url, invocation),
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
semconv.RPCSystemApacheDubbo,
semconv.RPCService(invoker.GetURL().ServiceKey()),
semconv.RPCMethod(invocation.MethodName()),
),
trace.WithAttributes(buildSpanAttributes(sideConsumer, url, invocation)...),
)
defer span.End()

Expand Down
125 changes: 125 additions & 0 deletions filter/otel/trace/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import (
"github.com/golang/mock/gomock"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"go.opentelemetry.io/otel/trace"
)

Expand Down Expand Up @@ -645,3 +649,124 @@ func Test_otelClientFilter_Invoke(t *testing.T) {
})
}
}

// newRecordingProvider returns an in-memory TracerProvider and its span
// recorder so tests can assert the span name/kind/attributes produced by the
// filters.
func newRecordingProvider() (*sdktrace.TracerProvider, *tracetest.SpanRecorder) {
sr := tracetest.NewSpanRecorder()
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))
return tp, sr
}

// newSpanTestInvoker builds a mock invoker/invocation backed by a fully
// populated URL, used by the span name/attribute assertions below.
func newSpanTestInvoker(ctrl *gomock.Controller, methodName string) (*MockInvoker, *MockInvocation) {
res := NewMockResult(ctrl)
res.EXPECT().Error().Return(nil).AnyTimes()

url, _ := common.NewURL("tri://127.0.0.1:20000/com.foo.BarService?interface=com.foo.BarService&group=g1&version=1.0.0")

invoker := NewMockInvoker(ctrl)
invoker.EXPECT().GetURL().Return(url).AnyTimes()
invoker.EXPECT().Invoke(gomock.Any(), gomock.Any()).Return(res).AnyTimes()

invocation := NewMockInvocation(ctrl)
invocation.EXPECT().ActualMethodName().Return(methodName).AnyTimes()
invocation.EXPECT().MethodName().Return(methodName).AnyTimes()
invocation.EXPECT().SetAttachment(gomock.Any(), gomock.Any()).Return().AnyTimes()
invocation.EXPECT().Attachments().Return(map[string]any{}).AnyTimes()

return invoker, invocation
}

func attrMap(kvs []attribute.KeyValue) map[attribute.Key]attribute.Value {
m := make(map[attribute.Key]attribute.Value, len(kvs))
for _, kv := range kvs {
m[kv.Key] = kv.Value
}
return m
}

// assertCommonSpanAttributes checks the attributes shared by consumer and
// provider spans, verifying alignment with OTel semantic conventions and the
// dubbo.* namespace.
func assertCommonSpanAttributes(t *testing.T, attrs map[attribute.Key]attribute.Value, wantSide string) {
t.Helper()
strCases := []struct {
key attribute.Key
want string
}{
{semconv.RPCSystemKey, "apache_dubbo"},
{semconv.RPCServiceKey, "g1/com.foo.BarService:1.0.0"},
{semconv.RPCMethodKey, "SayHello"},
{DubboSideKey, wantSide},
{DubboProtocolKey, "tri"},
{DubboGroupKey, "g1"},
{DubboVersionKey, "1.0.0"},
{semconv.ServerAddressKey, "127.0.0.1"},
}
for _, c := range strCases {
v, ok := attrs[c.key]
if !ok {
t.Errorf("missing attribute %q", c.key)
continue
}
if v.AsString() != c.want {
t.Errorf("attribute %q = %q, want %q", c.key, v.AsString(), c.want)
}
}
if v, ok := attrs[semconv.ServerPortKey]; !ok || v.AsInt64() != 20000 {
t.Errorf("attribute %q = %v (ok=%v), want 20000", semconv.ServerPortKey, v.AsInt64(), ok)
}
}

func Test_otelServerFilter_SpanNameAndAttributes(t *testing.T) {
ctrl := gomock.NewController(t)
tp, sr := newRecordingProvider()
invoker, invocation := newSpanTestInvoker(ctrl, "SayHello")

f := &otelServerFilter{
Propagators: otel.GetTextMapPropagator(),
TracerProvider: tp,
}
f.Invoke(context.Background(), invoker, invocation)

spans := sr.Ended()
if len(spans) != 1 {
t.Fatalf("expected 1 span, got %d", len(spans))
}
span := spans[0]
if got, want := span.Name(), "dubbo.provider g1/com.foo.BarService:1.0.0/SayHello"; got != want {
t.Errorf("span name = %q, want %q", got, want)
}
if span.SpanKind() != trace.SpanKindServer {
t.Errorf("span kind = %v, want server", span.SpanKind())
}
assertCommonSpanAttributes(t, attrMap(span.Attributes()), sideProvider)
}

func Test_otelClientFilter_SpanNameAndAttributes(t *testing.T) {
ctrl := gomock.NewController(t)
tp, sr := newRecordingProvider()
invoker, invocation := newSpanTestInvoker(ctrl, "SayHello")

f := &otelClientFilter{
Propagators: otel.GetTextMapPropagator(),
TracerProvider: tp,
}
f.Invoke(context.Background(), invoker, invocation)

spans := sr.Ended()
if len(spans) != 1 {
t.Fatalf("expected 1 span, got %d", len(spans))
}
span := spans[0]
if got, want := span.Name(), "dubbo.consumer g1/com.foo.BarService:1.0.0/SayHello"; got != want {
t.Errorf("span name = %q, want %q", got, want)
}
if span.SpanKind() != trace.SpanKindClient {
t.Errorf("span kind = %v, want client", span.SpanKind())
}
assertCommonSpanAttributes(t, attrMap(span.Attributes()), sideConsumer)
}
20 changes: 20 additions & 0 deletions filter/otel/trace/semconv.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,23 @@ var (
RPCMessageTypeSent = RPCMessageTypeKey.String("SENT")
RPCMessageTypeReceived = RPCMessageTypeKey.String("RECEIVED")
)

// These describe information that has no OpenTelemetry semantic-convention
// equivalent, so they live under a stable "dubbo.*" namespace.
var (
// DubboSideKey records the invocation side: "consumer" or "provider".
DubboSideKey = attribute.Key("dubbo.side")
// DubboProtocolKey records the Dubbo protocol in use, e.g. "dubbo", "tri".
DubboProtocolKey = attribute.Key("dubbo.protocol")
// DubboGroupKey records the Dubbo service group.
DubboGroupKey = attribute.Key("dubbo.group")
// DubboVersionKey records the Dubbo service version.
DubboVersionKey = attribute.Key("dubbo.version")
Comment on lines +37 to +43

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dubbo.side这些常量可以移到common/const里面

)

const (
// sideConsumer / sideProvider are the values used for DubboSideKey and the
// span-name prefix ("dubbo.consumer" / "dubbo.provider").
sideConsumer = "consumer"
sideProvider = "provider"
)
Comment on lines +46 to +51

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

4 changes: 2 additions & 2 deletions protocol/triple/triple_invoker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -644,8 +644,8 @@ func TestTripleClientOTELTraceparentIsolation(t *testing.T) {
require.Len(t, invoker.calls, 2)
endedSpans := spanRecorder.Ended()
serverReadOnly := findEndedSpanByName(t, endedSpans, "service-a-handler")
clientBReadOnly := findEndedSpanByName(t, endedSpans, "CallServiceB")
clientCReadOnly := findEndedSpanByName(t, endedSpans, "CallServiceC")
clientBReadOnly := findEndedSpanByName(t, endedSpans, "dubbo.consumer org.apache.dubbo.test.DownstreamService/CallServiceB")
clientCReadOnly := findEndedSpanByName(t, endedSpans, "dubbo.consumer org.apache.dubbo.test.DownstreamService/CallServiceC")

clientBOutgoing := parseTraceparent(t, invoker.calls[0].outgoingTraceparent)
clientCOutgoing := parseTraceparent(t, invoker.calls[1].outgoingTraceparent)
Expand Down
Loading