1// Package mlog providers helpers on top of slog.Logger.
3// Packages of mox that are fit or use by external code take an *slog.Logger as
4// parameter for logging. Internally, and packages not intended for reuse,
5// logging is done with mlog.Log. It providers convenience functions for:
6// logging error values, tracing (protocol messages), uncoditional printing
9// An mlog provides a handler for an mlog.Log for formatting log lines. Lines are
10// logged as "logfmt" lines for "mox serve". For command-line tools, the lines are
11// printed with colon-separated level, message and error, followed by
12// semicolon-separated attributes.
30var noctx = context.Background()
32// Logfmt enabled output in logfmt, instead of output more suitable for
33// command-line tools. Must be set early in a program lifecycle.
36// LogStringer is used when formatting field values during logging. If a value
37// implements it, LogString is called for the value to log.
38type LogStringer interface {
42var lowestLevel atomic.Int32 // For quick initial check.
43var config atomic.Pointer[map[string]slog.Level] // For secondary complete check for match.
46 SetConfig(map[string]slog.Level{"": LevelDebug})
49// SetConfig atomically sets the new log levels used by all Log instances.
50func SetConfig(c map[string]slog.Level) {
57 lowestLevel.Store(int32(lowest))
62 // When the configured log level is any of the Trace levels, all protocol messages
63 // are printed. But protocol "data" (like an email message in the SMTP DATA
64 // command) is replaced with "..." unless the configured level is LevelTracedata.
65 // Likewise, protocol messages with authentication data (e.g. plaintext base64
66 // passwords) are replaced with "***" unless the configured level is LevelTraceauth
68 LevelTracedata = slog.LevelDebug - 8
69 LevelTraceauth = slog.LevelDebug - 6
70 LevelTrace = slog.LevelDebug - 4
71 LevelDebug = slog.LevelDebug
72 LevelInfo = slog.LevelInfo
73 LevelWarn = slog.LevelWarn
74 LevelError = slog.LevelError
75 LevelFatal = slog.LevelError + 4 // Printed regardless of configured log level.
76 LevelPrint = slog.LevelError + 8 // Printed regardless of configured log level.
79// Levelstrings map log levels to human-readable names.
80var LevelStrings = map[slog.Level]string{
81 LevelTracedata: "tracedata",
82 LevelTraceauth: "traceauth",
92// Levels map the human-readable log level to a level.
93var Levels = map[string]slog.Level{
94 "tracedata": LevelTracedata,
95 "traceauth": LevelTraceauth,
105// Log wraps an slog.Logger, providing convenience functions.
110// New returns a Log that adds a "pkg" attribute. If logger is nil, a new
111// Logger is created with a custom handler.
112func New(pkg string, logger *slog.Logger) Log {
114 logger = slog.New(&handler{})
116 return Log{logger}.WithPkg(pkg)
119// WithCid adds a attribute "cid".
120// Also see WithContext.
121func (l Log) WithCid(cid int64) Log {
122 return l.With(slog.Int64("cid", cid))
127// CidKey can be used with context.WithValue to store a "cid" in a context, for logging.
128var CidKey key = "cid"
130// WithContext adds cid from context, if present. Context are often passed to
131// functions, especially between packages, to pass a "cid" for an operation. At the
132// start of a function (especially if exported) a variable "log" is often
133// instantiated from a package-level logger, with WithContext for its cid.
134// Ideally, a Log could be passed instead, but contexts are more pervasive. For the same
135// reason WithContext is more common than WithCid.
136func (l Log) WithContext(ctx context.Context) Log {
137 cidv := ctx.Value(CidKey)
142 return l.WithCid(cid)
145// With adds attributes to to each logged line.
146func (l Log) With(attrs ...slog.Attr) Log {
147 return Log{slog.New(l.Logger.Handler().WithAttrs(attrs))}
150// WithPkg ensures pkg is added as attribute to logged lines. If the handler is
151// an mlog handler, pkg is only added if not already the last added package.
152func (l Log) WithPkg(pkg string) Log {
153 h := l.Logger.Handler()
154 if ph, ok := h.(*handler); ok {
155 if len(ph.Pkgs) > 0 && ph.Pkgs[len(ph.Pkgs)-1] == pkg {
158 return Log{slog.New(ph.WithPkg(pkg))}
160 return Log{slog.New(h.WithAttrs([]slog.Attr{slog.String("pkg", pkg)}))}
163// WithFunc sets fn to be called for additional attributes. Fn is only called
164// when the line is logged.
165// If the underlying handler is not an mlog.handler, this method has no effect.
166// Caller must take care of preventing data races.
167func (l Log) WithFunc(fn func() []slog.Attr) Log {
168 h := l.Logger.Handler()
169 if ph, ok := h.(*handler); ok {
170 return Log{slog.New(ph.WithFunc(fn))}
172 // Ignored for other handlers, only used internally (smtpserver, imapserver).
176// Check logs an error if err is not nil. Intended for logging errors that are good
177// to know, but would not influence program flow.
178func (l Log) Check(err error, msg string, attrs ...slog.Attr) {
180 l.Errorx(msg, err, attrs...)
184func errAttr(err error) slog.Attr {
185 return slog.Any("err", err)
188// todo: consider taking a context parameter. it would require all code be refactored. we may want to do this if callers really depend on passing attrs through context. the mox code base does not do that. it makes all call sites more tedious, and requires passing around ctx everywhere, so consider carefully.
190func (l Log) Debug(msg string, attrs ...slog.Attr) {
191 l.Logger.LogAttrs(noctx, LevelDebug, msg, attrs...)
194func (l Log) Debugx(msg string, err error, attrs ...slog.Attr) {
196 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
198 l.Logger.LogAttrs(noctx, LevelDebug, msg, attrs...)
201func (l Log) Info(msg string, attrs ...slog.Attr) {
202 l.Logger.LogAttrs(noctx, LevelInfo, msg, attrs...)
205func (l Log) Infox(msg string, err error, attrs ...slog.Attr) {
207 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
209 l.Logger.LogAttrs(noctx, LevelInfo, msg, attrs...)
212func (l Log) Warn(msg string, attrs ...slog.Attr) {
213 l.Logger.LogAttrs(noctx, LevelWarn, msg, attrs...)
216func (l Log) Warnx(msg string, err error, attrs ...slog.Attr) {
218 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
220 l.Logger.LogAttrs(noctx, LevelWarn, msg, attrs...)
223func (l Log) Error(msg string, attrs ...slog.Attr) {
224 l.Logger.LogAttrs(noctx, LevelError, msg, attrs...)
227func (l Log) Errorx(msg string, err error, attrs ...slog.Attr) {
229 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
231 l.Logger.LogAttrs(noctx, LevelError, msg, attrs...)
234func (l Log) Fatal(msg string, attrs ...slog.Attr) {
235 l.Logger.LogAttrs(noctx, LevelFatal, msg, attrs...)
239func (l Log) Fatalx(msg string, err error, attrs ...slog.Attr) {
241 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
243 l.Logger.LogAttrs(noctx, LevelFatal, msg, attrs...)
247func (l Log) Print(msg string, attrs ...slog.Attr) {
248 l.Logger.LogAttrs(noctx, LevelPrint, msg, attrs...)
251func (l Log) Printx(msg string, err error, attrs ...slog.Attr) {
253 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
255 l.Logger.LogAttrs(noctx, LevelPrint, msg, attrs...)
258// Trace logs at trace/traceauth/tracedata level.
259// If the active log level is any of the trace levels, the data is logged.
260// If level is for tracedata, but the active level doesn't trace data, data is replaced with "...".
261// If level is for traceauth, but the active level doesn't trace auth, data is replaced with "***".
262func (l Log) Trace(level slog.Level, prefix string, data []byte) {
264 if !h.Enabled(noctx, level) {
267 ph, ok := h.(*handler)
269 msg := prefix + string(data)
270 r := slog.NewRecord(time.Now(), level, msg, 0)
274 filterLevel, ok := ph.configMatch(level)
280 if hideData, hideAuth := traceLevel(filterLevel, level); hideData {
285 msg = prefix + string(data)
287 r := slog.NewRecord(time.Time{}, level, msg, 0)
288 ph.write(filterLevel, r)
291func traceLevel(level, recordLevel slog.Level) (hideData, hideAuth bool) {
292 hideData = recordLevel == LevelTracedata && level > LevelTracedata
293 hideAuth = recordLevel == LevelTraceauth && level > LevelTraceauth
300 Group string // Empty or with dot-separated names, ending with a dot.
301 Fn func() []slog.Attr // Only called when record is actually being logged.
304func match(minLevel, level slog.Level) bool {
305 return level >= LevelFatal || level >= minLevel || minLevel <= LevelTrace && level <= LevelTrace
308func (h *handler) Enabled(ctx context.Context, level slog.Level) bool {
309 return match(slog.Level(lowestLevel.Load()), level)
312func (h *handler) configMatch(level slog.Level) (slog.Level, bool) {
314 for i := len(h.Pkgs) - 1; i >= 0; i-- {
315 if l, ok := c[h.Pkgs[i]]; ok {
316 return l, match(l, level)
320 return l, match(l, level)
323func (h *handler) Handle(ctx context.Context, r slog.Record) error {
324 l, ok := h.configMatch(r.Level)
328 if hideData, hideAuth := traceLevel(l, r.Level); hideData {
336// Reuse buffers to format log lines into.
337var logBuffersStore [32][256]byte
338var logBuffers = make(chan []byte, 200)
341 for i := range logBuffersStore {
342 logBuffers <- logBuffersStore[i][:]
346// escape logfmt string if required, otherwise return original string.
347func formatString(s string) string {
348 for _, c := range s {
349 if c <= ' ' || c == '"' || c == '\\' || c == '=' || c >= 0x7f {
350 return fmt.Sprintf("%q", s)
356func stringValue(iscid, nested bool, v any) string {
357 // Handle some common types first.
361 switch r := v.(type) {
365 return strconv.Itoa(r)
368 return fmt.Sprintf("%x", v)
370 return strconv.FormatInt(r, 10)
377 return fmt.Sprintf("%v", v)
379 return base64.RawURLEncoding.EncodeToString(r)
381 if nested && len(r) == 0 {
382 // Drop field from logging.
385 return "[" + strings.Join(r, ",") + "]"
389 return r.Format(time.RFC3339)
392 rv := reflect.ValueOf(v)
393 if rv.Kind() == reflect.Ptr && rv.IsNil() {
397 if r, ok := v.(LogStringer); ok {
400 if r, ok := v.(fmt.Stringer); ok {
404 if rv.Kind() == reflect.Ptr {
406 return stringValue(iscid, nested, rv.Interface())
408 if rv.Kind() == reflect.Slice {
410 if nested && n == 0 {
414 b := &strings.Builder{}
416 for i := 0; i < n; i++ {
420 b.WriteString(stringValue(false, true, rv.Index(i).Interface()))
424 } else if rv.Kind() != reflect.Struct {
425 return fmt.Sprintf("%v", v)
430 // We first try making a string without recursing into structs/pointers/interfaces,
431 // but will try again with those fields if we otherwise would otherwise log an
433 for j := 0; j < 2; j++ {
435 b := &strings.Builder{}
436 for i := 0; i < n; i++ {
438 if !t.Field(i).IsExported() {
441 if j == 0 && (fv.Kind() == reflect.Struct || fv.Kind() == reflect.Ptr || fv.Kind() == reflect.Interface) {
445 vs := stringValue(false, true, fv.Interface())
453 k := strings.ToLower(t.Field(i).Name)
454 b.WriteString(k + "=" + vs)
464func writeAttr(w io.Writer, separator, group string, a slog.Attr) {
465 switch a.Value.Kind() {
471 for _, a := range a.Value.Group() {
472 writeAttr(w, separator, group, a)
477 if a.Value.Kind() == slog.KindLogValuer {
478 vv = a.Value.Resolve().Any()
482 s := stringValue(a.Key == "cid", false, vv)
483 fmt.Fprint(w, separator, group, a.Key, "=", formatString(s))
487func (h *handler) write(l slog.Level, r slog.Record) error {
488 // Reuse a buffer, or temporarily allocate a new one.
491 case buf = <-logBuffers:
496 buf = make([]byte, 128)
499 b := bytes.NewBuffer(buf[:0])
500 eb := &errWriter{b, nil}
504 ensurePkgs := func() {
507 for _, pkg := range h.Pkgs {
508 writeAttr(eb, " ", "", slog.String("pkg", pkg))
513 fmt.Fprint(eb, "l=", LevelStrings[r.Level], " m=")
514 fmt.Fprintf(eb, "%q", r.Message)
516 r.Attrs(func(a slog.Attr) bool {
517 if n > 0 || a.Key != "err" || h.Group != "" {
520 writeAttr(eb, " ", h.Group, a)
525 for _, a := range h.Attrs {
526 writeAttr(eb, " ", h.Group, a)
529 for _, a := range h.Fn() {
530 writeAttr(eb, " ", h.Group, a)
536 ensurePkgs := func() {
539 for _, pkg := range h.Pkgs {
540 writeAttr(eb, "; ", "", slog.String("pkg", pkg))
545 fmt.Fprint(eb, LevelStrings[r.Level], ": ", r.Message)
547 r.Attrs(func(a slog.Attr) bool {
548 if n == 0 && a.Key == "err" && h.Group == "" {
549 fmt.Fprint(eb, ": ", a.Value.String())
553 writeAttr(eb, "; ", h.Group, a)
559 for _, a := range h.Attrs {
560 writeAttr(eb, "; ", h.Group, a)
563 for _, a := range h.Fn() {
564 writeAttr(eb, "; ", h.Group, a)
574 // todo: for mox serve, do writes in separate goroutine.
575 _, err := os.Stderr.Write(b.Bytes())
579type errWriter struct {
584func (w *errWriter) Write(buf []byte) (int, error) {
589 n, w.Err = w.Writer.Write(buf)
593func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler {
596 nh.Attrs = append([]slog.Attr{}, h.Attrs...)
598 nh.Attrs = append(nh.Attrs, attrs...)
602func (h *handler) WithGroup(name string) slog.Handler {
607 nh.Group += name + "."
611func (h *handler) WithPkg(pkg string) *handler {
614 nh.Pkgs = append([]string{}, nh.Pkgs...)
616 nh.Pkgs = append(nh.Pkgs, pkg)
620func (h *handler) WithFunc(fn func() []slog.Attr) *handler {
626type logWriter struct {
632func (w logWriter) Write(buf []byte) (int, error) {
633 err := strings.TrimSpace(string(buf))
634 w.log.LogAttrs(noctx, w.level, w.msg, slog.String("err", err))
638// LogWriter returns a writer that turns each write into a logging call on "log"
639// with given "level" and "msg" and the written content as an error.
640// Can be used for making a Go log.Logger for use in http.Server.ErrorLog.
641func LogWriter(log Log, level slog.Level, msg string) io.Writer {
642 return logWriter{log, level, msg}