• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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	"slices"
10	"time"
11)
12
13const nAttrsInline = 5
14
15// A Record holds information about a log event.
16// Copies of a Record share state.
17// Do not modify a Record after handing out a copy to it.
18// Call [NewRecord] to create a new Record.
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.
96// It omits empty groups.
97func (r *Record) AddAttrs(attrs ...Attr) {
98	var i int
99	for i = 0; i < len(attrs) && r.nFront < len(r.front); i++ {
100		a := attrs[i]
101		if a.Value.isEmptyGroup() {
102			continue
103		}
104		r.front[r.nFront] = a
105		r.nFront++
106	}
107	// Check if a copy was modified by slicing past the end
108	// and seeing if the Attr there is non-zero.
109	if cap(r.back) > len(r.back) {
110		end := r.back[:len(r.back)+1][len(r.back)]
111		if !end.isEmpty() {
112			// Don't panic; copy and muddle through.
113			r.back = slices.Clip(r.back)
114			r.back = append(r.back, String("!BUG", "AddAttrs unsafely called on copy of Record made without using Record.Clone"))
115		}
116	}
117	ne := countEmptyGroups(attrs[i:])
118	r.back = slices.Grow(r.back, len(attrs[i:])-ne)
119	for _, a := range attrs[i:] {
120		if !a.Value.isEmptyGroup() {
121			r.back = append(r.back, a)
122		}
123	}
124}
125
126// Add converts the args to Attrs as described in [Logger.Log],
127// then appends the Attrs to the [Record]'s list of Attrs.
128// It omits empty groups.
129func (r *Record) Add(args ...any) {
130	var a Attr
131	for len(args) > 0 {
132		a, args = argsToAttr(args)
133		if a.Value.isEmptyGroup() {
134			continue
135		}
136		if r.nFront < len(r.front) {
137			r.front[r.nFront] = a
138			r.nFront++
139		} else {
140			if r.back == nil {
141				r.back = make([]Attr, 0, countAttrs(args)+1)
142			}
143			r.back = append(r.back, a)
144		}
145	}
146}
147
148// countAttrs returns the number of Attrs that would be created from args.
149func countAttrs(args []any) int {
150	n := 0
151	for i := 0; i < len(args); i++ {
152		n++
153		if _, ok := args[i].(string); ok {
154			i++
155		}
156	}
157	return n
158}
159
160const badKey = "!BADKEY"
161
162// argsToAttr turns a prefix of the nonempty args slice into an Attr
163// and returns the unconsumed portion of the slice.
164// If args[0] is an Attr, it returns it.
165// If args[0] is a string, it treats the first two elements as
166// a key-value pair.
167// Otherwise, it treats args[0] as a value with a missing key.
168func argsToAttr(args []any) (Attr, []any) {
169	switch x := args[0].(type) {
170	case string:
171		if len(args) == 1 {
172			return String(badKey, x), nil
173		}
174		return Any(x, args[1]), args[2:]
175
176	case Attr:
177		return x, args[1:]
178
179	default:
180		return Any(badKey, x), args[1:]
181	}
182}
183
184// Source describes the location of a line of source code.
185type Source struct {
186	// Function is the package path-qualified function name containing the
187	// source line. If non-empty, this string uniquely identifies a single
188	// function in the program. This may be the empty string if not known.
189	Function string `json:"function"`
190	// File and Line are the file name and line number (1-based) of the source
191	// line. These may be the empty string and zero, respectively, if not known.
192	File string `json:"file"`
193	Line int    `json:"line"`
194}
195
196// group returns the non-zero fields of s as a slice of attrs.
197// It is similar to a LogValue method, but we don't want Source
198// to implement LogValuer because it would be resolved before
199// the ReplaceAttr function was called.
200func (s *Source) group() Value {
201	var as []Attr
202	if s.Function != "" {
203		as = append(as, String("function", s.Function))
204	}
205	if s.File != "" {
206		as = append(as, String("file", s.File))
207	}
208	if s.Line != 0 {
209		as = append(as, Int("line", s.Line))
210	}
211	return GroupValue(as...)
212}
213
214// source returns a Source for the log event.
215// If the Record was created without the necessary information,
216// or if the location is unavailable, it returns a non-nil *Source
217// with zero fields.
218func (r Record) source() *Source {
219	fs := runtime.CallersFrames([]uintptr{r.PC})
220	f, _ := fs.Next()
221	return &Source{
222		Function: f.Function,
223		File:     f.File,
224		Line:     f.Line,
225	}
226}
227