gerrors

package module
v0.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 26, 2024 License: MIT Imports: 10 Imported by: 0

README

gerrors CI Flow Maintainability Test Coverage GitHub release (latest SemVer)

gerrors

An extensive general-purpose error handling for Go applications. This package wraps errors with extensive information that can help debugging or pinpointing the actual reason behind the error while hiding non-sense from the end user.

What?

A mechanism to standardize error messages with tailord information for different parties, including end-users, client developers, and server developer themselves.

All variables, constants, types, methods, and functions have already been throughly documented. Please check gerrors documentation on pkg.go.dev.

Why?

When we return error messages to our clients (not users), we need to follow a standard that has been decided among the team. We need to implement a wrapper on top of our decided standard and use it throughout the source code to avoid any confusion or any kind of misrepresentings.

This package comes handy in these situation. You decide on the standard you want to follow and create a Formatter that satisfy it and start creating your universally parsable error messages.

On the other hand, in some occasions, more detailed information need to be shared with the client while user-facing error stays clean. This kind of metadata helps server and client to talk the same language and share useful information to pinpoint the cause of the error without going back and force through the source code, logs, errors and so on.

Documentation

Overview

Package gerrors helps to have rich and cohesive error handling throughout the code base. This package helps with formatting the final error message and all the error's metadata that can be used by different parties to understand the error.

gerrors can be used by any package for simply handling and logging errors with detailed information. Furthermore, it can be used whenever a client is involved where a general message need to be shown to the end user, but more information about the error need to be shared with the client. Error types of gerror have metadata attached to them that can help with this.

Every gerror type can be easily converted to a fully fledged gRPC error type which follows Google's AIP 193 that includes gRPC error message and status, alongside more information regarding the error represented by error details protocol buffer message. In other words, this package can be used to send comprehensive structured error information to the receiver through gRPC, REST, or any other protocol.

Formatter

This package has a default formatter, but there is possibility to customize every aspect of it. NewFormatter accepts a variadic number of FormatterOption which have some helper functions to customize the formatter and has been explained in their section.

Formatter uses text/template to generate the final error message. It uses a default template, which can be customized using WithTemplate helper function. More information on the available variables have been explained in helper's documentation.

gRPC

gerrors defines a set of default error codes that can translate to different error messages and different gRPC error codes. These default error codes have been defined to help using the package without much customization. However, they can be easily customized using WithCustomCoreCallback helper function.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/seinshah/gerrors"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/status"
)

func main() {
	// Use default formatter and print error message using the default template.
	err := gerrors.DefaultFormatter.New(errors.New("error"), gerrors.Unknown, "key", "value")
	fmt.Println(err.Error())

	// Use default formatter and get gRPC status error from gerrors.
	gerr := gerrors.DefaultFormatter.New(errors.New("error"), gerrors.Unknown, "key", "value").Grpc()

	st, ok := status.FromError(gerr)
	if !ok {
		fmt.Println("error converting to gRPC status error")

		return
	}

	fmt.Println(st.Message())

	if len(st.Details()) != 1 {
		fmt.Println("converted error to gRPC status error was not of type gerrors.GeneralError")

		return
	}

	details, ok := st.Details()[0].(*errdetails.ErrorInfo)
	if !ok {
		fmt.Println("converted error to gRPC status error was not of type gerrors.GeneralError")

		return
	}

	fmt.Println(details.GetMetadata())
}
Output:

Index

Examples

Constants

View Source
const (
	// MetadataIdentifier is the ket for accessing the error identifier.
	MetadataIdentifier = "_identifier"

	// MetadataErrorCode is the key for accessing the gerrors internal
	// or customized error code.
	MetadataErrorCode = "_error_code"

	// MetadataDefaultMessage is the key for accessing the default gerrors core
	// error message.
	MetadataDefaultMessage = "_default_message"

	// MetadataOriginalError is the key for accessing the original error
	// which was used during initializing GeneralError.
	MetadataOriginalError = "_original_error"
)

