2023-01-30 16:27:06 +03:00
|
|
|
// Package mlog provides logging with log levels and fields.
|
|
|
|
//
|
|
|
|
// Each log level has a function to log with and without error.
|
|
|
|
// Each such function takes a varargs list of fields (key value pairs) to log.
|
|
|
|
// Variable data should be in fields. Logging strings themselves should be
|
|
|
|
// constant, for easier log processing (e.g. building metrics based on log
|
|
|
|
// messages).
|
|
|
|
//
|
|
|
|
// The log levels can be configured per originating package, e.g. smtpclient,
|
|
|
|
// imapserver. The configuration is application-global, so each Log instance
|
|
|
|
// uses the same log levels.
|
|
|
|
//
|
|
|
|
// Print* should be used for lines that always should be printed, regardless of
|
|
|
|
// configured log levels. Useful for startup logging and subcommands.
|
|
|
|
//
|
|
|
|
// Fatal* stops the program. Its log text is always printed.
|
|
|
|
package mlog
|
|
|
|
|
|
|
|
// todo: log with source=path:linenumber? and/or stacktrace (perhaps optional)
|
|
|
|
// todo: should we turn errors logged with an context.Canceled from a level error into level info?
|
|
|
|
// todo: rethink format. perhaps simply using %#v is more useful for many types?
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"reflect"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"sync/atomic"
|
|
|
|
)
|
|
|
|
|
|
|
|
var Logfmt bool
|
|
|
|
|
|
|
|
type Level int
|
|
|
|
|
|
|
|
var LevelStrings = map[Level]string{
|
2023-02-03 22:33:19 +03:00
|
|
|
LevelPrint: "print",
|
|
|
|
LevelFatal: "fatal",
|
|
|
|
LevelError: "error",
|
|
|
|
LevelInfo: "info",
|
|
|
|
LevelDebug: "debug",
|
|
|
|
LevelTrace: "trace",
|
|
|
|
LevelTraceauth: "traceauth",
|
|
|
|
LevelTracedata: "tracedata",
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
var Levels = map[string]Level{
|
2023-02-03 22:33:19 +03:00
|
|
|
"print": LevelPrint,
|
|
|
|
"fatal": LevelFatal,
|
|
|
|
"error": LevelError,
|
|
|
|
"info": LevelInfo,
|
|
|
|
"debug": LevelDebug,
|
|
|
|
"trace": LevelTrace,
|
|
|
|
"traceauth": LevelTraceauth,
|
|
|
|
"tracedata": LevelTracedata,
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
2023-02-03 22:33:19 +03:00
|
|
|
LevelPrint Level = 0 // Printed regardless of configured log level.
|
|
|
|
LevelFatal Level = 1 // Printed regardless of configured log level.
|
|
|
|
LevelError Level = 2
|
|
|
|
LevelInfo Level = 3
|
|
|
|
LevelDebug Level = 4
|
|
|
|
LevelTrace Level = 5
|
|
|
|
LevelTraceauth Level = 6
|
|
|
|
LevelTracedata Level = 7
|
2023-01-30 16:27:06 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// Holds a map[string]Level, mapping a package (field pkg in logs) to a log level.
|
|
|
|
// The empty string is the default/fallback log level.
|
|
|
|
var config atomic.Value
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
config.Store(map[string]Level{"": LevelError})
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetConfig atomically sets the new log levels used by all Log instances.
|
|
|
|
func SetConfig(c map[string]Level) {
|
|
|
|
config.Store(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pair is a field/value pair, for use in logged lines.
|
|
|
|
type Pair struct {
|
|
|
|
key string
|
|
|
|
value any
|
|
|
|
}
|
|
|
|
|
|
|
|
// Field is a shorthand for making a Pair.
|
|
|
|
func Field(k string, v any) Pair {
|
|
|
|
return Pair{k, v}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Log is an instance potentially with its own field/value pair added to any
|
|
|
|
// logging output.
|
|
|
|
type Log struct {
|
|
|
|
fields []Pair
|
|
|
|
moreFields func() []Pair
|
|
|
|
}
|
|
|
|
|
|
|
|
// New returns a new Log instance. Each log invocation adds field "pkg".
|
|
|
|
func New(pkg string) *Log {
|
|
|
|
return &Log{
|
|
|
|
fields: []Pair{{"pkg", pkg}},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type key string
|
|
|
|
|
|
|
|
// CidKey can be used with context.WithValue to store a "cid" in a context, for logging.
|
|
|
|
var CidKey key = "cid"
|
|
|
|
|
|
|
|
// WithCid adds a field "cid".
|
|
|
|
// Also see WithContext.
|
|
|
|
func (l *Log) WithCid(cid int64) *Log {
|
|
|
|
return l.Fields(Pair{"cid", cid})
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithContext adds cid from context, if present. Context are often passed to
|
|
|
|
// functions, especially between packages, to pass a "cid" for an operation. At the
|
|
|
|
// start of a function (especially if exported) a variable "log" is often
|
|
|
|
// instantiated from a package-level variable "xlog", with WithContext for its cid.
|
|
|
|
// A *Log could be passed instead, but contexts are more pervasive. For the same
|
|
|
|
// reason WithContext is more common than WithCid.
|
|
|
|
func (l *Log) WithContext(ctx context.Context) *Log {
|
|
|
|
cidv := ctx.Value(CidKey)
|
|
|
|
if cidv == nil {
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
cid := cidv.(int64)
|
|
|
|
return l.WithCid(cid)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Field adds fields to the logger. Each logged line adds these fields.
|
|
|
|
func (l *Log) Fields(fields ...Pair) *Log {
|
|
|
|
nl := *l
|
|
|
|
nl.fields = append(fields, nl.fields...)
|
|
|
|
return &nl
|
|
|
|
}
|
|
|
|
|
|
|
|
// MoreFields sets a function on the logger that is called just before logging,
|
|
|
|
// to retrieve additional fields to log.
|
|
|
|
func (l *Log) MoreFields(fn func() []Pair) *Log {
|
|
|
|
nl := *l
|
|
|
|
nl.moreFields = fn
|
|
|
|
return &nl
|
|
|
|
}
|
|
|
|
|
2023-02-03 22:33:19 +03:00
|
|
|
func (l *Log) Trace(traceLevel Level, text string) bool {
|
|
|
|
return l.logx(traceLevel, nil, text)
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Log) Fatal(text string, fields ...Pair) { l.Fatalx(text, nil, fields...) }
|
|
|
|
func (l *Log) Fatalx(text string, err error, fields ...Pair) {
|
|
|
|
l.plog(LevelFatal, err, text, fields...)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Log) Print(text string, fields ...Pair) bool {
|
|
|
|
return l.logx(LevelPrint, nil, text, fields...)
|
|
|
|
}
|
|
|
|
func (l *Log) Printx(text string, err error, fields ...Pair) bool {
|
|
|
|
return l.logx(LevelPrint, err, text, fields...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Log) Debug(text string, fields ...Pair) bool {
|
|
|
|
return l.logx(LevelDebug, nil, text, fields...)
|
|
|
|
}
|
|
|
|
func (l *Log) Debugx(text string, err error, fields ...Pair) bool {
|
|
|
|
return l.logx(LevelDebug, err, text, fields...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Log) Info(text string, fields ...Pair) bool { return l.logx(LevelInfo, nil, text, fields...) }
|
|
|
|
func (l *Log) Infox(text string, err error, fields ...Pair) bool {
|
|
|
|
return l.logx(LevelInfo, err, text, fields...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Log) Error(text string, fields ...Pair) bool {
|
|
|
|
return l.logx(LevelError, nil, text, fields...)
|
|
|
|
}
|
|
|
|
func (l *Log) Errorx(text string, err error, fields ...Pair) bool {
|
|
|
|
return l.logx(LevelError, err, text, fields...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Log) logx(level Level, err error, text string, fields ...Pair) bool {
|
2023-02-03 22:33:19 +03:00
|
|
|
if ok, high := l.match(level); ok {
|
|
|
|
// Nothing.
|
|
|
|
} else if high >= LevelTrace && level == LevelTraceauth {
|
|
|
|
text = "***"
|
|
|
|
} else if high >= LevelTrace && level == LevelTracedata {
|
|
|
|
text = "..."
|
|
|
|
} else {
|
2023-01-30 16:27:06 +03:00
|
|
|
return false
|
|
|
|
}
|
2023-02-03 22:33:19 +03:00
|
|
|
if level > LevelTrace {
|
|
|
|
level = LevelTrace
|
|
|
|
}
|
2023-01-30 16:27:06 +03:00
|
|
|
l.plog(level, err, text, fields...)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// escape logfmt string if required, otherwise return original string.
|
|
|
|
func logfmtValue(s string) string {
|
|
|
|
for _, c := range s {
|
|
|
|
if c == '"' || c == '\\' || c <= ' ' || c == '=' || c >= 0x7f {
|
|
|
|
return fmt.Sprintf("%q", s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
func stringValue(iscid, nested bool, v any) string {
|
|
|
|
// Handle some common types first.
|
|
|
|
if v == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
switch r := v.(type) {
|
|
|
|
case string:
|
|
|
|
return r
|
|
|
|
case int:
|
|
|
|
return strconv.Itoa(r)
|
|
|
|
case int64:
|
|
|
|
if iscid {
|
|
|
|
return fmt.Sprintf("%x", v)
|
|
|
|
}
|
|
|
|
return strconv.FormatInt(r, 10)
|
|
|
|
case bool:
|
|
|
|
if r {
|
|
|
|
return "true"
|
|
|
|
}
|
|
|
|
return "false"
|
|
|
|
case float64:
|
|
|
|
return fmt.Sprintf("%v", v)
|
|
|
|
case []byte:
|
|
|
|
return base64.RawURLEncoding.EncodeToString(r)
|
|
|
|
case []string:
|
|
|
|
if nested && len(r) == 0 {
|
|
|
|
// Drop field from logging.
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return "[" + strings.Join(r, ",") + "]"
|
|
|
|
}
|
|
|
|
|
|
|
|
rv := reflect.ValueOf(v)
|
|
|
|
if rv.Kind() == reflect.Ptr && rv.IsNil() {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
if r, ok := v.(fmt.Stringer); ok {
|
|
|
|
return r.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
if rv.Kind() == reflect.Ptr {
|
|
|
|
rv = rv.Elem()
|
|
|
|
return stringValue(iscid, nested, rv.Interface())
|
|
|
|
}
|
|
|
|
if rv.Kind() == reflect.Slice {
|
|
|
|
n := rv.Len()
|
|
|
|
if nested && n == 0 {
|
|
|
|
// Drop field.
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
b := &strings.Builder{}
|
|
|
|
b.WriteString("[")
|
|
|
|
for i := 0; i < n; i++ {
|
|
|
|
if i > 0 {
|
|
|
|
b.WriteString(";")
|
|
|
|
}
|
|
|
|
b.WriteString(stringValue(false, true, rv.Index(i).Interface()))
|
|
|
|
}
|
|
|
|
b.WriteString("]")
|
|
|
|
return b.String()
|
|
|
|
} else if rv.Kind() != reflect.Struct {
|
|
|
|
return fmt.Sprintf("%v", v)
|
|
|
|
}
|
|
|
|
n := rv.NumField()
|
|
|
|
t := rv.Type()
|
|
|
|
b := &strings.Builder{}
|
|
|
|
first := true
|
|
|
|
for i := 0; i < n; i++ {
|
|
|
|
fv := rv.Field(i)
|
|
|
|
if !t.Field(i).IsExported() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if fv.Kind() == reflect.Struct || fv.Kind() == reflect.Ptr || fv.Kind() == reflect.Interface {
|
|
|
|
// Don't recurse.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
vs := stringValue(false, true, fv.Interface())
|
|
|
|
if vs == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if !first {
|
|
|
|
b.WriteByte(' ')
|
|
|
|
}
|
|
|
|
first = false
|
|
|
|
k := strings.ToLower(t.Field(i).Name)
|
|
|
|
b.WriteString(k + "=" + logfmtValue(vs))
|
|
|
|
}
|
|
|
|
return b.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Log) plog(level Level, err error, text string, fields ...Pair) {
|
|
|
|
fields = append(l.fields, fields...)
|
|
|
|
if l.moreFields != nil {
|
|
|
|
fields = append(fields, l.moreFields()...)
|
|
|
|
}
|
|
|
|
// We build up a buffer so we can do a single atomic write of the data. Otherwise partial log lines may interleaf.
|
|
|
|
b := &bytes.Buffer{}
|
|
|
|
if Logfmt {
|
|
|
|
fmt.Fprintf(b, "l=%s m=%s", LevelStrings[level], logfmtValue(text))
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(b, " err=%s", logfmtValue(err.Error()))
|
|
|
|
}
|
|
|
|
for i := 0; i < len(fields); i++ {
|
|
|
|
kv := fields[i]
|
|
|
|
fmt.Fprintf(b, " %s=%s", kv.key, logfmtValue(stringValue(kv.key == "cid", false, kv.value)))
|
|
|
|
}
|
|
|
|
b.WriteString("\n")
|
|
|
|
} else {
|
|
|
|
fmt.Fprintf(b, "%s: %s", LevelStrings[level], logfmtValue(text))
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(b, ": %s", logfmtValue(err.Error()))
|
|
|
|
}
|
|
|
|
if len(fields) > 0 {
|
|
|
|
fmt.Fprint(b, " (")
|
|
|
|
for i := 0; i < len(fields); i++ {
|
|
|
|
if i > 0 {
|
|
|
|
fmt.Fprint(b, "; ")
|
|
|
|
}
|
|
|
|
kv := fields[i]
|
|
|
|
fmt.Fprintf(b, "%s: %s", kv.key, logfmtValue(stringValue(kv.key == "cid", false, kv.value)))
|
|
|
|
}
|
|
|
|
fmt.Fprint(b, ")")
|
|
|
|
}
|
|
|
|
b.WriteString("\n")
|
|
|
|
}
|
|
|
|
os.Stderr.Write(b.Bytes())
|
|
|
|
}
|
|
|
|
|
2023-02-03 22:33:19 +03:00
|
|
|
func (l *Log) match(level Level) (bool, Level) {
|
2023-01-30 16:27:06 +03:00
|
|
|
if level == LevelPrint || level == LevelFatal {
|
2023-02-03 22:33:19 +03:00
|
|
|
return true, level
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
cl := config.Load().(map[string]Level)
|
|
|
|
|
|
|
|
seen := false
|
2023-02-03 22:33:19 +03:00
|
|
|
var high Level
|
2023-01-30 16:27:06 +03:00
|
|
|
for _, kv := range l.fields {
|
|
|
|
if kv.key != "pkg" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
pkg, ok := kv.value.(string)
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
v, ok := cl[pkg]
|
2023-02-03 22:33:19 +03:00
|
|
|
if v > high {
|
|
|
|
high = v
|
|
|
|
}
|
2023-01-30 16:27:06 +03:00
|
|
|
if ok && v >= level {
|
2023-02-03 22:33:19 +03:00
|
|
|
return true, high
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
seen = seen || ok
|
|
|
|
}
|
|
|
|
if seen {
|
2023-02-03 22:33:19 +03:00
|
|
|
return false, high
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
v, ok := cl[""]
|
2023-02-03 22:33:19 +03:00
|
|
|
if v > high {
|
|
|
|
high = v
|
|
|
|
}
|
|
|
|
return ok && v >= level, v
|
2023-01-30 16:27:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
type errWriter struct {
|
|
|
|
log *Log
|
|
|
|
level Level
|
|
|
|
msg string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *errWriter) Write(buf []byte) (int, error) {
|
|
|
|
err := errors.New(strings.TrimSpace(string(buf)))
|
|
|
|
w.log.logx(w.level, err, w.msg)
|
|
|
|
return len(buf), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ErrWriter returns a writer that turns each write into a logging call on "log"
|
|
|
|
// with given "level" and "msg" and the written content as an error.
|
|
|
|
// Can be used for making a Go log.Logger for use in http.Server.ErrorLog.
|
|
|
|
func ErrWriter(log *Log, level Level, msg string) io.Writer {
|
|
|
|
return &errWriter{log, level, msg}
|
|
|
|
}
|