Skip to content

Commit

Permalink
Merge pull request #9 from aserto-dev/wrapped_error
Browse files Browse the repository at this point in the history
add WrappedError and logger extraction from ctx
  • Loading branch information
gimmyxd authored May 17, 2024
2 parents 8e92a25 + 769fd0c commit ee7a3ba
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 5 deletions.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/cover.out
/tenant.db
/.ext
/.ext

# https://github.com/golang/go/issues/53502
# go.work.sum is machine specific and should not be checked in
# go.work.sum

.DS_Store
41 changes: 41 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package errors

import (
"context"

"github.com/pkg/errors"
)

// ContextError represents a standard error
// that can also encapsulate a context.
type ContextError struct {
Err error
Ctx context.Context
}

func WithContext(err error, ctx context.Context) *ContextError {
return &ContextError{
Err: err,
Ctx: ctx,
}
}

func WrapContext(err error, ctx context.Context, message string) *ContextError {
return WithContext(errors.Wrap(err, message), ctx)
}

func WrapfContext(err error, ctx context.Context, format string, args ...interface{}) *ContextError {
return WithContext(errors.Wrapf(err, format, args...), ctx)
}

func (ce *ContextError) Error() string {
return ce.Err.Error()
}

func (ce *ContextError) Cause() error {
return errors.Cause(ce.Unwrap())
}

func (ce *ContextError) Unwrap() error {
return ce.Err
}
50 changes: 48 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package errors

