Last active
November 13, 2021 18:03
-
-
Save sebble/1dfcbdae9fbbbcce1ceea41d7c987229 to your computer and use it in GitHub Desktop.
An idea for capturing context of errors in go for better logging
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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