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)
429 b := &strings.Builder{}
431 for i := 0; i < n; i++ {
433 if !t.Field(i).IsExported() {
436 if fv.Kind() == reflect.Struct || fv.Kind() == reflect.Ptr || fv.Kind() == reflect.Interface {
440 vs := stringValue(false, true, fv.Interface())
448 k := strings.ToLower(t.Field(i).Name)
449 b.WriteString(k + "=" + vs)
454func writeAttr(w io.Writer, separator, group string, a slog.Attr) {
455 switch a.Value.Kind() {
461 for _, a := range a.Value.Group() {
462 writeAttr(w, separator, group, a)
467 if a.Value.Kind() == slog.KindLogValuer {
468 vv = a.Value.Resolve().Any()
472 s := stringValue(a.Key == "cid", false, vv)
473 fmt.Fprint(w, separator, group, a.Key, "=", formatString(s))
477func (h *handler) write(l slog.Level, r slog.Record) error {
478 // Reuse a buffer, or temporarily allocate a new one.
481 case buf = <-logBuffers:
486 buf = make([]byte, 128)
489 b := bytes.NewBuffer(buf[:0])
490 eb := &errWriter{b, nil}
494 ensurePkgs := func() {
497 for _, pkg := range h.Pkgs {
498 writeAttr(eb, " ", "", slog.String("pkg", pkg))
503 fmt.Fprint(eb, "l=", LevelStrings[r.Level], " m=")
504 fmt.Fprintf(eb, "%q", r.Message)
506 r.Attrs(func(a slog.Attr) bool {
507 if n > 0 || a.Key != "err" || h.Group != "" {
510 writeAttr(eb, " ", h.Group, a)
515 for _, a := range h.Attrs {
516 writeAttr(eb, " ", h.Group, a)
519 for _, a := range h.Fn() {
520 writeAttr(eb, " ", h.Group, a)
526 ensurePkgs := func() {
529 for _, pkg := range h.Pkgs {
530 writeAttr(eb, "; ", "", slog.String("pkg", pkg))
535 fmt.Fprint(eb, LevelStrings[r.Level], ": ", r.Message)
537 r.Attrs(func(a slog.Attr) bool {
538 if n == 0 && a.Key == "err" && h.Group == "" {
539 fmt.Fprint(eb, ": ", a.Value.String())
543 writeAttr(eb, "; ", h.Group, a)
549 for _, a := range h.Attrs {
550 writeAttr(eb, "; ", h.Group, a)
553 for _, a := range h.Fn() {
554 writeAttr(eb, "; ", h.Group, a)
564 // todo: for mox serve, do writes in separate goroutine.
565 _, err := os.Stderr.Write(b.Bytes())
569type errWriter struct {
574func (w *errWriter) Write(buf []byte) (int, error) {
579 n, w.Err = w.Writer.Write(buf)
583func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler {
586 nh.Attrs = append([]slog.Attr{}, h.Attrs...)
588 nh.Attrs = append(nh.Attrs, attrs...)
592func (h *handler) WithGroup(name string) slog.Handler {
597 nh.Group += name + "."
601func (h *handler) WithPkg(pkg string) *handler {
604 nh.Pkgs = append([]string{}, nh.Pkgs...)
606 nh.Pkgs = append(nh.Pkgs, pkg)
610func (h *handler) WithFunc(fn func() []slog.Attr) *handler {
616type logWriter struct {
622func (w logWriter) Write(buf []byte) (int, error) {
623 err := strings.TrimSpace(string(buf))
624 w.log.LogAttrs(noctx, w.level, w.msg, slog.String("err", err))
628// LogWriter returns a writer that turns each write into a logging call on "log"
629// with given "level" and "msg" and the written content as an error.
630// Can be used for making a Go log.Logger for use in http.Server.ErrorLog.
631func LogWriter(log Log, level slog.Level, msg string) io.Writer {
632 return logWriter{log, level, msg}