Variables

View Source
var (
	// DefaultFormatter holds a ready to use formatter with the default options.
	// DefaultFormatter uses the default template with no pre-defined labels and
	// no logger. It replaces invalid values with the missingValueReplacement.
	// DefaultFormatter uses defaultCoreCallback function for translating error
	// codes to human and machine readable strings and information.
	DefaultFormatter = NewFormatter()
)

Functions

func GetDefaultMapping

func GetDefaultMapping() map[Code]CoreError

GetDefaultMapping returns a map that contains translation between package's default error codes to detailed information. These information can be customized. Check Formatter and WithLookuper for more information.

func GrpcError

func GrpcError(err error) error

GrpcError accepts an error of gerrors.GeneralError type and returns a gRPC error by translating the error to gRPC error and attach all labels as the metadata. It supports Google's AIP 193. If the input is not of GeneralError type, it smply returns a gRPC error with google.golang.org/grpc/codes.Unknown error code with the input error message as the message. If the receiver is receiving the error in gRPC error format, you can check this blog post on how to parse the error and extract the information from it.

Types

type Code

type Code int

Code is gerrors internal error type. If a customized core call back function is used, customized error codes should be of this type as well.

const (
	// Unknown generates an unhandled error and is useful whenever the error
	// we want to generate is unknown to us.
	// It translates to GRPC Unknown codes.Code.
	// If Grpc method is called on an error that i not of gerrors type,
	// this error code will be used to convert the error to gerrors,
	// regardless of whether a custom core callback is provided or not.
	Unknown Code = iota + 1

	// NotFound generate a not found error and is useful whenever some database
	// or similar lookup fails because of no record.
	// It translates to GRPC NotFound codes.Code.
	NotFound

	// InvalidArgument generates an invalid argument error and is useful whenever
	// the provided argument by caller is not of a valid type and therefore cannot
	// be processed.
	// It translates to GRPC InvalidArgument codes.code.
	InvalidArgument

	// Marshal generates a marshaling error and is useful whenever there is an error
	// related to marshaling or unmarshaling of some data to some other data.
	// It translates to GRPC Internal codes.Code.
	Marshal

	// Storage generates a storage error and is useful for whenever there is an error
	// because of some storage operation which could be a file-system, database, redis,
	// etc.
	// It translates to GRPC Internal codes.Code.
	Storage

	// Threshold generates an out of range error and is useful for whenever some
	// received argument is out of your expected range. The value is still a valid
	// type and format, but out of your expected range.
	// It translates to GRPC OutOfRange codes.code.
	Threshold

	// Unimplemented generate an unimplemented error and is useful for whenever
	// there is a request from user that has not been implemented yet.
	// This error type is used by GRPC internal packages in some special cases as well.
	// It translates to GRPC Unimplemented codes.code.
	Unimplemented

	// Unauthorized generate an unauthorized error and is useful for whenever
	// the request is not permitted for the requester. It might be that there
	// is no authorized user in the context or header at all or that user is not
	// allowed to perform the operation in question.
	// It translates to grpc Unauthenticated codes.Code.
	Unauthorized

	// Internal generates an internal error and is useful for whenever there is
	// an error that is related to non user-facing error. System is solely responsible
	// for causing these kind of errors.
	// It translates to GRPC Internal codes.Code.
	Internal

	// Unavailable generates an unavailable error and is useful for whenever
	// the requested action is not available to the requester.
	// It translates to GRPC Unavailable codes.Code.
	Unavailable

	// ExternalRequest generates an internal error and is useful for whenever
	// system fails when calling a third-party service. The third-party service
	// can be within organization or outside of organization.
	// It translates to GRPC Internal codes.Code.
	ExternalRequest
)

type CoreError

