Skip to content

Instantly share code, notes, and snippets.

@sebble
Last active November 13, 2021 18:03
Show Gist options
  • Save sebble/1dfcbdae9fbbbcce1ceea41d7c987229 to your computer and use it in GitHub Desktop.
Save sebble/1dfcbdae9fbbbcce1ceea41d7c987229 to your computer and use it in GitHub Desktop.
An idea for capturing context of errors in go for better logging
package main
import (
"errors"
"fmt"
"os"
"github.com/go-kit/log"
)
// Refs: https://go.dev/blog/go1.13-errors
// Anything that implements the keyvals slice for logfmt
type LogfmtErrorer interface {
Keyvals() []interface{}
}
// A basic error type
type LogfmtError struct {
Msg string
keyvals []interface{}
}
// An error type suitable for wrapping other errors (Go v1.13+)
type LogfmtWrappedError struct {
Msg string
keyvals []interface{}
Err error
}
// The minimal implementation for both `error` and `LogfmtErrorer`
func (e *LogfmtError) Error() string { return e.Msg }
func (e *LogfmtError) Keyvals() []interface{} { return e.keyvals }
func (e *LogfmtWrappedError) Error() string { return e.Msg + ": " + e.Err.Error() }
func (e *LogfmtWrappedError) Unwrap() error { return e.Err }
func (e *LogfmtWrappedError) Keyvals() []interface{} { return e.keyvals }
// Unwrap an error stack to create a structure log
func LoggerWithContext(logger log.Logger, e error) log.Logger {
for errStack := errors.Unwrap(e); errStack != nil; errStack = errors.Unwrap(errStack) {
switch v := errStack.(type) {
case LogfmtErrorer:
logger = log.With(logger, v.Keyvals()...)
}
}
return logger
}
// main system loop, sets up and tears down
func main() {
// init standard Logfmt logger
logger := log.NewLogfmtLogger(os.Stderr)
logger2 := log.NewJSONLogger(os.Stderr)
// do the main stuff
err := doMain()
// handle the system error
if err != nil {
logger = LoggerWithContext(logger, err)
logger.Log("msg", err)
logger2 = LoggerWithContext(logger2, err)
logger2.Log("msg", err)
}
// clean up
}
// actual functional code
func doMain() error {
// attempt some partial action
err := doAction("one")
if err != nil {
return fmt.Errorf("domain failed because action of Y: %w", err)
}
// this action never happens, but could
err = doAction("two")
if err != nil {
return fmt.Errorf("domain failed because action of Y: %w", err)
}
// no errors
return nil
}
// an action that will ultimately fail
func doAction(act string) error {
// the call that fails (with a parameter)
err := someDeepErr(5)
if err != nil {
// We have some context so we add it, but preserve the original error
return &LogfmtWrappedError{fmt.Sprintf("action '%s' failed because deepErr of X", act), []interface{}{"action", act}, err}
}
// or nothing went wrong, no error...
return nil
}
// the call that fails
func someDeepErr(x int) error {
// create an error with some context
return &LogfmtError{"bad x error", []interface{}{"x", 5}}
}
action=one x=5 msg="domain failed because action of Y: action 'one' failed because deepErr of X: bad x error"
{"action":"one","msg":"domain failed because action of Y: action 'one' failed because deepErr of X: bad x error","x":5}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment