|
|
|
@@ -4,6 +4,7 @@ package wrapper // import "go.unistack.org/micro/v3/tracer/wrapper"
|
|
|
|
import (
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
|
|
"go.unistack.org/micro/v3/client"
|
|
|
|
"go.unistack.org/micro/v3/client"
|
|
|
|
"go.unistack.org/micro/v3/metadata"
|
|
|
|
"go.unistack.org/micro/v3/metadata"
|
|
|
|
@@ -11,107 +12,87 @@ import (
|
|
|
|
"go.unistack.org/micro/v3/tracer"
|
|
|
|
"go.unistack.org/micro/v3/tracer"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var DefaultHeadersExctract = []string{metadata.HeaderXRequestID}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func ExtractDefaultLabels(md metadata.Metadata) []interface{} {
|
|
|
|
|
|
|
|
labels := make([]interface{}, 0, len(DefaultHeadersExctract))
|
|
|
|
|
|
|
|
for _, k := range DefaultHeadersExctract {
|
|
|
|
|
|
|
|
if v, ok := md.Get(k); ok {
|
|
|
|
|
|
|
|
labels = append(labels, strings.ToLower(k), v)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return labels
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
var (
|
|
|
|
DefaultClientCallObserver = func(ctx context.Context, req client.Request, rsp interface{}, opts []client.CallOption, sp tracer.Span, err error) {
|
|
|
|
DefaultClientCallObserver = func(ctx context.Context, req client.Request, rsp interface{}, opts []client.CallOption, sp tracer.Span, err error) {
|
|
|
|
sp.SetName(fmt.Sprintf("Call %s.%s", req.Service(), req.Method()))
|
|
|
|
|
|
|
|
var labels []interface{}
|
|
|
|
var labels []interface{}
|
|
|
|
if md, ok := metadata.FromOutgoingContext(ctx); ok {
|
|
|
|
if md, ok := metadata.FromOutgoingContext(ctx); ok {
|
|
|
|
labels = make([]interface{}, 0, len(md)+1)
|
|
|
|
labels = append(labels, ExtractDefaultLabels(md)...)
|
|
|
|
for k, v := range md {
|
|
|
|
|
|
|
|
labels = append(labels, k, v)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
labels = append(labels, "error", err.Error())
|
|
|
|
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
labels = append(labels, "kind", sp.Kind())
|
|
|
|
sp.AddLabels(labels...)
|
|
|
|
sp.SetLabels(labels...)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DefaultClientStreamObserver = func(ctx context.Context, req client.Request, opts []client.CallOption, stream client.Stream, sp tracer.Span, err error) {
|
|
|
|
DefaultClientStreamObserver = func(ctx context.Context, req client.Request, opts []client.CallOption, stream client.Stream, sp tracer.Span, err error) {
|
|
|
|
sp.SetName(fmt.Sprintf("Stream %s.%s", req.Service(), req.Method()))
|
|
|
|
|
|
|
|
var labels []interface{}
|
|
|
|
var labels []interface{}
|
|
|
|
if md, ok := metadata.FromOutgoingContext(ctx); ok {
|
|
|
|
if md, ok := metadata.FromOutgoingContext(ctx); ok {
|
|
|
|
labels = make([]interface{}, 0, len(md))
|
|
|
|
labels = append(labels, ExtractDefaultLabels(md)...)
|
|
|
|
for k, v := range md {
|
|
|
|
|
|
|
|
labels = append(labels, k, v)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
labels = append(labels, "error", err.Error())
|
|
|
|
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
labels = append(labels, "kind", sp.Kind())
|
|
|
|
sp.AddLabels(labels...)
|
|
|
|
sp.SetLabels(labels...)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DefaultClientPublishObserver = func(ctx context.Context, msg client.Message, opts []client.PublishOption, sp tracer.Span, err error) {
|
|
|
|
DefaultClientPublishObserver = func(ctx context.Context, msg client.Message, opts []client.PublishOption, sp tracer.Span, err error) {
|
|
|
|
sp.SetName(fmt.Sprintf("Publish %s", msg.Topic()))
|
|
|
|
|
|
|
|
var labels []interface{}
|
|
|
|
var labels []interface{}
|
|
|
|
if md, ok := metadata.FromOutgoingContext(ctx); ok {
|
|
|
|
if md, ok := metadata.FromOutgoingContext(ctx); ok {
|
|
|
|
labels = make([]interface{}, 0, len(md))
|
|
|
|
labels = append(labels, ExtractDefaultLabels(md)...)
|
|
|
|
for k, v := range md {
|
|
|
|
|
|
|
|
labels = append(labels, k, v)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
labels = append(labels, ExtractDefaultLabels(msg.Metadata())...)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
labels = append(labels, "error", err.Error())
|
|
|
|
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
labels = append(labels, "kind", sp.Kind())
|
|
|
|
sp.AddLabels(labels...)
|
|
|
|
sp.SetLabels(labels...)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DefaultServerHandlerObserver = func(ctx context.Context, req server.Request, rsp interface{}, sp tracer.Span, err error) {
|
|
|
|
DefaultServerHandlerObserver = func(ctx context.Context, req server.Request, rsp interface{}, sp tracer.Span, err error) {
|
|
|
|
sp.SetName(fmt.Sprintf("Handler %s.%s", req.Service(), req.Method()))
|
|
|
|
|
|
|
|
var labels []interface{}
|
|
|
|
var labels []interface{}
|
|
|
|
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
|
|
|
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
|
|
|
labels = make([]interface{}, 0, len(md))
|
|
|
|
labels = append(labels, ExtractDefaultLabels(md)...)
|
|
|
|
for k, v := range md {
|
|
|
|
|
|
|
|
labels = append(labels, k, v)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
labels = append(labels, "error", err.Error())
|
|
|
|
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
labels = append(labels, "kind", sp.Kind())
|
|
|
|
sp.AddLabels(labels...)
|
|
|
|
sp.SetLabels(labels...)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DefaultServerSubscriberObserver = func(ctx context.Context, msg server.Message, sp tracer.Span, err error) {
|
|
|
|
DefaultServerSubscriberObserver = func(ctx context.Context, msg server.Message, sp tracer.Span, err error) {
|
|
|
|
sp.SetName(fmt.Sprintf("Subscriber %s", msg.Topic()))
|
|
|
|
|
|
|
|
var labels []interface{}
|
|
|
|
var labels []interface{}
|
|
|
|
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
|
|
|
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
|
|
|
labels = make([]interface{}, 0, len(md))
|
|
|
|
labels = append(labels, ExtractDefaultLabels(md)...)
|
|
|
|
for k, v := range md {
|
|
|
|
|
|
|
|
labels = append(labels, k, v)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
labels = append(labels, ExtractDefaultLabels(msg.Header())...)
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
labels = append(labels, "error", err.Error())
|
|
|
|
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
labels = append(labels, "kind", sp.Kind())
|
|
|
|
sp.AddLabels(labels...)
|
|
|
|
sp.SetLabels(labels...)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DefaultClientCallFuncObserver = func(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions, sp tracer.Span, err error) {
|
|
|
|
DefaultClientCallFuncObserver = func(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions, sp tracer.Span, err error) {
|
|
|
|
sp.SetName(fmt.Sprintf("Call %s.%s", req.Service(), req.Method()))
|
|
|
|
sp.SetName(fmt.Sprintf("%s.%s call", req.Service(), req.Method()))
|
|
|
|
var labels []interface{}
|
|
|
|
var labels []interface{}
|
|
|
|
if md, ok := metadata.FromOutgoingContext(ctx); ok {
|
|
|
|
if md, ok := metadata.FromOutgoingContext(ctx); ok {
|
|
|
|
labels = make([]interface{}, 0, len(md))
|
|
|
|
labels = append(labels, ExtractDefaultLabels(md)...)
|
|
|
|
for k, v := range md {
|
|
|
|
|
|
|
|
labels = append(labels, k, v)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
labels = append(labels, "error", err.Error())
|
|
|
|
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
labels = append(labels, "kind", sp.Kind())
|
|
|
|
sp.AddLabels(labels...)
|
|
|
|
sp.SetLabels(labels...)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DefaultSkipEndpoints = []string{"Meter.Metrics", "Health.Live", "Health.Ready", "Health.Version"}
|
|
|
|
DefaultSkipEndpoints = []string{"Meter.Metrics", "Health.Live", "Health.Ready", "Health.Version"}
|
|
|
|
@@ -241,16 +222,22 @@ func (ot *tWrapper) Call(ctx context.Context, req client.Request, rsp interface{
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sp, ok := tracer.SpanFromContext(ctx)
|
|
|
|
nctx, sp := ot.opts.Tracer.Start(ctx, fmt.Sprintf("%s.%s rpc-client", req.Service(), req.Method()),
|
|
|
|
if !ok {
|
|
|
|
tracer.WithSpanKind(tracer.SpanKindClient),
|
|
|
|
ctx, sp = ot.opts.Tracer.Start(ctx, "", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
|
|
tracer.WithSpanLabels(
|
|
|
|
}
|
|
|
|
"rpc.service", req.Service(),
|
|
|
|
|
|
|
|
"rpc.method", req.Method(),
|
|
|
|
|
|
|
|
"rpc.flavor", "rpc",
|
|
|
|
|
|
|
|
"rpc.call", "/"+req.Service()+"/"+req.Endpoint(),
|
|
|
|
|
|
|
|
"rpc.call_type", "unary",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
defer sp.Finish()
|
|
|
|
defer sp.Finish()
|
|
|
|
|
|
|
|
|
|
|
|
err := ot.Client.Call(ctx, req, rsp, opts...)
|
|
|
|
err := ot.Client.Call(nctx, req, rsp, opts...)
|
|
|
|
|
|
|
|
|
|
|
|
for _, o := range ot.opts.ClientCallObservers {
|
|
|
|
for _, o := range ot.opts.ClientCallObservers {
|
|
|
|
o(ctx, req, rsp, opts, sp, err)
|
|
|
|
o(nctx, req, rsp, opts, sp, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
return err
|
|
|
|
@@ -264,32 +251,36 @@ func (ot *tWrapper) Stream(ctx context.Context, req client.Request, opts ...clie
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sp, ok := tracer.SpanFromContext(ctx)
|
|
|
|
nctx, sp := ot.opts.Tracer.Start(ctx, fmt.Sprintf("%s.%s rpc-client", req.Service(), req.Method()),
|
|
|
|
if !ok {
|
|
|
|
tracer.WithSpanKind(tracer.SpanKindClient),
|
|
|
|
ctx, sp = ot.opts.Tracer.Start(ctx, "", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
|
|
tracer.WithSpanLabels(
|
|
|
|
}
|
|
|
|
"rpc.service", req.Service(),
|
|
|
|
|
|
|
|
"rpc.method", req.Method(),
|
|
|
|
|
|
|
|
"rpc.flavor", "rpc",
|
|
|
|
|
|
|
|
"rpc.call", "/"+req.Service()+"/"+req.Endpoint(),
|
|
|
|
|
|
|
|
"rpc.call_type", "stream",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
defer sp.Finish()
|
|
|
|
defer sp.Finish()
|
|
|
|
|
|
|
|
|
|
|
|
stream, err := ot.Client.Stream(ctx, req, opts...)
|
|
|
|
stream, err := ot.Client.Stream(nctx, req, opts...)
|
|
|
|
|
|
|
|
|
|
|
|
for _, o := range ot.opts.ClientStreamObservers {
|
|
|
|
for _, o := range ot.opts.ClientStreamObservers {
|
|
|
|
o(ctx, req, opts, stream, sp, err)
|
|
|
|
o(nctx, req, opts, stream, sp, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return stream, err
|
|
|
|
return stream, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (ot *tWrapper) Publish(ctx context.Context, msg client.Message, opts ...client.PublishOption) error {
|
|
|
|
func (ot *tWrapper) Publish(ctx context.Context, msg client.Message, opts ...client.PublishOption) error {
|
|
|
|
sp, ok := tracer.SpanFromContext(ctx)
|
|
|
|
nctx, sp := ot.opts.Tracer.Start(ctx, msg.Topic()+" publish", tracer.WithSpanKind(tracer.SpanKindProducer))
|
|
|
|
if !ok {
|
|
|
|
|
|
|
|
ctx, sp = ot.opts.Tracer.Start(ctx, "", tracer.WithSpanKind(tracer.SpanKindProducer))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
defer sp.Finish()
|
|
|
|
defer sp.Finish()
|
|
|
|
|
|
|
|
sp.AddLabels("messaging.destination.name", msg.Topic())
|
|
|
|
err := ot.Client.Publish(ctx, msg, opts...)
|
|
|
|
sp.AddLabels("messaging.operation", "publish")
|
|
|
|
|
|
|
|
err := ot.Client.Publish(nctx, msg, opts...)
|
|
|
|
|
|
|
|
|
|
|
|
for _, o := range ot.opts.ClientPublishObservers {
|
|
|
|
for _, o := range ot.opts.ClientPublishObservers {
|
|
|
|
o(ctx, msg, opts, sp, err)
|
|
|
|
o(nctx, msg, opts, sp, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
return err
|
|
|
|
@@ -303,32 +294,41 @@ func (ot *tWrapper) ServerHandler(ctx context.Context, req server.Request, rsp i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sp, ok := tracer.SpanFromContext(ctx)
|
|
|
|
callType := "unary"
|
|
|
|
if !ok {
|
|
|
|
if req.Stream() {
|
|
|
|
ctx, sp = ot.opts.Tracer.Start(ctx, "", tracer.WithSpanKind(tracer.SpanKindServer))
|
|
|
|
callType = "stream"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nctx, sp := ot.opts.Tracer.Start(ctx, fmt.Sprintf("%s.%s rpc-server", req.Service(), req.Method()),
|
|
|
|
|
|
|
|
tracer.WithSpanKind(tracer.SpanKindServer),
|
|
|
|
|
|
|
|
tracer.WithSpanLabels(
|
|
|
|
|
|
|
|
"rpc.service", req.Service(),
|
|
|
|
|
|
|
|
"rpc.method", req.Method(),
|
|
|
|
|
|
|
|
"rpc.flavor", "rpc",
|
|
|
|
|
|
|
|
"rpc.call", "/"+req.Service()+"/"+req.Endpoint(),
|
|
|
|
|
|
|
|
"rpc.call_type", callType,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
defer sp.Finish()
|
|
|
|
defer sp.Finish()
|
|
|
|
|
|
|
|
|
|
|
|
err := ot.serverHandler(ctx, req, rsp)
|
|
|
|
err := ot.serverHandler(nctx, req, rsp)
|
|
|
|
|
|
|
|
|
|
|
|
for _, o := range ot.opts.ServerHandlerObservers {
|
|
|
|
for _, o := range ot.opts.ServerHandlerObservers {
|
|
|
|
o(ctx, req, rsp, sp, err)
|
|
|
|
o(nctx, req, rsp, sp, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (ot *tWrapper) ServerSubscriber(ctx context.Context, msg server.Message) error {
|
|
|
|
func (ot *tWrapper) ServerSubscriber(ctx context.Context, msg server.Message) error {
|
|
|
|
sp, ok := tracer.SpanFromContext(ctx)
|
|
|
|
nctx, sp := ot.opts.Tracer.Start(ctx, msg.Topic()+" process", tracer.WithSpanKind(tracer.SpanKindConsumer))
|
|
|
|
if !ok {
|
|
|
|
|
|
|
|
ctx, sp = ot.opts.Tracer.Start(ctx, "", tracer.WithSpanKind(tracer.SpanKindConsumer))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
defer sp.Finish()
|
|
|
|
defer sp.Finish()
|
|
|
|
|
|
|
|
sp.AddLabels("messaging.operation", "process")
|
|
|
|
err := ot.serverSubscriber(ctx, msg)
|
|
|
|
sp.AddLabels("messaging.source.name", msg.Topic())
|
|
|
|
|
|
|
|
err := ot.serverSubscriber(nctx, msg)
|
|
|
|
|
|
|
|
|
|
|
|
for _, o := range ot.opts.ServerSubscriberObservers {
|
|
|
|
for _, o := range ot.opts.ServerSubscriberObservers {
|
|
|
|
o(ctx, msg, sp, err)
|
|
|
|
o(nctx, msg, sp, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
return err
|
|
|
|
@@ -366,16 +366,23 @@ func (ot *tWrapper) ClientCallFunc(ctx context.Context, addr string, req client.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sp, ok := tracer.SpanFromContext(ctx)
|
|
|
|
nctx, sp := ot.opts.Tracer.Start(ctx, fmt.Sprintf("%s.%s rpc-client", req.Service(), req.Method()),
|
|
|
|
if !ok {
|
|
|
|
tracer.WithSpanKind(tracer.SpanKindClient),
|
|
|
|
ctx, sp = ot.opts.Tracer.Start(ctx, "", tracer.WithSpanKind(tracer.SpanKindClient))
|
|
|
|
tracer.WithSpanLabels(
|
|
|
|
}
|
|
|
|
"rpc.service", req.Service(),
|
|
|
|
|
|
|
|
"rpc.method", req.Method(),
|
|
|
|
|
|
|
|
"rpc.flavor", "rpc",
|
|
|
|
|
|
|
|
"rpc.call", "/"+req.Service()+"/"+req.Endpoint(),
|
|
|
|
|
|
|
|
"rpc.call_type", "unary",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
defer sp.Finish()
|
|
|
|
defer sp.Finish()
|
|
|
|
|
|
|
|
|
|
|
|
err := ot.clientCallFunc(ctx, addr, req, rsp, opts)
|
|
|
|
err := ot.clientCallFunc(nctx, addr, req, rsp, opts)
|
|
|
|
|
|
|
|
|
|
|
|
for _, o := range ot.opts.ClientCallFuncObservers {
|
|
|
|
for _, o := range ot.opts.ClientCallFuncObservers {
|
|
|
|
o(ctx, addr, req, rsp, opts, sp, err)
|
|
|
|
o(nctx, addr, req, rsp, opts, sp, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
return err
|
|
|
|
|