type CoreError interface {
	// GetInternalCode returns the gerrors internal code of type Code.
	GetInternalCode() Code

	// GetIdentifier returns a human-readable that explains the code
	// in words. (one or two words)
	GetIdentifier() string

	// GetDefaultMessage returns a default longer message that explains
	// the error code. If a new error is being created with a nil initial
	// error, this default message will be used to explain the error.
	GetDefaultMessage() string
}

CoreError is an interface that every error mapper should implement. Each mapper for an error maps that error code to the rest of error's information. That information should implement this interface to make error's data accessible.

type CoreGRPCError

type CoreGRPCError interface {
	// GetGRPCCode returns a gRPC error code that can be matched to
	// an internal gerrors error code.
	GetGRPCCode() codes.Code
}

CoreGRPCError can provide support for gRPC error messages. If the provided error mapper implements this interface, the error can be converted to a gRPC error.

type Formatter

type Formatter struct {
	// contains filtered or unexported fields
}

Formatter is the main component of this package which provide handlers to create new errors. Formatter controls how every error will be parsed and formatted. Errors generated by a single formatter, will have a certain set of similar behaviors. These behaviors can be customized while creating a new formatter.

func NewFormatter

func NewFormatter(opts ...FormatterOption) *Formatter

NewFormatter creates a new formatter with the default options. Check DefaultFormatter for more information on the default options. It accepts a variadic number of FormatterOptions for customizing the returned formatter. Check helper functions that returns FormatterOption for more information.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/seinshah/gerrors"
)

func main() {
	f := gerrors.NewFormatter()

	err := f.New(errors.New("error"), gerrors.Unknown, "key", "value")
	fmt.Println(err.Error())
}
Output:

Example (WithOptions)
package main

import (
	"errors"
	"fmt"

	"github.com/seinshah/gerrors"
	"google.golang.org/grpc/codes"
)

type CustomCoreError struct{}

func main() {
	f := gerrors.NewFormatter(
		// Error method now returns an output populated based on this template.
		gerrors.WithTemplate(
			"custom: {{.Identifier}}({{.ErrorCode}}) - {{.DefaultMessage}} - {{.GrpcErrorCode}} - {{.Labels}}",
		),
		// Any error we create on this formatter uses this lookuper.
		// Due to our implementation here, since the unknown error code is
		// 100 and no other code mapping has defined, any error you pass
		// will be translated to the information of error code 100.
		gerrors.WithLookuper(
			gerrors.NewMapper(gerrors.Code(100), map[gerrors.Code]gerrors.CoreError{
				gerrors.Code(100): CustomCoreError{},
			}),
		),
		// If we pass key values to the formatter or during error creation,
		// and the value is missing (uneven number of parameters), that key
		// will be completely ignored and won't be added to the error's labels.
		gerrors.WithDisabledMissingValueReplacement(),
		// Any error created using this formatter or any clone from this formatter
		// will have this key value in its labels.
		// Because of previous option, ignored key will be ignored from error's labels.
		gerrors.WithLabels("always", true, "ignored"),
	)

	err := f.New(errors.New("error"), gerrors.Unknown, "key", "value")
	fmt.Println(err.Error())

}

func (CustomCoreError) GetGRPCCode() codes.Code {
	return codes.Internal
}

func (CustomCoreError) GetInternalCode() gerrors.Code {
	return gerrors.Code(100)
}

func (CustomCoreError) GetDefaultMessage() string {
	return "custom core error"
}

func (CustomCoreError) GetIdentifier() string {
	return "custom"
}
Output:

custom: custom(100) - custom core error - 13 - map[_default_message:custom core error _error_code:100 _identifier:custom _original_error:error always:true key:value]

func (*Formatter) AddLabels

func (f *Formatter) AddLabels(keyValues ...any) *Formatter

AddLabels adds a set of labels to the formatter. keyValues should be pairs of data, where the first element is a key and must be a string and follows [maxKeyLength] and [keyRE]. The second element is the value and will be converted to string. If value is missing [missingValueReplacement] and [allowMissingValue] are used to decide how to handle it. If the key has invalid characters or is too long, it will be modified to a valid key.

