errnie

package module
v2.5.8 Latest Latest
Warning

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

Go to latest
Published: Nov 18, 2021 License: Unlicense Imports: 19 Imported by: 1

README

errnie

Errnie started out as a way to emulate the rescue method found in Ruby.

It mutated a few times over into performing highly opinionated error handling as a side-gig.

Eventually I got tired of having to inject it into everything, so its final state now is to be a highly opinionated ambient context handling the things I am tired of handling.

It is a relatively thin wrapper around Go's error object and debug package to simplify error handling and prevent putting if statements all over the code while also giving you a much more direct and verbose tracing feature in the console that can be controlled using a config file.

An example of what to expect when turning debug and traching features on.

❯ go run main.go run
 TRACER  🔍 <- tracer.go 51 TraceIn []
 TRACER  🔍 <- tracer.go 51 TraceIn []
 ERRNIE  🐞 -- logging potential errors [[set session to region ]]
  DEBUG  set session to region
 TRACER  🐞 context.go 39 Handles [[<nil>]]
 TRACER  🐞 context.go 54 Handles [[<nil>]]
 ERRNIE  🐞 -- adding potential errors [[<nil>]]
 ERRNIE  🐞 -- finding real errors [[<nil>]]
 TRACER  🐞 context.go 55 Handles [[<nil>]]
 TRACER  👍 context.go 39 Handles [OK]

Things to Know

You cannot/should not use this library without a Viper based config workflow.

While errnie will technically function without using the viper package it is not a recommended approach, as you will have no control over its output mode, and it will therefore not show any kind of debug or trace information.

To enable tracing and debug messages your apps config file should have the following key/value setup (I usually just put it at the top).

errnie:
  debug: true
  trace: true

errnie will use viper's viper.GetViper().GetString("errnie.debug") (and the same for trace) approach to pick up the global instance of viper, which also lives as an ambient context in your app if you use it.

