diff --git a/internal/ingest/conversion_test.go b/internal/ingest/conversion_test.go index 5311e61a..8e588e4e 100644 --- a/internal/ingest/conversion_test.go +++ b/internal/ingest/conversion_test.go @@ -185,6 +185,67 @@ func TestTraceSemanticConventionPriority(t *testing.T) { } } +func TestTraceRPCSemanticConventions(t *testing.T) { + for _, tc := range []struct { + name string + attrs map[string]string + service, method string + }{ + {"old convention", map[string]string{"rpc.service": "oteldemo.CurrencyService", "rpc.method": "Convert"}, "oteldemo.CurrencyService", "Convert"}, + {"grpc", map[string]string{"rpc.system.name": "grpc", "rpc.method": "oteldemo.CurrencyService/Convert"}, "oteldemo.CurrencyService", "Convert"}, + {"deprecated grpc", map[string]string{"rpc.system": "grpc", "rpc.method": "oteldemo.CurrencyService/Convert"}, "oteldemo.CurrencyService", "Convert"}, + {"connect rpc", map[string]string{"rpc.system.name": "connectrpc", "rpc.method": "oteldemo.CurrencyService/Convert"}, "oteldemo.CurrencyService", "Convert"}, + {"connect rpc with deprecated system", map[string]string{"rpc.system.name": "connectrpc", "rpc.system": "connect_rpc", "rpc.method": "oteldemo.CurrencyService/Convert"}, "oteldemo.CurrencyService", "Convert"}, + {"deprecated connect rpc", map[string]string{"rpc.system": "connect_rpc", "rpc.method": "oteldemo.CurrencyService/Convert"}, "oteldemo.CurrencyService", "Convert"}, + {"jsonrpc", map[string]string{"rpc.system.name": "jsonrpc", "rpc.method": "textDocument/hover"}, "", "textDocument/hover"}, + {"deprecated jsonrpc", map[string]string{"rpc.system": "jsonrpc", "rpc.method": "textDocument/hover"}, "", "textDocument/hover"}, + {"unknown system", map[string]string{"rpc.system.name": "unknown", "rpc.method": "service/method"}, "", "service/method"}, + {"deprecated unknown system", map[string]string{"rpc.system": "unknown", "rpc.method": "service/method"}, "", "service/method"}, + {"no system", map[string]string{"rpc.method": "service/method"}, "", "service/method"}, + {"current grpc wins", map[string]string{"rpc.system.name": "grpc", "rpc.system": "jsonrpc", "rpc.method": "service/method"}, "service", "method"}, + {"current jsonrpc wins", map[string]string{"rpc.system.name": "jsonrpc", "rpc.system": "grpc", "rpc.method": "textDocument/hover"}, "", "textDocument/hover"}, + {"explicit service with qualified method", map[string]string{"rpc.system.name": "grpc", "rpc.service": "CurrencyService", "rpc.method": "oteldemo.CurrencyService/Convert"}, "CurrencyService", "oteldemo.CurrencyService/Convert"}, + {"method without slash", map[string]string{"rpc.method": "Convert"}, "", "Convert"}, + {"last slash", map[string]string{"rpc.system.name": "grpc", "rpc.method": "package/CurrencyService/Convert"}, "package/CurrencyService", "Convert"}, + {"trailing slash", map[string]string{"rpc.system.name": "grpc", "rpc.method": "oteldemo.CurrencyService/"}, "oteldemo.CurrencyService", ""}, + {"leading slash", map[string]string{"rpc.system.name": "grpc", "rpc.method": "/Convert"}, "", "Convert"}, + {"slash only", map[string]string{"rpc.system.name": "grpc", "rpc.method": "/"}, "", ""}, + {"empty method", map[string]string{"rpc.method": ""}, "", ""}, + {"empty service with qualified method", map[string]string{"rpc.system.name": "grpc", "rpc.service": "", "rpc.method": "oteldemo.CurrencyService/Convert"}, "", "oteldemo.CurrencyService/Convert"}, + {"empty values", map[string]string{"rpc.service": "", "rpc.method": ""}, "", ""}, + {"service only", map[string]string{"rpc.service": "CurrencyService"}, "CurrencyService", ""}, + {"absent", nil, "", ""}, + } { + t.Run(tc.name, func(t *testing.T) { + var attrs []*common.KeyValue + for k, v := range tc.attrs { + attrs = append(attrs, &common.KeyValue{Key: k, Value: &common.AnyValue{Value: &common.AnyValue_StringValue{StringValue: v}}}) + } + var got telemetrystore.Batch + srv := NewServer(config.Config{}, conversionSubmitter(func(_ context.Context, b telemetrystore.Batch) error { got = b; return nil })) + req := &collectortrace.ExportTraceServiceRequest{ResourceSpans: []*tracepb.ResourceSpans{{ScopeSpans: []*tracepb.ScopeSpans{{Spans: []*tracepb.Span{{Attributes: attrs}}}}}}} + if _, err := srv.exportTraces(t.Context(), req); err != nil { + t.Fatal(err) + } + if len(got.Spans) != 1 { + t.Fatalf("got %d spans, want 1", len(got.Spans)) + } + span := got.Spans[0] + if span.RPCService != tc.service || span.RPCMethod != tc.method { + t.Fatalf("extracted service=%q method=%q, want service=%q method=%q", span.RPCService, span.RPCMethod, tc.service, tc.method) + } + if len(span.Attributes) != len(tc.attrs) { + t.Fatalf("raw attributes changed: got %v, want %v", span.Attributes, tc.attrs) + } + for k, v := range tc.attrs { + if span.Attributes[k] != v { + t.Errorf("raw attribute %q = %v, want %q", k, span.Attributes[k], v) + } + } + }) + } +} + func BenchmarkExportConversion(b *testing.B) { traces, logs, metrics := conversionExports(250) srv := NewServer(config.Config{DefaultNamespace: "fallback"}, conversionSubmitter(func(context.Context, telemetrystore.Batch) error { return nil })) diff --git a/internal/ingest/server.go b/internal/ingest/server.go index 0c2ae926..26ba46dd 100644 --- a/internal/ingest/server.go +++ b/internal/ingest/server.go @@ -135,6 +135,14 @@ func (s *Server) exportTraces(ctx context.Context, req *collectortrace.ExportTra ServiceVersion: serviceVersion, DeploymentEnv: deploymentEnv, } + // gRPC and Connect RPC (current and deprecated spellings) fold rpc.service into a qualified rpc.method. + if _, ok := attrs["rpc.service"]; !ok { + rpcSystem := spanAttr(sp.Attributes, "rpc.system.name", "rpc.system") + if i := strings.LastIndex(row.RPCMethod, "/"); i >= 0 && (rpcSystem == "grpc" || rpcSystem == "connectrpc" || rpcSystem == "connect_rpc") { + row.RPCService = row.RPCMethod[:i] + row.RPCMethod = row.RPCMethod[i+1:] + } + } // Pre-extracted exception info from span events. excType, excMsg := extractException(sp.Events) row.ExceptionType = excType