func (*Formatter) Clone

func (f *Formatter) Clone() *Formatter

Clone returns a copy of the formatter. Any change to this copy is safe since it would not change the original formatter. The global formatter can be cloned whenever you enter a new scope to be customized for that scope only.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/seinshah/gerrors"
	"google.golang.org/grpc/codes"
)

type CustomCoreError struct{}

func main() {
	f := gerrors.NewFormatter(
		gerrors.WithTemplate("{{.Labels}}"),
		gerrors.WithLabels("override", "main", "remains", "yes"),
		gerrors.WithLookuper(
			gerrors.NewMapper(gerrors.Code(100), map[gerrors.Code]gerrors.CoreError{
				gerrors.Code(100): CustomCoreError{},
			}),
		),
	)

	f2 := f.Clone()
	f2.AddLabels("override", "cloned", "new", true)

	err := f2.New(errors.New("error"), gerrors.Unknown, "key", "value")
	fmt.Println(err.Error())

}

func (CustomCoreError) GetGRPCCode() codes.Code {
	return codes.Internal
}

func (CustomCoreError) GetInternalCode() gerrors.Code {
	return gerrors.Code(100)
}

func (CustomCoreError) GetDefaultMessage() string {
	return "custom core error"
}

func (CustomCoreError) GetIdentifier() string {
	return "custom"
}
Output:

map[_default_message:custom core error _error_code:100 _identifier:custom _original_error:error key:value new:true override:cloned remains:yes]

func (*Formatter) LabelsMap

func (f *Formatter) LabelsMap() map[string]string

LabelsMap returns formatter's default labels as a map. To have a slice of labels, use LabelsSlice.

func (*Formatter) LabelsSlice

func (f *Formatter) LabelsSlice() []string

LabelsSlice return formatter's default labels as a slice. Even indexed elements are keys and odd indexed elements are values sequentially attached to the slice. To have a map of labels, use LabelsMap.

func (*Formatter) MissingValueReplacement

func (f *Formatter) MissingValueReplacement() (string, bool)

MissingValueReplacement returns whether replacing missing values is allowed or not. And if it is allowed, what will be replaced for missing values.

func (*Formatter) New

func (f *Formatter) New(inputErr error, code Code, metadataKeyValues ...any) *GeneralError

New creates a new GeneralError instance using the provided formatter. inputErr is the error that is triggered prior to the creation of the error and it can be nil. If it's nil, the final error message will be the code's default message. Any new error can have a list of key values as the metadata. These key values will be appended to the formatter's default labels. If the formatter has a logger, it will also log the error at Error level.

func (*Formatter) NewWithLogLevel

func (f *Formatter) NewWithLogLevel(
	inputErr error,
	code Code,
	level LogLevel,
	metadataKeyValues ...any,
) *GeneralError

NewWithLogLevel is the same as New, but it allows the caller to control the log level. If the logger is not provided to the formatter, this method is exactly the same as New where logging will be ignored.

type FormatterOption

type FormatterOption func(*Formatter)

FormatterOption is the approach for customizing the formatter. Every option used during creation of a new formatter is of this type. Most of the functions starting with "With" are helpers to customize the formatter.

func WithDisabledMissingValueReplacement

func WithDisabledMissingValueReplacement() FormatterOption

WithDisabledMissingValueReplacement allows user to disallow replacing missing values. In this case, a label with missing or invalid value will be ignored and not added to the error.

func WithLabels

func WithLabels(keyValues ...any) FormatterOption

WithLabels add a set of default labels to the formatter. All these labels will be included in every error generated by the formatter. It can be used to group errors together in a function scope or a call scope.

func WithLogger

func WithLogger(logger logger) FormatterOption