These days it is probably the most common approach for Go programs to use both (cobra)[https://github.com/spf13/cobra] and (viper)[https://github.com/spf13/viper] to build a CLI into your app, so the chances are high you are already using this.

To add to that standard patterns and introduce a very low-maintance situation around managing config files I have started a while back to embed a default config file into the binaries for my projects and personally I have had a lot of success with this approach.

It means that even when you (accidentally) remove the config file, or changed it in such a way that you cannot get back to a working state, all you need to do it (remove it) run the program and it will write a fresh default to your home directory and of course the same holds true for the first time you ever run the project.

Below is an abbreviated example of how to implement this in a standard cmd/root.go setup that is common to all projects implementing cobra.

This is compatible with Linux, Mac, and Windows.

# ~/myproject/cmd/cfg/.myproject.yml
errnie:
  debug: true
  trace: true

myproject:
  someconfigkey:
    somenestedkey: somevalue
// ~/myproject/cmd/root.go
package cmd

/* Use meta comment to embed anything in the cfg directory as a mini file system into the binary */
//go:embed cfg/*
var embedded embed.FS

/* MISSING: viper config file command line flags binding logic */

// Disregard error handling, for brevity.
home, _ := os.UserHomeDir()

// Check if config file exists in home directory of current user, otherwise write out the default
// from the embedded file system.
if _, err := os.Stat(slug); os.IsNotExist(err) {
    fs, err := embedded.Open("cfg/" + cfgFile)
    defer fs.Close()
    buf, _ := io.ReadAll(fs)
    _ = ioutil.WriteFile(home+"/"+cfgFile, buf, 0644)
}

/* MISSING: viper config file loading/initialization logic */

Usage Patterns

Logging Errors

This package provides a suggested ambient context pattern, because I got sick of passing these common things around to every object as an injected dependency. Personally I need this everywhere at all times.

In its simplest form you can use errnie as a simple one-line replacement for the standard way most people handle errors in Go.

// Instead of...
err := somepackage.SomeMethodThatReturnsAnError()

if err != nil {
    fmt.Println(err)
}

// You can just do...
errnie.Log(somepackage.SomeMethodThatReturnsAnError())

errnie itself will hold an exported boolean value representing its internal state, i.e. whether or not an actual error event took place. You can access that in implementation using something like the following.

// This allows you to handle logging the error as well as handling it in a concise manner.
if ok := errnie.Log(somepackage.SomeMethodThatReturnsAnError()).OK; !ok {
    // Do some...
}

Should you have a need to get to the actual error inside, this is also an option, though ideally this package was designed for this to be a rather unique situation.

// This allows you to handle logging the error as well as handling it in a concise manner.
if err := errnie.Log(somepackage.SomeMethodThatReturnsAnError()).ERR; err != nil {
    // Do some...
}
Handling Errors

Handling errors is pretty much the same as logging them, however the method to do so has a couple of magic features that are good to be aware about from the start.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Trace

func Trace(suffix ...interface{}) string

Trace is the generic tracing method one can call, designed to go at any desired place in the code.

func TraceIn

func TraceIn(suffix ...interface{}) string

TraceIn was designed to go at the beginning of a method, but this is not required. Following that pattern does have the benefit of the information being structured in a way that makes sense.

func TraceOut

func TraceOut(suffix ...interface{}) string

TraceOut can be used at the end of a method, before the return. One good use-case is to see which data the method is returning with.

Types

type Advisor

type Advisor interface {
	Static(*ring.Ring) bool
	Dynamic(<-chan bytes.Buffer) State
	// contains filtered or unexported methods
}

Advisor describes an interface that contains two methods of analyzing a buffer of error values. It provides an abstraction layer over a complicated error state in a system.

func NewAdvisor

func NewAdvisor(advisorType Advisor) Advisor

NewAdvisor... I'd hate to call it a factory, but it sure seems like one.

type AmbientContext

type AmbientContext struct {
	Logs *Logger

	ERR error
	OK  bool
	// contains filtered or unexported fields
}

AmbientContext is the main wrapper for errnie and tries to keep the amount of having to deal with this tool as minimal as possible. I wanted something that I did not have to pass around all the time, cluttering up method signatures and basically being a repetative hassle. I feel in Go's case, having your own favorite way of error handling always at the ready can only be a good thing. And yes, I got inspired (and shamelessly stole) this setup from Viper.

func Add

func Add(errs ...interface{}) *AmbientContext

Add is a proxy method to facilitate shorter style syntax.

func Handles

func Handles(errs ...interface{}) *AmbientContext

Handles is a proxy method to facilitate shorter style syntax.

func Log

func Log(errs ...interface{}) *AmbientContext

Debug is a proxy method to facilitate shorter style syntax. <Deprecated: See proxy referenced implementation>

func New

func New() *AmbientContext

New gives us back a reference to the instance, so we should be able to call the package anywhere we want in our host code.

func (*AmbientContext) Add

func (ambctx *AmbientContext) Add(errs ...interface{}) *AmbientContext

Add the errors to the internal error collector, a ring buffer that will keep them in memory so the can be used in combination with an Advisor, together providing a mechanism to judge state (`health`) of a chain od methods.

func (*AmbientContext) Dump

func (ambctx *AmbientContext) Dump() chan Error

Dump errnie's internal error collector out to the log receiver, which could be anything from the local console to a log data store. This actually returns a channel of errnie's internal Error type, which is a razor thin wrapper around Go's error type, value, no, type... Errors are Weird (not values).

func (*AmbientContext) Handles

func (ambctx *AmbientContext) Handles(errs ...interface{}) *AmbientContext

Handles the error in some semi-significant want so we don't have to think too much about it and sets the err and ok values so we can do some nifty syntactical sugar tricks upstream.

func (*AmbientContext) Log

func (ambctx *AmbientContext) Log(msgs ...interface{}) *AmbientContext

Log a list of errors or any other object you want to inspect. Good use-cases for this method are info or debug lines. Maybe errors you don't care about handling, but I would suggest using a no-op handler for that. <Deprecated, use the new format: `errnie.Logs.Error(x)`> (Error(x) can be replaced with any of errnie's known log levels.)

func (*AmbientContext) Trace

func (ambctx *AmbientContext) Trace(suffix ...interface{}) string

Trace is the proxied method described above with a similar name.

func (*AmbientContext) TraceIn

