github.com/searKing/golang/go@v1.2.117/log/slog/handler.go (about)

     1  // Copyright 2023 The searKing Author. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package slog
     6  
     7  import (
     8  	"io"
     9  	"log/slog"
    10  	"os"
    11  	"runtime"
    12  	"slices"
    13  	"sync"
    14  	"time"
    15  	"unicode/utf8"
    16  
    17  	"github.com/searKing/golang/go/log/slog/internal/buffer"
    18  	"golang.org/x/term"
    19  )
    20  
    21  var (
    22  	timeNow       = time.Now // Stubbed out for testing.
    23  	baseTimestamp time.Time
    24  	getPid        = os.Getpid // Stubbed out for testing.
    25  )
    26  
    27  func init() {
    28  	baseTimestamp = timeNow()
    29  }
    30  
    31  // Keys for "built-in" attributes.
    32  const (
    33  	// ErrorKey is the key used by the handlers for the error
    34  	// when the log method is called. The associated Value is an [error].
    35  	ErrorKey = "error"
    36  )
    37  
    38  type TimestampMode int
    39  
    40  const (
    41  	_ TimestampMode = iota
    42  
    43  	// DisableTimestamp disable timestamp logging. useful when output is redirected to logging
    44  	// system that already adds timestamps.
    45  	DisableTimestamp
    46  
    47  	// SinceStartTimestamp enable the time passed since beginning of execution instead of
    48  	// logging the full timestamp when a TTY is attached.
    49  	SinceStartTimestamp
    50  )
    51  
    52  // sharedVar shared const expvar among handler and children handler...
    53  type sharedVar struct {
    54  	once *sync.Once
    55  
    56  	// Whether the logger's out is to a terminal
    57  	isTerminal bool
    58  	// The max length of the level text, generated dynamically on init
    59  	maxLevelText int
    60  	// The process id of the caller.
    61  	pid int
    62  }
    63  
    64  func (h *sharedVar) init(w io.Writer) {
    65  	h.once.Do(func() {
    66  		if f, ok := w.(*os.File); ok {
    67  			h.isTerminal = term.IsTerminal(int(f.Fd()))
    68  		}
    69  		// Get the max length of the level text
    70  		for _, level := range []slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError} {
    71  			levelTextLength := utf8.RuneCount([]byte(level.String()))
    72  			if levelTextLength > h.maxLevelText {
    73  				h.maxLevelText = levelTextLength
    74  			}
    75  		}
    76  		h.pid = getPid()
    77  	})
    78  }
    79  
    80  type commonHandler struct {
    81  	// replace level.String()
    82  	ReplaceLevelString func(l slog.Level) string
    83  
    84  	// the separator between attributes.
    85  	AttrSep string
    86  
    87  	// Set to true to bypass checking for a TTY before outputting colors.
    88  	ForceColors bool
    89  
    90  	// Force disabling colors.
    91  	DisableColors bool
    92  
    93  	// Force quoting of all values
    94  	ForceQuote bool
    95  
    96  	// DisableQuote disables quoting for all values.
    97  	// DisableQuote will have a lower priority than ForceQuote.
    98  	// If both of them are set to true, quote will be forced on all values.
    99  	DisableQuote bool
   100  
   101  	// ForceGoroutineId enables goroutine id instead of pid.
   102  	ForceGoroutineId bool
   103  
   104  	TimestampMode TimestampMode
   105  
   106  	// TimestampFormat to use for display when a full timestamp is printed
   107  	TimestampFormat string
   108  
   109  	// Disables the glog style :[IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg msg...
   110  	// replace with :[IWEF] [yyyymmdd] [hh:mm:ss.uuuuuu] [threadid] [file:line] msg msg...
   111  	HumanReadable bool
   112  
   113  	// PadLevelText Adds padding the level text so that all the levels output at the same length
   114  	// PadLevelText is a superset of the DisableLevelTruncation option
   115  	PadLevelText bool
   116  
   117  	// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
   118  	EnvironmentOverrideColors bool
   119  
   120  	// SourcePrettier can be set by the user to modify the content
   121  	// of the file, function and file keys when AddSource is
   122  	// activated. If any of the returned value is the empty string the
   123  	// corresponding key will be removed from slog attrs.
   124  	SourcePrettier func(r slog.Record) *slog.Source
   125  
   126  	// WithFuncName append Caller's func name
   127  	WithFuncName bool
   128  
   129  	opts slog.HandlerOptions
   130  
   131  	sharedVar *sharedVar
   132  
   133  	preformattedAttrs []byte
   134  	// groupPrefix is for the text handler only.
   135  	// It holds the prefix for groups that were already pre-formatted.
   136  	// A group will appear here when a call to WithGroup is followed by
   137  	// a call to WithAttrs.
   138  	groupPrefix string
   139  	groups      []string    // all groups started from WithGroup
   140  	nOpenGroups int         // the number of groups opened in preformattedAttrs
   141  	mu          *sync.Mutex // mutex shared among all clones of this handler
   142  	w           io.Writer
   143  }
   144  
   145  // NewCommonHandler creates a CommonHandler that writes to w,
   146  // using the given options.
   147  // If opts is nil, the default options are used.
   148  // A [CommonHandler] is a low-level primitive for making structured log.
   149  // [NewGlogHandler] or [NewGlogHumanHandler] recommended.
   150  func NewCommonHandler(w io.Writer, opts *slog.HandlerOptions) *commonHandler {
   151  	if opts == nil {
   152  		opts = &slog.HandlerOptions{}
   153  	}
   154  	return &commonHandler{
   155  		opts:      *opts,
   156  		sharedVar: &sharedVar{once: &sync.Once{}},
   157  		mu:        &sync.Mutex{},
   158  		w:         w,
   159  	}
   160  }
   161  
   162  func (h *commonHandler) clone() *commonHandler {
   163  	// We can't use assignment because we can't copy the mutex.
   164  	h2 := *h
   165  	h2.preformattedAttrs = slices.Clip(h.preformattedAttrs)
   166  	h2.groups = slices.Clip(h.groups)
   167  	return &h2
   168  }
   169  
   170  // enabled reports whether l is greater than or equal to the
   171  // minimum level.
   172  func (h *commonHandler) enabled(l slog.Level) bool {
   173  	minLevel := slog.LevelInfo
   174  	if h.opts.Level != nil {
   175  		minLevel = h.opts.Level.Level()
   176  	}
   177  	return l >= minLevel
   178  }
   179  
   180  func (h *commonHandler) withAttrs(as []slog.Attr) *commonHandler {
   181  	// We are going to ignore empty groups, so if the entire slice consists of
   182  	// them, there is nothing to do.
   183  	if countEmptyGroups(as) == len(as) {
   184  		return h
   185  	}
   186  	h2 := h.clone()
   187  	// Pre-format the attributes as an optimization.
   188  	buf := buffer.New()
   189  	buf.Write(h2.preformattedAttrs)
   190  	state := h2.newHandleState(buf, false, "")
   191  	defer state.free()
   192  	defer func() {
   193  		h2.preformattedAttrs = buf.Bytes()
   194  	}()
   195  	state.prefix.WriteString(h.groupPrefix)
   196  	if len(h2.preformattedAttrs) > 0 {
   197  		state.sep = h.attrSep()
   198  	}
   199  	state.openGroups()
   200  	for _, a := range as {
   201  		state.appendAttr(a)
   202  	}
   203  	// Remember the new prefix for later keys.
   204  	h2.groupPrefix = state.prefix.String()
   205  	// Remember how many opened groups are in preformattedAttrs,
   206  	// so we don't open them again when we handle a Record.
   207  	h2.nOpenGroups = len(h2.groups)
   208  	return h2
   209  }
   210  
   211  func (h *commonHandler) withGroup(name string) *commonHandler {
   212  	h2 := h.clone()
   213  	h2.groups = append(h2.groups, name)
   214  	return h2
   215  }
   216  
   217  // handle is the internal implementation of Handler.Handle
   218  // used by GlogHandler and HumanGlogHandler.
   219  // header formats a log header as defined by the C++ implementation.
   220  // It returns a buffer containing the formatted header and the user's file and line number.
   221  // The depth specifies how many stack frames above lives the source line to be identified in the log message.
   222  //
   223  // # LOG LINE PREFIX FORMAT
   224  //
   225  // Log lines have this form:
   226  //
   227  //	Lyyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg...
   228  //
   229  // where the fields are defined as follows:
   230  //
   231  //	L                A single character, representing the log level
   232  //	                 (eg 'I' for INFO)
   233  //	yyyy             The year
   234  //	mm               The month (zero padded; ie May is '05')
   235  //	dd               The day (zero padded)
   236  //	hh:mm:ss.uuuuuu  Time in hours, minutes and fractional seconds
   237  //	threadid         The space-padded thread ID as returned by GetTID()
   238  //	                 (this matches the PID on Linux)
   239  //	file             The file name
   240  //	line             The line number
   241  //	msg              The user-supplied message
   242  //
   243  // Example:
   244  //
   245  //	I1103 11:57:31.739339 24395 google.cc:2341] Command line: ./some_prog
   246  //	I1103 11:57:31.739403 24395 google.cc:2342] Process id 24395
   247  //
   248  // NOTE: although the microseconds are useful for comparing events on
   249  // a single machine, clocks on different machines may not be well
   250  // synchronized.  Hence, use caution when comparing the low bits of
   251  // timestamps from different machines.
   252  func (h *commonHandler) handle(r slog.Record) error {
   253  	h.sharedVar.init(h.w)
   254  
   255  	state := h.newHandleState(buffer.New(), true, "")
   256  	defer state.free()
   257  	// Built-in attributes. They are not in a group.
   258  	stateGroups := state.groups
   259  	state.groups = nil // So ReplaceAttrs sees no groups instead of the pre groups.
   260  	rep := h.opts.ReplaceAttr
   261  	if h.isColored() {
   262  		state.color = levelColor(r.Level)
   263  	}
   264  	// level
   265  	state.appendLevel(r.Level, h.PadLevelText, h.sharedVar.maxLevelText, h.HumanReadable)
   266  	// time
   267  	t := r.Time // strip monotonic to match Attr behavior
   268  	mode := h.TimestampMode
   269  	if rep != nil {
   270  		a := rep(nil, slog.Time(slog.TimeKey, r.Time))
   271  		if a.Equal(slog.Attr{}) {
   272  			// disable timestamp logging if time is removed.
   273  			t = time.Time{}
   274  			mode = DisableTimestamp
   275  		} else if a.Value.Kind() == slog.KindTime {
   276  			t = a.Value.Time()
   277  		}
   278  	}
   279  	state.appendGlogTime(t, h.TimestampFormat, mode, h.HumanReadable)
   280  	state.appendPid(h.ForceGoroutineId, h.HumanReadable)
   281  	// source
   282  	if h.opts.AddSource {
   283  		if h.SourcePrettier != nil {
   284  			state.appendSource(h.SourcePrettier(r), h.WithFuncName, h.HumanReadable)
   285  		} else {
   286  			state.appendSource(source(r), h.WithFuncName, h.HumanReadable)
   287  		}
   288  	} else {
   289  		if !h.HumanReadable {
   290  			state.buf.WriteString("]")
   291  		}
   292  	}
   293  
   294  	var hasMessage bool
   295  	if rep != nil {
   296  		a := rep(nil, slog.String(slog.MessageKey, r.Message))
   297  		if !isEmptyAttr(a) {
   298  			state.buf.WriteString(" ")
   299  			state.appendValue(a.Value)
   300  			hasMessage = true
   301  		}
   302  	} else if r.Message != "" {
   303  		state.buf.WriteString(" ")
   304  		// take message as well formatted raw string, may be with color and so on, disable quote
   305  		state.appendString(r.Message)
   306  		hasMessage = true
   307  	}
   308  	if !hasMessage {
   309  		state.sep = " "
   310  	} else {
   311  		state.sep = h.attrSep()
   312  	}
   313  
   314  	state.groups = stateGroups // Restore groups passed to ReplaceAttrs.
   315  	state.appendNonBuiltIns(r)
   316  	state.buf.WriteByte('\n')
   317  
   318  	h.mu.Lock()
   319  	defer h.mu.Unlock()
   320  	_, err := h.w.Write(state.buf.Bytes())
   321  	return err
   322  }
   323  
   324  func (s *handleState) appendNonBuiltIns(r slog.Record) {
   325  	// preformatted Attrs
   326  	if len(s.h.preformattedAttrs) > 0 {
   327  		s.buf.WriteString(s.sep)
   328  		s.buf.Write(s.h.preformattedAttrs)
   329  		s.sep = s.h.attrSep()
   330  	}
   331  	// Attrs in Record -- unlike the built-in ones, they are in groups started
   332  	// from WithGroup.
   333  	// If the record has no Attrs, don't output any groups.
   334  	if r.NumAttrs() > 0 {
   335  		s.prefix.WriteString(s.h.groupPrefix)
   336  		s.openGroups()
   337  		r.Attrs(func(a slog.Attr) bool {
   338  			s.appendAttr(a)
   339  			return true
   340  		})
   341  	}
   342  }
   343  
   344  // attrSep returns the separator between attributes.
   345  func (h *commonHandler) attrSep() string {
   346  	if h.AttrSep != "" {
   347  		return h.AttrSep
   348  	}
   349  	return " "
   350  }
   351  
   352  var groupPool = sync.Pool{New: func() any {
   353  	s := make([]string, 0, 10)
   354  	return &s
   355  }}
   356  
   357  func (h *commonHandler) newHandleState(buf *buffer.Buffer, freeBuf bool, sep string) handleState {
   358  	s := handleState{
   359  		h:       h,
   360  		buf:     buf,
   361  		freeBuf: freeBuf,
   362  		sep:     sep,
   363  		prefix:  buffer.New(),
   364  	}
   365  	if h.opts.ReplaceAttr != nil {
   366  		s.groups = groupPool.Get().(*[]string)
   367  		*s.groups = append(*s.groups, h.groups[:h.nOpenGroups]...)
   368  	}
   369  	return s
   370  }
   371  
   372  func (h *commonHandler) isColored() bool {
   373  	isColored := h.ForceColors || (h.sharedVar.isTerminal && (runtime.GOOS != "windows"))
   374  
   375  	if h.EnvironmentOverrideColors {
   376  		switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
   377  		case ok && force != "0":
   378  			isColored = true
   379  		case ok && force == "0", os.Getenv("CLICOLOR") == "0":
   380  			isColored = false
   381  		}
   382  	}
   383  
   384  	return isColored && !h.DisableColors
   385  }