From 63ed3264ab476a4a1afe26476a801effb81550df Mon Sep 17 00:00:00 2001 From: eric <1048315650@qq.com> Date: Tue, 21 Jan 2025 18:24:50 +0800 Subject: [PATCH] feat(v0.4.5): v0.4.5 --- .../kratos/middleware/ktracing/metadata.go | 46 ++++ .../middleware/ktracing/metadata_test.go | 107 ++++++++ adapter/kratos/middleware/ktracing/span.go | 152 +++++++++++ .../middleware/ktracing/statshandler.go | 43 ++++ .../middleware/ktracing/statshandler_test.go | 80 ++++++ adapter/kratos/middleware/ktracing/tracer.go | 81 ++++++ adapter/kratos/middleware/ktracing/tracing.go | 96 +++++++ .../middleware/ktracing/tracing_test.go | 235 ++++++++++++++++++ 8 files changed, 840 insertions(+) create mode 100644 adapter/kratos/middleware/ktracing/metadata.go create mode 100644 adapter/kratos/middleware/ktracing/metadata_test.go create mode 100644 adapter/kratos/middleware/ktracing/span.go create mode 100644 adapter/kratos/middleware/ktracing/statshandler.go create mode 100644 adapter/kratos/middleware/ktracing/statshandler_test.go create mode 100644 adapter/kratos/middleware/ktracing/tracer.go create mode 100644 adapter/kratos/middleware/ktracing/tracing.go create mode 100644 adapter/kratos/middleware/ktracing/tracing_test.go diff --git a/adapter/kratos/middleware/ktracing/metadata.go b/adapter/kratos/middleware/ktracing/metadata.go new file mode 100644 index 0000000..fca8ca5 --- /dev/null +++ b/adapter/kratos/middleware/ktracing/metadata.go @@ -0,0 +1,46 @@ +package ktracing + +import ( + "context" + + "go.opentelemetry.io/otel/propagation" + + "github.com/go-kratos/kratos/v2" + "github.com/go-kratos/kratos/v2/metadata" +) + +const serviceHeader = "x-md-service-name" + +// Metadata is tracing metadata propagator +type Metadata struct{} + +var _ propagation.TextMapPropagator = Metadata{} + +// Inject sets metadata key-values from ctx into the carrier. +func (b Metadata) Inject(ctx context.Context, carrier propagation.TextMapCarrier) { + app, ok := kratos.FromContext(ctx) + if ok { + carrier.Set(serviceHeader, app.Name()) + } +} + +// Extract returns a copy of parent with the metadata from the carrier added. +func (b Metadata) Extract(parent context.Context, carrier propagation.TextMapCarrier) context.Context { + name := carrier.Get(serviceHeader) + if name == "" { + return parent + } + if md, ok := metadata.FromServerContext(parent); ok { + md.Set(serviceHeader, name) + return parent + } + md := metadata.New() + md.Set(serviceHeader, name) + parent = metadata.NewServerContext(parent, md) + return parent +} + +// Fields returns the keys who's values are set with Inject. +func (b Metadata) Fields() []string { + return []string{serviceHeader} +} diff --git a/adapter/kratos/middleware/ktracing/metadata_test.go b/adapter/kratos/middleware/ktracing/metadata_test.go new file mode 100644 index 0000000..de9b8e6 --- /dev/null +++ b/adapter/kratos/middleware/ktracing/metadata_test.go @@ -0,0 +1,107 @@ +package ktracing + +import ( + "context" + "reflect" + "testing" + + "github.com/go-kratos/kratos/v2" + "github.com/go-kratos/kratos/v2/metadata" + + "go.opentelemetry.io/otel/propagation" +) + +func TestMetadata_Inject(t *testing.T) { + type args struct { + appName string + carrier propagation.TextMapCarrier + } + tests := []struct { + name string + args args + want string + }{ + { + name: "https://go-kratos.dev", + args: args{"https://go-kratos.dev", propagation.HeaderCarrier{}}, + want: "https://go-kratos.dev", + }, + { + name: "https://github.com/go-kratos/kratos", + args: args{"https://github.com/go-kratos/kratos", propagation.HeaderCarrier{"mode": []string{"test"}}}, + want: "https://github.com/go-kratos/kratos", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := kratos.New(kratos.Name(tt.args.appName)) + ctx := kratos.NewContext(context.Background(), a) + m := new(Metadata) + m.Inject(ctx, tt.args.carrier) + if res := tt.args.carrier.Get(serviceHeader); tt.want != res { + t.Errorf("Get(serviceHeader) :%s want: %s", res, tt.want) + } + }) + } +} + +func TestMetadata_Extract(t *testing.T) { + type args struct { + parent context.Context + carrier propagation.TextMapCarrier + } + tests := []struct { + name string + args args + want string + crash bool + }{ + { + name: "https://go-kratos.dev", + args: args{ + parent: context.Background(), + carrier: propagation.HeaderCarrier{"X-Md-Service-Name": []string{"https://go-kratos.dev"}}, + }, + want: "https://go-kratos.dev", + }, + { + name: "https://github.com/go-kratos/kratos", + args: args{ + parent: metadata.NewServerContext(context.Background(), metadata.Metadata{}), + carrier: propagation.HeaderCarrier{"X-Md-Service-Name": []string{"https://github.com/go-kratos/kratos"}}, + }, + want: "https://github.com/go-kratos/kratos", + }, + { + name: "https://github.com/go-kratos/kratos", + args: args{ + parent: metadata.NewServerContext(context.Background(), metadata.Metadata{}), + carrier: propagation.HeaderCarrier{"X-Md-Service-Name": nil}, + }, + crash: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := Metadata{} + ctx := b.Extract(tt.args.parent, tt.args.carrier) + md, ok := metadata.FromServerContext(ctx) + if !ok { + if tt.crash { + return + } + t.Errorf("expect %v, got %v", true, ok) + } + if !reflect.DeepEqual(md.Get(serviceHeader), tt.want) { + t.Errorf("expect %v, got %v", tt.want, md.Get(serviceHeader)) + } + }) + } +} + +func TestFields(t *testing.T) { + b := Metadata{} + if !reflect.DeepEqual(b.Fields(), []string{"x-md-service-name"}) { + t.Errorf("expect %v, got %v", []string{"x-md-service-name"}, b.Fields()) + } +} diff --git a/adapter/kratos/middleware/ktracing/span.go b/adapter/kratos/middleware/ktracing/span.go new file mode 100644 index 0000000..a90f52e --- /dev/null +++ b/adapter/kratos/middleware/ktracing/span.go @@ -0,0 +1,152 @@ +package ktracing + +import ( + "context" + "net" + "net/url" + "strings" + + "github.com/go-kratos/kratos/v2/metadata" + "github.com/go-kratos/kratos/v2/transport" + "github.com/go-kratos/kratos/v2/transport/http" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/peer" + "google.golang.org/protobuf/proto" +) + +func setClientSpan(ctx context.Context, span trace.Span, m interface{}) { + var ( + attrs []attribute.KeyValue + remote string + operation string + rpcKind string + ) + tr, ok := transport.FromClientContext(ctx) + if ok { + operation = tr.Operation() + rpcKind = tr.Kind().String() + switch tr.Kind() { + case transport.KindHTTP: + if ht, ok := tr.(http.Transporter); ok { + method := ht.Request().Method + route := ht.PathTemplate() + path := ht.Request().URL.Path + attrs = append(attrs, semconv.HTTPMethodKey.String(method)) + attrs = append(attrs, semconv.HTTPRouteKey.String(route)) + attrs = append(attrs, semconv.HTTPTargetKey.String(path)) + remote = ht.Request().Host + } + case transport.KindGRPC: + remote, _ = parseTarget(tr.Endpoint()) + } + } + attrs = append(attrs, semconv.RPCSystemKey.String(rpcKind)) + _, mAttrs := parseFullMethod(operation) + attrs = append(attrs, mAttrs...) + if remote != "" { + attrs = append(attrs, peerAttr(remote)...) + } + if p, ok := m.(proto.Message); ok { + attrs = append(attrs, attribute.Key("send_msg.size").Int(proto.Size(p))) + } + + span.SetAttributes(attrs...) +} + +func setServerSpan(ctx context.Context, span trace.Span, m interface{}) { + var ( + attrs []attribute.KeyValue + remote string + operation string + rpcKind string + ) + tr, ok := transport.FromServerContext(ctx) + if ok { + operation = tr.Operation() + rpcKind = tr.Kind().String() + switch tr.Kind() { + case transport.KindHTTP: + if ht, ok := tr.(http.Transporter); ok { + method := ht.Request().Method + route := ht.PathTemplate() + path := ht.Request().URL.Path + attrs = append(attrs, semconv.HTTPMethodKey.String(method)) + attrs = append(attrs, semconv.HTTPRouteKey.String(route)) + attrs = append(attrs, semconv.HTTPTargetKey.String(path)) + remote = ht.Request().RemoteAddr + } + case transport.KindGRPC: + if p, ok := peer.FromContext(ctx); ok { + remote = p.Addr.String() + } + } + } + attrs = append(attrs, semconv.RPCSystemKey.String(rpcKind)) + _, mAttrs := parseFullMethod(operation) + attrs = append(attrs, mAttrs...) + attrs = append(attrs, peerAttr(remote)...) + if p, ok := m.(proto.Message); ok { + attrs = append(attrs, attribute.Key("recv_msg.size").Int(proto.Size(p))) + } + if md, ok := metadata.FromServerContext(ctx); ok { + attrs = append(attrs, semconv.PeerServiceKey.String(md.Get(serviceHeader))) + } + + span.SetAttributes(attrs...) +} + +// parseFullMethod returns a span name following the OpenTelemetry semantic +// conventions as well as all applicable span attribute.KeyValue attributes based +// on a gRPC's FullMethod. +func parseFullMethod(fullMethod string) (string, []attribute.KeyValue) { + name := strings.TrimLeft(fullMethod, "/") + parts := strings.SplitN(name, "/", 2) + if len(parts) != 2 { //nolint:mnd + // Invalid format, does not follow `/package.service/method`. + return name, []attribute.KeyValue{attribute.Key("rpc.operation").String(fullMethod)} + } + + var attrs []attribute.KeyValue + if service := parts[0]; service != "" { + attrs = append(attrs, semconv.RPCServiceKey.String(service)) + } + if method := parts[1]; method != "" { + attrs = append(attrs, semconv.RPCMethodKey.String(method)) + } + return name, attrs +} + +// peerAttr returns attributes about the peer address. +func peerAttr(addr string) []attribute.KeyValue { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return []attribute.KeyValue(nil) + } + + if host == "" { + host = "127.0.0.1" + } + + return []attribute.KeyValue{ + semconv.NetPeerIPKey.String(host), + semconv.NetPeerPortKey.String(port), + } +} + +func parseTarget(endpoint string) (address string, err error) { + var u *url.URL + u, err = url.Parse(endpoint) + if err != nil { + if u, err = url.Parse("http://" + endpoint); err != nil { + return "", err + } + return u.Host, nil + } + if len(u.Path) > 1 { + return u.Path[1:], nil + } + return endpoint, nil +} diff --git a/adapter/kratos/middleware/ktracing/statshandler.go b/adapter/kratos/middleware/ktracing/statshandler.go new file mode 100644 index 0000000..65c4db9 --- /dev/null +++ b/adapter/kratos/middleware/ktracing/statshandler.go @@ -0,0 +1,43 @@ +package ktracing + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/stats" +) + +// ClientHandler is tracing ClientHandler +type ClientHandler struct{} + +// HandleConn exists to satisfy gRPC stats.Handler. +func (c *ClientHandler) HandleConn(_ context.Context, _ stats.ConnStats) { + fmt.Println("Handle connection.") +} + +// TagConn exists to satisfy gRPC stats.Handler. +func (c *ClientHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { + return ctx +} + +// HandleRPC implements per-RPC tracing and stats instrumentation. +func (c *ClientHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) { + if _, ok := rs.(*stats.OutHeader); !ok { + return + } + p, ok := peer.FromContext(ctx) + if !ok { + return + } + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + span.SetAttributes(peerAttr(p.Addr.String())...) + } +} + +// TagRPC implements per-RPC context management. +func (c *ClientHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context { + return ctx +} diff --git a/adapter/kratos/middleware/ktracing/statshandler_test.go b/adapter/kratos/middleware/ktracing/statshandler_test.go new file mode 100644 index 0000000..3ca8c5f --- /dev/null +++ b/adapter/kratos/middleware/ktracing/statshandler_test.go @@ -0,0 +1,80 @@ +package ktracing + +import ( + "context" + "net" + "testing" + + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/stats" +) + +type ctxKey string + +const testKey ctxKey = "MY_TEST_KEY" + +func TestClient_HandleConn(_ *testing.T) { + (&ClientHandler{}).HandleConn(context.Background(), nil) +} + +func TestClient_TagConn(t *testing.T) { + client := &ClientHandler{} + ctx := context.WithValue(context.Background(), testKey, 123) + + if client.TagConn(ctx, nil).Value(testKey) != 123 { + t.Errorf(`The context value must be 123 for the "MY_KEY_TEST" key, %v given.`, client.TagConn(ctx, nil).Value(testKey)) + } +} + +func TestClient_TagRPC(t *testing.T) { + client := &ClientHandler{} + ctx := context.WithValue(context.Background(), testKey, 123) + + if client.TagRPC(ctx, nil).Value(testKey) != 123 { + t.Errorf(`The context value must be 123 for the "MY_KEY_TEST" key, %v given.`, client.TagConn(ctx, nil).Value(testKey)) + } +} + +type mockSpan struct { + trace.Span + mockSpanCtx *trace.SpanContext +} + +func (m *mockSpan) SpanContext() trace.SpanContext { + return *m.mockSpanCtx +} + +func TestClient_HandleRPC(_ *testing.T) { + client := &ClientHandler{} + ctx := context.Background() + rs := stats.OutHeader{} + + // Handle stats.RPCStats is not type of stats.OutHeader case + client.HandleRPC(context.TODO(), nil) + + // Handle context doesn't have the peerkey filled with a Peer instance + client.HandleRPC(ctx, &rs) + + // Handle context with the peerkey filled with a Peer instance + ip, _ := net.ResolveIPAddr("ip", "1.1.1.1") + ctx = peer.NewContext(ctx, &peer.Peer{ + Addr: ip, + }) + client.HandleRPC(ctx, &rs) + + // Handle context with Span + _, span := noop.NewTracerProvider().Tracer("Tracer").Start(ctx, "Spanname") + spanCtx := trace.SpanContext{} + spanID := [8]byte{12, 12, 12, 12, 12, 12, 12, 12} + traceID := [16]byte{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12} + spanCtx = spanCtx.WithTraceID(traceID) + spanCtx = spanCtx.WithSpanID(spanID) + mSpan := mockSpan{ + Span: span, + mockSpanCtx: &spanCtx, + } + ctx = trace.ContextWithSpan(ctx, &mSpan) + client.HandleRPC(ctx, &rs) +} diff --git a/adapter/kratos/middleware/ktracing/tracer.go b/adapter/kratos/middleware/ktracing/tracer.go new file mode 100644 index 0000000..0a5b4ab --- /dev/null +++ b/adapter/kratos/middleware/ktracing/tracer.go @@ -0,0 +1,81 @@ +package ktracing + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/proto" + + "github.com/go-kratos/kratos/v2/errors" +) + +// Tracer is otel span tracer +type Tracer struct { + tracer trace.Tracer + kind trace.SpanKind + opt *options +} + +// NewTracer create tracer instance +func NewTracer(kind trace.SpanKind, opts ...Option) *Tracer { + op := options{ + propagator: propagation.NewCompositeTextMapPropagator(Metadata{}, propagation.Baggage{}, propagation.TraceContext{}), + tracerName: "kratos", + } + for _, o := range opts { + o(&op) + } + if op.tracerProvider == nil { + op.tracerProvider = otel.GetTracerProvider() + } + + switch kind { + case trace.SpanKindClient: + return &Tracer{tracer: op.tracerProvider.Tracer(op.tracerName), kind: kind, opt: &op} + case trace.SpanKindServer: + return &Tracer{tracer: op.tracerProvider.Tracer(op.tracerName), kind: kind, opt: &op} + default: + panic(fmt.Sprintf("unsupported span kind: %v", kind)) + } +} + +// Start start tracing span +func (t *Tracer) Start(ctx context.Context, operation string, carrier propagation.TextMapCarrier) (context.Context, trace.Span) { + if t.kind == trace.SpanKindServer { + ctx = t.opt.propagator.Extract(ctx, carrier) + } + ctx, span := t.tracer.Start(ctx, + operation, + trace.WithSpanKind(t.kind), + ) + // carrier 注入一下, 用于后续其他业务可以通过 carrier 获取 + t.opt.propagator.Inject(ctx, carrier) + return ctx, span +} + +// End finish tracing span +func (t *Tracer) End(_ context.Context, span trace.Span, m interface{}, err error) { + if err != nil { + span.RecordError(err) + if e := errors.FromError(err); e != nil { + span.SetAttributes(attribute.Key("rpc.status_code").Int64(int64(e.Code))) + } + span.SetStatus(codes.Error, err.Error()) + } else { + span.SetStatus(codes.Ok, "OK") + } + + if p, ok := m.(proto.Message); ok { + if t.kind == trace.SpanKindServer { + span.SetAttributes(attribute.Key("send_msg.size").Int(proto.Size(p))) + } else { + span.SetAttributes(attribute.Key("recv_msg.size").Int(proto.Size(p))) + } + } + span.End() +} diff --git a/adapter/kratos/middleware/ktracing/tracing.go b/adapter/kratos/middleware/ktracing/tracing.go new file mode 100644 index 0000000..5851d23 --- /dev/null +++ b/adapter/kratos/middleware/ktracing/tracing.go @@ -0,0 +1,96 @@ +package ktracing + +import ( + "context" + + "github.com/go-kratos/kratos/v2/log" + + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + + "github.com/go-kratos/kratos/v2/middleware" + "github.com/go-kratos/kratos/v2/transport" +) + +// Option is tracing option. +type Option func(*options) + +type options struct { + tracerName string + tracerProvider trace.TracerProvider + propagator propagation.TextMapPropagator +} + +// WithPropagator with tracer propagator. +func WithPropagator(propagator propagation.TextMapPropagator) Option { + return func(opts *options) { + opts.propagator = propagator + } +} + +// WithTracerProvider with tracer provider. +// By default, it uses the global provider that is set by otel.SetTracerProvider(provider). +func WithTracerProvider(provider trace.TracerProvider) Option { + return func(opts *options) { + opts.tracerProvider = provider + } +} + +// WithTracerName with tracer name +func WithTracerName(tracerName string) Option { + return func(opts *options) { + opts.tracerName = tracerName + } +} + +// Server returns a new server middleware for OpenTelemetry. +func Server(opts ...Option) middleware.Middleware { + tracer := NewTracer(trace.SpanKindServer, opts...) + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (reply interface{}, err error) { + if tr, ok := transport.FromServerContext(ctx); ok { + var span trace.Span + ctx, span = tracer.Start(ctx, tr.Operation(), tr.RequestHeader()) + setServerSpan(ctx, span, req) + defer func() { tracer.End(ctx, span, reply, err) }() + } + return handler(ctx, req) + } + } +} + +// Client returns a new client middleware for OpenTelemetry. +func Client(opts ...Option) middleware.Middleware { + tracer := NewTracer(trace.SpanKindClient, opts...) + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (reply interface{}, err error) { + if tr, ok := transport.FromClientContext(ctx); ok { + var span trace.Span + ctx, span = tracer.Start(ctx, tr.Operation(), tr.RequestHeader()) + setClientSpan(ctx, span, req) + defer func() { tracer.End(ctx, span, reply, err) }() + } + return handler(ctx, req) + } + } +} + +// TraceID returns a traceid valuer. +func TraceID() log.Valuer { + return func(ctx context.Context) interface{} { + if span := trace.SpanContextFromContext(ctx); span.HasTraceID() { + return span.TraceID().String() + } + return "" + } +} + +// SpanID returns a spanid valuer. +func SpanID() log.Valuer { + return func(ctx context.Context) interface{} { + if span := trace.SpanContextFromContext(ctx); span.HasSpanID() { + return span.SpanID().String() + } + return "" + } +} diff --git a/adapter/kratos/middleware/ktracing/tracing_test.go b/adapter/kratos/middleware/ktracing/tracing_test.go new file mode 100644 index 0000000..62fa966 --- /dev/null +++ b/adapter/kratos/middleware/ktracing/tracing_test.go @@ -0,0 +1,235 @@ +package ktracing + +import ( + "context" + "net/http" + "os" + "reflect" + "testing" + + "go.opentelemetry.io/otel/propagation" + tracesdk "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/transport" +) + +var _ transport.Transporter = (*mockTransport)(nil) + +type headerCarrier http.Header + +// Get returns the value associated with the passed key. +func (hc headerCarrier) Get(key string) string { + return http.Header(hc).Get(key) +} + +// Set stores the key-value pair. +func (hc headerCarrier) Set(key string, value string) { + http.Header(hc).Set(key, value) +} + +// Add value to the key-value pair. +func (hc headerCarrier) Add(key string, value string) { + http.Header(hc).Add(key, value) +} + +// Keys lists the keys stored in this carrier. +func (hc headerCarrier) Keys() []string { + keys := make([]string, 0, len(hc)) + for k := range http.Header(hc) { + keys = append(keys, k) + } + return keys +} + +// Values returns a slice value associated with the passed key. +func (hc headerCarrier) Values(key string) []string { + return http.Header(hc).Values(key) +} + +type mockTransport struct { + kind transport.Kind + endpoint string + operation string + header headerCarrier + request *http.Request +} + +func (tr *mockTransport) Kind() transport.Kind { return tr.kind } +func (tr *mockTransport) Endpoint() string { return tr.endpoint } +func (tr *mockTransport) Operation() string { return tr.operation } +func (tr *mockTransport) RequestHeader() transport.Header { return tr.header } +func (tr *mockTransport) ReplyHeader() transport.Header { return tr.header } +func (tr *mockTransport) Request() *http.Request { + if tr.request == nil { + rq, _ := http.NewRequest(http.MethodGet, "/endpoint", nil) + + return rq + } + + return tr.request +} +func (tr *mockTransport) PathTemplate() string { return "" } + +func TestTracer(t *testing.T) { + carrier := headerCarrier{} + tp := tracesdk.NewTracerProvider(tracesdk.WithSampler(tracesdk.TraceIDRatioBased(0))) + + // caller use Inject + cliTracer := NewTracer( + trace.SpanKindClient, + WithTracerProvider(tp), + WithPropagator( + propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{}), + ), + ) + + ts := &mockTransport{kind: transport.KindHTTP, header: carrier} + + ctx, aboveSpan := cliTracer.Start(transport.NewClientContext(context.Background(), ts), ts.Operation(), ts.RequestHeader()) + defer cliTracer.End(ctx, aboveSpan, nil, nil) + + // server use Extract fetch traceInfo from carrier + svrTracer := NewTracer(trace.SpanKindServer, WithPropagator(propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{}))) + ts = &mockTransport{kind: transport.KindHTTP, header: carrier} + + ctx, span := svrTracer.Start(transport.NewServerContext(ctx, ts), ts.Operation(), ts.RequestHeader()) + defer svrTracer.End(ctx, span, nil, nil) + + if aboveSpan.SpanContext().TraceID() != span.SpanContext().TraceID() { + t.Fatalf("TraceID failed to deliver") + } + + if v, ok := transport.FromClientContext(ctx); !ok || len(v.RequestHeader().Keys()) == 0 { + t.Fatalf("traceHeader failed to deliver") + } +} + +func TestServer(t *testing.T) { + tr := &mockTransport{ + kind: transport.KindHTTP, + endpoint: "server:2233", + operation: "/test.server/hello", + header: headerCarrier{}, + } + + tracer := NewTracer( + trace.SpanKindClient, + WithTracerProvider(tracesdk.NewTracerProvider()), + ) + + logger := log.NewStdLogger(os.Stdout) + logger = log.With(logger, "span_id", SpanID()) + logger = log.With(logger, "trace_id", TraceID()) + + var ( + childSpanID string + childTraceID string + ) + next := func(ctx context.Context, req interface{}) (interface{}, error) { + _ = log.WithContext(ctx, logger).Log(log.LevelInfo, + "kind", "server", + ) + childSpanID = SpanID()(ctx).(string) + childTraceID = TraceID()(ctx).(string) + return req.(string) + "https://go-kratos.dev", nil + } + + var ctx context.Context + ctx, span := tracer.Start( + transport.NewServerContext(context.Background(), tr), + tr.Operation(), + tr.RequestHeader(), + ) + + _, err := Server( + WithTracerProvider(tracesdk.NewTracerProvider()), + WithPropagator(propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{})), + )(next)(ctx, "test server: ") + + span.End() + if err != nil { + t.Errorf("expected nil, got %v", err) + } + if childSpanID == "" { + t.Errorf("expected empty, got %v", childSpanID) + } + if reflect.DeepEqual(span.SpanContext().SpanID().String(), childSpanID) { + t.Errorf("span.SpanContext().SpanID().String()(%v) is not equal to childSpanID(%v)", span.SpanContext().SpanID().String(), childSpanID) + } + if !reflect.DeepEqual(span.SpanContext().TraceID().String(), childTraceID) { + t.Errorf("expected %v, got %v", childTraceID, span.SpanContext().TraceID().String()) + } + + _, err = Server( + WithTracerProvider(tracesdk.NewTracerProvider()), + WithPropagator(propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{})), + )(next)(context.Background(), "test server: ") + if err != nil { + t.Errorf("expected error, got nil") + } + if childSpanID != "" { + t.Errorf("expected empty, got %v", childSpanID) + } + if childTraceID != "" { + t.Errorf("expected empty, got %v", childTraceID) + } +} + +func TestClient(t *testing.T) { + tr := &mockTransport{ + kind: transport.KindHTTP, + endpoint: "server:2233", + operation: "/test.server/hello", + header: headerCarrier{}, + } + + tracer := NewTracer( + trace.SpanKindClient, + WithTracerProvider(tracesdk.NewTracerProvider()), + ) + + logger := log.NewStdLogger(os.Stdout) + logger = log.With(logger, "span_id", SpanID()) + logger = log.With(logger, "trace_id", TraceID()) + + var ( + childSpanID string + childTraceID string + ) + next := func(ctx context.Context, req interface{}) (interface{}, error) { + _ = log.WithContext(ctx, logger).Log(log.LevelInfo, + "kind", "client", + ) + childSpanID = SpanID()(ctx).(string) + childTraceID = TraceID()(ctx).(string) + return req.(string) + "https://go-kratos.dev", nil + } + + var ctx context.Context + ctx, span := tracer.Start( + transport.NewClientContext(context.Background(), tr), + tr.Operation(), + tr.RequestHeader(), + ) + + _, err := Client( + WithTracerProvider(tracesdk.NewTracerProvider()), + WithPropagator(propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{})), + )(next)(ctx, "test client: ") + + span.End() + if err != nil { + t.Errorf("expected nil, got %v", err) + } + if childSpanID == "" { + t.Errorf("expected empty, got %v", childSpanID) + } + if reflect.DeepEqual(span.SpanContext().SpanID().String(), childSpanID) { + t.Errorf("span.SpanContext().SpanID().String()(%v) is not equal to childSpanID(%v)", span.SpanContext().SpanID().String(), childSpanID) + } + if !reflect.DeepEqual(span.SpanContext().TraceID().String(), childTraceID) { + t.Errorf("expected %v, got %v", childTraceID, span.SpanContext().TraceID().String()) + } +}