import (
"context"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -204,7 +205,6 @@ func (e *AsertoError) Time(key string, value time.Time) *AsertoError {
func (e *AsertoError) FromReader(key string, value io.Reader) *AsertoError {
buf := &strings.Builder{}
_, err := io.Copy(buf, value)

if err != nil {
return e.Err(err)
}
Expand Down Expand Up @@ -252,7 +252,6 @@ func (e *AsertoError) GRPCStatus() *status.Status {
Metadata: e.Data(),
Domain: e.Code,
})

if err != nil {
return status.New(codes.Internal, "internal failure setting up error details, please contact the administrator")
}
Expand All @@ -272,6 +271,10 @@ func (e *AsertoError) WithHTTPStatus(httpStatus int) *AsertoError {
return c
}

func (e *AsertoError) Ctx(ctx context.Context) error {
return WithContext(e, ctx)
}

// Returns an Aserto error based on a given grpcStatus. The details that are not of type errdetails.ErrorInfo are dropped.
// and if there are details from multiple errors, the aserto error will be constructed based on the first one.
func FromGRPCStatus(grpcStatus status.Status) *AsertoError {
Expand All @@ -297,6 +300,33 @@ func FromGRPCStatus(grpcStatus status.Status) *AsertoError {
return result
}

/**
* Retrieves the most inner logger associated with an error.
*/
func Logger(err error) *zerolog.Logger {
var logger *zerolog.Logger
var ce *ContextError

if err == nil {
return logger
}

for {
if errors.As(err, &ce) {
if ctxLogger := extractLogger(ce.Ctx); ctxLogger != nil {
logger = ctxLogger
}
}

err = errors.Unwrap(err)
if err == nil {
break
}
}

return logger
}

func UnwrapAsertoError(err error) *AsertoError {
if err == nil {
return nil
Expand Down Expand Up @@ -351,3 +381,19 @@ func Equals(err1, err2 error) bool {
func CodeToAsertoError(code string) *AsertoError {
return asertoErrors[code]
}

/**
* Retrieve the logger associated with the context using zerolog.Ctx(ctx).
* If the retrieved logger is either the default context logger or has a disabled level, it returns nil.
*/
func extractLogger(ctx context.Context) *zerolog.Logger {
if ctx == nil {
return nil
}
logger := zerolog.Ctx(ctx)
if logger == zerolog.DefaultContextLogger || logger.GetLevel() == zerolog.Disabled {
logger = nil
}

return logger
}
132 changes: 130 additions & 2 deletions errors_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package errors_test

import (
"context"
"net/http"
"os"
"testing"

"github.com/pkg/errors"
"github.com/rs/zerolog"

cerr "github.com/aserto-dev/errors"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -98,7 +101,6 @@ func TestFromGRPCStatus(t *testing.T) {
Metadata: initialErr.Data(),
Domain: initialErr.Code,
})

if err != nil {
assert.Fail(err.Error())
}
Expand Down Expand Up @@ -126,7 +128,6 @@ func TestEquals(t *testing.T) {
err2 := ErrAlreadyExists.Msgf("error 2").Str("key2", "val2").Err(errors.New("zoom"))

assert.True(cerr.Equals(err1, err2))

}

func TestEqualsNil(t *testing.T) {
Expand Down Expand Up @@ -198,3 +199,130 @@ func TestWithHttpError(t *testing.T) {
unAerr := cerr.UnwrapAsertoError(aerr)
assert.Equal(http.StatusNotAcceptable, unAerr.HTTPCode)
}

// returns nil logger if error is nil.
func TestLoggerWithNilError(t *testing.T) {
assert := require.New(t)

var err error
logger := cerr.Logger(err)
assert.Nil(logger)
}

func TestLoggerWithWrappedNilError(t *testing.T) {
assert := require.New(t)

var err error
ctx := context.Background()

logger := cerr.Logger(cerr.WithContext(err, ctx))
assert.Nil(logger)
}

func TestLoggerWithWrappedErrorsWithEmptyContext(t *testing.T) {
assert := require.New(t)

ctx := context.Background()
err := cerr.WithContext(cerr.NewAsertoError("E00001", codes.Internal, http.StatusInternalServerError, "internal error"), ctx)
wrappedErr := errors.Wrap(err, "wrapped error")

logger := cerr.Logger(wrappedErr)
assert.Nil(logger)
}

func TestLoggerWithWrappedErrorsWithLoggerContext(t *testing.T) {
assert := require.New(t)
initialLogger := zerolog.New(os.Stderr)

ctx := context.Background()
ctx = initialLogger.WithContext(ctx)
err := cerr.WithContext(cerr.NewAsertoError("E00001", codes.Internal, http.StatusInternalServerError, "internal error"), ctx)
wrappedErr := errors.Wrap(err, "wrapped error")

logger := cerr.Logger(wrappedErr)
assert.NotNil(logger)
assert.Equal(logger, zerolog.Ctx(ctx))
}

func TestLoggerWithWrappedMultipleWithoutErrorsWithContext(t *testing.T) {
assert := require.New(t)
initialLogger := zerolog.New(os.Stderr)

ctx := context.Background()
ctx = initialLogger.WithContext(ctx)
err := cerr.WithContext(cerr.NewAsertoError("E00001", codes.Internal, http.StatusInternalServerError, "internal error"), ctx)
errWithoutCtx := cerr.NewAsertoError("E00002", codes.Internal, http.StatusInternalServerError, "internal error")
wrappedErr := errWithoutCtx.Err(errors.Wrap(err, "wrapped error"))

logger := cerr.Logger(wrappedErr)
assert.NotNil(logger)
assert.Equal(logger, zerolog.Ctx(ctx))
}

func TestLoggerWithWrappedMultipleErrorsWithContext(t *testing.T) {
assert := require.New(t)
initialLogger := zerolog.New(os.Stderr)

ctx := context.Background()
ctx = initialLogger.WithContext(ctx)
err := cerr.WithContext(cerr.NewAsertoError("E00001", codes.Internal, http.StatusInternalServerError, "internal error"), ctx)
errWithoutCtx := cerr.NewAsertoError("E00002", codes.Internal, http.StatusInternalServerError, "internal error")
wrappedErr := errors.Wrap(errWithoutCtx.Err(err), "wrapped error")

logger := cerr.Logger(wrappedErr)
assert.NotNil(logger)
assert.Equal(logger, zerolog.Ctx(ctx))
}

func TestLoggerWithWrappedMultipleErrorsWithMultipleContexts(t *testing.T) {
assert := require.New(t)
initialLogger := zerolog.New(os.Stderr)
ctx1 := context.Background()
ctx2 := initialLogger.WithContext(ctx1)
err := cerr.WithContext(cerr.NewAsertoError("E00001", codes.Internal, http.StatusInternalServerError, "internal error"), ctx1)
wrappedErr := cerr.WithContext(cerr.WithContext(err, ctx2), ctx1)

logger := cerr.Logger(wrappedErr)
ctx1Logger := zerolog.Ctx(ctx1)
ctx2Logger := zerolog.Ctx(ctx2)

assert.NotNil(logger)
assert.NotEqual(logger, ctx1Logger)
assert.Equal(logger, ctx2Logger)
}

func TestLoggerWithWrappedMultipleErrorsWithMultipleContextsOuter(t *testing.T) {
assert := require.New(t)
initialLogger := zerolog.New(os.Stderr)
ctx1 := context.Background()
ctx2 := initialLogger.WithContext(ctx1)
err := cerr.WithContext(cerr.NewAsertoError("E00001", codes.Internal, http.StatusInternalServerError, "internal error"), ctx1)
err2 := cerr.WithContext(cerr.NewAsertoError("E00002", codes.Internal, http.StatusInternalServerError, "internal error"), ctx2)
wrappedErr := errors.Wrap(errors.Wrap(err2, err.Error()), "wrapped error")

logger := cerr.Logger(wrappedErr)
ctx1Logger := zerolog.Ctx(ctx1)
ctx2Logger := zerolog.Ctx(ctx2)

assert.NotNil(logger)
assert.NotEqual(logger, ctx1Logger)
assert.Equal(logger, ctx2Logger)
}

func TestLoggerWithWrappedMultipleAsertoErrorsWithMultipleContextsOuter(t *testing.T) {
assert := require.New(t)
initialLogger := zerolog.New(os.Stderr)
ctx1 := context.Background()
ctx2 := initialLogger.WithContext(ctx1)
err := cerr.NewAsertoError("E00001", codes.Internal, http.StatusInternalServerError, "internal error").Ctx(ctx1)
err2 := cerr.NewAsertoError("E00002", codes.Internal, http.StatusInternalServerError, "internal error").Ctx(ctx2)
wrappedErr := errors.Wrap(errors.Wrap(err2, err.Error()), "wrapped error")

logger := cerr.Logger(wrappedErr)
ctx1Logger := zerolog.Ctx(ctx1)
ctx2Logger := zerolog.Ctx(ctx2)

assert.NotNil(logger)
assert.NotEqual(logger, ctx1Logger)
assert.Equal(logger, ctx2Logger)
}

0 comments on commit ee7a3ba

Please sign in to comment.