func (ambctx *AmbientContext) TraceIn(suffix ...interface{}) string

TraceIn is the proxied method described above with a similar name.

func (*AmbientContext) TraceOut

func (ambctx *AmbientContext) TraceOut(suffix ...interface{}) string

TraceOut is the proxied method described above with a similar name.

func (*AmbientContext) With

func (ambctx *AmbientContext) With(opcode OpCode) *AmbientContext

With is a method to chain onto a previous method (most likely `Handles`) and designed to specify the type of action to take when an error has been detected.

type Collector

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

Collector saves Errors in a ring buffer.

func NewCollector

func NewCollector(ringSize int) *Collector

NewCollector sets up the ring buffer we will collect our errors in.

func (*Collector) Add

func (collector *Collector) Add(errs []interface{}) bool

Add an error to the Collector's ring buffer and report OK if no errors.

func (*Collector) Dump

func (collector *Collector) Dump() chan Error

Dump returns all the errors currently present in the ring buffer.

type ConsoleLogger

type ConsoleLogger struct{}

ConsoleLogger is a canonical implementation of the LogChannel interface, and provides the basic local terminal output for log messages.

func (ConsoleLogger) Critical

func (logChannel ConsoleLogger) Critical(msgs ...interface{}) bool

func (ConsoleLogger) Debug

func (logChannel ConsoleLogger) Debug(msgs ...interface{}) bool

Debug shows a muted colored message in the terminal when `debug: true` in the host program's config file.

func (ConsoleLogger) Error

func (logChannel ConsoleLogger) Error(msgs ...interface{}) bool

func (ConsoleLogger) Fatal

func (logChannel ConsoleLogger) Fatal(msgs ...interface{}) bool

func (ConsoleLogger) Info

func (logChannel ConsoleLogger) Info(msgs ...interface{}) bool

func (ConsoleLogger) Panic

func (logChannel ConsoleLogger) Panic(msgs ...interface{}) bool

func (ConsoleLogger) Warning

func (logChannel ConsoleLogger) Warning(msgs ...interface{}) bool

type DefaultAdvisor

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

DefaultAdvisor is the built in and most basic implementation of the concept.

func (DefaultAdvisor) Dynamic

func (advisor DefaultAdvisor) Dynamic(<-chan bytes.Buffer) State

Dynamic takes a bytes.Buffer channel so we can send it metadata. The call stack would be an idea for instance. Dynamic advice will also be twice as broad in value scope.

func (DefaultAdvisor) Static

func (advisor DefaultAdvisor) Static(ringBuffer *ring.Ring) bool

Static is the entry point for the fast and loose method of determining state.

type ElasticLogger

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

func (ElasticLogger) Critical

func (logChannel ElasticLogger) Critical(msgs ...interface{}) bool

func (ElasticLogger) Debug

func (logChannel ElasticLogger) Debug(msgs ...interface{}) bool

func (ElasticLogger) Error

func (logChannel ElasticLogger) Error(msgs ...interface{}) bool

func (ElasticLogger) Fatal

func (logChannel ElasticLogger) Fatal(msgs ...interface{}) bool

func (ElasticLogger) Info

func (logChannel ElasticLogger) Info(msgs ...interface{}) bool

func (ElasticLogger) Panic

func (logChannel ElasticLogger) Panic(msgs ...interface{}) bool

func (ElasticLogger) Warning

func (logChannel ElasticLogger) Warning(msgs ...interface{}) bool

type ErrType

type ErrType uint

ErrType defines an enumerable type.

const (
	PANIC ErrType = iota
	FATAL
	CRITICAL
	ERROR
	WARNING
	INFO
	DEBUG
)

type Error

type Error struct {
	Err     error
	ErrType ErrType
}

Error wraps Go's built in error type to extend its functionality with a severity level.

func (Error) ToString

func (wrapper Error) ToString() string

ToString outputs the error message as it sits in the wrapperd Go error.

type FileLogger

type FileLogger struct{}

func (FileLogger) Critical

func (logChannel FileLogger) Critical(msgs ...interface{}) bool

func (FileLogger) Debug

func (logChannel FileLogger) Debug(msgs ...interface{}) bool

func (FileLogger) Error

