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.
29 "github.com/prometheus/client_golang/prometheus"
30 "github.com/prometheus/client_golang/prometheus/promauto"
33var noctx = context.Background()
36 metricLogInfo = promauto.NewCounter(
37 prometheus.CounterOpts{
38 Name: "mox_logging_level_info_total",
39 Help: "Total number of logging events at level info.",
42 metricLogWarn = promauto.NewCounter(
43 prometheus.CounterOpts{
44 Name: "mox_logging_level_warn_total",
45 Help: "Total number of logging events at level warn.",
48 metricLogError = promauto.NewCounter(
49 prometheus.CounterOpts{
50 Name: "mox_logging_level_error_total",
51 Help: "Total number of logging events at level error.",
56// Logfmt enabled output in logfmt, instead of output more suitable for
57// command-line tools. Must be set early in a program lifecycle.
60// LogStringer is used when formatting field values during logging. If a value
61// implements it, LogString is called for the value to log.
62type LogStringer interface {
66var lowestLevel atomic.Int32 // For quick initial check.
67var config atomic.Pointer[map[string]slog.Level] // For secondary complete check for match.
70 SetConfig(map[string]slog.Level{"": LevelDebug})
73// SetConfig atomically sets the new log levels used by all Log instances.
74func SetConfig(c map[string]slog.Level) {
81 lowestLevel.Store(int32(lowest))
86 // When the configured log level is any of the Trace levels, all protocol messages
87 // are printed. But protocol "data" (like an email message in the SMTP DATA
88 // command) is replaced with "..." unless the configured level is LevelTracedata.
89 // Likewise, protocol messages with authentication data (e.g. plaintext base64
90 // passwords) are replaced with "***" unless the configured level is LevelTraceauth
92 LevelTracedata = slog.LevelDebug - 8
93 LevelTraceauth = slog.LevelDebug - 6
94 LevelTrace = slog.LevelDebug - 4
95 LevelDebug = slog.LevelDebug
96 LevelInfo = slog.LevelInfo
97 LevelWarn = slog.LevelWarn
98 LevelError = slog.LevelError
99 LevelFatal = slog.LevelError + 4 // Printed regardless of configured log level.
100 LevelPrint = slog.LevelError + 8 // Printed regardless of configured log level.
103// Levelstrings map log levels to human-readable names.
104var LevelStrings = map[slog.Level]string{
105 LevelTracedata: "tracedata",
106 LevelTraceauth: "traceauth",
116// Levels map the human-readable log level to a level.
117var Levels = map[string]slog.Level{
118 "tracedata": LevelTracedata,
119 "traceauth": LevelTraceauth,
129// Log wraps an slog.Logger, providing convenience functions.
134// New returns a Log that adds a "pkg" attribute. If logger is nil, a new
135// Logger is created with a custom handler.
136func New(pkg string, logger *slog.Logger) Log {
138 logger = slog.New(&handler{})
140 return Log{logger}.WithPkg(pkg)
143// WithCid adds a attribute "cid".
144// Also see WithContext.
145func (l Log) WithCid(cid int64) Log {
146 return l.With(slog.Int64("cid", cid))
151// CidKey can be used with context.WithValue to store a "cid" in a context, for logging.
152var CidKey key = "cid"
154// WithContext adds cid from context, if present. Context are often passed to
155// functions, especially between packages, to pass a "cid" for an operation. At the
156// start of a function (especially if exported) a variable "log" is often
157// instantiated from a package-level logger, with WithContext for its cid.
158// Ideally, a Log could be passed instead, but contexts are more pervasive. For the same
159// reason WithContext is more common than WithCid.
160func (l Log) WithContext(ctx context.Context) Log {
161 cidv := ctx.Value(CidKey)
166 return l.WithCid(cid)
169// With adds attributes to to each logged line.
170func (l Log) With(attrs ...slog.Attr) Log {
171 return Log{slog.New(l.Logger.Handler().WithAttrs(attrs))}
174// WithPkg ensures pkg is added as attribute to logged lines. If the handler is
175// an mlog handler, pkg is only added if not already the last added package.
176func (l Log) WithPkg(pkg string) Log {
177 h := l.Logger.Handler()
178 if ph, ok := h.(*handler); ok {
179 if len(ph.Pkgs) > 0 && ph.Pkgs[len(ph.Pkgs)-1] == pkg {
182 return Log{slog.New(ph.WithPkg(pkg))}
184 return Log{slog.New(h.WithAttrs([]slog.Attr{slog.String("pkg", pkg)}))}
187// WithFunc sets fn to be called for additional attributes. Fn is only called
188// when the line is logged.
189// If the underlying handler is not an mlog.handler, this method has no effect.
190// Caller must take care of preventing data races.
191func (l Log) WithFunc(fn func() []slog.Attr) Log {
192 h := l.Logger.Handler()
193 if ph, ok := h.(*handler); ok {
194 return Log{slog.New(ph.WithFunc(fn))}
196 // Ignored for other handlers, only used internally (smtpserver, imapserver).
200// Check logs an error if err is not nil. Intended for logging errors that are good
201// to know, but would not influence program flow.
202func (l Log) Check(err error, msg string, attrs ...slog.Attr) {
204 l.Errorx(msg, err, attrs...)
208func errAttr(err error) slog.Attr {
209 return slog.Any("err", err)
212// 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.
214func (l Log) Debug(msg string, attrs ...slog.Attr) {
215 l.Logger.LogAttrs(noctx, LevelDebug, msg, attrs...)
218func (l Log) Debugx(msg string, err error, attrs ...slog.Attr) {
220 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
222 l.Logger.LogAttrs(noctx, LevelDebug, msg, attrs...)
225func (l Log) Info(msg string, attrs ...slog.Attr) {
226 l.Logger.LogAttrs(noctx, LevelInfo, msg, attrs...)
229func (l Log) Infox(msg string, err error, attrs ...slog.Attr) {
231 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
233 l.Logger.LogAttrs(noctx, LevelInfo, msg, attrs...)
236func (l Log) Warn(msg string, attrs ...slog.Attr) {
237 l.Logger.LogAttrs(noctx, LevelWarn, msg, attrs...)
240func (l Log) Warnx(msg string, err error, attrs ...slog.Attr) {
242 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
244 l.Logger.LogAttrs(noctx, LevelWarn, msg, attrs...)
247func (l Log) Error(msg string, attrs ...slog.Attr) {
248 l.Logger.LogAttrs(noctx, LevelError, msg, attrs...)
251func (l Log) Errorx(msg string, err error, attrs ...slog.Attr) {
253 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
255 l.Logger.LogAttrs(noctx, LevelError, msg, attrs...)
258func (l Log) Fatal(msg string, attrs ...slog.Attr) {
259 l.Logger.LogAttrs(noctx, LevelFatal, msg, attrs...)
263func (l Log) Fatalx(msg string, err error, attrs ...slog.Attr) {
265 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
267 l.Logger.LogAttrs(noctx, LevelFatal, msg, attrs...)
271func (l Log) Print(msg string, attrs ...slog.Attr) {
272 l.Logger.LogAttrs(noctx, LevelPrint, msg, attrs...)
275func (l Log) Printx(msg string, err error, attrs ...slog.Attr) {
277 attrs = append([]slog.Attr{errAttr(err)}, attrs...)
279 l.Logger.LogAttrs(noctx, LevelPrint, msg, attrs...)
282// Trace logs at trace/traceauth/tracedata level.
283// If the active log level is any of the trace levels, the data is logged.
284// If level is for tracedata, but the active level doesn't trace data, data is replaced with "...".
285// If level is for traceauth, but the active level doesn't trace auth, data is replaced with "***".
286func (l Log) Trace(level slog.Level, prefix string, data []byte) {
288 if !h.Enabled(noctx, level) {
291 ph, ok := h.(*handler)
293 msg := prefix + string(data)
294 r := slog.NewRecord(time.Now(), level, msg, 0)
298 filterLevel, ok := ph.configMatch(level)
304 if hideData, hideAuth := traceLevel(filterLevel, level); hideData {
309 msg = prefix + string(data)
311 r := slog.NewRecord(time.Time{}, level, msg, 0)
312 ph.write(filterLevel, r)
315func traceLevel(level, recordLevel slog.Level) (hideData, hideAuth bool) {
316 hideData = recordLevel == LevelTracedata && level > LevelTracedata
317 hideAuth = recordLevel == LevelTraceauth && level > LevelTraceauth
324 Group string // Empty or with dot-separated names, ending with a dot.
325 Fn func() []slog.Attr // Only called when record is actually being logged.
328func match(minLevel, level slog.Level) bool {
329 return level >= LevelFatal || level >= minLevel || minLevel <= LevelTrace && level <= LevelTrace
332func (h *handler) Enabled(ctx context.Context, level slog.Level) bool {
333 return match(slog.Level(lowestLevel.Load()), level)
336func (h *handler) configMatch(level slog.Level) (slog.Level, bool) {
338 for i := len(h.Pkgs) - 1; i >= 0; i-- {
339 if l, ok := c[h.Pkgs[i]]; ok {
340 return l, match(l, level)
344 return l, match(l, level)
347func (h *handler) Handle(ctx context.Context, r slog.Record) error {
348 l, ok := h.configMatch(r.Level)
352 if r.Level >= LevelInfo {
353 if r.Level == LevelInfo {
355 } else if r.Level <= LevelWarn {
357 } else if r.Level <= LevelError {
361 if hideData, hideAuth := traceLevel(l, r.Level); hideData {
369// Reuse buffers to format log lines into.
370var logBuffersStore [32][256]byte
371var logBuffers = make(chan []byte, 200)
374 for i := range logBuffersStore {
375 logBuffers <- logBuffersStore[i][:]
379// escape logfmt string if required, otherwise return original string.
380func formatString(s string) string {
381 for _, c := range s {
382 if c <= ' ' || c == '"' || c == '\\' || c == '=' || c >= 0x7f {
383 return fmt.Sprintf("%q", s)
389func stringValue(iscid, nested bool, v any) string {
390 // Handle some common types first.
394 switch r := v.(type) {
398 return strconv.Itoa(r)
401 return fmt.Sprintf("%x", v)
403 return strconv.FormatInt(r, 10)
410 return fmt.Sprintf("%v", v)
412 return base64.RawURLEncoding.EncodeToString(r)
414 if nested && len(r) == 0 {
415 // Drop field from logging.
418 return "[" + strings.Join(r, ",") + "]"
422 return r.Format(time.RFC3339)
425 rv := reflect.ValueOf(v)
426 if rv.Kind() == reflect.Ptr && rv.IsNil() {
430 if r, ok := v.(LogStringer); ok {
433 if r, ok := v.(fmt.Stringer); ok {
437 if rv.Kind() == reflect.Ptr {
439 return stringValue(iscid, nested, rv.Interface())
441 if rv.Kind() == reflect.Slice {
443 if nested && n == 0 {
447 b := &strings.Builder{}
449 for i := 0; i < n; i++ {
453 b.WriteString(stringValue(false, true, rv.Index(i).Interface()))
457 } else if rv.Kind() != reflect.Struct {
458 return fmt.Sprintf("%v", v)
463 // We first try making a string without recursing into structs/pointers/interfaces,
464 // but will try again with those fields if we otherwise would otherwise log an
466 for j := 0; j < 2; j++ {
468 b := &strings.Builder{}
469 for i := 0; i < n; i++ {
471 if !t.Field(i).IsExported() {
474 if j == 0 && (fv.Kind() == reflect.Struct || fv.Kind() == reflect.Ptr || fv.Kind() == reflect.Interface) {
478 vs := stringValue(false, true, fv.Interface())
486 k := strings.ToLower(t.Field(i).Name)
487 b.WriteString(k + "=" + vs)
497func writeAttr(w io.Writer, separator, group string, a slog.Attr) {
498 switch a.Value.Kind() {
504 for _, a := range a.Value.Group() {
505 writeAttr(w, separator, group, a)
510 if a.Value.Kind() == slog.KindLogValuer {
511 vv = a.Value.Resolve().Any()
515 s := stringValue(a.Key == "cid", false, vv)
516 fmt.Fprint(w, separator, group, a.Key, "=", formatString(s))
520func (h *handler) write(l slog.Level, r slog.Record) error {
521 // Reuse a buffer, or temporarily allocate a new one.
524 case buf = <-logBuffers:
529 buf = make([]byte, 128)
532 b := bytes.NewBuffer(buf[:0])
533 eb := &errWriter{b, nil}
537 ensurePkgs := func() {
540 for _, pkg := range h.Pkgs {
541 writeAttr(eb, " ", "", slog.String("pkg", pkg))
546 fmt.Fprint(eb, "l=", LevelStrings[r.Level], " m=")
547 fmt.Fprintf(eb, "%q", r.Message)
549 r.Attrs(func(a slog.Attr) bool {
550 if n > 0 || a.Key != "err" || h.Group != "" {
553 writeAttr(eb, " ", h.Group, a)
558 for _, a := range h.Attrs {
559 writeAttr(eb, " ", h.Group, a)
562 for _, a := range h.Fn() {
563 writeAttr(eb, " ", h.Group, a)
569 ensurePkgs := func() {
572 for _, pkg := range h.Pkgs {
573 writeAttr(eb, "; ", "", slog.String("pkg", pkg))
578 fmt.Fprint(eb, LevelStrings[r.Level], ": ", r.Message)
580 r.Attrs(func(a slog.Attr) bool {
581 if n == 0 && a.Key == "err" && h.Group == "" {
582 fmt.Fprint(eb, ": ", a.Value.String())
586 writeAttr(eb, "; ", h.Group, a)
592 for _, a := range h.Attrs {
593 writeAttr(eb, "; ", h.Group, a)
596 for _, a := range h.Fn() {
597 writeAttr(eb, "; ", h.Group, a)
607 // todo: for mox serve, do writes in separate goroutine.
608 _, err := os.Stderr.Write(b.Bytes())
612type errWriter struct {
617func (w *errWriter) Write(buf []byte) (int, error) {
622 n, w.Err = w.Writer.Write(buf)
626func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler {
629 nh.Attrs = append([]slog.Attr{}, h.Attrs...)
631 nh.Attrs = append(nh.Attrs, attrs...)
635func (h *handler) WithGroup(name string) slog.Handler {
640 nh.Group += name + "."
644func (h *handler) WithPkg(pkg string) *handler {
647 nh.Pkgs = append([]string{}, nh.Pkgs...)
649 nh.Pkgs = append(nh.Pkgs, pkg)
653func (h *handler) WithFunc(fn func() []slog.Attr) *handler {
659type logWriter struct {
665func (w logWriter) Write(buf []byte) (int, error) {
666 err := strings.TrimSpace(string(buf))
667 w.log.LogAttrs(noctx, w.level, w.msg, slog.String("err", err))
671// LogWriter returns a writer that turns each write into a logging call on "log"
672// with given "level" and "msg" and the written content as an error.
673// Can be used for making a Go log.Logger for use in http.Server.ErrorLog.
674func LogWriter(log Log, level slog.Level, msg string) io.Writer {
675 return logWriter{log, level, msg}