record.go

  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
  5package slog
  6
  7import (
  8	"runtime"
  9	"time"
 10
 11	"golang.org/x/exp/slices"
 12)
 13
 14const 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.
 20type 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.
 58func 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.
 70func (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.
 76func (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.
 82func (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.
 96func (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.
112func (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.
130func 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
141const 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.
149func 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.
166type 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.
181func (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.
199func (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}