github.com/richardwilkes/toolbox@v1.121.0/log/tracelog/tracelog.go (about)

     1  // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the Mozilla Public
     4  // License, version 2.0. If a copy of the MPL was not distributed with
     5  // this file, You can obtain one at http://mozilla.org/MPL/2.0/.
     6  //
     7  // This Source Code Form is "Incompatible With Secondary Licenses", as
     8  // defined by the Mozilla Public License, version 2.0.
     9  
    10  package tracelog
    11  
    12  import (
    13  	"bytes"
    14  	"context"
    15  	"fmt"
    16  	"io"
    17  	"log/slog"
    18  	"os"
    19  	"sync"
    20  	"time"
    21  
    22  	"github.com/richardwilkes/toolbox"
    23  	"github.com/richardwilkes/toolbox/errs"
    24  )
    25  
    26  var _ slog.Handler = &Handler{}
    27  
    28  // Config is used to configure a Handler.
    29  type Config struct {
    30  	// Level is the minimum log level that will be emitted. Defaults to slog.LevelInfo if not set.
    31  	Level slog.Leveler
    32  	// LevelNames is an optional map of slog.Level to string that will be used to format the log level. If not set, the
    33  	// default slog level names will be used.
    34  	LevelNames map[slog.Level]string
    35  	// Sink is the io.Writer that will receive the formatted log output. Defaults to os.Stderr if not set.
    36  	Sink io.Writer
    37  	// BufferDepth greater than 0 will enable asynchronous delivery of log messages to the sink. When enabled, if there
    38  	// is no room remaining in the buffer, the message will be discarded rather than waiting for room to become
    39  	// available. Defaults to 0 for synchronous delivery.
    40  	BufferDepth int
    41  }
    42  
    43  // Normalize ensures that the Config is valid.
    44  func (c *Config) Normalize() {
    45  	if toolbox.IsNil(c.Level) {
    46  		c.Level = slog.LevelInfo
    47  	}
    48  	if c.Sink == nil {
    49  		c.Sink = os.Stderr
    50  	}
    51  	if c.BufferDepth < 0 {
    52  		c.BufferDepth = 0
    53  	}
    54  }
    55  
    56  // Handler provides a formatted text output that may include a stack trace on separate lines. The stack trace is
    57  // formatted such that most IDEs will auto-generate links for it within their consoles. Note that this slog.Handler is
    58  // not optimized for performance, as I expect those that need to run this is environments where that matters will use
    59  // one of the implementations provided by slog itself.
    60  type Handler struct {
    61  	level      slog.Leveler
    62  	levelNames map[slog.Level]string
    63  	delivery   chan []byte
    64  	lock       *sync.Mutex
    65  	sink       io.Writer
    66  	list       []entry
    67  }
    68  
    69  type entry struct {
    70  	group string
    71  	attrs []slog.Attr
    72  }
    73  
    74  // New creates a new Handler. May pass nil for cfg to use the defaults.
    75  func New(cfg *Config) *Handler {
    76  	if cfg == nil {
    77  		cfg = &Config{}
    78  	}
    79  	cfg.Normalize()
    80  	h := Handler{
    81  		level:      cfg.Level,
    82  		levelNames: cfg.LevelNames,
    83  		lock:       &sync.Mutex{},
    84  		sink:       cfg.Sink,
    85  	}
    86  	if cfg.BufferDepth > 0 {
    87  		h.delivery = make(chan []byte, cfg.BufferDepth)
    88  		go h.backgroundDelivery()
    89  	}
    90  	return &h
    91  }
    92  
    93  // Enabled implements slog.Handler.
    94  func (h *Handler) Enabled(_ context.Context, level slog.Level) bool {
    95  	return level >= h.level.Level()
    96  }
    97  
    98  // WithGroup implements slog.Handler.
    99  func (h *Handler) WithGroup(name string) slog.Handler {
   100  	if name == "" {
   101  		return h
   102  	}
   103  	return h.withGroupOrAttrs(entry{group: name})
   104  }
   105  
   106  // WithAttrs implements slog.Handler.
   107  func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
   108  	if len(attrs) == 0 {
   109  		return h
   110  	}
   111  	return h.withGroupOrAttrs(entry{attrs: attrs})
   112  }
   113  
   114  func (h *Handler) withGroupOrAttrs(ga entry) *Handler {
   115  	other := *h
   116  	other.list = make([]entry, len(h.list)+1)
   117  	copy(other.list, h.list)
   118  	other.list[len(other.list)-1] = ga
   119  	return &other
   120  }
   121  
   122  // Handle implements slog.Handler.
   123  func (h *Handler) Handle(_ context.Context, r slog.Record) error { //nolint:gocritic // Must use defined API
   124  	var buffer bytes.Buffer
   125  	if name, ok := h.levelNames[r.Level]; ok {
   126  		buffer.WriteString(name)
   127  	} else {
   128  		switch r.Level {
   129  		case slog.LevelDebug:
   130  			buffer.WriteString("DBG")
   131  		case slog.LevelInfo:
   132  			buffer.WriteString("INF")
   133  		case slog.LevelWarn:
   134  			buffer.WriteString("WRN")
   135  		case slog.LevelError:
   136  			buffer.WriteString("ERR")
   137  		default:
   138  			fmt.Fprintf(&buffer, "%3d", r.Level)
   139  		}
   140  	}
   141  	buffer.WriteString(r.Time.Round(0).Format(" | 2006-01-02 | 15:04:05.000 | "))
   142  	buffer.WriteString(r.Message)
   143  
   144  	s := &state{buffer: &buffer, needBar: true}
   145  	for _, ga := range h.list {
   146  		s.append(ga)
   147  	}
   148  	r.Attrs(func(attr slog.Attr) bool {
   149  		s.appendAttr(attr)
   150  		return true
   151  	})
   152  	buffer.WriteByte('\n')
   153  	if s.stackErr != nil {
   154  		buffer.WriteString(s.stackErr.StackTrace(true))
   155  		buffer.WriteByte('\n')
   156  	}
   157  	if h.delivery != nil {
   158  		select {
   159  		case h.delivery <- buffer.Bytes():
   160  		default:
   161  		}
   162  		return nil
   163  	}
   164  	h.lock.Lock()
   165  	defer h.lock.Unlock()
   166  	_, err := h.sink.Write(buffer.Bytes())
   167  	return err
   168  }
   169  
   170  func (h *Handler) backgroundDelivery() {
   171  	for data := range h.delivery {
   172  		_, _ = h.sink.Write(data) //nolint:errcheck // We don't care about errors here
   173  	}
   174  }
   175  
   176  type state struct {
   177  	buffer   *bytes.Buffer
   178  	stackErr errs.StackError
   179  	group    string
   180  	needBar  bool
   181  }
   182  
   183  func (s *state) append(ga entry) {
   184  	if ga.group != "" {
   185  		s.addGroup(ga.group)
   186  		return
   187  	}
   188  	for _, attr := range ga.attrs {
   189  		s.appendAttr(attr)
   190  	}
   191  }
   192  
   193  func (s *state) appendAttr(attr slog.Attr) {
   194  	if s.group == "" && attr.Key == errs.StackTraceKey {
   195  		if embedded, ok := attr.Value.Any().(interface{ StackError() errs.StackError }); ok {
   196  			s.stackErr = embedded.StackError()
   197  			return
   198  		}
   199  	}
   200  	attr.Value = attr.Value.Resolve()
   201  	if !attr.Equal(slog.Attr{}) {
   202  		switch attr.Value.Kind() {
   203  		case slog.KindString:
   204  			s.addBarIfNeeded()
   205  			s.writeGroupAndKey(attr.Key)
   206  			_, _ = fmt.Fprintf(s.buffer, "%q", attr.Value.String())
   207  		case slog.KindTime:
   208  			s.addBarIfNeeded()
   209  			s.writeGroupAndKey(attr.Key)
   210  			_, _ = s.buffer.WriteString(attr.Value.Time().Format(time.RFC3339Nano))
   211  		case slog.KindGroup:
   212  			attrs := attr.Value.Group()
   213  			if len(attrs) == 0 {
   214  				return
   215  			}
   216  			s.addBarIfNeeded()
   217  			savedPrefix := s.group
   218  			s.addGroup(attr.Key)
   219  			for _, attr = range attrs {
   220  				s.appendAttr(attr)
   221  			}
   222  			s.group = savedPrefix
   223  		default:
   224  			s.addBarIfNeeded()
   225  			s.writeGroupAndKey(attr.Key)
   226  			_, _ = s.buffer.WriteString(attr.Value.String())
   227  		}
   228  	}
   229  }
   230  
   231  func (s *state) writeGroupAndKey(key string) {
   232  	_ = s.buffer.WriteByte(' ')
   233  	_, _ = s.buffer.WriteString(s.group)
   234  	_, _ = s.buffer.WriteString(key)
   235  	_ = s.buffer.WriteByte('=')
   236  }
   237  
   238  func (s *state) addGroup(group string) {
   239  	group += "."
   240  	if s.group == "" {
   241  		s.group = group
   242  	} else {
   243  		s.group += group
   244  	}
   245  }
   246  
   247  func (s *state) addBarIfNeeded() {
   248  	if s.needBar {
   249  		s.buffer.WriteString(" |")
   250  		s.needBar = false
   251  	}
   252  }