Log levels
#
Basic log levelsThere are 6 basic log levels implemented by default:
// logLevels.gofunc Trace(msg string, adHocFields ...LogFields) { ... }func Debug(msg string, adHocFields ...LogFields) { ... }func Info(msg string, adHocFields ...LogFields) { ... }func Warn(msg string, adHocFields ...LogFields) { ... }func Error(msg string, adHocFields ...LogFields) { ... }func Fatal(msg string, adHocFields ...LogFields) { ... }
In order to call these methods, you will need a valid Logger
instance. See Logger creation for details.
The only difference between them is the value of the lvl
log field. Note that even if it's common to panic
when calling Fatal
, you will have to implement some Output
to simulate this behaviour (see OutputPanicOnFatal for details).
note
The lvl
of the log is represented using an uint64
and values that correspond to the power of two (1, 2, 4, 8, 16, ...). This way, it's possible to apply the and
bitwise operation and easily check to see if some log level X is enabled. For details, see LevelsEnabled usage
#
Extra Log LevelsIf you've read the Introduction, you may have noticed these two log levels called ErrorFrom
and FatalFrom
.
These two methods are another way to call Error
and Fatal
, respectively, but using an error
interface directly.
#
ErrorParserIf you're using a concise error
handling strategy, it should be easy to manipulate errors and
extract more information from them, beyond the error string.
The Configuration
struct accepts a function called ErrorParser
, that will take some error
interface and return a tuple containing the string that better represents the error, and some log fields extracted from the error itself:
// configuration.gotype Configuration struct { ... ErrorParser func(error) (string, LogFields) ...}
This function will be called from both ErrorFrom
and FatalFrom
methods, in order to extract more information about the error, to finally call the correspondent log method (Error
andFatal
, respectively).
Instead of doing something like this:
type SomeErrorStruct struct{ Value int}func (s *SomeErrorStruct) Error() string { return "error msg" }
func maybeReturnsError() error { return &SomeErrorStruct{Value: 10} }
func main() { someLogger := logger.NewDefault()
e := maybeReturnsError() if e != nil { err := e.(*SomeErrorStruct) someLogger.Fatal(e.Error(), logger.LogFields{ "errorValue": err.Value }) }}
You can do this:
type SomeErrorStruct struct{ Value int}func (s *SomeErrorStruct) Error() string { return "error msg" }
func maybeReturnsError() error { return &SomeErrorStruct{Value: 10} }
func MyErrorParser(e error) (string, logger.LogFields) { return e.Error(), logger.LogFields{ "errorValue": e.(*SomeErrorStruct).Value, }}
func main() { config := logger.DefaultConfig() config.ErrorParser = MyErrorParser someLogger := logger.New(config). Outputs(logger.OutputJsonToWriter(os.Stdout, nil))
e := maybeReturnsError() if e != nil { someLogger.FatalFrom(e) // { "errorValue": 10,"lvl": 32, "msg": "error msg" } }}
Note that the fields returned by the ErrorParser
will be placed before the AdHoc fields
, causing them to be overwritten by the AdHoc fields
if there's a clash of keys. Example:
// Suppose that the ErrorParser is set to:func MyErrorParser(_ error) (string, logger.LogFields) { return "some example error msg", logger.LogFields{ "someKey": "value", "anotherKey": "another value", }}
func main() { config := logger.DefaultConfig() config.ErrorParser = MyErrorParser someLogger := logger.New(config). Outputs(logger.OutputJsonToWriter(os.Stdout, nil))
// And you create the following log: someLogger.ErrorFrom(fmt.Errorf("any error"), logger.LogFields{ "someKey": "newValue", "thirtyKey": "another another value", }) /* { "msg": "some example error msg", "lvl": 16, "someKey": "newValue", "anotherKey": "another value", "thirtyKey": "another another value" } */}
#
Default ErrorParserThe library has a builtin ErrorParser
, that will return the e.Error()
as the msg and store the error
interface value inside the log fields, using the DefaultErrorKey
as the field key:
// configuration.go// Production code-fragmentfunc DefaultErrorParser(err error) (string, LogFields) { return err.Error(), LogFields{DefaultErrorKey: err}}const DefaultErrorKey = "error"
This logic is used to build the OutputPanicOnFatal, that extracts the error value (under the DefaultErrorKey
key) to forward it to the panic
call.
#
Standalone log levelsIf you don't want to create and maintain a Logger
instance by yourself, you can just use the standalone version of the log levels:
// standalone.gofunc Trace(msg string, adHocFields ...LogFields) { ... }func Debug(msg string, adHocFields ...LogFields) { ... }func Info(msg string, adHocFields ...LogFields) { ... }func Warn(msg string, adHocFields ...LogFields) { ... }func Error(msg string, adHocFields ...LogFields) { ... }func Fatal(msg string, adHocFields ...LogFields) { ... }
Under the hood, they will use a default Logger
instance. Something like this:
func Trace(msg string, adHocFields ...LogFields) { NewDefault().Trace(msg, adHocFields...)}
For more information, see Default Logger.
#
Extending the log levelsIf you want to create new custom log levels, you will need to create a new type that wrap the original Logger
.
The most straightway to do it, is to use embedded struct fields. Something like this:
type NewLogger struct { *logger.Logger}
This way, the NewLogger
will preserve the original log level methods and you can add new ones. Every log level should call the base log level method, that will actually handle the log. This method is exported by the Logger
api:
// logLevels.gofunc (l *Logger) Log(lvl uint64, msg string, adHocFields []LogFields) { ... }
You can notice that the adHoc
fields are represented as an slice, while the log level methods (Fatal
, Info
, etc) implement it as a variadic. Well, variadic arguments are just a slice, so if you want to create custom log levels using variadics too, you can just forward it to the Log
method:
type NewLogger struct { *logger.Logger}
func (l *NewLogger) NewLogLevel(msg string, adHocFields ...logger.LogFields) { l.Logger.Log(<newLogLevelUint64>, msg, adHocFields)}
Remember that it's not recommended to overlap new, custom log levels values (uint64
), with the builtin ones. They're all exported, so you can just check them or do something like this:
const ( newLogLevel uint64 = 1 << (iota + 6) anotherLogLevel anotherOne)
Note that it will continue the sequence after the last builtin log level, LvlFatal
. There's six builtin log levels (Trace, Debug, Info, Warn, Error and Fatal), that's why there's a 6
being added to the iota
.
If you want to omit some builtin log levels, just don't use embedded struct fields:
type NewLogger struct { original *logger.Logger}
func (l *NewLogger) Info(msg string, adHocFields ...logger.LogFields) { l.original.Info(msg, adHocFields...)}func (l *NewLogger) Error(msg string, adHocFields ...logger.LogFields) { l.original.Error(msg, adHocFields...)}
This way, there will be only two methods in the new custom Logger
api: Info
and Error
.
You can see a more concrete example by looking at the LoggerCLI
, here and here.