Skip to content
This repository has been archived by the owner on Apr 18, 2023. It is now read-only.

Commit

Permalink
feat: added error unwrapping
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Chodur <m.chodur@seznam.cz>
  • Loading branch information
FUSAKLA committed Oct 1, 2019
1 parent ae0d866 commit f09a023
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 6 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added
* Support for error unwrapping. (Supported for `github.com/pkg/errors` and native wrapping added in go1.13)

## [1.2.0](https://github.com/grpc-ecosystem/go-grpc-prometheus/releases/tag/v1.2.0) - 2018-06-04

### Added
Expand Down
2 changes: 0 additions & 2 deletions makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
SHELL="/bin/bash"

GOFILES_NOVENDOR = $(shell go list ./... | grep -v /vendor/)

all: vet fmt test
Expand Down
57 changes: 57 additions & 0 deletions packages/grpcstatus/grpcstatus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package grpcstatus

import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type gRPCStatus interface {
GRPCStatus() *status.Status
}

func unwrapPkgErrorsGRPCStatus(err error) (*status.Status, bool) {
type causer interface {
Cause() error
}

// Unwrapping the github.com/pkg/errors causer interface, using `Cause` directly could miss some error implementing
// the `GRPCStatus` function so we have to check it on our selves.
unwrappedCauser := err
for unwrappedCauser != nil {
if s, ok := unwrappedCauser.(gRPCStatus); ok {
return s.GRPCStatus(), true
}
cause, ok := unwrappedCauser.(causer)
if !ok {
break
}
unwrappedCauser = cause.Cause()
}
return nil, false
}

// Since error can be wrapped and the `FromError` function only checks for `GRPCStatus` function
// and as a fallback uses the `Unknown` gRPC status we need to unwrap the error if possible to get the original status.
// pkg/errors and Go native errors packages have two different approaches so we try to unwrap both types.
// Eventually should be implemented in the go-grpc status function `FromError`. See https://github.com/grpc/grpc-go/issues/2934
func GrpcStatusFromError(err error) (s *status.Status, ok bool) {
s, ok = status.FromError(err)
if ok {
return s, true
}

// Try to unwrap `github.com/pkg/errors` wrapped error
s, ok = unwrapPkgErrorsGRPCStatus(err)
if ok {
return s, true
}

// Try to unwrap native wrapped errors using `fmt.Errorf` and `%w`
s, ok = unwrapNativeWrappedGRPCStatus(err)
if ok {
return s, true
}

// We failed to unwrap any GRPSStatus so return default `Unknown`
return status.New(codes.Unknown, err.Error()), false
}
28 changes: 28 additions & 0 deletions packages/grpcstatus/grpcstatus1.13+_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// +build go1.13
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.

package grpcstatus

import (
"fmt"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"testing"
)

func TestNativeErrorUnwrapping(t *testing.T) {
gRPCCode := codes.FailedPrecondition
gRPCError := status.Errorf(gRPCCode, "Userspace error.")
expectedGRPCStatus, _ := status.FromError(gRPCError)
testedErrors := [...]error{
fmt.Errorf("go native wrapped error: %w", gRPCError),
}

for _, e := range testedErrors {
resultingStatus, ok := GrpcStatusFromError(e)
require.True(t, ok)
require.Equal(t, expectedGRPCStatus, resultingStatus)
}
}
38 changes: 38 additions & 0 deletions packages/grpcstatus/grpcstatus_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.

package grpcstatus

import (
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"testing"
)

// Own implementation of pkg/errors withStack to avoid additional dependency
type wrappedError struct {
cause error
msg string
}

func (w *wrappedError) Error() string { return w.msg + ": " + w.cause.Error() }

func (w *wrappedError) Cause() error { return w.cause }

func TestErrorUnwrapping(t *testing.T) {
gRPCCode := codes.FailedPrecondition
gRPCError := status.Errorf(gRPCCode, "Userspace error.")
expectedGRPCStatus, _ := status.FromError(gRPCError)
testedErrors := [...]error{
gRPCError,
errors.Wrap(gRPCError, "pkg/errors wrapped error: "),
}

for _, e := range testedErrors {
resultingStatus, ok := GrpcStatusFromError(e)
require.True(t, ok)
require.Equal(t, expectedGRPCStatus, resultingStatus)
}
}
11 changes: 11 additions & 0 deletions packages/grpcstatus/native_unwrap1.12-.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// +build !go1.13

package grpcstatus

import (
"google.golang.org/grpc/status"
)

func unwrapNativeWrappedGRPCStatus(err error) (*status.Status, bool) {
return nil, false
}
17 changes: 17 additions & 0 deletions packages/grpcstatus/native_unwrap1.13+.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// +build go1.13

package grpcstatus

import (
"errors"
"google.golang.org/grpc/status"
)

func unwrapNativeWrappedGRPCStatus(err error) (*status.Status, bool) {
// Unwrapping the native Go unwrap interface
var unwrappedStatus gRPCStatus
if ok := errors.As(err, &unwrappedStatus); ok {
return unwrappedStatus.GRPCStatus(), true
}
return nil, false
}
8 changes: 4 additions & 4 deletions server_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package grpc_prometheus

import (
"context"

"github.com/grpc-ecosystem/go-grpc-prometheus/packages/grpcstatus"
prom "github.com/prometheus/client_golang/prometheus"

"google.golang.org/grpc"
"google.golang.org/grpc/status"
)

// ServerMetrics represents a collection of metrics to be registered on a
Expand Down Expand Up @@ -106,7 +106,7 @@ func (m *ServerMetrics) UnaryServerInterceptor() func(ctx context.Context, req i
monitor := newServerReporter(m, Unary, info.FullMethod)
monitor.ReceivedMessage()
resp, err := handler(ctx, req)
st, _ := status.FromError(err)
st, _ := grpcstatus.GrpcStatusFromError(err)
monitor.Handled(st.Code())
if err == nil {
monitor.SentMessage()
Expand All @@ -120,7 +120,7 @@ func (m *ServerMetrics) StreamServerInterceptor() func(srv interface{}, ss grpc.
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
monitor := newServerReporter(m, streamRPCType(info), info.FullMethod)
err := handler(srv, &monitoredServerStream{ss, monitor})
st, _ := status.FromError(err)
st, _ := grpcstatus.GrpcStatusFromError(err)
monitor.Handled(st.Code())
return err
}
Expand Down

0 comments on commit f09a023

Please sign in to comment.