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.
30 "github.com/prometheus/client_golang/prometheus"
31 "github.com/prometheus/client_golang/prometheus/promauto"
35var noctx = context.Background()
38 metricLogInfo = promauto.NewCounter(
39 prometheus.CounterOpts{
40 Name: "mox_logging_level_info_total",
41 Help: "Total number of logging events at level info.",
44 metricLogWarn = promauto.NewCounter(
45 prometheus.CounterOpts{
46 Name: "mox_logging_level_warn_total",
47 Help: "Total number of logging events at level warn.",
50 metricLogError = promauto.NewCounter(
51 prometheus.CounterOpts{
52 Name: "mox_logging_level_error_total",
53 Help: "Total number of logging events at level error.",
58// Logfmt enabled output in logfmt, instead of output more suitable for
59// command-line tools. Must be set early in a program lifecycle.
62// LogStringer is used when formatting field values during logging. If a value
63// implements it, LogString is called for the value to log.
64type LogStringer interface {
68var lowestLevel atomic.Int32 // For quick initial check.
69var config atomic.Pointer[map[string]slog.Level] // For secondary complete check for match.
72 SetConfig(map[string]slog.Level{"": LevelDebug})
75// SetConfig atomically sets the new log levels used by all Log instances.
76func SetConfig(c map[string]slog.Level) {
83 lowestLevel.Store(int32(lowest))
88 // When the configured log level is any of the Trace levels, all protocol messages
89 // are printed. But protocol "data" (like an email message in the SMTP DATA
90 // command) is replaced with "..." unless the configured level is LevelTracedata.
91 // Likewise, protocol messages with authentication data (e.g. plaintext base64
92 // passwords) are replaced with "***" unless the configured level is LevelTraceauth
94 LevelTracedata = slog.LevelDebug - 8
95 LevelTraceauth = slog.LevelDebug - 6
96 LevelTrace = slog.LevelDebug - 4
97 LevelDebug = slog.LevelDebug
98 LevelInfo = slog.LevelInfo
99 LevelWarn = slog.LevelWarn
100 LevelError = slog.LevelError
101 LevelFatal = slog.LevelError + 4 // Printed regardless of configured log level.
102 LevelPrint = slog.LevelError + 8 // Printed regardless of configured log level.
105// Levelstrings map log levels to human-readable names.
106var LevelStrings = map[slog.Level]string{
107 LevelTracedata: "tracedata",
108 LevelTraceauth: "traceauth",
118// Levels map the human-readable log level to a level.
119var Levels = map[string]slog.Level{
120 "tracedata": LevelTracedata,
121 "traceauth": LevelTraceauth,
131// Log wraps an slog.Logger, providing convenience functions.
136// New returns a Log that adds a "pkg" attribute. If logger is nil, a new
137// Logger is created with a custom handler.
138func New(pkg string, logger *slog.Logger) Log {
140 logger = slog.New(&handler{})
142 return Log{logger}.WithPkg(pkg)
145// WithCid adds a attribute "cid".
146// Also see WithContext.
147func (l Log) WithCid(cid int64) Log {
148 return l.With(slog.Int64("cid", cid))
153// CidKey can be used with context.WithValue to store a "cid" in a context, for logging.
154var CidKey key = "cid"
156// WithContext adds cid from context, if present. Context are often passed to
157// functions, especially between packages, to pass a "cid" for an operation. At the
158// start of a function (especially if exported) a variable "log" is often
159// instantiated from a package-level logger, with WithContext for its cid.
160// Ideally, a Log could be passed instead, but contexts are more pervasive. For the same
161// reason WithContext is more common than WithCid.
162func (l Log) WithContext(ctx context.Context) Log {
163 cidv := ctx.Value(CidKey)
168 return l.WithCid(cid)
171// With adds attributes to to each logged line.
172func (l Log) With(attrs ...slog.Attr) Log {
173 return Log{slog.New(l.Logger.Handler().WithAttrs(attrs))}
176// WithPkg ensures pkg is added as attribute to logged lines. If the handler is
177// an mlog handler, pkg is only added if not already the last added package.
178func (l Log) WithPkg(pkg string) Log {
179 h := l.Logger.Handler()
180 if ph, ok := h.(*handler); ok {
181 if len(ph.Pkgs) > 0 && ph.Pkgs[len(ph.Pkgs)-1] == pkg {
184 return Log{slog.New(ph.WithPkg(pkg))}
186 return Log{slog.New(h.WithAttrs([]slog.Attr{slog.String("pkg", pkg)}))}
189// WithFunc sets fn to be called for additional attributes. Fn is only called
190// when the line is logged.
191// If the underlying handler is not an mlog.handler, this method has no effect.
192// Caller must take care of preventing data races.
193func (l Log) WithFunc(fn func() []slog.Attr) Log {
194 h := l.Logger.Handler()
195 if ph, ok := h.(*handler); ok {
196 return Log{slog.New(ph.WithFunc(fn))}
198 // Ignored for other handlers, only used internally (smtpserver, imapserver).
202// Check logs an error if err is not nil. Intended for logging errors that are good
203// to know, but would not influence program flow. Context deadline/cancelation
204// errors and timeouts are logged at "info" level. Closed remote connections are
205// logged at "debug" level. Other errors at "error" level.
206func (l Log) Check(err error, msg string, attrs ...slog.Attr) {
211 if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) || errors.Is(err, os.ErrDeadlineExceeded) {
213 } else if IsClosed(err) {
216 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
217 l.Logger.LogAttrs(noctx, level, msg, attrs...)
220func errAttr(err error) slog.Attr {
221 return slog.Any("err", err)
224// 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.
226func (l Log) Debug(msg string, attrs ...slog.Attr) {
227 l.Logger.LogAttrs(noctx, LevelDebug, msg, attrs...)
230func (l Log) Debugx(msg string, err error, attrs ...slog.Attr) {
232 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
234 l.Logger.LogAttrs(noctx, LevelDebug, msg, attrs...)
237func (l Log) Info(msg string, attrs ...slog.Attr) {
238 l.Logger.LogAttrs(noctx, LevelInfo, msg, attrs...)
241func (l Log) Infox(msg string, err error, attrs ...slog.Attr) {
243 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
245 l.Logger.LogAttrs(noctx, LevelInfo, msg, attrs...)
248func (l Log) Warn(msg string, attrs ...slog.Attr) {
249 l.Logger.LogAttrs(noctx, LevelWarn, msg, attrs...)
252func (l Log) Warnx(msg string, err error, attrs ...slog.Attr) {
254 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
256 l.Logger.LogAttrs(noctx, LevelWarn, msg, attrs...)
259func (l Log) Error(msg string, attrs ...slog.Attr) {
260 l.Logger.LogAttrs(noctx, LevelError, msg, attrs...)
263func (l Log) Errorx(msg string, err error, attrs ...slog.Attr) {
265 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
267 l.Logger.LogAttrs(noctx, LevelError, msg, attrs...)
270func (l Log) Fatal(msg string, attrs ...slog.Attr) {
271 l.Logger.LogAttrs(noctx, LevelFatal, msg, attrs...)
275func (l Log) Fatalx(msg string, err error, attrs ...slog.Attr) {
277 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
279 l.Logger.LogAttrs(noctx, LevelFatal, msg, attrs...)
283func (l Log) Print(msg string, attrs ...slog.Attr) {
284 l.Logger.LogAttrs(noctx, LevelPrint, msg, attrs...)
287func (l Log) Printx(msg string, err error, attrs ...slog.Attr) {
289 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
291 l.Logger.LogAttrs(noctx, LevelPrint, msg, attrs...)
294// Trace logs at trace/traceauth/tracedata level.
295// If the active log level is any of the trace levels, the data is logged.
296// If level is for tracedata, but the active level doesn't trace data, data is replaced with "...".
297// If level is for traceauth, but the active level doesn't trace auth, data is replaced with "***".
298func (l Log) Trace(level slog.Level, prefix string, data []byte) {
300 if !h.Enabled(noctx, level) {
303 ph, ok := h.(*handler)
305 msg := prefix + string(data)
306 r := slog.NewRecord(time.Now(), level, msg, 0)
310 filterLevel, ok := ph.configMatch(level)
317 if hideData, hideAuth := traceLevel(filterLevel, level); hideData {
323 msg = prefix + string(data)
326 r := slog.NewRecord(time.Time{}, level, msg, 0)
328 r.AddAttrs(slog.Int("size", size))
330 ph.write(filterLevel, r)
333func traceLevel(level, recordLevel slog.Level) (hideData, hideAuth bool) {
334 hideData = recordLevel == LevelTracedata && level > LevelTracedata
335 hideAuth = recordLevel == LevelTraceauth && level > LevelTraceauth
342 Group string // Empty or with dot-separated names, ending with a dot.
343 Fn func() []slog.Attr // Only called when record is actually being logged.
346func match(minLevel, level slog.Level) bool {
347 return level >= LevelFatal || level >= minLevel || minLevel <= LevelTrace && level <= LevelTrace
350func (h *handler) Enabled(ctx context.Context, level slog.Level) bool {
351 return match(slog.Level(lowestLevel.Load()), level)
354func (h *handler) configMatch(level slog.Level) (slog.Level, bool) {
356 for i := len(h.Pkgs) - 1; i >= 0; i-- {
357 if l, ok := c[h.Pkgs[i]]; ok {
358 return l, match(l, level)
362 return l, match(l, level)
365func (h *handler) Handle(ctx context.Context, r slog.Record) error {
366 l, ok := h.configMatch(r.Level)
370 if r.Level >= LevelInfo {
371 if r.Level == LevelInfo {
373 } else if r.Level <= LevelWarn {
375 } else if r.Level <= LevelError {
379 if hideData, hideAuth := traceLevel(l, r.Level); hideData {
387// Reuse buffers to format log lines into.
388var logBuffersStore [32][256]byte
389var logBuffers = make(chan []byte, 200)
392 for i := range logBuffersStore {
393 logBuffers <- logBuffersStore[i][:]
397// escape logfmt string if required, otherwise return original string.
398func formatString(s string) string {
399 for _, c := range s {
400 if c <= ' ' || c == '"' || c == '\\' || c == '=' || c >= 0x7f {
401 return fmt.Sprintf("%q", s)
407var typeTime = reflect.TypeFor[time.Time]()
409func stringValue(iscid, nested bool, v any) string {
410 // Handle some common types first.
414 switch r := v.(type) {
418 return strconv.Itoa(r)
421 return fmt.Sprintf("%x", v)
423 return strconv.FormatInt(r, 10)
430 return fmt.Sprintf("%v", v)
432 return base64.RawURLEncoding.EncodeToString(r)
434 if nested && len(r) == 0 {
435 // Drop field from logging.
438 return "[" + strings.Join(r, ",") + "]"
442 return r.Format(time.RFC3339)
445 rv := reflect.ValueOf(v)
446 if rv.Kind() == reflect.Ptr && rv.IsNil() {
450 if r, ok := v.(LogStringer); ok {
453 if r, ok := v.(fmt.Stringer); ok {
457 if rv.Kind() == reflect.Ptr {
459 return stringValue(iscid, nested, rv.Interface())
461 if rv.Kind() == reflect.Slice {
463 if nested && n == 0 {
467 b := &strings.Builder{}
473 b.WriteString(stringValue(false, true, rv.Index(i).Interface()))
477 } else if rv.Kind() != reflect.Struct {
478 return fmt.Sprintf("%v", v)
483 // We first try making a string without recursing into structs/pointers/interfaces,
484 // but will try again with those fields if we otherwise would otherwise log an
488 b := &strings.Builder{}
491 if !t.Field(i).IsExported() {
494 // todo: decide on better approach about which fields to include while preventing recursion
495 if j == 0 && (fv.Kind() == reflect.Struct || fv.Kind() == reflect.Ptr || fv.Kind() == reflect.Interface) && fv.Type() != typeTime {
499 vs := stringValue(false, true, fv.Interface())
507 k := strings.ToLower(t.Field(i).Name)
508 b.WriteString(k + "=" + vs)
518func writeAttr(w io.Writer, separator, group string, a slog.Attr) {
519 switch a.Value.Kind() {
525 for _, a := range a.Value.Group() {
526 writeAttr(w, separator, group, a)
531 if a.Value.Kind() == slog.KindLogValuer {
532 vv = a.Value.Resolve().Any()
536 s := stringValue(a.Key == "cid", false, vv)
537 fmt.Fprint(w, separator, group, a.Key, "=", formatString(s))
541func (h *handler) write(l slog.Level, r slog.Record) error {
542 // Reuse a buffer, or temporarily allocate a new one.
545 case buf = <-logBuffers:
550 buf = make([]byte, 128)
553 b := bytes.NewBuffer(buf[:0])
554 eb := &errWriter{b, nil}
558 ensurePkgs := func() {
561 for _, pkg := range h.Pkgs {
562 writeAttr(eb, " ", "", slog.String("pkg", pkg))
567 fmt.Fprint(eb, "l=", LevelStrings[r.Level], " m=")
568 fmt.Fprintf(eb, "%q", r.Message)
570 r.Attrs(func(a slog.Attr) bool {
571 if n > 0 || a.Key != "err" || h.Group != "" {
574 writeAttr(eb, " ", h.Group, a)
579 for _, a := range h.Attrs {
580 writeAttr(eb, " ", h.Group, a)
583 for _, a := range h.Fn() {
584 writeAttr(eb, " ", h.Group, a)
590 ensurePkgs := func() {
593 for _, pkg := range h.Pkgs {
594 writeAttr(eb, "; ", "", slog.String("pkg", pkg))
600 if r.Level <= LevelTrace {
601 msg = fmt.Sprintf("%q", msg)
603 fmt.Fprint(eb, LevelStrings[r.Level], ": ", msg)
605 r.Attrs(func(a slog.Attr) bool {
606 if n == 0 && a.Key == "err" && h.Group == "" {
607 fmt.Fprint(eb, ": ", a.Value.String())
611 writeAttr(eb, "; ", h.Group, a)
617 for _, a := range h.Attrs {
618 writeAttr(eb, "; ", h.Group, a)
621 for _, a := range h.Fn() {
622 writeAttr(eb, "; ", h.Group, a)
632 // todo: for mox serve, do writes in separate goroutine.
633 _, err := os.Stderr.Write(b.Bytes())
637type errWriter struct {
642func (w *errWriter) Write(buf []byte) (int, error) {
647 n, w.Err = w.Writer.Write(buf)
651func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler {
654 nh.Attrs = slices.Clone(h.Attrs)
656 nh.Attrs = append(nh.Attrs, attrs...)
660func (h *handler) WithGroup(name string) slog.Handler {
665 nh.Group += name + "."
669func (h *handler) WithPkg(pkg string) *handler {
672 nh.Pkgs = slices.Clone(nh.Pkgs)
674 nh.Pkgs = append(nh.Pkgs, pkg)
678func (h *handler) WithFunc(fn func() []slog.Attr) *handler {
684type logWriter struct {
690func (w logWriter) Write(buf []byte) (int, error) {
691 err := strings.TrimSpace(string(buf))
692 w.log.LogAttrs(noctx, w.level, w.msg, slog.String("err", err))
696// LogWriter returns a writer that turns each write into a logging call on "log"
697// with given "level" and "msg" and the written content as an error.
698// Can be used for making a Go log.Logger for use in http.Server.ErrorLog.
699func LogWriter(log Log, level slog.Level, msg string) io.Writer {
700 return logWriter{log, level, msg}