record.go raw

   1  // Copyright 2022 The Go Authors. 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  	"runtime"
   9  	"time"
  10  
  11  	"golang.org/x/exp/slices"
  12  )
  13  
  14  const nAttrsInline = 5
  15  
  16  // A Record holds information about a log event.
  17  // Copies of a Record share state.
  18  // Do not modify a Record after handing out a copy to it.
  19  // Use [Record.Clone] to create a copy with no shared state.
  20  type Record struct {
  21  	// The time at which the output method (Log, Info, etc.) was called.
  22  	Time time.Time
  23  
  24  	// The log message.
  25  	Message string
  26  
  27  	// The level of the event.
  28  	Level Level
  29  
  30  	// The program counter at the time the record was constructed, as determined
  31  	// by runtime.Callers. If zero, no program counter is available.
  32  	//
  33  	// The only valid use for this value is as an argument to
  34  	// [runtime.CallersFrames]. In particular, it must not be passed to
  35  	// [runtime.FuncForPC].
  36  	PC uintptr
  37  
  38  	// Allocation optimization: an inline array sized to hold
  39  	// the majority of log calls (based on examination of open-source
  40  	// code). It holds the start of the list of Attrs.
  41  	front [nAttrsInline]Attr
  42  
  43  	// The number of Attrs in front.
  44  	nFront int
  45  
  46  	// The list of Attrs except for those in front.
  47  	// Invariants:
  48  	//   - len(back) > 0 iff nFront == len(front)
  49  	//   - Unused array elements are zero. Used to detect mistakes.
  50  	back []Attr
  51  }
  52  
  53  // NewRecord creates a Record from the given arguments.
  54  // Use [Record.AddAttrs] to add attributes to the Record.
  55  //
  56  // NewRecord is intended for logging APIs that want to support a [Handler] as
  57  // a backend.
  58  func NewRecord(t time.Time, level Level, msg string, pc uintptr) Record {
  59  	return Record{
  60  		Time:    t,
  61  		Message: msg,
  62  		Level:   level,
  63  		PC:      pc,
  64  	}
  65  }
  66  
  67  // Clone returns a copy of the record with no shared state.
  68  // The original record and the clone can both be modified
  69  // without interfering with each other.
  70  func (r Record) Clone() Record {
  71  	r.back = slices.Clip(r.back) // prevent append from mutating shared array
  72  	return r
  73  }
  74  
  75  // NumAttrs returns the number of attributes in the Record.
  76  func (r Record) NumAttrs() int {
  77  	return r.nFront + len(r.back)
  78  }
  79  
  80  // Attrs calls f on each Attr in the Record.
  81  // Iteration stops if f returns false.
  82  func (r Record) Attrs(f func(Attr) bool) {
  83  	for i := 0; i < r.nFront; i++ {
  84  		if !f(r.front[i]) {
  85  			return
  86  		}
  87  	}
  88  	for _, a := range r.back {
  89  		if !f(a) {
  90  			return
  91  		}
  92  	}
  93  }
  94  
  95  // AddAttrs appends the given Attrs to the Record's list of Attrs.
  96  func (r *Record) AddAttrs(attrs ...Attr) {
  97  	n := copy(r.front[r.nFront:], attrs)
  98  	r.nFront += n
  99  	// Check if a copy was modified by slicing past the end
 100  	// and seeing if the Attr there is non-zero.
 101  	if cap(r.back) > len(r.back) {
 102  		end := r.back[:len(r.back)+1][len(r.back)]
 103  		if !end.isEmpty() {
 104  			panic("copies of a slog.Record were both modified")
 105  		}
 106  	}
 107  	r.back = append(r.back, attrs[n:]...)
 108  }
 109  
 110  // Add converts the args to Attrs as described in [Logger.Log],
 111  // then appends the Attrs to the Record's list of Attrs.
 112  func (r *Record) Add(args ...any) {
 113  	var a Attr
 114  	for len(args) > 0 {
 115  		a, args = argsToAttr(args)
 116  		if r.nFront < len(r.front) {
 117  			r.front[r.nFront] = a
 118  			r.nFront++
 119  		} else {
 120  			if r.back == nil {
 121  				r.back = make([]Attr, 0, countAttrs(args))
 122  			}
 123  			r.back = append(r.back, a)
 124  		}
 125  	}
 126  
 127  }
 128  
 129  // countAttrs returns the number of Attrs that would be created from args.
 130  func countAttrs(args []any) int {
 131  	n := 0
 132  	for i := 0; i < len(args); i++ {
 133  		n++
 134  		if _, ok := args[i].(string); ok {
 135  			i++
 136  		}
 137  	}
 138  	return n
 139  }
 140  
 141  const badKey = "!BADKEY"
 142  
 143  // argsToAttr turns a prefix of the nonempty args slice into an Attr
 144  // and returns the unconsumed portion of the slice.
 145  // If args[0] is an Attr, it returns it.
 146  // If args[0] is a string, it treats the first two elements as
 147  // a key-value pair.
 148  // Otherwise, it treats args[0] as a value with a missing key.
 149  func argsToAttr(args []any) (Attr, []any) {
 150  	switch x := args[0].(type) {
 151  	case string:
 152  		if len(args) == 1 {
 153  			return String(badKey, x), nil
 154  		}
 155  		return Any(x, args[1]), args[2:]
 156  
 157  	case Attr:
 158  		return x, args[1:]
 159  
 160  	default:
 161  		return Any(badKey, x), args[1:]
 162  	}
 163  }
 164  
 165  // Source describes the location of a line of source code.
 166  type Source struct {
 167  	// Function is the package path-qualified function name containing the
 168  	// source line. If non-empty, this string uniquely identifies a single
 169  	// function in the program. This may be the empty string if not known.
 170  	Function string `json:"function"`
 171  	// File and Line are the file name and line number (1-based) of the source
 172  	// line. These may be the empty string and zero, respectively, if not known.
 173  	File string `json:"file"`
 174  	Line int    `json:"line"`
 175  }
 176  
 177  // attrs returns the non-zero fields of s as a slice of attrs.
 178  // It is similar to a LogValue method, but we don't want Source
 179  // to implement LogValuer because it would be resolved before
 180  // the ReplaceAttr function was called.
 181  func (s *Source) group() Value {
 182  	var as []Attr
 183  	if s.Function != "" {
 184  		as = append(as, String("function", s.Function))
 185  	}
 186  	if s.File != "" {
 187  		as = append(as, String("file", s.File))
 188  	}
 189  	if s.Line != 0 {
 190  		as = append(as, Int("line", s.Line))
 191  	}
 192  	return GroupValue(as...)
 193  }
 194  
 195  // source returns a Source for the log event.
 196  // If the Record was created without the necessary information,
 197  // or if the location is unavailable, it returns a non-nil *Source
 198  // with zero fields.
 199  func (r Record) source() *Source {
 200  	fs := runtime.CallersFrames([]uintptr{r.PC})
 201  	f, _ := fs.Next()
 202  	return &Source{
 203  		Function: f.Function,
 204  		File:     f.File,
 205  		Line:     f.Line,
 206  	}
 207  }
 208