func (logChannel FileLogger) Error(msgs ...interface{}) bool

func (FileLogger) Fatal

func (logChannel FileLogger) Fatal(msgs ...interface{}) bool

func (FileLogger) Info

func (logChannel FileLogger) Info(msgs ...interface{}) bool

func (FileLogger) Panic

func (logChannel FileLogger) Panic(msgs ...interface{}) bool

func (FileLogger) Warning

func (logChannel FileLogger) Warning(msgs ...interface{}) bool

type Guard

type Guard struct {
	Err error
	// contains filtered or unexported fields
}

Guard is a way to override any existing interrupt messages such as panics and recover from them either silently or by invoking a custom recovery function.

func NewGuard

func NewGuard(handler func()) *Guard

NewGuard constructs an instance of a guard that can live basically anywhere and sit ready to either Check or Rescue.

func (*Guard) Check

func (guard *Guard) Check()

Check is a method to force a guard to panic on an error and trigger it's own recovery (Rescue) method. Be careful for infinite loops.

func (*Guard) Rescue

func (guard *Guard) Rescue() func()

Rescue a method from errors and panics. If a handler was passed into the guard object it will be the first thing that runs after recovery.

type LogChannel

type LogChannel interface {
	Panic(msgs ...interface{}) bool
	Fatal(msgs ...interface{}) bool
	Critical(msgs ...interface{}) bool
	Error(msgs ...interface{}) bool
	Info(msgs ...interface{}) bool
	Warning(msgs ...interface{}) bool
	Debug(msgs ...interface{}) bool
}

func NewConsoleLogger

func NewConsoleLogger() LogChannel

func NewElasticLogger

func NewElasticLogger() LogChannel

func NewFileLogger

func NewFileLogger() LogChannel

type LogMessage

type LogMessage struct {
	Timestamp time.Time `json:"timestamp"`
	Level     string    `json:"level"`
	Message   string    `json:"message"`
}

type Logger

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

Logger is a custom logger which allows a consistent interface to multiple output channels.

var Logs *Logger

func NewLogger

func NewLogger(channels ...LogChannel) *Logger

func (*Logger) Debug

func (logger *Logger) Debug(msgs ...interface{}) bool

func (*Logger) Error

func (logger *Logger) Error(msgs ...interface{}) bool

func (*Logger) Info

func (logger *Logger) Info(msgs ...interface{}) bool

func (*Logger) LogWithLevel

func (logger *Logger) LogWithLevel(level string, msgs ...interface{}) bool

func (*Logger) Send

func (logger *Logger) Send(msgs ...interface{}) bool

func (*Logger) Warning

func (logger *Logger) Warning(msgs ...interface{}) bool

type OpCode

type OpCode uint

OpCode is a type that can be used to identify an intent when using errnie as the handler of one or multiple errors. To use it you need to use the `errnie.Handles()` and wrap it around the error. To use errnie's built-in handlers you can use it `in the middle` like so:

```go errnie.Handles(err).With(errnie.KILL) ```

in which case you have chosen to exit the program if the error was not nil.

const (
	// NOOP is the empty value for this enumerable type and does nothing.
	NOOP OpCode = iota
	// KILL wraps the error and if triggered will exit the program.
	KILL
	// RECV attempts to recover the program from any kind of error.
	RECV
	// RETR will attempt a retry of the previous operation.
	RETR
	// RTRN will return out of the current method.
	RTRN
)

type Sampler

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

type State

type State uint

State defines an enumerable type of 4 arbitrary values of degradation. Use them however suits you.

const (
	OK State = iota
	NO
	BAD
	DEAD
)

type Tracer

type Tracer struct{}

Tracer provides some basic stack tracing features at the moment which expose the method chains that are in use for any given operation. This is planned to be extended quite a bit in the near future.

func NewTracer

func NewTracer(on bool) *Tracer

NewTracer constructs the Tracer object and hands back a pointer to it.

func (Tracer) Caller

func (tracer Tracer) Caller(prefix string, suffix ...interface{}) string

Caller extracts useful information from the call stack and presents it in a structured form to the user to aid in debugging processes.

Jump to

Keyboard shortcuts

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