mediocre-go-lib/mlog/mlog.go

376 lines
9.5 KiB
Go

// Package mlog is a generic logging library. The log methods come in different
// severities: Debug, Info, Warn, Error, and Fatal.
//
// The log methods take in a string describing the error, and a set of key/value
// pairs giving the specific context around the error. The string is intended to
// always be the same no matter what, while the key/value pairs give information
// like which userID the error happened to, or any other relevant contextual
// information.
//
// Examples:
//
// Info("Something important has occurred")
// Error("Could not open file", llog.KV{"filename": filename}, llog.ErrKV(err))
//
package mlog
import (
"bufio"
"fmt"
"io"
"os"
"sort"
"strconv"
"sync"
)
// Truncate is a helper function to truncate a string to a given size. It will
// add 3 trailing elipses, so the returned string will be at most size+3
// characters long
func Truncate(s string, size int) string {
if len(s) <= size {
return s
}
return s[:size] + "..."
}
////////////////////////////////////////////////////////////////////////////////
// Level describes the severity of a particular log message, and can be compared
// to the severity of any other Level
type Level interface {
// String gives the string form of the level, e.g. "INFO" or "ERROR"
String() string
// Uint gives an integer indicator of the severity of the level, with zero
// being most severe. If a Level with Uint of zero is logged then the Logger
// implementation provided by this package will exit the process (i.e. zero
// is used as Fatal).
Uint() uint
}
type level struct {
s string
i uint
}
func (l level) String() string {
return l.s
}
func (l level) Uint() uint {
return l.i
}
// All pre-defined log levels
var (
DebugLevel Level = level{s: "DEBUG", i: 40}
InfoLevel Level = level{s: "INFO", i: 30}
WarnLevel Level = level{s: "WARN", i: 20}
ErrorLevel Level = level{s: "ERROR", i: 10}
FatalLevel Level = level{s: "FATAL", i: 0}
)
////////////////////////////////////////////////////////////////////////////////
// KVer is used to provide context to a log entry in the form of a dynamic set
// of key/value pairs which can be different for every entry.
//
// Each returned KV should be modifiable.
type KVer interface {
KV() KV
}
// KVerFunc is a function which implements the KVer interface by calling itself.
type KVerFunc func() KV
// KV implements the KVer interface by calling the KVerFunc itself.
func (kvf KVerFunc) KV() KV {
return kvf()
}
// KV is a set of key/value pairs which provides context for a log entry by a
// KVer. KV is itself also a KVer.
type KV map[string]interface{}
// KV implements the KVer method by returning a copy of the KV
func (kv KV) KV() KV {
nkv := make(KV, len(kv))
for k, v := range kv {
nkv[k] = v
}
return nkv
}
// Set returns a copy of the KV being called on with the given key/val set on
// it. The original KV is unaffected
func (kv KV) Set(k string, v interface{}) KV {
nkv := kv.KV()
nkv[k] = v
return nkv
}
// this may take in any amount of nil values, but should never return nil
func mergeInto(kv KVer, kvs ...KVer) KV {
if kv == nil {
kv = KV(nil) // will return empty map when KV is called on it
}
kvm := kv.KV()
for _, kv := range kvs {
if kv == nil {
continue
}
for k, v := range kv.KV() {
kvm[k] = v
}
}
return kvm
}
type merger struct {
base KVer
rest []KVer
}
// Merge takes in multiple KVers and returns a single KVer which is the union of
// all the passed in ones. Key/Vals on the rightmost of the set take precedence
// over conflicting ones to the left.
//
// The KVer returned will call KV() on each of the passed in KVers every time
// its KV method is called.
func Merge(kvs ...KVer) KVer {
if len(kvs) == 0 {
return merger{}
}
return merger{base: kvs[0], rest: kvs[1:]}
}
// MergeInto is a convenience function which acts similarly to Merge.
func MergeInto(kv KVer, kvs ...KVer) KVer {
return merger{base: kv, rest: kvs}
}
func (m merger) KV() KV {
return mergeInto(m.base, m.rest...)
}
// Prefix prefixes the all keys returned from the given KVer with the given
// prefix string.
func Prefix(kv KVer, prefix string) KVer {
return KVerFunc(func() KV {
kvv := kv.KV()
newKVV := make(KV, len(kvv))
for k, v := range kvv {
newKVV[prefix+k] = v
}
return newKVV
})
}
////////////////////////////////////////////////////////////////////////////////
// Stringer generates and returns a string.
type Stringer interface {
String() string
}
// String is simply a string which implements Stringer.
type String string
func (str String) String() string {
return string(str)
}
// Message describes a message to be logged, after having already resolved the
// KVer
type Message struct {
Level
Description Stringer
KVer
}
func stringSlice(kv KV) [][2]string {
slice := make([][2]string, 0, len(kv))
for k, v := range kv {
slice = append(slice, [2]string{
k,
strconv.QuoteToGraphic(fmt.Sprint(v)),
})
}
sort.Slice(slice, func(i, j int) bool {
return slice[i][0] < slice[j][0]
})
return slice
}
// Handler is a function which can process Messages in some way.
//
// NOTE that Logger does not handle thread-safety, that must be done inside the
// Handler if necessary.
type Handler func(msg Message) error
// DefaultFormat formats and writs the Message to the given Writer using mlog's
// default format.
func DefaultFormat(w io.Writer, msg Message) error {
var err error
write := func(s string, args ...interface{}) {
if err == nil {
_, err = fmt.Fprintf(w, s, args...)
}
}
write("~ %s -- %s", msg.Level.String(), msg.Description.String())
if msg.KVer != nil {
if kv := msg.KV(); len(kv) > 0 {
write(" --")
for _, kve := range stringSlice(kv) {
write(" %s=%s", kve[0], kve[1])
}
}
}
write("\n")
return err
}
// DefaultHandler initializes and returns a Handler which will write all
// messages to os.Stderr in a thread-safe way. This is the Handler which
// NewLogger will use automatically.
func DefaultHandler() Handler {
l := new(sync.Mutex)
bw := bufio.NewWriter(os.Stderr)
return func(msg Message) error {
l.Lock()
defer l.Unlock()
err := DefaultFormat(bw, msg)
if err == nil {
err = bw.Flush()
}
return err
}
}
// Logger directs Messages to an internal Handler and provides convenient
// methods for creating and modifying its own behavior.
type Logger struct {
w io.Writer
h Handler
maxLevel uint
kv KVer
testMsgWrittenCh chan struct{} // only initialized/used in tests
}
// NewLogger initializes and returns a new instance of Logger which will write
// to the DefaultHandler.
func NewLogger() *Logger {
return &Logger{
h: DefaultHandler(),
maxLevel: InfoLevel.Uint(),
}
}
// Handler returns the Handler currently in use by the Logger.
func (l *Logger) Handler() Handler {
return l.h
}
func (l *Logger) clone() *Logger {
l2 := *l
return &l2
}
// WithMaxLevelUint returns a copy of the Logger which will not log any messages
// with a higher Level.Uint value than the one given. The returned Logger is
// identical in all other aspects.
func (l *Logger) WithMaxLevelUint(i uint) *Logger {
l = l.clone()
l.maxLevel = i
return l
}
// WithMaxLevel returns a copy of the Logger which will not log any messages
// with a higher Level.Uint value than of the Level given. The returned Logger
// is identical in all other aspects.
func (l *Logger) WithMaxLevel(lvl Level) *Logger {
return l.WithMaxLevelUint(lvl.Uint())
}
// WithHandler returns a copy of the Logger which will use the given Handler in
// order to process messages. The returned Logger is identical in all other
// aspects.
func (l *Logger) WithHandler(h Handler) *Logger {
l = l.clone()
l.h = h
return l
}
// WithKV returns a copy of the Logger which will use the merging of the given
// KVers as a base KVer for all log messages. If the original Logger already had
// a base KVer (via a previous WithKV call) then this set will be merged onto
// that one.
func (l *Logger) WithKV(kvs ...KVer) *Logger {
l = l.clone()
l.kv = MergeInto(l.kv, kvs...)
return l
}
// Log can be used to manually log a message of some custom defined Level. kvs
// will be Merge'd automatically. If the Level is a fatal (Uint() == 0) then
// calling this will never return, and the process will have os.Exit(1) called.
func (l *Logger) Log(msg Message) {
if l.maxLevel < msg.Level.Uint() {
return
}
if l.kv != nil {
msg.KVer = MergeInto(l.kv, msg.KVer)
}
if err := l.h(msg); err != nil {
go l.Error("Logger.Handler returned error", ErrKV(err))
return
}
if l.testMsgWrittenCh != nil {
l.testMsgWrittenCh <- struct{}{}
}
if msg.Level.Uint() == 0 {
os.Exit(1)
}
}
func mkMsg(lvl Level, descr string, kvs ...KVer) Message {
return Message{
Level: lvl,
Description: String(descr),
KVer: Merge(kvs...),
}
}
// Debug logs a DebugLevel message, merging the KVers together first
func (l *Logger) Debug(descr string, kvs ...KVer) {
l.Log(mkMsg(DebugLevel, descr, kvs...))
}
// Info logs a InfoLevel message, merging the KVers together first
func (l *Logger) Info(descr string, kvs ...KVer) {
l.Log(mkMsg(InfoLevel, descr, kvs...))
}
// Warn logs a WarnLevel message, merging the KVers together first
func (l *Logger) Warn(descr string, kvs ...KVer) {
l.Log(mkMsg(WarnLevel, descr, kvs...))
}
// Error logs a ErrorLevel message, merging the KVers together first
func (l *Logger) Error(descr string, kvs ...KVer) {
l.Log(mkMsg(ErrorLevel, descr, kvs...))
}
// Fatal logs a FatalLevel message, merging the KVers together first. A Fatal
// message automatically stops the process with an os.Exit(1)
func (l *Logger) Fatal(descr string, kvs ...KVer) {
l.Log(mkMsg(FatalLevel, descr, kvs...))
}