WithLogger attach a logger to the formatter. provided logger should at least implement the [logger] interface. If the provide logger implements other type of loggers as well (e.g [infoLogger], [traceLogger], ...), we can control how the created error should be logged by the formatter. If formatter is not configured with a logger or if the logger does not implement the provided logger, formatter simply ignore logging the error.

func WithLookuper

func WithLookuper(l Lookuper) FormatterOption

WithLookuper customizes the default mapper that translates Code to CoreError. Using this formatter option, it is possible to completely revamp all the Code constants from acceptable error code and define a new set of error codes and a new mapper that can translate these new error codes to more details.

func WithMissingValueReplacement

func WithMissingValueReplacement(replacement string) FormatterOption

WithMissingValueReplacement allows the formatter to accepts missing values when labels are being added to the formatter. In case a value is missing, or is invalid, formatter will replace it with this replacement string. Replacing missing values is allowed by default.

func WithTemplate

func WithTemplate(templateString string) FormatterOption

WithTemplate customizes formatter defaultTemplate. This template should follow text/template syntax. Function panics if template is invalid. Supported variables are:

  • {{.Identifier}}: the identifier of the error. (e.g. unavailable, internal, ...)

  • {{.ErrorCode}}: core error code. (e.g. 1, 2, ...)

  • {{.GrpcErrorCode}}: grpc error code. (e.g. 2, 5, ...)

  • {{.Message}}: the message of the provided error or default message of the error code.

  • {{.DefaultMessage}}: the default message of the error code.

  • {{.Labels}}: formatter's label plus error-specific labels. Treat it as a map.

    f := NewFormatter(WithTemplate("error: {{.Identifier}}(code {{.ErrorCode}}) - {{.Message}}"))

type GeneralError

type GeneralError struct {
	// contains filtered or unexported fields
}

GeneralError is the error type defined, controlled, and handled by gerrors package.

func (*GeneralError) Error

func (ge *GeneralError) Error() string

Error allows GeneralError to implement the error interface. It uses the formatter template and different information of the GeneralError to generate the error message.

func (*GeneralError) Grpc

func (ge *GeneralError) Grpc() error

Grpc is the method defined on GeneralError which returns the gRPC error. See Grpc function for more details.

func (*GeneralError) Metadata

func (ge *GeneralError) Metadata() map[string]string

Metadata returns all the combined labels of the given GeneralError.

func (*GeneralError) MetadataSlice

func (ge *GeneralError) MetadataSlice() []any

type LogLevel

type LogLevel int

LogLevel is used while creating new error with NewWithLogLevel method. If the formatter is configured to have a logger and it's logger implements the proper log level interface, error will be logged at the provided level.

const (
	// LogLevelOff will ignore logging the error event if formatter's logger is set.
	LogLevelOff LogLevel = iota - 1
	LogLevelError
	LogLevelWarn
	LogLevelInfo
	LogLevelDebug
	LogLevelTrace
)

type Lookuper

type Lookuper interface {
	Lookup(code Code) CoreError
}

Lookuper is an interface that shows how a mapper should be implemented. Every mapper should have a lookup method to translate Code to CoreError.

type Mapper

type Mapper struct {
	// contains filtered or unexported fields
}

Mapper is the data type that maps gerrors error code to the core error which holds more information about the error code. This type will be used by different methods to translate the error code to the underlying error details.

func NewMapper

func NewMapper(unknownErrorCode Code, mapping map[Code]CoreError) *Mapper

NewMapper initiates the Mapper with all available one-to-one mapping information from an error code to error details. mapping is a map that maps the Code to CoreError. This can be customized based on your needs. unknownErrorCode will be used whenever mapper cannot translate a Code to CoreError and this unknown code is used as a fallback.

func (*Mapper) Lookup

func (m *Mapper) Lookup(code Code) CoreError

Lookup helps Mapper to implement Lookuper interface that can be passed to Formatter and acts as the translator for translating Code to CoreError.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL