diff --git a/internal/xerrors/transport.go b/internal/xerrors/transport.go index 6390c2a32..3adb600cf 100644 --- a/internal/xerrors/transport.go +++ b/internal/xerrors/transport.go @@ -182,5 +182,11 @@ func TransportError(err error) Error { if errors.As(err, &t) { return t } + if s, ok := grpcStatus.FromError(err); ok { + return &transportError{ + status: s, + err: err, + } + } return nil } diff --git a/metrics/error.go b/metrics/error.go deleted file mode 100644 index 56ac88e07..000000000 --- a/metrics/error.go +++ /dev/null @@ -1,34 +0,0 @@ -package metrics - -import ( - "context" - "errors" - "io" - "net" - - "github.com/ydb-platform/ydb-go-sdk/v3" -) - -func errorBrief(err error) string { - if err == nil { - return "OK" - } - var ( - netErr *net.OpError - ydbErr ydb.Error - ) - switch { - case errors.As(err, &netErr): - return "network/" + netErr.Op + " -> " + netErr.Err.Error() - case errors.Is(err, io.EOF): - return "io/EOF" - case errors.Is(err, context.DeadlineExceeded): - return "context/DeadlineExceeded" - case errors.Is(err, context.Canceled): - return "context/Canceled" - case errors.As(err, &ydbErr): - return ydbErr.Name() - default: - return "unknown" - } -} diff --git a/metrics/error_brief.go b/metrics/error_brief.go new file mode 100644 index 000000000..68fdf95f0 --- /dev/null +++ b/metrics/error_brief.go @@ -0,0 +1,55 @@ +package metrics + +import ( + "context" + "io" + "net" + + "github.com/ydb-platform/ydb-go-sdk/v3/internal/allocator" + "github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors" +) + +func errorBrief(err error) string { + if err == nil { + return "OK" + } + if xerrors.Is(err, io.EOF) { + return "io/EOF" + } + if netErr := (*net.OpError)(nil); xerrors.As(err, &netErr) { + buffer := allocator.Buffers.Get() + defer allocator.Buffers.Put(buffer) + buffer.WriteString("network") + if netErr.Op != "" { + buffer.WriteByte('/') + buffer.WriteString(netErr.Op) + } + if netErr.Addr != nil { + buffer.WriteByte('[') + buffer.WriteString(netErr.Addr.String()) + buffer.WriteByte(']') + } + if netErr.Err != nil { + buffer.WriteByte('(') + buffer.WriteString(errorBrief(netErr.Err)) + buffer.WriteByte(')') + } + return buffer.String() + } + if xerrors.Is(err, context.DeadlineExceeded) { + return "context/DeadlineExceeded" + } + if xerrors.Is(err, context.Canceled) { + return "context/Canceled" + } + if xerrors.IsTransportError(err) { + return xerrors.TransportError(err).Name() + } + if xerrors.IsOperationError(err) { + return xerrors.OperationError(err).Name() + } + if ydbErr := xerrors.Error(nil); xerrors.As(err, &ydbErr) { + return ydbErr.Name() + } + return "unknown" +} diff --git a/metrics/error_brief_test.go b/metrics/error_brief_test.go new file mode 100644 index 000000000..072e440c6 --- /dev/null +++ b/metrics/error_brief_test.go @@ -0,0 +1,323 @@ +package metrics + +import ( + "context" + "fmt" + "io" + "net" + "testing" + + "github.com/stretchr/testify/require" + "github.com/ydb-platform/ydb-go-genproto/protos/Ydb" + grpcCodes "google.golang.org/grpc/codes" + grpcStatus "google.golang.org/grpc/status" + + "github.com/ydb-platform/ydb-go-sdk/v3/internal/stack" + "github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors" +) + +func stackRecord() string { + return stack.Record(1, + stack.PackagePath(false), + stack.PackageName(false), + stack.StructName(false), + stack.FunctionName(false), + stack.Lambda(false), + ) +} + +func TestErrorBrief(t *testing.T) { + for _, tt := range []struct { + name string + err error + brief string + }{ + { + name: stackRecord(), + err: nil, + brief: "OK", + }, + { + name: stackRecord(), + err: context.Canceled, + brief: "context/Canceled", + }, + { + name: stackRecord(), + err: xerrors.WithStackTrace(context.Canceled), + brief: "context/Canceled", + }, + { + name: stackRecord(), + err: context.DeadlineExceeded, + brief: "context/DeadlineExceeded", + }, + { + name: stackRecord(), + err: xerrors.WithStackTrace(context.DeadlineExceeded), + brief: "context/DeadlineExceeded", + }, + { + name: stackRecord(), + err: fmt.Errorf("test"), + brief: "unknown", + }, + { + name: stackRecord(), + err: io.EOF, + brief: "io/EOF", + }, + { + name: stackRecord(), + err: &net.OpError{ + Op: "write", + Addr: &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 2135, + }, + Err: grpcStatus.Error(grpcCodes.Unavailable, ""), + }, + brief: "network/write[0.0.0.0:2135](transport/Unavailable)", + }, + { + name: stackRecord(), + err: xerrors.Retryable(fmt.Errorf("test")), + brief: "retryable/CUSTOM", + }, + { + name: stackRecord(), + err: xerrors.Retryable(fmt.Errorf("test"), xerrors.WithName("SomeName")), + brief: "retryable/SomeName", + }, + { + name: stackRecord(), + err: xerrors.WithStackTrace(xerrors.Retryable(fmt.Errorf("test"))), + brief: "retryable/CUSTOM", + }, + { + name: stackRecord(), + err: xerrors.WithStackTrace( + xerrors.Retryable(fmt.Errorf("test"), xerrors.WithName("SomeName")), + ), + brief: "retryable/SomeName", + }, + { + name: stackRecord(), + err: xerrors.WithStackTrace(&net.OpError{ + Op: "write", + Addr: &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 2135, + }, + Err: grpcStatus.Error(grpcCodes.Unavailable, ""), + }), + brief: "network/write[0.0.0.0:2135](transport/Unavailable)", + }, + { + name: stackRecord(), + err: grpcStatus.Error(grpcCodes.Unavailable, ""), + brief: "transport/Unavailable", + }, + { + name: stackRecord(), + err: xerrors.Transport( + grpcStatus.Error(grpcCodes.Unavailable, ""), + ), + brief: "transport/Unavailable", + }, + { + name: stackRecord(), + err: xerrors.Operation( + xerrors.WithStatusCode(Ydb.StatusIds_BAD_REQUEST), + ), + brief: "operation/BAD_REQUEST", + }, + // errors with stack trace + { + name: stackRecord(), + err: xerrors.WithStackTrace(fmt.Errorf("test")), + brief: "unknown", + }, + { + name: stackRecord(), + err: xerrors.WithStackTrace(io.EOF), + brief: "io/EOF", + }, + { + name: stackRecord(), + err: xerrors.WithStackTrace( + grpcStatus.Error(grpcCodes.Unavailable, ""), + ), + brief: "transport/Unavailable", + }, + { + name: stackRecord(), + err: xerrors.WithStackTrace(xerrors.Transport( + grpcStatus.Error(grpcCodes.Unavailable, ""), + )), + brief: "transport/Unavailable", + }, + { + name: stackRecord(), + err: xerrors.WithStackTrace(xerrors.Operation( + xerrors.WithStatusCode(Ydb.StatusIds_BAD_REQUEST), + )), + brief: "operation/BAD_REQUEST", + }, + // joined errors + { + name: stackRecord(), + err: xerrors.Join(fmt.Errorf("test")), + brief: "unknown", + }, + { + name: stackRecord(), + err: xerrors.Join( + fmt.Errorf("test"), + xerrors.Retryable(fmt.Errorf("test")), + ), + brief: "retryable/CUSTOM", + }, + { + name: stackRecord(), + err: xerrors.Join( + fmt.Errorf("test"), + xerrors.Retryable(fmt.Errorf("test"), xerrors.WithName("SomeName")), + ), + brief: "retryable/SomeName", + }, + { + name: stackRecord(), + err: xerrors.Join( + fmt.Errorf("test"), + xerrors.Retryable(fmt.Errorf("test"), xerrors.WithName("SomeName")), + grpcStatus.Error(grpcCodes.Unavailable, "test"), + ), + brief: "transport/Unavailable", + }, + { + name: stackRecord(), + err: xerrors.Join(io.EOF), + brief: "io/EOF", + }, + { + name: stackRecord(), + err: xerrors.Join( + grpcStatus.Error(grpcCodes.Unavailable, ""), + ), + brief: "transport/Unavailable", + }, + { + name: stackRecord(), + err: xerrors.Join(xerrors.Transport( + grpcStatus.Error(grpcCodes.Unavailable, ""), + )), + brief: "transport/Unavailable", + }, + { + name: stackRecord(), + err: xerrors.Join(xerrors.Operation( + xerrors.WithStatusCode(Ydb.StatusIds_BAD_REQUEST), + )), + brief: "operation/BAD_REQUEST", + }, + // joined errors with stack trace + { + name: stackRecord(), + err: xerrors.Join(xerrors.WithStackTrace(fmt.Errorf("test"))), + brief: "unknown", + }, + { + name: stackRecord(), + err: xerrors.Join(xerrors.WithStackTrace(io.EOF)), + brief: "io/EOF", + }, + { + name: stackRecord(), + err: xerrors.Join(xerrors.WithStackTrace(xerrors.Transport( + grpcStatus.Error(grpcCodes.Unavailable, ""), + ))), + brief: "transport/Unavailable", + }, + { + name: stackRecord(), + err: xerrors.Join(xerrors.WithStackTrace(xerrors.Operation( + xerrors.WithStatusCode(Ydb.StatusIds_BAD_REQUEST), + ))), + brief: "operation/BAD_REQUEST", + }, + // joined errors (mixed types) + { + name: stackRecord(), + err: xerrors.Join( + xerrors.WithStackTrace(fmt.Errorf("test")), + xerrors.WithStackTrace(io.EOF), + ), + brief: "io/EOF", + }, + { + name: stackRecord(), + err: xerrors.WithStackTrace(xerrors.Join( + xerrors.WithStackTrace(fmt.Errorf("test")), + xerrors.WithStackTrace(io.EOF), + )), + brief: "io/EOF", + }, + { + name: stackRecord(), + err: xerrors.Join( + io.EOF, + grpcStatus.Error(grpcCodes.Unavailable, ""), + xerrors.WithStackTrace(xerrors.Operation( + xerrors.WithStatusCode(Ydb.StatusIds_BAD_REQUEST), + )), + ), + brief: "io/EOF", + }, + { + name: stackRecord(), + err: xerrors.Join( + &net.OpError{ + Op: "write", + Addr: &net.TCPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 2135, + }, + Err: grpcStatus.Error(grpcCodes.Unavailable, ""), + }, + io.EOF, + grpcStatus.Error(grpcCodes.Unavailable, ""), + xerrors.WithStackTrace(xerrors.Operation( + xerrors.WithStatusCode(Ydb.StatusIds_BAD_REQUEST), + )), + ), + brief: "io/EOF", + }, + { + name: stackRecord(), + err: xerrors.Join( + grpcStatus.Error(grpcCodes.Unavailable, ""), + xerrors.WithStackTrace(xerrors.Operation( + xerrors.WithStatusCode(Ydb.StatusIds_BAD_REQUEST), + )), + ), + brief: "transport/Unavailable", + }, + { + name: stackRecord(), + err: xerrors.WithStackTrace(xerrors.Join( + xerrors.WithStackTrace(xerrors.Transport( + grpcStatus.Error(grpcCodes.Unavailable, ""), + )), + xerrors.WithStackTrace(xerrors.Operation( + xerrors.WithStatusCode(Ydb.StatusIds_BAD_REQUEST), + )), + )), + brief: "transport/Unavailable", + }, + } { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.brief, errorBrief(tt.err)) + }) + } +}