Initial commit: Go 1.23 release state
This commit is contained in:
41
src/log/example_test.go
Normal file
41
src/log/example_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package log_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func ExampleLogger() {
|
||||
var (
|
||||
buf bytes.Buffer
|
||||
logger = log.New(&buf, "logger: ", log.Lshortfile)
|
||||
)
|
||||
|
||||
logger.Print("Hello, log file!")
|
||||
|
||||
fmt.Print(&buf)
|
||||
// Output:
|
||||
// logger: example_test.go:19: Hello, log file!
|
||||
}
|
||||
|
||||
func ExampleLogger_Output() {
|
||||
var (
|
||||
buf bytes.Buffer
|
||||
logger = log.New(&buf, "INFO: ", log.Lshortfile)
|
||||
|
||||
infof = func(info string) {
|
||||
logger.Output(2, info)
|
||||
}
|
||||
)
|
||||
|
||||
infof("Hello world")
|
||||
|
||||
fmt.Print(&buf)
|
||||
// Output:
|
||||
// INFO: example_test.go:36: Hello world
|
||||
}
|
||||
12
src/log/internal/internal.go
Normal file
12
src/log/internal/internal.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains definitions used by both log and log/slog.
|
||||
package internal
|
||||
|
||||
// DefaultOutput holds a function which calls the default log.Logger's
|
||||
// output function.
|
||||
// It allows slog.defaultHandler to call into an unexported function of
|
||||
// the log package.
|
||||
var DefaultOutput func(pc uintptr, data []byte) error
|
||||
458
src/log/log.go
Normal file
458
src/log/log.go
Normal file
@@ -0,0 +1,458 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package log implements a simple logging package. It defines a type, [Logger],
|
||||
// with methods for formatting output. It also has a predefined 'standard'
|
||||
// Logger accessible through helper functions Print[f|ln], Fatal[f|ln], and
|
||||
// Panic[f|ln], which are easier to use than creating a Logger manually.
|
||||
// That logger writes to standard error and prints the date and time
|
||||
// of each logged message.
|
||||
// Every log message is output on a separate line: if the message being
|
||||
// printed does not end in a newline, the logger will add one.
|
||||
// The Fatal functions call [os.Exit](1) after writing the log message.
|
||||
// The Panic functions call panic after writing the log message.
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/internal"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// These flags define which text to prefix to each log entry generated by the [Logger].
|
||||
// Bits are or'ed together to control what's printed.
|
||||
// With the exception of the Lmsgprefix flag, there is no
|
||||
// control over the order they appear (the order listed here)
|
||||
// or the format they present (as described in the comments).
|
||||
// The prefix is followed by a colon only when Llongfile or Lshortfile
|
||||
// is specified.
|
||||
// For example, flags Ldate | Ltime (or LstdFlags) produce,
|
||||
//
|
||||
// 2009/01/23 01:23:23 message
|
||||
//
|
||||
// while flags Ldate | Ltime | Lmicroseconds | Llongfile produce,
|
||||
//
|
||||
// 2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
|
||||
const (
|
||||
Ldate = 1 << iota // the date in the local time zone: 2009/01/23
|
||||
Ltime // the time in the local time zone: 01:23:23
|
||||
Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
|
||||
Llongfile // full file name and line number: /a/b/c/d.go:23
|
||||
Lshortfile // final file name element and line number: d.go:23. overrides Llongfile
|
||||
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
|
||||
Lmsgprefix // move the "prefix" from the beginning of the line to before the message
|
||||
LstdFlags = Ldate | Ltime // initial values for the standard logger
|
||||
)
|
||||
|
||||
// A Logger represents an active logging object that generates lines of
|
||||
// output to an [io.Writer]. Each logging operation makes a single call to
|
||||
// the Writer's Write method. A Logger can be used simultaneously from
|
||||
// multiple goroutines; it guarantees to serialize access to the Writer.
|
||||
type Logger struct {
|
||||
outMu sync.Mutex
|
||||
out io.Writer // destination for output
|
||||
|
||||
prefix atomic.Pointer[string] // prefix on each line to identify the logger (but see Lmsgprefix)
|
||||
flag atomic.Int32 // properties
|
||||
isDiscard atomic.Bool
|
||||
}
|
||||
|
||||
// New creates a new [Logger]. The out variable sets the
|
||||
// destination to which log data will be written.
|
||||
// The prefix appears at the beginning of each generated log line, or
|
||||
// after the log header if the [Lmsgprefix] flag is provided.
|
||||
// The flag argument defines the logging properties.
|
||||
func New(out io.Writer, prefix string, flag int) *Logger {
|
||||
l := new(Logger)
|
||||
l.SetOutput(out)
|
||||
l.SetPrefix(prefix)
|
||||
l.SetFlags(flag)
|
||||
return l
|
||||
}
|
||||
|
||||
// SetOutput sets the output destination for the logger.
|
||||
func (l *Logger) SetOutput(w io.Writer) {
|
||||
l.outMu.Lock()
|
||||
defer l.outMu.Unlock()
|
||||
l.out = w
|
||||
l.isDiscard.Store(w == io.Discard)
|
||||
}
|
||||
|
||||
var std = New(os.Stderr, "", LstdFlags)
|
||||
|
||||
// Default returns the standard logger used by the package-level output functions.
|
||||
func Default() *Logger { return std }
|
||||
|
||||
// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding.
|
||||
func itoa(buf *[]byte, i int, wid int) {
|
||||
// Assemble decimal in reverse order.
|
||||
var b [20]byte
|
||||
bp := len(b) - 1
|
||||
for i >= 10 || wid > 1 {
|
||||
wid--
|
||||
q := i / 10
|
||||
b[bp] = byte('0' + i - q*10)
|
||||
bp--
|
||||
i = q
|
||||
}
|
||||
// i < 10
|
||||
b[bp] = byte('0' + i)
|
||||
*buf = append(*buf, b[bp:]...)
|
||||
}
|
||||
|
||||
// formatHeader writes log header to buf in following order:
|
||||
// - l.prefix (if it's not blank and Lmsgprefix is unset),
|
||||
// - date and/or time (if corresponding flags are provided),
|
||||
// - file and line number (if corresponding flags are provided),
|
||||
// - l.prefix (if it's not blank and Lmsgprefix is set).
|
||||
func formatHeader(buf *[]byte, t time.Time, prefix string, flag int, file string, line int) {
|
||||
if flag&Lmsgprefix == 0 {
|
||||
*buf = append(*buf, prefix...)
|
||||
}
|
||||
if flag&(Ldate|Ltime|Lmicroseconds) != 0 {
|
||||
if flag&LUTC != 0 {
|
||||
t = t.UTC()
|
||||
}
|
||||
if flag&Ldate != 0 {
|
||||
year, month, day := t.Date()
|
||||
itoa(buf, year, 4)
|
||||
*buf = append(*buf, '/')
|
||||
itoa(buf, int(month), 2)
|
||||
*buf = append(*buf, '/')
|
||||
itoa(buf, day, 2)
|
||||
*buf = append(*buf, ' ')
|
||||
}
|
||||
if flag&(Ltime|Lmicroseconds) != 0 {
|
||||
hour, min, sec := t.Clock()
|
||||
itoa(buf, hour, 2)
|
||||
*buf = append(*buf, ':')
|
||||
itoa(buf, min, 2)
|
||||
*buf = append(*buf, ':')
|
||||
itoa(buf, sec, 2)
|
||||
if flag&Lmicroseconds != 0 {
|
||||
*buf = append(*buf, '.')
|
||||
itoa(buf, t.Nanosecond()/1e3, 6)
|
||||
}
|
||||
*buf = append(*buf, ' ')
|
||||
}
|
||||
}
|
||||
if flag&(Lshortfile|Llongfile) != 0 {
|
||||
if flag&Lshortfile != 0 {
|
||||
short := file
|
||||
for i := len(file) - 1; i > 0; i-- {
|
||||
if file[i] == '/' {
|
||||
short = file[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
file = short
|
||||
}
|
||||
*buf = append(*buf, file...)
|
||||
*buf = append(*buf, ':')
|
||||
itoa(buf, line, -1)
|
||||
*buf = append(*buf, ": "...)
|
||||
}
|
||||
if flag&Lmsgprefix != 0 {
|
||||
*buf = append(*buf, prefix...)
|
||||
}
|
||||
}
|
||||
|
||||
var bufferPool = sync.Pool{New: func() any { return new([]byte) }}
|
||||
|
||||
func getBuffer() *[]byte {
|
||||
p := bufferPool.Get().(*[]byte)
|
||||
*p = (*p)[:0]
|
||||
return p
|
||||
}
|
||||
|
||||
func putBuffer(p *[]byte) {
|
||||
// Proper usage of a sync.Pool requires each entry to have approximately
|
||||
// the same memory cost. To obtain this property when the stored type
|
||||
// contains a variably-sized buffer, we add a hard limit on the maximum buffer
|
||||
// to place back in the pool.
|
||||
//
|
||||
// See https://go.dev/issue/23199
|
||||
if cap(*p) > 64<<10 {
|
||||
*p = nil
|
||||
}
|
||||
bufferPool.Put(p)
|
||||
}
|
||||
|
||||
// Output writes the output for a logging event. The string s contains
|
||||
// the text to print after the prefix specified by the flags of the
|
||||
// Logger. A newline is appended if the last character of s is not
|
||||
// already a newline. Calldepth is used to recover the PC and is
|
||||
// provided for generality, although at the moment on all pre-defined
|
||||
// paths it will be 2.
|
||||
func (l *Logger) Output(calldepth int, s string) error {
|
||||
calldepth++ // +1 for this frame.
|
||||
return l.output(0, calldepth, func(b []byte) []byte {
|
||||
return append(b, s...)
|
||||
})
|
||||
}
|
||||
|
||||
// output can take either a calldepth or a pc to get source line information.
|
||||
// It uses the pc if it is non-zero.
|
||||
func (l *Logger) output(pc uintptr, calldepth int, appendOutput func([]byte) []byte) error {
|
||||
if l.isDiscard.Load() {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now() // get this early.
|
||||
|
||||
// Load prefix and flag once so that their value is consistent within
|
||||
// this call regardless of any concurrent changes to their value.
|
||||
prefix := l.Prefix()
|
||||
flag := l.Flags()
|
||||
|
||||
var file string
|
||||
var line int
|
||||
if flag&(Lshortfile|Llongfile) != 0 {
|
||||
if pc == 0 {
|
||||
var ok bool
|
||||
_, file, line, ok = runtime.Caller(calldepth)
|
||||
if !ok {
|
||||
file = "???"
|
||||
line = 0
|
||||
}
|
||||
} else {
|
||||
fs := runtime.CallersFrames([]uintptr{pc})
|
||||
f, _ := fs.Next()
|
||||
file = f.File
|
||||
if file == "" {
|
||||
file = "???"
|
||||
}
|
||||
line = f.Line
|
||||
}
|
||||
}
|
||||
|
||||
buf := getBuffer()
|
||||
defer putBuffer(buf)
|
||||
formatHeader(buf, now, prefix, flag, file, line)
|
||||
*buf = appendOutput(*buf)
|
||||
if len(*buf) == 0 || (*buf)[len(*buf)-1] != '\n' {
|
||||
*buf = append(*buf, '\n')
|
||||
}
|
||||
|
||||
l.outMu.Lock()
|
||||
defer l.outMu.Unlock()
|
||||
_, err := l.out.Write(*buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func init() {
|
||||
internal.DefaultOutput = func(pc uintptr, data []byte) error {
|
||||
return std.output(pc, 0, func(buf []byte) []byte {
|
||||
return append(buf, data...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Print calls l.Output to print to the logger.
|
||||
// Arguments are handled in the manner of [fmt.Print].
|
||||
func (l *Logger) Print(v ...any) {
|
||||
l.output(0, 2, func(b []byte) []byte {
|
||||
return fmt.Append(b, v...)
|
||||
})
|
||||
}
|
||||
|
||||
// Printf calls l.Output to print to the logger.
|
||||
// Arguments are handled in the manner of [fmt.Printf].
|
||||
func (l *Logger) Printf(format string, v ...any) {
|
||||
l.output(0, 2, func(b []byte) []byte {
|
||||
return fmt.Appendf(b, format, v...)
|
||||
})
|
||||
}
|
||||
|
||||
// Println calls l.Output to print to the logger.
|
||||
// Arguments are handled in the manner of [fmt.Println].
|
||||
func (l *Logger) Println(v ...any) {
|
||||
l.output(0, 2, func(b []byte) []byte {
|
||||
return fmt.Appendln(b, v...)
|
||||
})
|
||||
}
|
||||
|
||||
// Fatal is equivalent to l.Print() followed by a call to [os.Exit](1).
|
||||
func (l *Logger) Fatal(v ...any) {
|
||||
l.Output(2, fmt.Sprint(v...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Fatalf is equivalent to l.Printf() followed by a call to [os.Exit](1).
|
||||
func (l *Logger) Fatalf(format string, v ...any) {
|
||||
l.Output(2, fmt.Sprintf(format, v...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Fatalln is equivalent to l.Println() followed by a call to [os.Exit](1).
|
||||
func (l *Logger) Fatalln(v ...any) {
|
||||
l.Output(2, fmt.Sprintln(v...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Panic is equivalent to l.Print() followed by a call to panic().
|
||||
func (l *Logger) Panic(v ...any) {
|
||||
s := fmt.Sprint(v...)
|
||||
l.Output(2, s)
|
||||
panic(s)
|
||||
}
|
||||
|
||||
// Panicf is equivalent to l.Printf() followed by a call to panic().
|
||||
func (l *Logger) Panicf(format string, v ...any) {
|
||||
s := fmt.Sprintf(format, v...)
|
||||
l.Output(2, s)
|
||||
panic(s)
|
||||
}
|
||||
|
||||
// Panicln is equivalent to l.Println() followed by a call to panic().
|
||||
func (l *Logger) Panicln(v ...any) {
|
||||
s := fmt.Sprintln(v...)
|
||||
l.Output(2, s)
|
||||
panic(s)
|
||||
}
|
||||
|
||||
// Flags returns the output flags for the logger.
|
||||
// The flag bits are [Ldate], [Ltime], and so on.
|
||||
func (l *Logger) Flags() int {
|
||||
return int(l.flag.Load())
|
||||
}
|
||||
|
||||
// SetFlags sets the output flags for the logger.
|
||||
// The flag bits are [Ldate], [Ltime], and so on.
|
||||
func (l *Logger) SetFlags(flag int) {
|
||||
l.flag.Store(int32(flag))
|
||||
}
|
||||
|
||||
// Prefix returns the output prefix for the logger.
|
||||
func (l *Logger) Prefix() string {
|
||||
if p := l.prefix.Load(); p != nil {
|
||||
return *p
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetPrefix sets the output prefix for the logger.
|
||||
func (l *Logger) SetPrefix(prefix string) {
|
||||
l.prefix.Store(&prefix)
|
||||
}
|
||||
|
||||
// Writer returns the output destination for the logger.
|
||||
func (l *Logger) Writer() io.Writer {
|
||||
l.outMu.Lock()
|
||||
defer l.outMu.Unlock()
|
||||
return l.out
|
||||
}
|
||||
|
||||
// SetOutput sets the output destination for the standard logger.
|
||||
func SetOutput(w io.Writer) {
|
||||
std.SetOutput(w)
|
||||
}
|
||||
|
||||
// Flags returns the output flags for the standard logger.
|
||||
// The flag bits are [Ldate], [Ltime], and so on.
|
||||
func Flags() int {
|
||||
return std.Flags()
|
||||
}
|
||||
|
||||
// SetFlags sets the output flags for the standard logger.
|
||||
// The flag bits are [Ldate], [Ltime], and so on.
|
||||
func SetFlags(flag int) {
|
||||
std.SetFlags(flag)
|
||||
}
|
||||
|
||||
// Prefix returns the output prefix for the standard logger.
|
||||
func Prefix() string {
|
||||
return std.Prefix()
|
||||
}
|
||||
|
||||
// SetPrefix sets the output prefix for the standard logger.
|
||||
func SetPrefix(prefix string) {
|
||||
std.SetPrefix(prefix)
|
||||
}
|
||||
|
||||
// Writer returns the output destination for the standard logger.
|
||||
func Writer() io.Writer {
|
||||
return std.Writer()
|
||||
}
|
||||
|
||||
// These functions write to the standard logger.
|
||||
|
||||
// Print calls Output to print to the standard logger.
|
||||
// Arguments are handled in the manner of [fmt.Print].
|
||||
func Print(v ...any) {
|
||||
std.output(0, 2, func(b []byte) []byte {
|
||||
return fmt.Append(b, v...)
|
||||
})
|
||||
}
|
||||
|
||||
// Printf calls Output to print to the standard logger.
|
||||
// Arguments are handled in the manner of [fmt.Printf].
|
||||
func Printf(format string, v ...any) {
|
||||
std.output(0, 2, func(b []byte) []byte {
|
||||
return fmt.Appendf(b, format, v...)
|
||||
})
|
||||
}
|
||||
|
||||
// Println calls Output to print to the standard logger.
|
||||
// Arguments are handled in the manner of [fmt.Println].
|
||||
func Println(v ...any) {
|
||||
std.output(0, 2, func(b []byte) []byte {
|
||||
return fmt.Appendln(b, v...)
|
||||
})
|
||||
}
|
||||
|
||||
// Fatal is equivalent to [Print] followed by a call to [os.Exit](1).
|
||||
func Fatal(v ...any) {
|
||||
std.Output(2, fmt.Sprint(v...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Fatalf is equivalent to [Printf] followed by a call to [os.Exit](1).
|
||||
func Fatalf(format string, v ...any) {
|
||||
std.Output(2, fmt.Sprintf(format, v...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Fatalln is equivalent to [Println] followed by a call to [os.Exit](1).
|
||||
func Fatalln(v ...any) {
|
||||
std.Output(2, fmt.Sprintln(v...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Panic is equivalent to [Print] followed by a call to panic().
|
||||
func Panic(v ...any) {
|
||||
s := fmt.Sprint(v...)
|
||||
std.Output(2, s)
|
||||
panic(s)
|
||||
}
|
||||
|
||||
// Panicf is equivalent to [Printf] followed by a call to panic().
|
||||
func Panicf(format string, v ...any) {
|
||||
s := fmt.Sprintf(format, v...)
|
||||
std.Output(2, s)
|
||||
panic(s)
|
||||
}
|
||||
|
||||
// Panicln is equivalent to [Println] followed by a call to panic().
|
||||
func Panicln(v ...any) {
|
||||
s := fmt.Sprintln(v...)
|
||||
std.Output(2, s)
|
||||
panic(s)
|
||||
}
|
||||
|
||||
// Output writes the output for a logging event. The string s contains
|
||||
// the text to print after the prefix specified by the flags of the
|
||||
// Logger. A newline is appended if the last character of s is not
|
||||
// already a newline. Calldepth is the count of the number of
|
||||
// frames to skip when computing the file name and line number
|
||||
// if [Llongfile] or [Lshortfile] is set; a value of 1 will print the details
|
||||
// for the caller of Output.
|
||||
func Output(calldepth int, s string) error {
|
||||
return std.Output(calldepth+1, s) // +1 for this frame.
|
||||
}
|
||||
280
src/log/log_test.go
Normal file
280
src/log/log_test.go
Normal file
@@ -0,0 +1,280 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package log
|
||||
|
||||
// These tests are too simple.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
Rdate = `[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]`
|
||||
Rtime = `[0-9][0-9]:[0-9][0-9]:[0-9][0-9]`
|
||||
Rmicroseconds = `\.[0-9][0-9][0-9][0-9][0-9][0-9]`
|
||||
Rline = `(63|65):` // must update if the calls to l.Printf / l.Print below move
|
||||
Rlongfile = `.*/[A-Za-z0-9_\-]+\.go:` + Rline
|
||||
Rshortfile = `[A-Za-z0-9_\-]+\.go:` + Rline
|
||||
)
|
||||
|
||||
type tester struct {
|
||||
flag int
|
||||
prefix string
|
||||
pattern string // regexp that log output must match; we add ^ and expected_text$ always
|
||||
}
|
||||
|
||||
var tests = []tester{
|
||||
// individual pieces:
|
||||
{0, "", ""},
|
||||
{0, "XXX", "XXX"},
|
||||
{Ldate, "", Rdate + " "},
|
||||
{Ltime, "", Rtime + " "},
|
||||
{Ltime | Lmsgprefix, "XXX", Rtime + " XXX"},
|
||||
{Ltime | Lmicroseconds, "", Rtime + Rmicroseconds + " "},
|
||||
{Lmicroseconds, "", Rtime + Rmicroseconds + " "}, // microsec implies time
|
||||
{Llongfile, "", Rlongfile + " "},
|
||||
{Lshortfile, "", Rshortfile + " "},
|
||||
{Llongfile | Lshortfile, "", Rshortfile + " "}, // shortfile overrides longfile
|
||||
// everything at once:
|
||||
{Ldate | Ltime | Lmicroseconds | Llongfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rlongfile + " "},
|
||||
{Ldate | Ltime | Lmicroseconds | Lshortfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rshortfile + " "},
|
||||
{Ldate | Ltime | Lmicroseconds | Llongfile | Lmsgprefix, "XXX", Rdate + " " + Rtime + Rmicroseconds + " " + Rlongfile + " XXX"},
|
||||
{Ldate | Ltime | Lmicroseconds | Lshortfile | Lmsgprefix, "XXX", Rdate + " " + Rtime + Rmicroseconds + " " + Rshortfile + " XXX"},
|
||||
}
|
||||
|
||||
// Test using Println("hello", 23, "world") or using Printf("hello %d world", 23)
|
||||
func testPrint(t *testing.T, flag int, prefix string, pattern string, useFormat bool) {
|
||||
buf := new(strings.Builder)
|
||||
SetOutput(buf)
|
||||
SetFlags(flag)
|
||||
SetPrefix(prefix)
|
||||
if useFormat {
|
||||
Printf("hello %d world", 23)
|
||||
} else {
|
||||
Println("hello", 23, "world")
|
||||
}
|
||||
line := buf.String()
|
||||
line = line[0 : len(line)-1]
|
||||
pattern = "^" + pattern + "hello 23 world$"
|
||||
matched, err := regexp.MatchString(pattern, line)
|
||||
if err != nil {
|
||||
t.Fatal("pattern did not compile:", err)
|
||||
}
|
||||
if !matched {
|
||||
t.Errorf("log output should match %q is %q", pattern, line)
|
||||
}
|
||||
SetOutput(os.Stderr)
|
||||
}
|
||||
|
||||
func TestDefault(t *testing.T) {
|
||||
if got := Default(); got != std {
|
||||
t.Errorf("Default [%p] should be std [%p]", got, std)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
for _, testcase := range tests {
|
||||
testPrint(t, testcase.flag, testcase.prefix, testcase.pattern, false)
|
||||
testPrint(t, testcase.flag, testcase.prefix, testcase.pattern, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
const testString = "test"
|
||||
var b strings.Builder
|
||||
l := New(&b, "", 0)
|
||||
l.Println(testString)
|
||||
if expect := testString + "\n"; b.String() != expect {
|
||||
t.Errorf("log output should match %q is %q", expect, b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonNewLogger(t *testing.T) {
|
||||
var l Logger
|
||||
l.SetOutput(new(bytes.Buffer)) // minimal work to initialize a Logger
|
||||
l.Print("hello")
|
||||
}
|
||||
|
||||
func TestOutputRace(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
l := New(&b, "", 0)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(100)
|
||||
for i := 0; i < 100; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
l.SetFlags(0)
|
||||
l.Output(0, "")
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestFlagAndPrefixSetting(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
l := New(&b, "Test:", LstdFlags)
|
||||
f := l.Flags()
|
||||
if f != LstdFlags {
|
||||
t.Errorf("Flags 1: expected %x got %x", LstdFlags, f)
|
||||
}
|
||||
l.SetFlags(f | Lmicroseconds)
|
||||
f = l.Flags()
|
||||
if f != LstdFlags|Lmicroseconds {
|
||||
t.Errorf("Flags 2: expected %x got %x", LstdFlags|Lmicroseconds, f)
|
||||
}
|
||||
p := l.Prefix()
|
||||
if p != "Test:" {
|
||||
t.Errorf(`Prefix: expected "Test:" got %q`, p)
|
||||
}
|
||||
l.SetPrefix("Reality:")
|
||||
p = l.Prefix()
|
||||
if p != "Reality:" {
|
||||
t.Errorf(`Prefix: expected "Reality:" got %q`, p)
|
||||
}
|
||||
// Verify a log message looks right, with our prefix and microseconds present.
|
||||
l.Print("hello")
|
||||
pattern := "^Reality:" + Rdate + " " + Rtime + Rmicroseconds + " hello\n"
|
||||
matched, err := regexp.Match(pattern, b.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("pattern %q did not compile: %s", pattern, err)
|
||||
}
|
||||
if !matched {
|
||||
t.Error("message did not match pattern")
|
||||
}
|
||||
|
||||
// Ensure that a newline is added only if the buffer lacks a newline suffix.
|
||||
b.Reset()
|
||||
l.SetFlags(0)
|
||||
l.SetPrefix("\n")
|
||||
l.Output(0, "")
|
||||
if got := b.String(); got != "\n" {
|
||||
t.Errorf("message mismatch:\ngot %q\nwant %q", got, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUTCFlag(t *testing.T) {
|
||||
var b strings.Builder
|
||||
l := New(&b, "Test:", LstdFlags)
|
||||
l.SetFlags(Ldate | Ltime | LUTC)
|
||||
// Verify a log message looks right in the right time zone. Quantize to the second only.
|
||||
now := time.Now().UTC()
|
||||
l.Print("hello")
|
||||
want := fmt.Sprintf("Test:%d/%.2d/%.2d %.2d:%.2d:%.2d hello\n",
|
||||
now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
|
||||
got := b.String()
|
||||
if got == want {
|
||||
return
|
||||
}
|
||||
// It's possible we crossed a second boundary between getting now and logging,
|
||||
// so add a second and try again. This should very nearly always work.
|
||||
now = now.Add(time.Second)
|
||||
want = fmt.Sprintf("Test:%d/%.2d/%.2d %.2d:%.2d:%.2d hello\n",
|
||||
now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
|
||||
if got == want {
|
||||
return
|
||||
}
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
|
||||
func TestEmptyPrintCreatesLine(t *testing.T) {
|
||||
var b strings.Builder
|
||||
l := New(&b, "Header:", LstdFlags)
|
||||
l.Print()
|
||||
l.Println("non-empty")
|
||||
output := b.String()
|
||||
if n := strings.Count(output, "Header"); n != 2 {
|
||||
t.Errorf("expected 2 headers, got %d", n)
|
||||
}
|
||||
if n := strings.Count(output, "\n"); n != 2 {
|
||||
t.Errorf("expected 2 lines, got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscard(t *testing.T) {
|
||||
l := New(io.Discard, "", 0)
|
||||
s := strings.Repeat("a", 102400)
|
||||
c := testing.AllocsPerRun(100, func() { l.Printf("%s", s) })
|
||||
// One allocation for slice passed to Printf,
|
||||
// but none for formatting of long string.
|
||||
if c > 1 {
|
||||
t.Errorf("got %v allocs, want at most 1", c)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkItoa(b *testing.B) {
|
||||
dst := make([]byte, 0, 64)
|
||||
for i := 0; i < b.N; i++ {
|
||||
dst = dst[0:0]
|
||||
itoa(&dst, 2015, 4) // year
|
||||
itoa(&dst, 1, 2) // month
|
||||
itoa(&dst, 30, 2) // day
|
||||
itoa(&dst, 12, 2) // hour
|
||||
itoa(&dst, 56, 2) // minute
|
||||
itoa(&dst, 0, 2) // second
|
||||
itoa(&dst, 987654, 6) // microsecond
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPrintln(b *testing.B) {
|
||||
const testString = "test"
|
||||
var buf bytes.Buffer
|
||||
l := New(&buf, "", LstdFlags)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
l.Println(testString)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPrintlnNoFlags(b *testing.B) {
|
||||
const testString = "test"
|
||||
var buf bytes.Buffer
|
||||
l := New(&buf, "", 0)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
l.Println(testString)
|
||||
}
|
||||
}
|
||||
|
||||
// discard is identical to io.Discard,
|
||||
// but copied here to avoid the io.Discard optimization in Logger.
|
||||
type discard struct{}
|
||||
|
||||
func (discard) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func BenchmarkConcurrent(b *testing.B) {
|
||||
l := New(discard{}, "prefix: ", Ldate|Ltime|Lmicroseconds|Llongfile|Lmsgprefix)
|
||||
var group sync.WaitGroup
|
||||
for i := runtime.NumCPU(); i > 0; i-- {
|
||||
group.Add(1)
|
||||
go func() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.Output(0, "hello, world!")
|
||||
}
|
||||
defer group.Done()
|
||||
}()
|
||||
}
|
||||
group.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkDiscard(b *testing.B) {
|
||||
l := New(io.Discard, "", LstdFlags|Lshortfile)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.Printf("processing %d objects from bucket %q", 1234, "fizzbuzz")
|
||||
}
|
||||
}
|
||||
101
src/log/slog/attr.go
Normal file
101
src/log/slog/attr.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// An Attr is a key-value pair.
|
||||
type Attr struct {
|
||||
Key string
|
||||
Value Value
|
||||
}
|
||||
|
||||
// String returns an Attr for a string value.
|
||||
func String(key, value string) Attr {
|
||||
return Attr{key, StringValue(value)}
|
||||
}
|
||||
|
||||
// Int64 returns an Attr for an int64.
|
||||
func Int64(key string, value int64) Attr {
|
||||
return Attr{key, Int64Value(value)}
|
||||
}
|
||||
|
||||
// Int converts an int to an int64 and returns
|
||||
// an Attr with that value.
|
||||
func Int(key string, value int) Attr {
|
||||
return Int64(key, int64(value))
|
||||
}
|
||||
|
||||
// Uint64 returns an Attr for a uint64.
|
||||
func Uint64(key string, v uint64) Attr {
|
||||
return Attr{key, Uint64Value(v)}
|
||||
}
|
||||
|
||||
// Float64 returns an Attr for a floating-point number.
|
||||
func Float64(key string, v float64) Attr {
|
||||
return Attr{key, Float64Value(v)}
|
||||
}
|
||||
|
||||
// Bool returns an Attr for a bool.
|
||||
func Bool(key string, v bool) Attr {
|
||||
return Attr{key, BoolValue(v)}
|
||||
}
|
||||
|
||||
// Time returns an Attr for a [time.Time].
|
||||
// It discards the monotonic portion.
|
||||
func Time(key string, v time.Time) Attr {
|
||||
return Attr{key, TimeValue(v)}
|
||||
}
|
||||
|
||||
// Duration returns an Attr for a [time.Duration].
|
||||
func Duration(key string, v time.Duration) Attr {
|
||||
return Attr{key, DurationValue(v)}
|
||||
}
|
||||
|
||||
// Group returns an Attr for a Group [Value].
|
||||
// The first argument is the key; the remaining arguments
|
||||
// are converted to Attrs as in [Logger.Log].
|
||||
//
|
||||
// Use Group to collect several key-value pairs under a single
|
||||
// key on a log line, or as the result of LogValue
|
||||
// in order to log a single value as multiple Attrs.
|
||||
func Group(key string, args ...any) Attr {
|
||||
return Attr{key, GroupValue(argsToAttrSlice(args)...)}
|
||||
}
|
||||
|
||||
func argsToAttrSlice(args []any) []Attr {
|
||||
var (
|
||||
attr Attr
|
||||
attrs []Attr
|
||||
)
|
||||
for len(args) > 0 {
|
||||
attr, args = argsToAttr(args)
|
||||
attrs = append(attrs, attr)
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
// Any returns an Attr for the supplied value.
|
||||
// See [AnyValue] for how values are treated.
|
||||
func Any(key string, value any) Attr {
|
||||
return Attr{key, AnyValue(value)}
|
||||
}
|
||||
|
||||
// Equal reports whether a and b have equal keys and values.
|
||||
func (a Attr) Equal(b Attr) bool {
|
||||
return a.Key == b.Key && a.Value.Equal(b.Value)
|
||||
}
|
||||
|
||||
func (a Attr) String() string {
|
||||
return a.Key + "=" + a.Value.String()
|
||||
}
|
||||
|
||||
// isEmpty reports whether a has an empty key and a nil value.
|
||||
// That can be written as Attr{} or Any("", nil).
|
||||
func (a Attr) isEmpty() bool {
|
||||
return a.Key == "" && a.Value.num == 0 && a.Value.any == nil
|
||||
}
|
||||
74
src/log/slog/attr_test.go
Normal file
74
src/log/slog/attr_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"internal/testenv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAttrNoAlloc(t *testing.T) {
|
||||
testenv.SkipIfOptimizationOff(t)
|
||||
// Assign values just to make sure the compiler doesn't optimize away the statements.
|
||||
var (
|
||||
i int64
|
||||
u uint64
|
||||
f float64
|
||||
b bool
|
||||
s string
|
||||
x any
|
||||
p = &i
|
||||
d time.Duration
|
||||
)
|
||||
a := int(testing.AllocsPerRun(5, func() {
|
||||
i = Int64("key", 1).Value.Int64()
|
||||
u = Uint64("key", 1).Value.Uint64()
|
||||
f = Float64("key", 1).Value.Float64()
|
||||
b = Bool("key", true).Value.Bool()
|
||||
s = String("key", "foo").Value.String()
|
||||
d = Duration("key", d).Value.Duration()
|
||||
x = Any("key", p).Value.Any()
|
||||
}))
|
||||
if a != 0 {
|
||||
t.Errorf("got %d allocs, want zero", a)
|
||||
}
|
||||
_ = u
|
||||
_ = f
|
||||
_ = b
|
||||
_ = s
|
||||
_ = x
|
||||
}
|
||||
|
||||
func BenchmarkAttrString(b *testing.B) {
|
||||
var (
|
||||
is string
|
||||
u string
|
||||
f string
|
||||
bn string
|
||||
s string
|
||||
x string
|
||||
ds string
|
||||
p = &is
|
||||
d time.Duration
|
||||
)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
is = Int64("key", 1).String()
|
||||
u = Uint64("key", 1).String()
|
||||
f = Float64("key", 1).String()
|
||||
bn = Bool("key", true).String()
|
||||
s = String("key", "foo").String()
|
||||
ds = Duration("key", d).String()
|
||||
x = Any("key", p).String()
|
||||
}
|
||||
_ = u
|
||||
_ = f
|
||||
_ = bn
|
||||
_ = s
|
||||
_ = x
|
||||
_ = ds
|
||||
_ = p
|
||||
}
|
||||
322
src/log/slog/doc.go
Normal file
322
src/log/slog/doc.go
Normal file
@@ -0,0 +1,322 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package slog provides structured logging,
|
||||
in which log records include a message,
|
||||
a severity level, and various other attributes
|
||||
expressed as key-value pairs.
|
||||
|
||||
It defines a type, [Logger],
|
||||
which provides several methods (such as [Logger.Info] and [Logger.Error])
|
||||
for reporting events of interest.
|
||||
|
||||
Each Logger is associated with a [Handler].
|
||||
A Logger output method creates a [Record] from the method arguments
|
||||
and passes it to the Handler, which decides how to handle it.
|
||||
There is a default Logger accessible through top-level functions
|
||||
(such as [Info] and [Error]) that call the corresponding Logger methods.
|
||||
|
||||
A log record consists of a time, a level, a message, and a set of key-value
|
||||
pairs, where the keys are strings and the values may be of any type.
|
||||
As an example,
|
||||
|
||||
slog.Info("hello", "count", 3)
|
||||
|
||||
creates a record containing the time of the call,
|
||||
a level of Info, the message "hello", and a single
|
||||
pair with key "count" and value 3.
|
||||
|
||||
The [Info] top-level function calls the [Logger.Info] method on the default Logger.
|
||||
In addition to [Logger.Info], there are methods for Debug, Warn and Error levels.
|
||||
Besides these convenience methods for common levels,
|
||||
there is also a [Logger.Log] method which takes the level as an argument.
|
||||
Each of these methods has a corresponding top-level function that uses the
|
||||
default logger.
|
||||
|
||||
The default handler formats the log record's message, time, level, and attributes
|
||||
as a string and passes it to the [log] package.
|
||||
|
||||
2022/11/08 15:28:26 INFO hello count=3
|
||||
|
||||
For more control over the output format, create a logger with a different handler.
|
||||
This statement uses [New] to create a new logger with a [TextHandler]
|
||||
that writes structured records in text form to standard error:
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||
|
||||
[TextHandler] output is a sequence of key=value pairs, easily and unambiguously
|
||||
parsed by machine. This statement:
|
||||
|
||||
logger.Info("hello", "count", 3)
|
||||
|
||||
produces this output:
|
||||
|
||||
time=2022-11-08T15:28:26.000-05:00 level=INFO msg=hello count=3
|
||||
|
||||
The package also provides [JSONHandler], whose output is line-delimited JSON:
|
||||
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
logger.Info("hello", "count", 3)
|
||||
|
||||
produces this output:
|
||||
|
||||
{"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3}
|
||||
|
||||
Both [TextHandler] and [JSONHandler] can be configured with [HandlerOptions].
|
||||
There are options for setting the minimum level (see Levels, below),
|
||||
displaying the source file and line of the log call, and
|
||||
modifying attributes before they are logged.
|
||||
|
||||
Setting a logger as the default with
|
||||
|
||||
slog.SetDefault(logger)
|
||||
|
||||
will cause the top-level functions like [Info] to use it.
|
||||
[SetDefault] also updates the default logger used by the [log] package,
|
||||
so that existing applications that use [log.Printf] and related functions
|
||||
will send log records to the logger's handler without needing to be rewritten.
|
||||
|
||||
Some attributes are common to many log calls.
|
||||
For example, you may wish to include the URL or trace identifier of a server request
|
||||
with all log events arising from the request.
|
||||
Rather than repeat the attribute with every log call, you can use [Logger.With]
|
||||
to construct a new Logger containing the attributes:
|
||||
|
||||
logger2 := logger.With("url", r.URL)
|
||||
|
||||
The arguments to With are the same key-value pairs used in [Logger.Info].
|
||||
The result is a new Logger with the same handler as the original, but additional
|
||||
attributes that will appear in the output of every call.
|
||||
|
||||
# Levels
|
||||
|
||||
A [Level] is an integer representing the importance or severity of a log event.
|
||||
The higher the level, the more severe the event.
|
||||
This package defines constants for the most common levels,
|
||||
but any int can be used as a level.
|
||||
|
||||
In an application, you may wish to log messages only at a certain level or greater.
|
||||
One common configuration is to log messages at Info or higher levels,
|
||||
suppressing debug logging until it is needed.
|
||||
The built-in handlers can be configured with the minimum level to output by
|
||||
setting [HandlerOptions.Level].
|
||||
The program's `main` function typically does this.
|
||||
The default value is LevelInfo.
|
||||
|
||||
Setting the [HandlerOptions.Level] field to a [Level] value
|
||||
fixes the handler's minimum level throughout its lifetime.
|
||||
Setting it to a [LevelVar] allows the level to be varied dynamically.
|
||||
A LevelVar holds a Level and is safe to read or write from multiple
|
||||
goroutines.
|
||||
To vary the level dynamically for an entire program, first initialize
|
||||
a global LevelVar:
|
||||
|
||||
var programLevel = new(slog.LevelVar) // Info by default
|
||||
|
||||
Then use the LevelVar to construct a handler, and make it the default:
|
||||
|
||||
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})
|
||||
slog.SetDefault(slog.New(h))
|
||||
|
||||
Now the program can change its logging level with a single statement:
|
||||
|
||||
programLevel.Set(slog.LevelDebug)
|
||||
|
||||
# Groups
|
||||
|
||||
Attributes can be collected into groups.
|
||||
A group has a name that is used to qualify the names of its attributes.
|
||||
How this qualification is displayed depends on the handler.
|
||||
[TextHandler] separates the group and attribute names with a dot.
|
||||
[JSONHandler] treats each group as a separate JSON object, with the group name as the key.
|
||||
|
||||
Use [Group] to create a Group attribute from a name and a list of key-value pairs:
|
||||
|
||||
slog.Group("request",
|
||||
"method", r.Method,
|
||||
"url", r.URL)
|
||||
|
||||
TextHandler would display this group as
|
||||
|
||||
request.method=GET request.url=http://example.com
|
||||
|
||||
JSONHandler would display it as
|
||||
|
||||
"request":{"method":"GET","url":"http://example.com"}
|
||||
|
||||
Use [Logger.WithGroup] to qualify all of a Logger's output
|
||||
with a group name. Calling WithGroup on a Logger results in a
|
||||
new Logger with the same Handler as the original, but with all
|
||||
its attributes qualified by the group name.
|
||||
|
||||
This can help prevent duplicate attribute keys in large systems,
|
||||
where subsystems might use the same keys.
|
||||
Pass each subsystem a different Logger with its own group name so that
|
||||
potential duplicates are qualified:
|
||||
|
||||
logger := slog.Default().With("id", systemID)
|
||||
parserLogger := logger.WithGroup("parser")
|
||||
parseInput(input, parserLogger)
|
||||
|
||||
When parseInput logs with parserLogger, its keys will be qualified with "parser",
|
||||
so even if it uses the common key "id", the log line will have distinct keys.
|
||||
|
||||
# Contexts
|
||||
|
||||
Some handlers may wish to include information from the [context.Context] that is
|
||||
available at the call site. One example of such information
|
||||
is the identifier for the current span when tracing is enabled.
|
||||
|
||||
The [Logger.Log] and [Logger.LogAttrs] methods take a context as a first
|
||||
argument, as do their corresponding top-level functions.
|
||||
|
||||
Although the convenience methods on Logger (Info and so on) and the
|
||||
corresponding top-level functions do not take a context, the alternatives ending
|
||||
in "Context" do. For example,
|
||||
|
||||
slog.InfoContext(ctx, "message")
|
||||
|
||||
It is recommended to pass a context to an output method if one is available.
|
||||
|
||||
# Attrs and Values
|
||||
|
||||
An [Attr] is a key-value pair. The Logger output methods accept Attrs as well as
|
||||
alternating keys and values. The statement
|
||||
|
||||
slog.Info("hello", slog.Int("count", 3))
|
||||
|
||||
behaves the same as
|
||||
|
||||
slog.Info("hello", "count", 3)
|
||||
|
||||
There are convenience constructors for [Attr] such as [Int], [String], and [Bool]
|
||||
for common types, as well as the function [Any] for constructing Attrs of any
|
||||
type.
|
||||
|
||||
The value part of an Attr is a type called [Value].
|
||||
Like an [any], a Value can hold any Go value,
|
||||
but it can represent typical values, including all numbers and strings,
|
||||
without an allocation.
|
||||
|
||||
For the most efficient log output, use [Logger.LogAttrs].
|
||||
It is similar to [Logger.Log] but accepts only Attrs, not alternating
|
||||
keys and values; this allows it, too, to avoid allocation.
|
||||
|
||||
The call
|
||||
|
||||
logger.LogAttrs(ctx, slog.LevelInfo, "hello", slog.Int("count", 3))
|
||||
|
||||
is the most efficient way to achieve the same output as
|
||||
|
||||
slog.InfoContext(ctx, "hello", "count", 3)
|
||||
|
||||
# Customizing a type's logging behavior
|
||||
|
||||
If a type implements the [LogValuer] interface, the [Value] returned from its LogValue
|
||||
method is used for logging. You can use this to control how values of the type
|
||||
appear in logs. For example, you can redact secret information like passwords,
|
||||
or gather a struct's fields in a Group. See the examples under [LogValuer] for
|
||||
details.
|
||||
|
||||
A LogValue method may return a Value that itself implements [LogValuer]. The [Value.Resolve]
|
||||
method handles these cases carefully, avoiding infinite loops and unbounded recursion.
|
||||
Handler authors and others may wish to use [Value.Resolve] instead of calling LogValue directly.
|
||||
|
||||
# Wrapping output methods
|
||||
|
||||
The logger functions use reflection over the call stack to find the file name
|
||||
and line number of the logging call within the application. This can produce
|
||||
incorrect source information for functions that wrap slog. For instance, if you
|
||||
define this function in file mylog.go:
|
||||
|
||||
func Infof(logger *slog.Logger, format string, args ...any) {
|
||||
logger.Info(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
and you call it like this in main.go:
|
||||
|
||||
Infof(slog.Default(), "hello, %s", "world")
|
||||
|
||||
then slog will report the source file as mylog.go, not main.go.
|
||||
|
||||
A correct implementation of Infof will obtain the source location
|
||||
(pc) and pass it to NewRecord.
|
||||
The Infof function in the package-level example called "wrapping"
|
||||
demonstrates how to do this.
|
||||
|
||||
# Working with Records
|
||||
|
||||
Sometimes a Handler will need to modify a Record
|
||||
before passing it on to another Handler or backend.
|
||||
A Record contains a mixture of simple public fields (e.g. Time, Level, Message)
|
||||
and hidden fields that refer to state (such as attributes) indirectly. This
|
||||
means that modifying a simple copy of a Record (e.g. by calling
|
||||
[Record.Add] or [Record.AddAttrs] to add attributes)
|
||||
may have unexpected effects on the original.
|
||||
Before modifying a Record, use [Record.Clone] to
|
||||
create a copy that shares no state with the original,
|
||||
or create a new Record with [NewRecord]
|
||||
and build up its Attrs by traversing the old ones with [Record.Attrs].
|
||||
|
||||
# Performance considerations
|
||||
|
||||
If profiling your application demonstrates that logging is taking significant time,
|
||||
the following suggestions may help.
|
||||
|
||||
If many log lines have a common attribute, use [Logger.With] to create a Logger with
|
||||
that attribute. The built-in handlers will format that attribute only once, at the
|
||||
call to [Logger.With]. The [Handler] interface is designed to allow that optimization,
|
||||
and a well-written Handler should take advantage of it.
|
||||
|
||||
The arguments to a log call are always evaluated, even if the log event is discarded.
|
||||
If possible, defer computation so that it happens only if the value is actually logged.
|
||||
For example, consider the call
|
||||
|
||||
slog.Info("starting request", "url", r.URL.String()) // may compute String unnecessarily
|
||||
|
||||
The URL.String method will be called even if the logger discards Info-level events.
|
||||
Instead, pass the URL directly:
|
||||
|
||||
slog.Info("starting request", "url", &r.URL) // calls URL.String only if needed
|
||||
|
||||
The built-in [TextHandler] will call its String method, but only
|
||||
if the log event is enabled.
|
||||
Avoiding the call to String also preserves the structure of the underlying value.
|
||||
For example [JSONHandler] emits the components of the parsed URL as a JSON object.
|
||||
If you want to avoid eagerly paying the cost of the String call
|
||||
without causing the handler to potentially inspect the structure of the value,
|
||||
wrap the value in a fmt.Stringer implementation that hides its Marshal methods.
|
||||
|
||||
You can also use the [LogValuer] interface to avoid unnecessary work in disabled log
|
||||
calls. Say you need to log some expensive value:
|
||||
|
||||
slog.Debug("frobbing", "value", computeExpensiveValue(arg))
|
||||
|
||||
Even if this line is disabled, computeExpensiveValue will be called.
|
||||
To avoid that, define a type implementing LogValuer:
|
||||
|
||||
type expensive struct { arg int }
|
||||
|
||||
func (e expensive) LogValue() slog.Value {
|
||||
return slog.AnyValue(computeExpensiveValue(e.arg))
|
||||
}
|
||||
|
||||
Then use a value of that type in log calls:
|
||||
|
||||
slog.Debug("frobbing", "value", expensive{arg})
|
||||
|
||||
Now computeExpensiveValue will only be called when the line is enabled.
|
||||
|
||||
The built-in handlers acquire a lock before calling [io.Writer.Write]
|
||||
to ensure that exactly one [Record] is written at a time in its entirety.
|
||||
Although each log record has a timestamp,
|
||||
the built-in handlers do not use that time to sort the written records.
|
||||
User-defined handlers are responsible for their own locking and sorting.
|
||||
|
||||
# Writing a handler
|
||||
|
||||
For a guide to writing a custom handler, see https://golang.org/s/slog-handler-guide.
|
||||
*/
|
||||
package slog
|
||||
93
src/log/slog/example_custom_levels_test.go
Normal file
93
src/log/slog/example_custom_levels_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
// This example demonstrates using custom log levels and custom log level names.
|
||||
// In addition to the default log levels, it introduces Trace, Notice, and
|
||||
// Emergency levels. The ReplaceAttr changes the way levels are printed for both
|
||||
// the standard log levels and the custom log levels.
|
||||
func ExampleHandlerOptions_customLevels() {
|
||||
// Exported constants from a custom logging package.
|
||||
const (
|
||||
LevelTrace = slog.Level(-8)
|
||||
LevelDebug = slog.LevelDebug
|
||||
LevelInfo = slog.LevelInfo
|
||||
LevelNotice = slog.Level(2)
|
||||
LevelWarning = slog.LevelWarn
|
||||
LevelError = slog.LevelError
|
||||
LevelEmergency = slog.Level(12)
|
||||
)
|
||||
|
||||
th := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
// Set a custom level to show all log output. The default value is
|
||||
// LevelInfo, which would drop Debug and Trace logs.
|
||||
Level: LevelTrace,
|
||||
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
// Remove time from the output for predictable test output.
|
||||
if a.Key == slog.TimeKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
|
||||
// Customize the name of the level key and the output string, including
|
||||
// custom level values.
|
||||
if a.Key == slog.LevelKey {
|
||||
// Rename the level key from "level" to "sev".
|
||||
a.Key = "sev"
|
||||
|
||||
// Handle custom level values.
|
||||
level := a.Value.Any().(slog.Level)
|
||||
|
||||
// This could also look up the name from a map or other structure, but
|
||||
// this demonstrates using a switch statement to rename levels. For
|
||||
// maximum performance, the string values should be constants, but this
|
||||
// example uses the raw strings for readability.
|
||||
switch {
|
||||
case level < LevelDebug:
|
||||
a.Value = slog.StringValue("TRACE")
|
||||
case level < LevelInfo:
|
||||
a.Value = slog.StringValue("DEBUG")
|
||||
case level < LevelNotice:
|
||||
a.Value = slog.StringValue("INFO")
|
||||
case level < LevelWarning:
|
||||
a.Value = slog.StringValue("NOTICE")
|
||||
case level < LevelError:
|
||||
a.Value = slog.StringValue("WARNING")
|
||||
case level < LevelEmergency:
|
||||
a.Value = slog.StringValue("ERROR")
|
||||
default:
|
||||
a.Value = slog.StringValue("EMERGENCY")
|
||||
}
|
||||
}
|
||||
|
||||
return a
|
||||
},
|
||||
})
|
||||
|
||||
logger := slog.New(th)
|
||||
ctx := context.Background()
|
||||
logger.Log(ctx, LevelEmergency, "missing pilots")
|
||||
logger.Error("failed to start engines", "err", "missing fuel")
|
||||
logger.Warn("falling back to default value")
|
||||
logger.Log(ctx, LevelNotice, "all systems are running")
|
||||
logger.Info("initiating launch")
|
||||
logger.Debug("starting background job")
|
||||
logger.Log(ctx, LevelTrace, "button clicked")
|
||||
|
||||
// Output:
|
||||
// sev=EMERGENCY msg="missing pilots"
|
||||
// sev=ERROR msg="failed to start engines" err="missing fuel"
|
||||
// sev=WARNING msg="falling back to default value"
|
||||
// sev=NOTICE msg="all systems are running"
|
||||
// sev=INFO msg="initiating launch"
|
||||
// sev=DEBUG msg="starting background job"
|
||||
// sev=TRACE msg="button clicked"
|
||||
}
|
||||
73
src/log/slog/example_level_handler_test.go
Normal file
73
src/log/slog/example_level_handler_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"log/slog/internal/slogtest"
|
||||
"os"
|
||||
)
|
||||
|
||||
// A LevelHandler wraps a Handler with an Enabled method
|
||||
// that returns false for levels below a minimum.
|
||||
type LevelHandler struct {
|
||||
level slog.Leveler
|
||||
handler slog.Handler
|
||||
}
|
||||
|
||||
// NewLevelHandler returns a LevelHandler with the given level.
|
||||
// All methods except Enabled delegate to h.
|
||||
func NewLevelHandler(level slog.Leveler, h slog.Handler) *LevelHandler {
|
||||
// Optimization: avoid chains of LevelHandlers.
|
||||
if lh, ok := h.(*LevelHandler); ok {
|
||||
h = lh.Handler()
|
||||
}
|
||||
return &LevelHandler{level, h}
|
||||
}
|
||||
|
||||
// Enabled implements Handler.Enabled by reporting whether
|
||||
// level is at least as large as h's level.
|
||||
func (h *LevelHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return level >= h.level.Level()
|
||||
}
|
||||
|
||||
// Handle implements Handler.Handle.
|
||||
func (h *LevelHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
return h.handler.Handle(ctx, r)
|
||||
}
|
||||
|
||||
// WithAttrs implements Handler.WithAttrs.
|
||||
func (h *LevelHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return NewLevelHandler(h.level, h.handler.WithAttrs(attrs))
|
||||
}
|
||||
|
||||
// WithGroup implements Handler.WithGroup.
|
||||
func (h *LevelHandler) WithGroup(name string) slog.Handler {
|
||||
return NewLevelHandler(h.level, h.handler.WithGroup(name))
|
||||
}
|
||||
|
||||
// Handler returns the Handler wrapped by h.
|
||||
func (h *LevelHandler) Handler() slog.Handler {
|
||||
return h.handler
|
||||
}
|
||||
|
||||
// This example shows how to Use a LevelHandler to change the level of an
|
||||
// existing Handler while preserving its other behavior.
|
||||
//
|
||||
// This example demonstrates increasing the log level to reduce a logger's
|
||||
// output.
|
||||
//
|
||||
// Another typical use would be to decrease the log level (to LevelDebug, say)
|
||||
// during a part of the program that was suspected of containing a bug.
|
||||
func ExampleHandler_levelHandler() {
|
||||
th := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: slogtest.RemoveTime})
|
||||
logger := slog.New(NewLevelHandler(slog.LevelWarn, th))
|
||||
logger.Info("not printed")
|
||||
logger.Warn("printed")
|
||||
|
||||
// Output:
|
||||
// level=WARN msg=printed
|
||||
}
|
||||
58
src/log/slog/example_log_level_test.go
Normal file
58
src/log/slog/example_log_level_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
"log/slog/internal/slogtest"
|
||||
"os"
|
||||
)
|
||||
|
||||
// This example shows how to use slog.SetLogLoggerLevel to change the minimal level
|
||||
// of the internal default handler for slog package before calling slog.SetDefault.
|
||||
func ExampleSetLogLoggerLevel_log() {
|
||||
defer log.SetFlags(log.Flags()) // revert changes after the example
|
||||
log.SetFlags(0)
|
||||
defer log.SetOutput(log.Writer()) // revert changes after the example
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
// Default logging level is slog.LevelInfo.
|
||||
log.Print("log debug") // log debug
|
||||
slog.Debug("debug") // no output
|
||||
slog.Info("info") // INFO info
|
||||
|
||||
// Set the default logging level to slog.LevelDebug.
|
||||
currentLogLevel := slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
defer slog.SetLogLoggerLevel(currentLogLevel) // revert changes after the example
|
||||
|
||||
log.Print("log debug") // log debug
|
||||
slog.Debug("debug") // DEBUG debug
|
||||
slog.Info("info") // INFO info
|
||||
|
||||
// Output:
|
||||
// log debug
|
||||
// INFO info
|
||||
// log debug
|
||||
// DEBUG debug
|
||||
// INFO info
|
||||
}
|
||||
|
||||
// This example shows how to use slog.SetLogLoggerLevel to change the minimal level
|
||||
// of the internal writer that uses the custom handler for log package after
|
||||
// calling slog.SetDefault.
|
||||
func ExampleSetLogLoggerLevel_slog() {
|
||||
// Set the default logging level to slog.LevelError.
|
||||
currentLogLevel := slog.SetLogLoggerLevel(slog.LevelError)
|
||||
defer slog.SetLogLoggerLevel(currentLogLevel) // revert changes after the example
|
||||
|
||||
defer slog.SetDefault(slog.Default()) // revert changes after the example
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: slogtest.RemoveTime})))
|
||||
|
||||
log.Print("error") // level=ERROR msg=error
|
||||
|
||||
// Output:
|
||||
// level=ERROR msg=error
|
||||
}
|
||||
35
src/log/slog/example_logvaluer_group_test.go
Normal file
35
src/log/slog/example_logvaluer_group_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog_test
|
||||
|
||||
import "log/slog"
|
||||
|
||||
type Name struct {
|
||||
First, Last string
|
||||
}
|
||||
|
||||
// LogValue implements slog.LogValuer.
|
||||
// It returns a group containing the fields of
|
||||
// the Name, so that they appear together in the log output.
|
||||
func (n Name) LogValue() slog.Value {
|
||||
return slog.GroupValue(
|
||||
slog.String("first", n.First),
|
||||
slog.String("last", n.Last))
|
||||
}
|
||||
|
||||
func ExampleLogValuer_group() {
|
||||
n := Name{"Perry", "Platypus"}
|
||||
slog.Info("mission accomplished", "agent", n)
|
||||
|
||||
// JSON Output would look in part like:
|
||||
// {
|
||||
// ...
|
||||
// "msg": "mission accomplished",
|
||||
// "agent": {
|
||||
// "first": "Perry",
|
||||
// "last": "Platypus"
|
||||
// }
|
||||
// }
|
||||
}
|
||||
31
src/log/slog/example_logvaluer_secret_test.go
Normal file
31
src/log/slog/example_logvaluer_secret_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog_test
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"log/slog/internal/slogtest"
|
||||
"os"
|
||||
)
|
||||
|
||||
// A token is a secret value that grants permissions.
|
||||
type Token string
|
||||
|
||||
// LogValue implements slog.LogValuer.
|
||||
// It avoids revealing the token.
|
||||
func (Token) LogValue() slog.Value {
|
||||
return slog.StringValue("REDACTED_TOKEN")
|
||||
}
|
||||
|
||||
// This example demonstrates a Value that replaces itself
|
||||
// with an alternative representation to avoid revealing secrets.
|
||||
func ExampleLogValuer_secret() {
|
||||
t := Token("shhhh!")
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: slogtest.RemoveTime}))
|
||||
logger.Info("permission granted", "user", "Perry", "token", t)
|
||||
|
||||
// Output:
|
||||
// level=INFO msg="permission granted" user=Perry token=REDACTED_TOKEN
|
||||
}
|
||||
37
src/log/slog/example_test.go
Normal file
37
src/log/slog/example_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog_test
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ExampleGroup() {
|
||||
r, _ := http.NewRequest("GET", "localhost", nil)
|
||||
// ...
|
||||
|
||||
logger := slog.New(
|
||||
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Key == slog.TimeKey && len(groups) == 0 {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}),
|
||||
)
|
||||
logger.Info("finished",
|
||||
slog.Group("req",
|
||||
slog.String("method", r.Method),
|
||||
slog.String("url", r.URL.String())),
|
||||
slog.Int("status", http.StatusOK),
|
||||
slog.Duration("duration", time.Second))
|
||||
|
||||
// Output:
|
||||
// level=INFO msg=finished req.method=GET req.url=localhost status=200 duration=1s
|
||||
}
|
||||
47
src/log/slog/example_wrap_test.go
Normal file
47
src/log/slog/example_wrap_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Infof is an example of a user-defined logging function that wraps slog.
|
||||
// The log record contains the source position of the caller of Infof.
|
||||
func Infof(logger *slog.Logger, format string, args ...any) {
|
||||
if !logger.Enabled(context.Background(), slog.LevelInfo) {
|
||||
return
|
||||
}
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(2, pcs[:]) // skip [Callers, Infof]
|
||||
r := slog.NewRecord(time.Now(), slog.LevelInfo, fmt.Sprintf(format, args...), pcs[0])
|
||||
_ = logger.Handler().Handle(context.Background(), r)
|
||||
}
|
||||
|
||||
func Example_wrapping() {
|
||||
replace := func(groups []string, a slog.Attr) slog.Attr {
|
||||
// Remove time.
|
||||
if a.Key == slog.TimeKey && len(groups) == 0 {
|
||||
return slog.Attr{}
|
||||
}
|
||||
// Remove the directory from the source's filename.
|
||||
if a.Key == slog.SourceKey {
|
||||
source := a.Value.Any().(*slog.Source)
|
||||
source.File = filepath.Base(source.File)
|
||||
}
|
||||
return a
|
||||
}
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: true, ReplaceAttr: replace}))
|
||||
Infof(logger, "message, %s", "formatted")
|
||||
|
||||
// Output:
|
||||
// level=INFO source=example_wrap_test.go:43 msg="message, formatted"
|
||||
}
|
||||
604
src/log/slog/handler.go
Normal file
604
src/log/slog/handler.go
Normal file
@@ -0,0 +1,604 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog/internal/buffer"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Handler handles log records produced by a Logger.
|
||||
//
|
||||
// A typical handler may print log records to standard error,
|
||||
// or write them to a file or database, or perhaps augment them
|
||||
// with additional attributes and pass them on to another handler.
|
||||
//
|
||||
// Any of the Handler's methods may be called concurrently with itself
|
||||
// or with other methods. It is the responsibility of the Handler to
|
||||
// manage this concurrency.
|
||||
//
|
||||
// Users of the slog package should not invoke Handler methods directly.
|
||||
// They should use the methods of [Logger] instead.
|
||||
type Handler interface {
|
||||
// Enabled reports whether the handler handles records at the given level.
|
||||
// The handler ignores records whose level is lower.
|
||||
// It is called early, before any arguments are processed,
|
||||
// to save effort if the log event should be discarded.
|
||||
// If called from a Logger method, the first argument is the context
|
||||
// passed to that method, or context.Background() if nil was passed
|
||||
// or the method does not take a context.
|
||||
// The context is passed so Enabled can use its values
|
||||
// to make a decision.
|
||||
Enabled(context.Context, Level) bool
|
||||
|
||||
// Handle handles the Record.
|
||||
// It will only be called when Enabled returns true.
|
||||
// The Context argument is as for Enabled.
|
||||
// It is present solely to provide Handlers access to the context's values.
|
||||
// Canceling the context should not affect record processing.
|
||||
// (Among other things, log messages may be necessary to debug a
|
||||
// cancellation-related problem.)
|
||||
//
|
||||
// Handle methods that produce output should observe the following rules:
|
||||
// - If r.Time is the zero time, ignore the time.
|
||||
// - If r.PC is zero, ignore it.
|
||||
// - Attr's values should be resolved.
|
||||
// - If an Attr's key and value are both the zero value, ignore the Attr.
|
||||
// This can be tested with attr.Equal(Attr{}).
|
||||
// - If a group's key is empty, inline the group's Attrs.
|
||||
// - If a group has no Attrs (even if it has a non-empty key),
|
||||
// ignore it.
|
||||
Handle(context.Context, Record) error
|
||||
|
||||
// WithAttrs returns a new Handler whose attributes consist of
|
||||
// both the receiver's attributes and the arguments.
|
||||
// The Handler owns the slice: it may retain, modify or discard it.
|
||||
WithAttrs(attrs []Attr) Handler
|
||||
|
||||
// WithGroup returns a new Handler with the given group appended to
|
||||
// the receiver's existing groups.
|
||||
// The keys of all subsequent attributes, whether added by With or in a
|
||||
// Record, should be qualified by the sequence of group names.
|
||||
//
|
||||
// How this qualification happens is up to the Handler, so long as
|
||||
// this Handler's attribute keys differ from those of another Handler
|
||||
// with a different sequence of group names.
|
||||
//
|
||||
// A Handler should treat WithGroup as starting a Group of Attrs that ends
|
||||
// at the end of the log event. That is,
|
||||
//
|
||||
// logger.WithGroup("s").LogAttrs(ctx, level, msg, slog.Int("a", 1), slog.Int("b", 2))
|
||||
//
|
||||
// should behave like
|
||||
//
|
||||
// logger.LogAttrs(ctx, level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2)))
|
||||
//
|
||||
// If the name is empty, WithGroup returns the receiver.
|
||||
WithGroup(name string) Handler
|
||||
}
|
||||
|
||||
type defaultHandler struct {
|
||||
ch *commonHandler
|
||||
// internal.DefaultOutput, except for testing
|
||||
output func(pc uintptr, data []byte) error
|
||||
}
|
||||
|
||||
func newDefaultHandler(output func(uintptr, []byte) error) *defaultHandler {
|
||||
return &defaultHandler{
|
||||
ch: &commonHandler{json: false},
|
||||
output: output,
|
||||
}
|
||||
}
|
||||
|
||||
func (*defaultHandler) Enabled(_ context.Context, l Level) bool {
|
||||
return l >= logLoggerLevel.Level()
|
||||
}
|
||||
|
||||
// Collect the level, attributes and message in a string and
|
||||
// write it with the default log.Logger.
|
||||
// Let the log.Logger handle time and file/line.
|
||||
func (h *defaultHandler) Handle(ctx context.Context, r Record) error {
|
||||
buf := buffer.New()
|
||||
buf.WriteString(r.Level.String())
|
||||
buf.WriteByte(' ')
|
||||
buf.WriteString(r.Message)
|
||||
state := h.ch.newHandleState(buf, true, " ")
|
||||
defer state.free()
|
||||
state.appendNonBuiltIns(r)
|
||||
return h.output(r.PC, *buf)
|
||||
}
|
||||
|
||||
func (h *defaultHandler) WithAttrs(as []Attr) Handler {
|
||||
return &defaultHandler{h.ch.withAttrs(as), h.output}
|
||||
}
|
||||
|
||||
func (h *defaultHandler) WithGroup(name string) Handler {
|
||||
return &defaultHandler{h.ch.withGroup(name), h.output}
|
||||
}
|
||||
|
||||
// HandlerOptions are options for a [TextHandler] or [JSONHandler].
|
||||
// A zero HandlerOptions consists entirely of default values.
|
||||
type HandlerOptions struct {
|
||||
// AddSource causes the handler to compute the source code position
|
||||
// of the log statement and add a SourceKey attribute to the output.
|
||||
AddSource bool
|
||||
|
||||
// Level reports the minimum record level that will be logged.
|
||||
// The handler discards records with lower levels.
|
||||
// If Level is nil, the handler assumes LevelInfo.
|
||||
// The handler calls Level.Level for each record processed;
|
||||
// to adjust the minimum level dynamically, use a LevelVar.
|
||||
Level Leveler
|
||||
|
||||
// ReplaceAttr is called to rewrite each non-group attribute before it is logged.
|
||||
// The attribute's value has been resolved (see [Value.Resolve]).
|
||||
// If ReplaceAttr returns a zero Attr, the attribute is discarded.
|
||||
//
|
||||
// The built-in attributes with keys "time", "level", "source", and "msg"
|
||||
// are passed to this function, except that time is omitted
|
||||
// if zero, and source is omitted if AddSource is false.
|
||||
//
|
||||
// The first argument is a list of currently open groups that contain the
|
||||
// Attr. It must not be retained or modified. ReplaceAttr is never called
|
||||
// for Group attributes, only their contents. For example, the attribute
|
||||
// list
|
||||
//
|
||||
// Int("a", 1), Group("g", Int("b", 2)), Int("c", 3)
|
||||
//
|
||||
// results in consecutive calls to ReplaceAttr with the following arguments:
|
||||
//
|
||||
// nil, Int("a", 1)
|
||||
// []string{"g"}, Int("b", 2)
|
||||
// nil, Int("c", 3)
|
||||
//
|
||||
// ReplaceAttr can be used to change the default keys of the built-in
|
||||
// attributes, convert types (for example, to replace a `time.Time` with the
|
||||
// integer seconds since the Unix epoch), sanitize personal information, or
|
||||
// remove attributes from the output.
|
||||
ReplaceAttr func(groups []string, a Attr) Attr
|
||||
}
|
||||
|
||||
// Keys for "built-in" attributes.
|
||||
const (
|
||||
// TimeKey is the key used by the built-in handlers for the time
|
||||
// when the log method is called. The associated Value is a [time.Time].
|
||||
TimeKey = "time"
|
||||
// LevelKey is the key used by the built-in handlers for the level
|
||||
// of the log call. The associated value is a [Level].
|
||||
LevelKey = "level"
|
||||
// MessageKey is the key used by the built-in handlers for the
|
||||
// message of the log call. The associated value is a string.
|
||||
MessageKey = "msg"
|
||||
// SourceKey is the key used by the built-in handlers for the source file
|
||||
// and line of the log call. The associated value is a *[Source].
|
||||
SourceKey = "source"
|
||||
)
|
||||
|
||||
type commonHandler struct {
|
||||
json bool // true => output JSON; false => output text
|
||||
opts HandlerOptions
|
||||
preformattedAttrs []byte
|
||||
// groupPrefix is for the text handler only.
|
||||
// It holds the prefix for groups that were already pre-formatted.
|
||||
// A group will appear here when a call to WithGroup is followed by
|
||||
// a call to WithAttrs.
|
||||
groupPrefix string
|
||||
groups []string // all groups started from WithGroup
|
||||
nOpenGroups int // the number of groups opened in preformattedAttrs
|
||||
mu *sync.Mutex
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (h *commonHandler) clone() *commonHandler {
|
||||
// We can't use assignment because we can't copy the mutex.
|
||||
return &commonHandler{
|
||||
json: h.json,
|
||||
opts: h.opts,
|
||||
preformattedAttrs: slices.Clip(h.preformattedAttrs),
|
||||
groupPrefix: h.groupPrefix,
|
||||
groups: slices.Clip(h.groups),
|
||||
nOpenGroups: h.nOpenGroups,
|
||||
w: h.w,
|
||||
mu: h.mu, // mutex shared among all clones of this handler
|
||||
}
|
||||
}
|
||||
|
||||
// enabled reports whether l is greater than or equal to the
|
||||
// minimum level.
|
||||
func (h *commonHandler) enabled(l Level) bool {
|
||||
minLevel := LevelInfo
|
||||
if h.opts.Level != nil {
|
||||
minLevel = h.opts.Level.Level()
|
||||
}
|
||||
return l >= minLevel
|
||||
}
|
||||
|
||||
func (h *commonHandler) withAttrs(as []Attr) *commonHandler {
|
||||
// We are going to ignore empty groups, so if the entire slice consists of
|
||||
// them, there is nothing to do.
|
||||
if countEmptyGroups(as) == len(as) {
|
||||
return h
|
||||
}
|
||||
h2 := h.clone()
|
||||
// Pre-format the attributes as an optimization.
|
||||
state := h2.newHandleState((*buffer.Buffer)(&h2.preformattedAttrs), false, "")
|
||||
defer state.free()
|
||||
state.prefix.WriteString(h.groupPrefix)
|
||||
if pfa := h2.preformattedAttrs; len(pfa) > 0 {
|
||||
state.sep = h.attrSep()
|
||||
if h2.json && pfa[len(pfa)-1] == '{' {
|
||||
state.sep = ""
|
||||
}
|
||||
}
|
||||
// Remember the position in the buffer, in case all attrs are empty.
|
||||
pos := state.buf.Len()
|
||||
state.openGroups()
|
||||
if !state.appendAttrs(as) {
|
||||
state.buf.SetLen(pos)
|
||||
} else {
|
||||
// Remember the new prefix for later keys.
|
||||
h2.groupPrefix = state.prefix.String()
|
||||
// Remember how many opened groups are in preformattedAttrs,
|
||||
// so we don't open them again when we handle a Record.
|
||||
h2.nOpenGroups = len(h2.groups)
|
||||
}
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *commonHandler) withGroup(name string) *commonHandler {
|
||||
h2 := h.clone()
|
||||
h2.groups = append(h2.groups, name)
|
||||
return h2
|
||||
}
|
||||
|
||||
// handle is the internal implementation of Handler.Handle
|
||||
// used by TextHandler and JSONHandler.
|
||||
func (h *commonHandler) handle(r Record) error {
|
||||
state := h.newHandleState(buffer.New(), true, "")
|
||||
defer state.free()
|
||||
if h.json {
|
||||
state.buf.WriteByte('{')
|
||||
}
|
||||
// Built-in attributes. They are not in a group.
|
||||
stateGroups := state.groups
|
||||
state.groups = nil // So ReplaceAttrs sees no groups instead of the pre groups.
|
||||
rep := h.opts.ReplaceAttr
|
||||
// time
|
||||
if !r.Time.IsZero() {
|
||||
key := TimeKey
|
||||
val := r.Time.Round(0) // strip monotonic to match Attr behavior
|
||||
if rep == nil {
|
||||
state.appendKey(key)
|
||||
state.appendTime(val)
|
||||
} else {
|
||||
state.appendAttr(Time(key, val))
|
||||
}
|
||||
}
|
||||
// level
|
||||
key := LevelKey
|
||||
val := r.Level
|
||||
if rep == nil {
|
||||
state.appendKey(key)
|
||||
state.appendString(val.String())
|
||||
} else {
|
||||
state.appendAttr(Any(key, val))
|
||||
}
|
||||
// source
|
||||
if h.opts.AddSource {
|
||||
state.appendAttr(Any(SourceKey, r.source()))
|
||||
}
|
||||
key = MessageKey
|
||||
msg := r.Message
|
||||
if rep == nil {
|
||||
state.appendKey(key)
|
||||
state.appendString(msg)
|
||||
} else {
|
||||
state.appendAttr(String(key, msg))
|
||||
}
|
||||
state.groups = stateGroups // Restore groups passed to ReplaceAttrs.
|
||||
state.appendNonBuiltIns(r)
|
||||
state.buf.WriteByte('\n')
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
_, err := h.w.Write(*state.buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *handleState) appendNonBuiltIns(r Record) {
|
||||
// preformatted Attrs
|
||||
if pfa := s.h.preformattedAttrs; len(pfa) > 0 {
|
||||
s.buf.WriteString(s.sep)
|
||||
s.buf.Write(pfa)
|
||||
s.sep = s.h.attrSep()
|
||||
if s.h.json && pfa[len(pfa)-1] == '{' {
|
||||
s.sep = ""
|
||||
}
|
||||
}
|
||||
// Attrs in Record -- unlike the built-in ones, they are in groups started
|
||||
// from WithGroup.
|
||||
// If the record has no Attrs, don't output any groups.
|
||||
nOpenGroups := s.h.nOpenGroups
|
||||
if r.NumAttrs() > 0 {
|
||||
s.prefix.WriteString(s.h.groupPrefix)
|
||||
// The group may turn out to be empty even though it has attrs (for
|
||||
// example, ReplaceAttr may delete all the attrs).
|
||||
// So remember where we are in the buffer, to restore the position
|
||||
// later if necessary.
|
||||
pos := s.buf.Len()
|
||||
s.openGroups()
|
||||
nOpenGroups = len(s.h.groups)
|
||||
empty := true
|
||||
r.Attrs(func(a Attr) bool {
|
||||
if s.appendAttr(a) {
|
||||
empty = false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if empty {
|
||||
s.buf.SetLen(pos)
|
||||
nOpenGroups = s.h.nOpenGroups
|
||||
}
|
||||
}
|
||||
if s.h.json {
|
||||
// Close all open groups.
|
||||
for range s.h.groups[:nOpenGroups] {
|
||||
s.buf.WriteByte('}')
|
||||
}
|
||||
// Close the top-level object.
|
||||
s.buf.WriteByte('}')
|
||||
}
|
||||
}
|
||||
|
||||
// attrSep returns the separator between attributes.
|
||||
func (h *commonHandler) attrSep() string {
|
||||
if h.json {
|
||||
return ","
|
||||
}
|
||||
return " "
|
||||
}
|
||||
|
||||
// handleState holds state for a single call to commonHandler.handle.
|
||||
// The initial value of sep determines whether to emit a separator
|
||||
// before the next key, after which it stays true.
|
||||
type handleState struct {
|
||||
h *commonHandler
|
||||
buf *buffer.Buffer
|
||||
freeBuf bool // should buf be freed?
|
||||
sep string // separator to write before next key
|
||||
prefix *buffer.Buffer // for text: key prefix
|
||||
groups *[]string // pool-allocated slice of active groups, for ReplaceAttr
|
||||
}
|
||||
|
||||
var groupPool = sync.Pool{New: func() any {
|
||||
s := make([]string, 0, 10)
|
||||
return &s
|
||||
}}
|
||||
|
||||
func (h *commonHandler) newHandleState(buf *buffer.Buffer, freeBuf bool, sep string) handleState {
|
||||
s := handleState{
|
||||
h: h,
|
||||
buf: buf,
|
||||
freeBuf: freeBuf,
|
||||
sep: sep,
|
||||
prefix: buffer.New(),
|
||||
}
|
||||
if h.opts.ReplaceAttr != nil {
|
||||
s.groups = groupPool.Get().(*[]string)
|
||||
*s.groups = append(*s.groups, h.groups[:h.nOpenGroups]...)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *handleState) free() {
|
||||
if s.freeBuf {
|
||||
s.buf.Free()
|
||||
}
|
||||
if gs := s.groups; gs != nil {
|
||||
*gs = (*gs)[:0]
|
||||
groupPool.Put(gs)
|
||||
}
|
||||
s.prefix.Free()
|
||||
}
|
||||
|
||||
func (s *handleState) openGroups() {
|
||||
for _, n := range s.h.groups[s.h.nOpenGroups:] {
|
||||
s.openGroup(n)
|
||||
}
|
||||
}
|
||||
|
||||
// Separator for group names and keys.
|
||||
const keyComponentSep = '.'
|
||||
|
||||
// openGroup starts a new group of attributes
|
||||
// with the given name.
|
||||
func (s *handleState) openGroup(name string) {
|
||||
if s.h.json {
|
||||
s.appendKey(name)
|
||||
s.buf.WriteByte('{')
|
||||
s.sep = ""
|
||||
} else {
|
||||
s.prefix.WriteString(name)
|
||||
s.prefix.WriteByte(keyComponentSep)
|
||||
}
|
||||
// Collect group names for ReplaceAttr.
|
||||
if s.groups != nil {
|
||||
*s.groups = append(*s.groups, name)
|
||||
}
|
||||
}
|
||||
|
||||
// closeGroup ends the group with the given name.
|
||||
func (s *handleState) closeGroup(name string) {
|
||||
if s.h.json {
|
||||
s.buf.WriteByte('}')
|
||||
} else {
|
||||
(*s.prefix) = (*s.prefix)[:len(*s.prefix)-len(name)-1 /* for keyComponentSep */]
|
||||
}
|
||||
s.sep = s.h.attrSep()
|
||||
if s.groups != nil {
|
||||
*s.groups = (*s.groups)[:len(*s.groups)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// appendAttrs appends the slice of Attrs.
|
||||
// It reports whether something was appended.
|
||||
func (s *handleState) appendAttrs(as []Attr) bool {
|
||||
nonEmpty := false
|
||||
for _, a := range as {
|
||||
if s.appendAttr(a) {
|
||||
nonEmpty = true
|
||||
}
|
||||
}
|
||||
return nonEmpty
|
||||
}
|
||||
|
||||
// appendAttr appends the Attr's key and value.
|
||||
// It handles replacement and checking for an empty key.
|
||||
// It reports whether something was appended.
|
||||
func (s *handleState) appendAttr(a Attr) bool {
|
||||
a.Value = a.Value.Resolve()
|
||||
if rep := s.h.opts.ReplaceAttr; rep != nil && a.Value.Kind() != KindGroup {
|
||||
var gs []string
|
||||
if s.groups != nil {
|
||||
gs = *s.groups
|
||||
}
|
||||
// a.Value is resolved before calling ReplaceAttr, so the user doesn't have to.
|
||||
a = rep(gs, a)
|
||||
// The ReplaceAttr function may return an unresolved Attr.
|
||||
a.Value = a.Value.Resolve()
|
||||
}
|
||||
// Elide empty Attrs.
|
||||
if a.isEmpty() {
|
||||
return false
|
||||
}
|
||||
// Special case: Source.
|
||||
if v := a.Value; v.Kind() == KindAny {
|
||||
if src, ok := v.Any().(*Source); ok {
|
||||
if s.h.json {
|
||||
a.Value = src.group()
|
||||
} else {
|
||||
a.Value = StringValue(fmt.Sprintf("%s:%d", src.File, src.Line))
|
||||
}
|
||||
}
|
||||
}
|
||||
if a.Value.Kind() == KindGroup {
|
||||
attrs := a.Value.Group()
|
||||
// Output only non-empty groups.
|
||||
if len(attrs) > 0 {
|
||||
// The group may turn out to be empty even though it has attrs (for
|
||||
// example, ReplaceAttr may delete all the attrs).
|
||||
// So remember where we are in the buffer, to restore the position
|
||||
// later if necessary.
|
||||
pos := s.buf.Len()
|
||||
// Inline a group with an empty key.
|
||||
if a.Key != "" {
|
||||
s.openGroup(a.Key)
|
||||
}
|
||||
if !s.appendAttrs(attrs) {
|
||||
s.buf.SetLen(pos)
|
||||
return false
|
||||
}
|
||||
if a.Key != "" {
|
||||
s.closeGroup(a.Key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.appendKey(a.Key)
|
||||
s.appendValue(a.Value)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *handleState) appendError(err error) {
|
||||
s.appendString(fmt.Sprintf("!ERROR:%v", err))
|
||||
}
|
||||
|
||||
func (s *handleState) appendKey(key string) {
|
||||
s.buf.WriteString(s.sep)
|
||||
if s.prefix != nil && len(*s.prefix) > 0 {
|
||||
// TODO: optimize by avoiding allocation.
|
||||
s.appendString(string(*s.prefix) + key)
|
||||
} else {
|
||||
s.appendString(key)
|
||||
}
|
||||
if s.h.json {
|
||||
s.buf.WriteByte(':')
|
||||
} else {
|
||||
s.buf.WriteByte('=')
|
||||
}
|
||||
s.sep = s.h.attrSep()
|
||||
}
|
||||
|
||||
func (s *handleState) appendString(str string) {
|
||||
if s.h.json {
|
||||
s.buf.WriteByte('"')
|
||||
*s.buf = appendEscapedJSONString(*s.buf, str)
|
||||
s.buf.WriteByte('"')
|
||||
} else {
|
||||
// text
|
||||
if needsQuoting(str) {
|
||||
*s.buf = strconv.AppendQuote(*s.buf, str)
|
||||
} else {
|
||||
s.buf.WriteString(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *handleState) appendValue(v Value) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// If it panics with a nil pointer, the most likely cases are
|
||||
// an encoding.TextMarshaler or error fails to guard against nil,
|
||||
// in which case "<nil>" seems to be the feasible choice.
|
||||
//
|
||||
// Adapted from the code in fmt/print.go.
|
||||
if v := reflect.ValueOf(v.any); v.Kind() == reflect.Pointer && v.IsNil() {
|
||||
s.appendString("<nil>")
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise just print the original panic message.
|
||||
s.appendString(fmt.Sprintf("!PANIC: %v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
var err error
|
||||
if s.h.json {
|
||||
err = appendJSONValue(s, v)
|
||||
} else {
|
||||
err = appendTextValue(s, v)
|
||||
}
|
||||
if err != nil {
|
||||
s.appendError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *handleState) appendTime(t time.Time) {
|
||||
if s.h.json {
|
||||
appendJSONTime(s, t)
|
||||
} else {
|
||||
*s.buf = appendRFC3339Millis(*s.buf, t)
|
||||
}
|
||||
}
|
||||
|
||||
func appendRFC3339Millis(b []byte, t time.Time) []byte {
|
||||
// Format according to time.RFC3339Nano since it is highly optimized,
|
||||
// but truncate it to use millisecond resolution.
|
||||
// Unfortunately, that format trims trailing 0s, so add 1/10 millisecond
|
||||
// to guarantee that there are exactly 4 digits after the period.
|
||||
const prefixLen = len("2006-01-02T15:04:05.000")
|
||||
n := len(b)
|
||||
t = t.Truncate(time.Millisecond).Add(time.Millisecond / 10)
|
||||
b = t.AppendFormat(b, time.RFC3339Nano)
|
||||
b = append(b[:n+prefixLen], b[n+prefixLen+1:]...) // drop the 4th digit
|
||||
return b
|
||||
}
|
||||
713
src/log/slog/handler_test.go
Normal file
713
src/log/slog/handler_test.go
Normal file
@@ -0,0 +1,713 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// TODO: verify that the output of Marshal{Text,JSON} is suitably escaped.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDefaultHandle(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
preAttrs := []Attr{Int("pre", 0)}
|
||||
attrs := []Attr{Int("a", 1), String("b", "two")}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
with func(Handler) Handler
|
||||
attrs []Attr
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no attrs",
|
||||
want: "INFO message",
|
||||
},
|
||||
{
|
||||
name: "attrs",
|
||||
attrs: attrs,
|
||||
want: "INFO message a=1 b=two",
|
||||
},
|
||||
{
|
||||
name: "preformatted",
|
||||
with: func(h Handler) Handler { return h.WithAttrs(preAttrs) },
|
||||
attrs: attrs,
|
||||
want: "INFO message pre=0 a=1 b=two",
|
||||
},
|
||||
{
|
||||
name: "groups",
|
||||
attrs: []Attr{
|
||||
Int("a", 1),
|
||||
Group("g",
|
||||
Int("b", 2),
|
||||
Group("h", Int("c", 3)),
|
||||
Int("d", 4)),
|
||||
Int("e", 5),
|
||||
},
|
||||
want: "INFO message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
|
||||
},
|
||||
{
|
||||
name: "group",
|
||||
with: func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
|
||||
attrs: attrs,
|
||||
want: "INFO message pre=0 s.a=1 s.b=two",
|
||||
},
|
||||
{
|
||||
name: "preformatted groups",
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithAttrs([]Attr{Int("p1", 1)}).
|
||||
WithGroup("s1").
|
||||
WithAttrs([]Attr{Int("p2", 2)}).
|
||||
WithGroup("s2")
|
||||
},
|
||||
attrs: attrs,
|
||||
want: "INFO message p1=1 s1.p2=2 s1.s2.a=1 s1.s2.b=two",
|
||||
},
|
||||
{
|
||||
name: "two with-groups",
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithAttrs([]Attr{Int("p1", 1)}).
|
||||
WithGroup("s1").
|
||||
WithGroup("s2")
|
||||
},
|
||||
attrs: attrs,
|
||||
want: "INFO message p1=1 s1.s2.a=1 s1.s2.b=two",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var got string
|
||||
var h Handler = newDefaultHandler(func(_ uintptr, b []byte) error {
|
||||
got = string(b)
|
||||
return nil
|
||||
})
|
||||
if test.with != nil {
|
||||
h = test.with(h)
|
||||
}
|
||||
r := NewRecord(time.Time{}, LevelInfo, "message", 0)
|
||||
r.AddAttrs(test.attrs...)
|
||||
if err := h.Handle(ctx, r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Errorf("\ngot %s\nwant %s", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentWrites(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
count := 1000
|
||||
for _, handlerType := range []string{"text", "json"} {
|
||||
t.Run(handlerType, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
var h Handler
|
||||
switch handlerType {
|
||||
case "text":
|
||||
h = NewTextHandler(&buf, nil)
|
||||
case "json":
|
||||
h = NewJSONHandler(&buf, nil)
|
||||
default:
|
||||
t.Fatalf("unexpected handlerType %q", handlerType)
|
||||
}
|
||||
sub1 := h.WithAttrs([]Attr{Bool("sub1", true)})
|
||||
sub2 := h.WithAttrs([]Attr{Bool("sub2", true)})
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < count; i++ {
|
||||
sub1Record := NewRecord(time.Time{}, LevelInfo, "hello from sub1", 0)
|
||||
sub1Record.AddAttrs(Int("i", i))
|
||||
sub2Record := NewRecord(time.Time{}, LevelInfo, "hello from sub2", 0)
|
||||
sub2Record.AddAttrs(Int("i", i))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := sub1.Handle(ctx, sub1Record); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := sub2.Handle(ctx, sub2Record); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
for i := 1; i <= 2; i++ {
|
||||
want := "hello from sub" + strconv.Itoa(i)
|
||||
n := strings.Count(buf.String(), want)
|
||||
if n != count {
|
||||
t.Fatalf("want %d occurrences of %q, got %d", count, want, n)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the common parts of TextHandler and JSONHandler.
|
||||
func TestJSONAndTextHandlers(t *testing.T) {
|
||||
// remove all Attrs
|
||||
removeAll := func(_ []string, a Attr) Attr { return Attr{} }
|
||||
|
||||
attrs := []Attr{String("a", "one"), Int("b", 2), Any("", nil)}
|
||||
preAttrs := []Attr{Int("pre", 3), String("x", "y")}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
replace func([]string, Attr) Attr
|
||||
addSource bool
|
||||
with func(Handler) Handler
|
||||
preAttrs []Attr
|
||||
attrs []Attr
|
||||
wantText string
|
||||
wantJSON string
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
attrs: attrs,
|
||||
wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2",
|
||||
wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2}`,
|
||||
},
|
||||
{
|
||||
name: "empty key",
|
||||
attrs: append(slices.Clip(attrs), Any("", "v")),
|
||||
wantText: `time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2 ""=v`,
|
||||
wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2,"":"v"}`,
|
||||
},
|
||||
{
|
||||
name: "cap keys",
|
||||
replace: upperCaseKey,
|
||||
attrs: attrs,
|
||||
wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message A=one B=2",
|
||||
wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","A":"one","B":2}`,
|
||||
},
|
||||
{
|
||||
name: "remove all",
|
||||
replace: removeAll,
|
||||
attrs: attrs,
|
||||
wantText: "",
|
||||
wantJSON: `{}`,
|
||||
},
|
||||
{
|
||||
name: "preformatted",
|
||||
with: func(h Handler) Handler { return h.WithAttrs(preAttrs) },
|
||||
preAttrs: preAttrs,
|
||||
attrs: attrs,
|
||||
wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message pre=3 x=y a=one b=2",
|
||||
wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","pre":3,"x":"y","a":"one","b":2}`,
|
||||
},
|
||||
{
|
||||
name: "preformatted cap keys",
|
||||
replace: upperCaseKey,
|
||||
with: func(h Handler) Handler { return h.WithAttrs(preAttrs) },
|
||||
preAttrs: preAttrs,
|
||||
attrs: attrs,
|
||||
wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message PRE=3 X=y A=one B=2",
|
||||
wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","PRE":3,"X":"y","A":"one","B":2}`,
|
||||
},
|
||||
{
|
||||
name: "preformatted remove all",
|
||||
replace: removeAll,
|
||||
with: func(h Handler) Handler { return h.WithAttrs(preAttrs) },
|
||||
preAttrs: preAttrs,
|
||||
attrs: attrs,
|
||||
wantText: "",
|
||||
wantJSON: "{}",
|
||||
},
|
||||
{
|
||||
name: "remove built-in",
|
||||
replace: removeKeys(TimeKey, LevelKey, MessageKey),
|
||||
attrs: attrs,
|
||||
wantText: "a=one b=2",
|
||||
wantJSON: `{"a":"one","b":2}`,
|
||||
},
|
||||
{
|
||||
name: "preformatted remove built-in",
|
||||
replace: removeKeys(TimeKey, LevelKey, MessageKey),
|
||||
with: func(h Handler) Handler { return h.WithAttrs(preAttrs) },
|
||||
attrs: attrs,
|
||||
wantText: "pre=3 x=y a=one b=2",
|
||||
wantJSON: `{"pre":3,"x":"y","a":"one","b":2}`,
|
||||
},
|
||||
{
|
||||
name: "groups",
|
||||
replace: removeKeys(TimeKey, LevelKey), // to simplify the result
|
||||
attrs: []Attr{
|
||||
Int("a", 1),
|
||||
Group("g",
|
||||
Int("b", 2),
|
||||
Group("h", Int("c", 3)),
|
||||
Int("d", 4)),
|
||||
Int("e", 5),
|
||||
},
|
||||
wantText: "msg=message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
|
||||
wantJSON: `{"msg":"message","a":1,"g":{"b":2,"h":{"c":3},"d":4},"e":5}`,
|
||||
},
|
||||
{
|
||||
name: "empty group",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
attrs: []Attr{Group("g"), Group("h", Int("a", 1))},
|
||||
wantText: "msg=message h.a=1",
|
||||
wantJSON: `{"msg":"message","h":{"a":1}}`,
|
||||
},
|
||||
{
|
||||
name: "nested empty group",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
attrs: []Attr{
|
||||
Group("g",
|
||||
Group("h",
|
||||
Group("i"), Group("j"))),
|
||||
},
|
||||
wantText: `msg=message`,
|
||||
wantJSON: `{"msg":"message"}`,
|
||||
},
|
||||
{
|
||||
name: "nested non-empty group",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
attrs: []Attr{
|
||||
Group("g",
|
||||
Group("h",
|
||||
Group("i"), Group("j", Int("a", 1)))),
|
||||
},
|
||||
wantText: `msg=message g.h.j.a=1`,
|
||||
wantJSON: `{"msg":"message","g":{"h":{"j":{"a":1}}}}`,
|
||||
},
|
||||
{
|
||||
name: "escapes",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
attrs: []Attr{
|
||||
String("a b", "x\t\n\000y"),
|
||||
Group(" b.c=\"\\x2E\t",
|
||||
String("d=e", "f.g\""),
|
||||
Int("m.d", 1)), // dot is not escaped
|
||||
},
|
||||
wantText: `msg=message "a b"="x\t\n\x00y" " b.c=\"\\x2E\t.d=e"="f.g\"" " b.c=\"\\x2E\t.m.d"=1`,
|
||||
wantJSON: `{"msg":"message","a b":"x\t\n\u0000y"," b.c=\"\\x2E\t":{"d=e":"f.g\"","m.d":1}}`,
|
||||
},
|
||||
{
|
||||
name: "LogValuer",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
attrs: []Attr{
|
||||
Int("a", 1),
|
||||
Any("name", logValueName{"Ren", "Hoek"}),
|
||||
Int("b", 2),
|
||||
},
|
||||
wantText: "msg=message a=1 name.first=Ren name.last=Hoek b=2",
|
||||
wantJSON: `{"msg":"message","a":1,"name":{"first":"Ren","last":"Hoek"},"b":2}`,
|
||||
},
|
||||
{
|
||||
// Test resolution when there is no ReplaceAttr function.
|
||||
name: "resolve",
|
||||
attrs: []Attr{
|
||||
Any("", &replace{Value{}}), // should be elided
|
||||
Any("name", logValueName{"Ren", "Hoek"}),
|
||||
},
|
||||
wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message name.first=Ren name.last=Hoek",
|
||||
wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","name":{"first":"Ren","last":"Hoek"}}`,
|
||||
},
|
||||
{
|
||||
name: "with-group",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
with: func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
|
||||
attrs: attrs,
|
||||
wantText: "msg=message pre=3 x=y s.a=one s.b=2",
|
||||
wantJSON: `{"msg":"message","pre":3,"x":"y","s":{"a":"one","b":2}}`,
|
||||
},
|
||||
{
|
||||
name: "preformatted with-groups",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithAttrs([]Attr{Int("p1", 1)}).
|
||||
WithGroup("s1").
|
||||
WithAttrs([]Attr{Int("p2", 2)}).
|
||||
WithGroup("s2").
|
||||
WithAttrs([]Attr{Int("p3", 3)})
|
||||
},
|
||||
attrs: attrs,
|
||||
wantText: "msg=message p1=1 s1.p2=2 s1.s2.p3=3 s1.s2.a=one s1.s2.b=2",
|
||||
wantJSON: `{"msg":"message","p1":1,"s1":{"p2":2,"s2":{"p3":3,"a":"one","b":2}}}`,
|
||||
},
|
||||
{
|
||||
name: "two with-groups",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithAttrs([]Attr{Int("p1", 1)}).
|
||||
WithGroup("s1").
|
||||
WithGroup("s2")
|
||||
},
|
||||
attrs: attrs,
|
||||
wantText: "msg=message p1=1 s1.s2.a=one s1.s2.b=2",
|
||||
wantJSON: `{"msg":"message","p1":1,"s1":{"s2":{"a":"one","b":2}}}`,
|
||||
},
|
||||
{
|
||||
name: "empty with-groups",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithGroup("x").WithGroup("y")
|
||||
},
|
||||
wantText: "msg=message",
|
||||
wantJSON: `{"msg":"message"}`,
|
||||
},
|
||||
{
|
||||
name: "empty with-groups, no non-empty attrs",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithGroup("x").WithAttrs([]Attr{Group("g")}).WithGroup("y")
|
||||
},
|
||||
wantText: "msg=message",
|
||||
wantJSON: `{"msg":"message"}`,
|
||||
},
|
||||
{
|
||||
name: "one empty with-group",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithGroup("x").WithAttrs([]Attr{Int("a", 1)}).WithGroup("y")
|
||||
},
|
||||
attrs: []Attr{Group("g", Group("h"))},
|
||||
wantText: "msg=message x.a=1",
|
||||
wantJSON: `{"msg":"message","x":{"a":1}}`,
|
||||
},
|
||||
{
|
||||
name: "GroupValue as Attr value",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
attrs: []Attr{{"v", AnyValue(IntValue(3))}},
|
||||
wantText: "msg=message v=3",
|
||||
wantJSON: `{"msg":"message","v":3}`,
|
||||
},
|
||||
{
|
||||
name: "byte slice",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
attrs: []Attr{Any("bs", []byte{1, 2, 3, 4})},
|
||||
wantText: `msg=message bs="\x01\x02\x03\x04"`,
|
||||
wantJSON: `{"msg":"message","bs":"AQIDBA=="}`,
|
||||
},
|
||||
{
|
||||
name: "json.RawMessage",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
attrs: []Attr{Any("bs", json.RawMessage([]byte("1234")))},
|
||||
wantText: `msg=message bs="1234"`,
|
||||
wantJSON: `{"msg":"message","bs":1234}`,
|
||||
},
|
||||
{
|
||||
name: "inline group",
|
||||
replace: removeKeys(TimeKey, LevelKey),
|
||||
attrs: []Attr{
|
||||
Int("a", 1),
|
||||
Group("", Int("b", 2), Int("c", 3)),
|
||||
Int("d", 4),
|
||||
},
|
||||
wantText: `msg=message a=1 b=2 c=3 d=4`,
|
||||
wantJSON: `{"msg":"message","a":1,"b":2,"c":3,"d":4}`,
|
||||
},
|
||||
{
|
||||
name: "Source",
|
||||
replace: func(gs []string, a Attr) Attr {
|
||||
if a.Key == SourceKey {
|
||||
s := a.Value.Any().(*Source)
|
||||
s.File = filepath.Base(s.File)
|
||||
return Any(a.Key, s)
|
||||
}
|
||||
return removeKeys(TimeKey, LevelKey)(gs, a)
|
||||
},
|
||||
addSource: true,
|
||||
wantText: `source=handler_test.go:$LINE msg=message`,
|
||||
wantJSON: `{"source":{"function":"log/slog.TestJSONAndTextHandlers","file":"handler_test.go","line":$LINE},"msg":"message"}`,
|
||||
},
|
||||
{
|
||||
name: "replace built-in with group",
|
||||
replace: func(_ []string, a Attr) Attr {
|
||||
if a.Key == TimeKey {
|
||||
return Group(TimeKey, "mins", 3, "secs", 2)
|
||||
}
|
||||
if a.Key == LevelKey {
|
||||
return Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
wantText: `time.mins=3 time.secs=2 msg=message`,
|
||||
wantJSON: `{"time":{"mins":3,"secs":2},"msg":"message"}`,
|
||||
},
|
||||
{
|
||||
name: "replace empty",
|
||||
replace: func([]string, Attr) Attr { return Attr{} },
|
||||
attrs: []Attr{Group("g", Int("a", 1))},
|
||||
wantText: "",
|
||||
wantJSON: `{}`,
|
||||
},
|
||||
{
|
||||
name: "replace empty 1",
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)})
|
||||
},
|
||||
replace: func([]string, Attr) Attr { return Attr{} },
|
||||
attrs: []Attr{Group("h", Int("b", 2))},
|
||||
wantText: "",
|
||||
wantJSON: `{}`,
|
||||
},
|
||||
{
|
||||
name: "replace empty 2",
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
|
||||
},
|
||||
replace: func([]string, Attr) Attr { return Attr{} },
|
||||
attrs: []Attr{Group("i", Int("c", 3))},
|
||||
wantText: "",
|
||||
wantJSON: `{}`,
|
||||
},
|
||||
{
|
||||
name: "replace empty 3",
|
||||
with: func(h Handler) Handler { return h.WithGroup("g") },
|
||||
replace: func([]string, Attr) Attr { return Attr{} },
|
||||
attrs: []Attr{Int("a", 1)},
|
||||
wantText: "",
|
||||
wantJSON: `{}`,
|
||||
},
|
||||
{
|
||||
name: "replace empty inline",
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
|
||||
},
|
||||
replace: func([]string, Attr) Attr { return Attr{} },
|
||||
attrs: []Attr{Group("", Int("c", 3))},
|
||||
wantText: "",
|
||||
wantJSON: `{}`,
|
||||
},
|
||||
{
|
||||
name: "replace partial empty attrs 1",
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
|
||||
},
|
||||
replace: func(groups []string, attr Attr) Attr {
|
||||
return removeKeys(TimeKey, LevelKey, MessageKey, "a")(groups, attr)
|
||||
},
|
||||
attrs: []Attr{Group("i", Int("c", 3))},
|
||||
wantText: "g.h.b=2 g.h.i.c=3",
|
||||
wantJSON: `{"g":{"h":{"b":2,"i":{"c":3}}}}`,
|
||||
},
|
||||
{
|
||||
name: "replace partial empty attrs 2",
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithAttrs([]Attr{Int("n", 4)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
|
||||
},
|
||||
replace: func(groups []string, attr Attr) Attr {
|
||||
return removeKeys(TimeKey, LevelKey, MessageKey, "a", "b")(groups, attr)
|
||||
},
|
||||
attrs: []Attr{Group("i", Int("c", 3))},
|
||||
wantText: "g.n=4 g.h.i.c=3",
|
||||
wantJSON: `{"g":{"n":4,"h":{"i":{"c":3}}}}`,
|
||||
},
|
||||
{
|
||||
name: "replace partial empty attrs 3",
|
||||
with: func(h Handler) Handler {
|
||||
return h.WithGroup("g").WithAttrs([]Attr{Int("x", 0)}).WithAttrs([]Attr{Int("a", 1)}).WithAttrs([]Attr{Int("n", 4)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
|
||||
},
|
||||
replace: func(groups []string, attr Attr) Attr {
|
||||
return removeKeys(TimeKey, LevelKey, MessageKey, "a", "c")(groups, attr)
|
||||
},
|
||||
attrs: []Attr{Group("i", Int("c", 3))},
|
||||
wantText: "g.x=0 g.n=4 g.h.b=2",
|
||||
wantJSON: `{"g":{"x":0,"n":4,"h":{"b":2}}}`,
|
||||
},
|
||||
{
|
||||
name: "replace resolved group",
|
||||
replace: func(groups []string, a Attr) Attr {
|
||||
if a.Value.Kind() == KindGroup {
|
||||
return Attr{"bad", IntValue(1)}
|
||||
}
|
||||
return removeKeys(TimeKey, LevelKey, MessageKey)(groups, a)
|
||||
},
|
||||
attrs: []Attr{Any("name", logValueName{"Perry", "Platypus"})},
|
||||
wantText: "name.first=Perry name.last=Platypus",
|
||||
wantJSON: `{"name":{"first":"Perry","last":"Platypus"}}`,
|
||||
},
|
||||
} {
|
||||
r := NewRecord(testTime, LevelInfo, "message", callerPC(2))
|
||||
line := strconv.Itoa(r.source().Line)
|
||||
r.AddAttrs(test.attrs...)
|
||||
var buf bytes.Buffer
|
||||
opts := HandlerOptions{ReplaceAttr: test.replace, AddSource: test.addSource}
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for _, handler := range []struct {
|
||||
name string
|
||||
h Handler
|
||||
want string
|
||||
}{
|
||||
{"text", NewTextHandler(&buf, &opts), test.wantText},
|
||||
{"json", NewJSONHandler(&buf, &opts), test.wantJSON},
|
||||
} {
|
||||
t.Run(handler.name, func(t *testing.T) {
|
||||
h := handler.h
|
||||
if test.with != nil {
|
||||
h = test.with(h)
|
||||
}
|
||||
buf.Reset()
|
||||
if err := h.Handle(nil, r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := strings.ReplaceAll(handler.want, "$LINE", line)
|
||||
got := strings.TrimSuffix(buf.String(), "\n")
|
||||
if got != want {
|
||||
t.Errorf("\ngot %s\nwant %s\n", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// removeKeys returns a function suitable for HandlerOptions.ReplaceAttr
|
||||
// that removes all Attrs with the given keys.
|
||||
func removeKeys(keys ...string) func([]string, Attr) Attr {
|
||||
return func(_ []string, a Attr) Attr {
|
||||
for _, k := range keys {
|
||||
if a.Key == k {
|
||||
return Attr{}
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
func upperCaseKey(_ []string, a Attr) Attr {
|
||||
a.Key = strings.ToUpper(a.Key)
|
||||
return a
|
||||
}
|
||||
|
||||
type logValueName struct {
|
||||
first, last string
|
||||
}
|
||||
|
||||
func (n logValueName) LogValue() Value {
|
||||
return GroupValue(
|
||||
String("first", n.first),
|
||||
String("last", n.last))
|
||||
}
|
||||
|
||||
func TestHandlerEnabled(t *testing.T) {
|
||||
levelVar := func(l Level) *LevelVar {
|
||||
var al LevelVar
|
||||
al.Set(l)
|
||||
return &al
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
leveler Leveler
|
||||
want bool
|
||||
}{
|
||||
{nil, true},
|
||||
{LevelWarn, false},
|
||||
{&LevelVar{}, true}, // defaults to Info
|
||||
{levelVar(LevelWarn), false},
|
||||
{LevelDebug, true},
|
||||
{levelVar(LevelDebug), true},
|
||||
} {
|
||||
h := &commonHandler{opts: HandlerOptions{Level: test.leveler}}
|
||||
got := h.enabled(LevelInfo)
|
||||
if got != test.want {
|
||||
t.Errorf("%v: got %t, want %t", test.leveler, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecondWith(t *testing.T) {
|
||||
// Verify that a second call to Logger.With does not corrupt
|
||||
// the original.
|
||||
var buf bytes.Buffer
|
||||
h := NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeKeys(TimeKey)})
|
||||
logger := New(h).With(
|
||||
String("app", "playground"),
|
||||
String("role", "tester"),
|
||||
Int("data_version", 2),
|
||||
)
|
||||
appLogger := logger.With("type", "log") // this becomes type=met
|
||||
_ = logger.With("type", "metric")
|
||||
appLogger.Info("foo")
|
||||
got := strings.TrimSpace(buf.String())
|
||||
want := `level=INFO msg=foo app=playground role=tester data_version=2 type=log`
|
||||
if got != want {
|
||||
t.Errorf("\ngot %s\nwant %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceAttrGroups(t *testing.T) {
|
||||
// Verify that ReplaceAttr is called with the correct groups.
|
||||
type ga struct {
|
||||
groups string
|
||||
key string
|
||||
val string
|
||||
}
|
||||
|
||||
var got []ga
|
||||
|
||||
h := NewTextHandler(io.Discard, &HandlerOptions{ReplaceAttr: func(gs []string, a Attr) Attr {
|
||||
v := a.Value.String()
|
||||
if a.Key == TimeKey {
|
||||
v = "<now>"
|
||||
}
|
||||
got = append(got, ga{strings.Join(gs, ","), a.Key, v})
|
||||
return a
|
||||
}})
|
||||
New(h).
|
||||
With(Int("a", 1)).
|
||||
WithGroup("g1").
|
||||
With(Int("b", 2)).
|
||||
WithGroup("g2").
|
||||
With(
|
||||
Int("c", 3),
|
||||
Group("g3", Int("d", 4)),
|
||||
Int("e", 5)).
|
||||
Info("m",
|
||||
Int("f", 6),
|
||||
Group("g4", Int("h", 7)),
|
||||
Int("i", 8))
|
||||
|
||||
want := []ga{
|
||||
{"", "a", "1"},
|
||||
{"g1", "b", "2"},
|
||||
{"g1,g2", "c", "3"},
|
||||
{"g1,g2,g3", "d", "4"},
|
||||
{"g1,g2", "e", "5"},
|
||||
{"", "time", "<now>"},
|
||||
{"", "level", "INFO"},
|
||||
{"", "msg", "m"},
|
||||
{"g1,g2", "f", "6"},
|
||||
{"g1,g2,g4", "h", "7"},
|
||||
{"g1,g2", "i", "8"},
|
||||
}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("\ngot %v\nwant %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
const rfc3339Millis = "2006-01-02T15:04:05.000Z07:00"
|
||||
|
||||
func TestWriteTimeRFC3339(t *testing.T) {
|
||||
for _, tm := range []time.Time{
|
||||
time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC),
|
||||
time.Date(2000, 1, 2, 3, 4, 5, 400, time.Local),
|
||||
time.Date(2000, 11, 12, 3, 4, 500, 5e7, time.UTC),
|
||||
} {
|
||||
got := string(appendRFC3339Millis(nil, tm))
|
||||
want := tm.Format(rfc3339Millis)
|
||||
if got != want {
|
||||
t.Errorf("got %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteTime(b *testing.B) {
|
||||
tm := time.Date(2022, 3, 4, 5, 6, 7, 823456789, time.Local)
|
||||
b.ResetTimer()
|
||||
var buf []byte
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf = appendRFC3339Millis(buf[:0], tm)
|
||||
}
|
||||
}
|
||||
50
src/log/slog/internal/benchmarks/benchmarks.go
Normal file
50
src/log/slog/internal/benchmarks/benchmarks.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package benchmarks contains benchmarks for slog.
|
||||
//
|
||||
// These benchmarks are loosely based on github.com/uber-go/zap/benchmarks.
|
||||
// They have the following desirable properties:
|
||||
//
|
||||
// - They test a complete log event, from the user's call to its return.
|
||||
//
|
||||
// - The benchmarked code is run concurrently in multiple goroutines, to
|
||||
// better simulate a real server (the most common environment for structured
|
||||
// logs).
|
||||
//
|
||||
// - Some handlers are optimistic versions of real handlers, doing real-world
|
||||
// tasks as fast as possible (and sometimes faster, in that an
|
||||
// implementation may not be concurrency-safe). This gives us an upper bound
|
||||
// on handler performance, so we can evaluate the (handler-independent) core
|
||||
// activity of the package in an end-to-end context without concern that a
|
||||
// slow handler implementation is skewing the results.
|
||||
//
|
||||
// - We also test the built-in handlers, for comparison.
|
||||
package benchmarks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testMessage = "Test logging, but use a somewhat realistic message length."
|
||||
|
||||
var (
|
||||
testTime = time.Date(2022, time.May, 1, 0, 0, 0, 0, time.UTC)
|
||||
testString = "7e3b3b2aaeff56a7108fe11e154200dd/7819479873059528190"
|
||||
testInt = 32768
|
||||
testDuration = 23 * time.Second
|
||||
testError = errors.New("fail")
|
||||
)
|
||||
|
||||
var testAttrs = []slog.Attr{
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
}
|
||||
|
||||
const wantText = "time=1651363200 level=0 msg=Test logging, but use a somewhat realistic message length. string=7e3b3b2aaeff56a7108fe11e154200dd/7819479873059528190 status=32768 duration=23000000000 time=1651363200 error=fail\n"
|
||||
152
src/log/slog/internal/benchmarks/benchmarks_test.go
Normal file
152
src/log/slog/internal/benchmarks/benchmarks_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package benchmarks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"internal/race"
|
||||
"io"
|
||||
"log/slog"
|
||||
"log/slog/internal"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&internal.IgnorePC, "nopc", false, "do not invoke runtime.Callers")
|
||||
}
|
||||
|
||||
// We pass Attrs inline because it affects allocations: building
|
||||
// up a list outside of the benchmarked code and passing it in with "..."
|
||||
// reduces measured allocations.
|
||||
|
||||
func BenchmarkAttrs(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
for _, handler := range []struct {
|
||||
name string
|
||||
h slog.Handler
|
||||
skipRace bool
|
||||
}{
|
||||
{"disabled", disabledHandler{}, false},
|
||||
{"async discard", newAsyncHandler(), true},
|
||||
{"fastText discard", newFastTextHandler(io.Discard), false},
|
||||
{"Text discard", slog.NewTextHandler(io.Discard, nil), false},
|
||||
{"JSON discard", slog.NewJSONHandler(io.Discard, nil), false},
|
||||
} {
|
||||
logger := slog.New(handler.h)
|
||||
b.Run(handler.name, func(b *testing.B) {
|
||||
if handler.skipRace && race.Enabled {
|
||||
b.Skip("skipping benchmark in race mode")
|
||||
}
|
||||
for _, call := range []struct {
|
||||
name string
|
||||
f func()
|
||||
}{
|
||||
{
|
||||
// The number should match nAttrsInline in slog/record.go.
|
||||
// This should exercise the code path where no allocations
|
||||
// happen in Record or Attr. If there are allocations, they
|
||||
// should only be from Duration.String and Time.String.
|
||||
"5 args",
|
||||
func() {
|
||||
logger.LogAttrs(nil, slog.LevelInfo, testMessage,
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"5 args ctx",
|
||||
func() {
|
||||
logger.LogAttrs(ctx, slog.LevelInfo, testMessage,
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"10 args",
|
||||
func() {
|
||||
logger.LogAttrs(nil, slog.LevelInfo, testMessage,
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
// Try an extreme value to see if the results are reasonable.
|
||||
"40 args",
|
||||
func() {
|
||||
logger.LogAttrs(nil, slog.LevelInfo, testMessage,
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
slog.String("string", testString),
|
||||
slog.Int("status", testInt),
|
||||
slog.Duration("duration", testDuration),
|
||||
slog.Time("time", testTime),
|
||||
slog.Any("error", testError),
|
||||
)
|
||||
},
|
||||
},
|
||||
} {
|
||||
b.Run(call.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
call.f()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
148
src/log/slog/internal/benchmarks/handlers.go
Normal file
148
src/log/slog/internal/benchmarks/handlers.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package benchmarks
|
||||
|
||||
// Handlers for benchmarking.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"log/slog/internal/buffer"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A fastTextHandler writes a Record to an io.Writer in a format similar to
|
||||
// slog.TextHandler, but without quoting or locking. It has a few other
|
||||
// performance-motivated shortcuts, like writing times as seconds since the
|
||||
// epoch instead of strings.
|
||||
//
|
||||
// It is intended to represent a high-performance Handler that synchronously
|
||||
// writes text (as opposed to binary).
|
||||
type fastTextHandler struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func newFastTextHandler(w io.Writer) slog.Handler {
|
||||
return &fastTextHandler{w: w}
|
||||
}
|
||||
|
||||
func (h *fastTextHandler) Enabled(context.Context, slog.Level) bool { return true }
|
||||
|
||||
func (h *fastTextHandler) Handle(_ context.Context, r slog.Record) error {
|
||||
buf := buffer.New()
|
||||
defer buf.Free()
|
||||
|
||||
if !r.Time.IsZero() {
|
||||
buf.WriteString("time=")
|
||||
h.appendTime(buf, r.Time)
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
buf.WriteString("level=")
|
||||
*buf = strconv.AppendInt(*buf, int64(r.Level), 10)
|
||||
buf.WriteByte(' ')
|
||||
buf.WriteString("msg=")
|
||||
buf.WriteString(r.Message)
|
||||
r.Attrs(func(a slog.Attr) bool {
|
||||
buf.WriteByte(' ')
|
||||
buf.WriteString(a.Key)
|
||||
buf.WriteByte('=')
|
||||
h.appendValue(buf, a.Value)
|
||||
return true
|
||||
})
|
||||
buf.WriteByte('\n')
|
||||
_, err := h.w.Write(*buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *fastTextHandler) appendValue(buf *buffer.Buffer, v slog.Value) {
|
||||
switch v.Kind() {
|
||||
case slog.KindString:
|
||||
buf.WriteString(v.String())
|
||||
case slog.KindInt64:
|
||||
*buf = strconv.AppendInt(*buf, v.Int64(), 10)
|
||||
case slog.KindUint64:
|
||||
*buf = strconv.AppendUint(*buf, v.Uint64(), 10)
|
||||
case slog.KindFloat64:
|
||||
*buf = strconv.AppendFloat(*buf, v.Float64(), 'g', -1, 64)
|
||||
case slog.KindBool:
|
||||
*buf = strconv.AppendBool(*buf, v.Bool())
|
||||
case slog.KindDuration:
|
||||
*buf = strconv.AppendInt(*buf, v.Duration().Nanoseconds(), 10)
|
||||
case slog.KindTime:
|
||||
h.appendTime(buf, v.Time())
|
||||
case slog.KindAny:
|
||||
a := v.Any()
|
||||
switch a := a.(type) {
|
||||
case error:
|
||||
buf.WriteString(a.Error())
|
||||
default:
|
||||
fmt.Fprint(buf, a)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("bad kind: %s", v.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *fastTextHandler) appendTime(buf *buffer.Buffer, t time.Time) {
|
||||
*buf = strconv.AppendInt(*buf, t.Unix(), 10)
|
||||
}
|
||||
|
||||
func (h *fastTextHandler) WithAttrs([]slog.Attr) slog.Handler {
|
||||
panic("fastTextHandler: With unimplemented")
|
||||
}
|
||||
|
||||
func (*fastTextHandler) WithGroup(string) slog.Handler {
|
||||
panic("fastTextHandler: WithGroup unimplemented")
|
||||
}
|
||||
|
||||
// An asyncHandler simulates a Handler that passes Records to a
|
||||
// background goroutine for processing.
|
||||
// Because sending to a channel can be expensive due to locking,
|
||||
// we simulate a lock-free queue by adding the Record to a ring buffer.
|
||||
// Omitting the locking makes this little more than a copy of the Record,
|
||||
// but that is a worthwhile thing to measure because Records are on the large
|
||||
// side. Since nothing actually reads from the ring buffer, it can handle an
|
||||
// arbitrary number of Records without either blocking or allocation.
|
||||
type asyncHandler struct {
|
||||
ringBuffer [100]slog.Record
|
||||
next int
|
||||
}
|
||||
|
||||
func newAsyncHandler() *asyncHandler {
|
||||
return &asyncHandler{}
|
||||
}
|
||||
|
||||
func (*asyncHandler) Enabled(context.Context, slog.Level) bool { return true }
|
||||
|
||||
func (h *asyncHandler) Handle(_ context.Context, r slog.Record) error {
|
||||
h.ringBuffer[h.next] = r.Clone()
|
||||
h.next = (h.next + 1) % len(h.ringBuffer)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*asyncHandler) WithAttrs([]slog.Attr) slog.Handler {
|
||||
panic("asyncHandler: With unimplemented")
|
||||
}
|
||||
|
||||
func (*asyncHandler) WithGroup(string) slog.Handler {
|
||||
panic("asyncHandler: WithGroup unimplemented")
|
||||
}
|
||||
|
||||
// A disabledHandler's Enabled method always returns false.
|
||||
type disabledHandler struct{}
|
||||
|
||||
func (disabledHandler) Enabled(context.Context, slog.Level) bool { return false }
|
||||
func (disabledHandler) Handle(context.Context, slog.Record) error { panic("should not be called") }
|
||||
|
||||
func (disabledHandler) WithAttrs([]slog.Attr) slog.Handler {
|
||||
panic("disabledHandler: With unimplemented")
|
||||
}
|
||||
|
||||
func (disabledHandler) WithGroup(string) slog.Handler {
|
||||
panic("disabledHandler: WithGroup unimplemented")
|
||||
}
|
||||
46
src/log/slog/internal/benchmarks/handlers_test.go
Normal file
46
src/log/slog/internal/benchmarks/handlers_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package benchmarks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandlers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
r := slog.NewRecord(testTime, slog.LevelInfo, testMessage, 0)
|
||||
r.AddAttrs(testAttrs...)
|
||||
t.Run("text", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
h := newFastTextHandler(&b)
|
||||
if err := h.Handle(ctx, r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := b.String()
|
||||
if got != wantText {
|
||||
t.Errorf("\ngot %q\nwant %q", got, wantText)
|
||||
}
|
||||
})
|
||||
t.Run("async", func(t *testing.T) {
|
||||
h := newAsyncHandler()
|
||||
if err := h.Handle(ctx, r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := h.ringBuffer[0]
|
||||
if !got.Time.Equal(r.Time) || !slices.EqualFunc(attrSlice(got), attrSlice(r), slog.Attr.Equal) {
|
||||
t.Errorf("got %+v, want %+v", got, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func attrSlice(r slog.Record) []slog.Attr {
|
||||
var as []slog.Attr
|
||||
r.Attrs(func(a slog.Attr) bool { as = append(as, a); return true })
|
||||
return as
|
||||
}
|
||||
66
src/log/slog/internal/buffer/buffer.go
Normal file
66
src/log/slog/internal/buffer/buffer.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package buffer provides a pool-allocated byte buffer.
|
||||
package buffer
|
||||
|
||||
import "sync"
|
||||
|
||||
// Buffer is a byte buffer.
|
||||
//
|
||||
// This implementation is adapted from the unexported type buffer
|
||||
// in go/src/fmt/print.go.
|
||||
type Buffer []byte
|
||||
|
||||
// Having an initial size gives a dramatic speedup.
|
||||
var bufPool = sync.Pool{
|
||||
New: func() any {
|
||||
b := make([]byte, 0, 1024)
|
||||
return (*Buffer)(&b)
|
||||
},
|
||||
}
|
||||
|
||||
func New() *Buffer {
|
||||
return bufPool.Get().(*Buffer)
|
||||
}
|
||||
|
||||
func (b *Buffer) Free() {
|
||||
// To reduce peak allocation, return only smaller buffers to the pool.
|
||||
const maxBufferSize = 16 << 10
|
||||
if cap(*b) <= maxBufferSize {
|
||||
*b = (*b)[:0]
|
||||
bufPool.Put(b)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) Reset() {
|
||||
b.SetLen(0)
|
||||
}
|
||||
|
||||
func (b *Buffer) Write(p []byte) (int, error) {
|
||||
*b = append(*b, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (b *Buffer) WriteString(s string) (int, error) {
|
||||
*b = append(*b, s...)
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
func (b *Buffer) WriteByte(c byte) error {
|
||||
*b = append(*b, c)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Buffer) String() string {
|
||||
return string(*b)
|
||||
}
|
||||
|
||||
func (b *Buffer) Len() int {
|
||||
return len(*b)
|
||||
}
|
||||
|
||||
func (b *Buffer) SetLen(n int) {
|
||||
*b = (*b)[:n]
|
||||
}
|
||||
40
src/log/slog/internal/buffer/buffer_test.go
Normal file
40
src/log/slog/internal/buffer/buffer_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"internal/race"
|
||||
"internal/testenv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
b := New()
|
||||
defer b.Free()
|
||||
b.WriteString("hello")
|
||||
b.WriteByte(',')
|
||||
b.Write([]byte(" world"))
|
||||
|
||||
got := b.String()
|
||||
want := "hello, world"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlloc(t *testing.T) {
|
||||
if race.Enabled {
|
||||
t.Skip("skipping test in race mode")
|
||||
}
|
||||
testenv.SkipIfOptimizationOff(t)
|
||||
got := int(testing.AllocsPerRun(5, func() {
|
||||
b := New()
|
||||
defer b.Free()
|
||||
b.WriteString("not 1K worth of bytes")
|
||||
}))
|
||||
if got != 0 {
|
||||
t.Errorf("got %d allocs, want 0", got)
|
||||
}
|
||||
}
|
||||
9
src/log/slog/internal/ignorepc.go
Normal file
9
src/log/slog/internal/ignorepc.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package internal
|
||||
|
||||
// If IgnorePC is true, do not invoke runtime.Callers to get the pc.
|
||||
// This is solely for benchmarking the slowdown from runtime.Callers.
|
||||
var IgnorePC = false
|
||||
18
src/log/slog/internal/slogtest/slogtest.go
Normal file
18
src/log/slog/internal/slogtest/slogtest.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package slogtest contains support functions for testing slog.
|
||||
package slogtest
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// RemoveTime removes the top-level time attribute.
|
||||
// It is intended to be used as a ReplaceAttr function,
|
||||
// to make example output deterministic.
|
||||
func RemoveTime(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Key == slog.TimeKey && len(groups) == 0 {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
}
|
||||
336
src/log/slog/json_handler.go
Normal file
336
src/log/slog/json_handler.go
Normal file
@@ -0,0 +1,336 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog/internal/buffer"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// JSONHandler is a [Handler] that writes Records to an [io.Writer] as
|
||||
// line-delimited JSON objects.
|
||||
type JSONHandler struct {
|
||||
*commonHandler
|
||||
}
|
||||
|
||||
// NewJSONHandler creates a [JSONHandler] that writes to w,
|
||||
// using the given options.
|
||||
// If opts is nil, the default options are used.
|
||||
func NewJSONHandler(w io.Writer, opts *HandlerOptions) *JSONHandler {
|
||||
if opts == nil {
|
||||
opts = &HandlerOptions{}
|
||||
}
|
||||
return &JSONHandler{
|
||||
&commonHandler{
|
||||
json: true,
|
||||
w: w,
|
||||
opts: *opts,
|
||||
mu: &sync.Mutex{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled reports whether the handler handles records at the given level.
|
||||
// The handler ignores records whose level is lower.
|
||||
func (h *JSONHandler) Enabled(_ context.Context, level Level) bool {
|
||||
return h.commonHandler.enabled(level)
|
||||
}
|
||||
|
||||
// WithAttrs returns a new [JSONHandler] whose attributes consists
|
||||
// of h's attributes followed by attrs.
|
||||
func (h *JSONHandler) WithAttrs(attrs []Attr) Handler {
|
||||
return &JSONHandler{commonHandler: h.commonHandler.withAttrs(attrs)}
|
||||
}
|
||||
|
||||
func (h *JSONHandler) WithGroup(name string) Handler {
|
||||
return &JSONHandler{commonHandler: h.commonHandler.withGroup(name)}
|
||||
}
|
||||
|
||||
// Handle formats its argument [Record] as a JSON object on a single line.
|
||||
//
|
||||
// If the Record's time is zero, the time is omitted.
|
||||
// Otherwise, the key is "time"
|
||||
// and the value is output as with json.Marshal.
|
||||
//
|
||||
// If the Record's level is zero, the level is omitted.
|
||||
// Otherwise, the key is "level"
|
||||
// and the value of [Level.String] is output.
|
||||
//
|
||||
// If the AddSource option is set and source information is available,
|
||||
// the key is "source", and the value is a record of type [Source].
|
||||
//
|
||||
// The message's key is "msg".
|
||||
//
|
||||
// To modify these or other attributes, or remove them from the output, use
|
||||
// [HandlerOptions.ReplaceAttr].
|
||||
//
|
||||
// Values are formatted as with an [encoding/json.Encoder] with SetEscapeHTML(false),
|
||||
// with two exceptions.
|
||||
//
|
||||
// First, an Attr whose Value is of type error is formatted as a string, by
|
||||
// calling its Error method. Only errors in Attrs receive this special treatment,
|
||||
// not errors embedded in structs, slices, maps or other data structures that
|
||||
// are processed by the [encoding/json] package.
|
||||
//
|
||||
// Second, an encoding failure does not cause Handle to return an error.
|
||||
// Instead, the error message is formatted as a string.
|
||||
//
|
||||
// Each call to Handle results in a single serialized call to io.Writer.Write.
|
||||
func (h *JSONHandler) Handle(_ context.Context, r Record) error {
|
||||
return h.commonHandler.handle(r)
|
||||
}
|
||||
|
||||
// Adapted from time.Time.MarshalJSON to avoid allocation.
|
||||
func appendJSONTime(s *handleState, t time.Time) {
|
||||
if y := t.Year(); y < 0 || y >= 10000 {
|
||||
// RFC 3339 is clear that years are 4 digits exactly.
|
||||
// See golang.org/issue/4556#c15 for more discussion.
|
||||
s.appendError(errors.New("time.Time year outside of range [0,9999]"))
|
||||
}
|
||||
s.buf.WriteByte('"')
|
||||
*s.buf = t.AppendFormat(*s.buf, time.RFC3339Nano)
|
||||
s.buf.WriteByte('"')
|
||||
}
|
||||
|
||||
func appendJSONValue(s *handleState, v Value) error {
|
||||
switch v.Kind() {
|
||||
case KindString:
|
||||
s.appendString(v.str())
|
||||
case KindInt64:
|
||||
*s.buf = strconv.AppendInt(*s.buf, v.Int64(), 10)
|
||||
case KindUint64:
|
||||
*s.buf = strconv.AppendUint(*s.buf, v.Uint64(), 10)
|
||||
case KindFloat64:
|
||||
// json.Marshal is funny about floats; it doesn't
|
||||
// always match strconv.AppendFloat. So just call it.
|
||||
// That's expensive, but floats are rare.
|
||||
if err := appendJSONMarshal(s.buf, v.Float64()); err != nil {
|
||||
return err
|
||||
}
|
||||
case KindBool:
|
||||
*s.buf = strconv.AppendBool(*s.buf, v.Bool())
|
||||
case KindDuration:
|
||||
// Do what json.Marshal does.
|
||||
*s.buf = strconv.AppendInt(*s.buf, int64(v.Duration()), 10)
|
||||
case KindTime:
|
||||
s.appendTime(v.Time())
|
||||
case KindAny:
|
||||
a := v.Any()
|
||||
_, jm := a.(json.Marshaler)
|
||||
if err, ok := a.(error); ok && !jm {
|
||||
s.appendString(err.Error())
|
||||
} else {
|
||||
return appendJSONMarshal(s.buf, a)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("bad kind: %s", v.Kind()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendJSONMarshal(buf *buffer.Buffer, v any) error {
|
||||
// Use a json.Encoder to avoid escaping HTML.
|
||||
var bb bytes.Buffer
|
||||
enc := json.NewEncoder(&bb)
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return err
|
||||
}
|
||||
bs := bb.Bytes()
|
||||
buf.Write(bs[:len(bs)-1]) // remove final newline
|
||||
return nil
|
||||
}
|
||||
|
||||
// appendEscapedJSONString escapes s for JSON and appends it to buf.
|
||||
// It does not surround the string in quotation marks.
|
||||
//
|
||||
// Modified from encoding/json/encode.go:encodeState.string,
|
||||
// with escapeHTML set to false.
|
||||
func appendEscapedJSONString(buf []byte, s string) []byte {
|
||||
char := func(b byte) { buf = append(buf, b) }
|
||||
str := func(s string) { buf = append(buf, s...) }
|
||||
|
||||
start := 0
|
||||
for i := 0; i < len(s); {
|
||||
if b := s[i]; b < utf8.RuneSelf {
|
||||
if safeSet[b] {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if start < i {
|
||||
str(s[start:i])
|
||||
}
|
||||
char('\\')
|
||||
switch b {
|
||||
case '\\', '"':
|
||||
char(b)
|
||||
case '\n':
|
||||
char('n')
|
||||
case '\r':
|
||||
char('r')
|
||||
case '\t':
|
||||
char('t')
|
||||
default:
|
||||
// This encodes bytes < 0x20 except for \t, \n and \r.
|
||||
str(`u00`)
|
||||
char(hex[b>>4])
|
||||
char(hex[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
c, size := utf8.DecodeRuneInString(s[i:])
|
||||
if c == utf8.RuneError && size == 1 {
|
||||
if start < i {
|
||||
str(s[start:i])
|
||||
}
|
||||
str(`\ufffd`)
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
// U+2028 is LINE SEPARATOR.
|
||||
// U+2029 is PARAGRAPH SEPARATOR.
|
||||
// They are both technically valid characters in JSON strings,
|
||||
// but don't work in JSONP, which has to be evaluated as JavaScript,
|
||||
// and can lead to security holes there. It is valid JSON to
|
||||
// escape them, so we do so unconditionally.
|
||||
// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
|
||||
if c == '\u2028' || c == '\u2029' {
|
||||
if start < i {
|
||||
str(s[start:i])
|
||||
}
|
||||
str(`\u202`)
|
||||
char(hex[c&0xF])
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
i += size
|
||||
}
|
||||
if start < len(s) {
|
||||
str(s[start:])
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
const hex = "0123456789abcdef"
|
||||
|
||||
// Copied from encoding/json/tables.go.
|
||||
//
|
||||
// safeSet holds the value true if the ASCII character with the given array
|
||||
// position can be represented inside a JSON string without any further
|
||||
// escaping.
|
||||
//
|
||||
// All values are true except for the ASCII control characters (0-31), the
|
||||
// double quote ("), and the backslash character ("\").
|
||||
var safeSet = [utf8.RuneSelf]bool{
|
||||
' ': true,
|
||||
'!': true,
|
||||
'"': false,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': true,
|
||||
'\'': true,
|
||||
'(': true,
|
||||
')': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
',': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'/': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
':': true,
|
||||
';': true,
|
||||
'<': true,
|
||||
'=': true,
|
||||
'>': true,
|
||||
'?': true,
|
||||
'@': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'V': true,
|
||||
'W': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'[': true,
|
||||
'\\': false,
|
||||
']': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'{': true,
|
||||
'|': true,
|
||||
'}': true,
|
||||
'~': true,
|
||||
'\u007f': true,
|
||||
}
|
||||
284
src/log/slog/json_handler_test.go
Normal file
284
src/log/slog/json_handler_test.go
Normal file
@@ -0,0 +1,284 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog/internal/buffer"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestJSONHandler(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
opts HandlerOptions
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"none",
|
||||
HandlerOptions{},
|
||||
`{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"m","a":1,"m":{"b":2}}`,
|
||||
},
|
||||
{
|
||||
"replace",
|
||||
HandlerOptions{ReplaceAttr: upperCaseKey},
|
||||
`{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"m","A":1,"M":{"b":2}}`,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
h := NewJSONHandler(&buf, &test.opts)
|
||||
r := NewRecord(testTime, LevelInfo, "m", 0)
|
||||
r.AddAttrs(Int("a", 1), Any("m", map[string]int{"b": 2}))
|
||||
if err := h.Handle(context.Background(), r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := strings.TrimSuffix(buf.String(), "\n")
|
||||
if got != test.want {
|
||||
t.Errorf("\ngot %s\nwant %s", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// for testing json.Marshaler
|
||||
type jsonMarshaler struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (j jsonMarshaler) String() string { return j.s } // should be ignored
|
||||
|
||||
func (j jsonMarshaler) MarshalJSON() ([]byte, error) {
|
||||
if j.s == "" {
|
||||
return nil, errors.New("json: empty string")
|
||||
}
|
||||
return []byte(fmt.Sprintf(`[%q]`, j.s)), nil
|
||||
}
|
||||
|
||||
type jsonMarshalerError struct {
|
||||
jsonMarshaler
|
||||
}
|
||||
|
||||
func (jsonMarshalerError) Error() string { return "oops" }
|
||||
|
||||
func TestAppendJSONValue(t *testing.T) {
|
||||
// jsonAppendAttrValue should always agree with json.Marshal.
|
||||
for _, value := range []any{
|
||||
"hello\r\n\t\a",
|
||||
`"[{escape}]"`,
|
||||
"<escapeHTML&>",
|
||||
// \u2028\u2029 is an edge case in JavaScript vs JSON.
|
||||
// \xF6 is an incomplete encoding.
|
||||
"\u03B8\u2028\u2029\uFFFF\xF6",
|
||||
`-123`,
|
||||
int64(-9_200_123_456_789_123_456),
|
||||
uint64(9_200_123_456_789_123_456),
|
||||
-12.75,
|
||||
1.23e-9,
|
||||
false,
|
||||
time.Minute,
|
||||
testTime,
|
||||
jsonMarshaler{"xyz"},
|
||||
jsonMarshalerError{jsonMarshaler{"pqr"}},
|
||||
LevelWarn,
|
||||
} {
|
||||
got := jsonValueString(AnyValue(value))
|
||||
want, err := marshalJSON(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("%v: got %s, want %s", value, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func marshalJSON(x any) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(x); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(buf.String()), nil
|
||||
}
|
||||
|
||||
func TestJSONAppendAttrValueSpecial(t *testing.T) {
|
||||
// Attr values that render differently from json.Marshal.
|
||||
for _, test := range []struct {
|
||||
value any
|
||||
want string
|
||||
}{
|
||||
{math.NaN(), `"!ERROR:json: unsupported value: NaN"`},
|
||||
{math.Inf(+1), `"!ERROR:json: unsupported value: +Inf"`},
|
||||
{math.Inf(-1), `"!ERROR:json: unsupported value: -Inf"`},
|
||||
{io.EOF, `"EOF"`},
|
||||
} {
|
||||
got := jsonValueString(AnyValue(test.value))
|
||||
if got != test.want {
|
||||
t.Errorf("%v: got %s, want %s", test.value, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func jsonValueString(v Value) string {
|
||||
var buf []byte
|
||||
s := &handleState{h: &commonHandler{json: true}, buf: (*buffer.Buffer)(&buf)}
|
||||
if err := appendJSONValue(s, v); err != nil {
|
||||
s.appendError(err)
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func BenchmarkJSONHandler(b *testing.B) {
|
||||
for _, bench := range []struct {
|
||||
name string
|
||||
opts HandlerOptions
|
||||
}{
|
||||
{"defaults", HandlerOptions{}},
|
||||
{"time format", HandlerOptions{
|
||||
ReplaceAttr: func(_ []string, a Attr) Attr {
|
||||
v := a.Value
|
||||
if v.Kind() == KindTime {
|
||||
return String(a.Key, v.Time().Format(rfc3339Millis))
|
||||
}
|
||||
if a.Key == "level" {
|
||||
return Attr{"severity", a.Value}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}},
|
||||
{"time unix", HandlerOptions{
|
||||
ReplaceAttr: func(_ []string, a Attr) Attr {
|
||||
v := a.Value
|
||||
if v.Kind() == KindTime {
|
||||
return Int64(a.Key, v.Time().UnixNano())
|
||||
}
|
||||
if a.Key == "level" {
|
||||
return Attr{"severity", a.Value}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}},
|
||||
} {
|
||||
b.Run(bench.name, func(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
l := New(NewJSONHandler(io.Discard, &bench.opts)).With(
|
||||
String("program", "my-test-program"),
|
||||
String("package", "log/slog"),
|
||||
String("traceID", "2039232309232309"),
|
||||
String("URL", "https://pkg.go.dev/golang.org/x/log/slog"))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.LogAttrs(ctx, LevelInfo, "this is a typical log message",
|
||||
String("module", "github.com/google/go-cmp"),
|
||||
String("version", "v1.23.4"),
|
||||
Int("count", 23),
|
||||
Int("number", 123456),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPreformatting(b *testing.B) {
|
||||
type req struct {
|
||||
Method string
|
||||
URL string
|
||||
TraceID string
|
||||
Addr string
|
||||
}
|
||||
|
||||
structAttrs := []any{
|
||||
String("program", "my-test-program"),
|
||||
String("package", "log/slog"),
|
||||
Any("request", &req{
|
||||
Method: "GET",
|
||||
URL: "https://pkg.go.dev/golang.org/x/log/slog",
|
||||
TraceID: "2039232309232309",
|
||||
Addr: "127.0.0.1:8080",
|
||||
}),
|
||||
}
|
||||
|
||||
outFile, err := os.Create(filepath.Join(b.TempDir(), "bench.log"))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := outFile.Close(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, bench := range []struct {
|
||||
name string
|
||||
wc io.Writer
|
||||
attrs []any
|
||||
}{
|
||||
{"separate", io.Discard, []any{
|
||||
String("program", "my-test-program"),
|
||||
String("package", "log/slog"),
|
||||
String("method", "GET"),
|
||||
String("URL", "https://pkg.go.dev/golang.org/x/log/slog"),
|
||||
String("traceID", "2039232309232309"),
|
||||
String("addr", "127.0.0.1:8080"),
|
||||
}},
|
||||
{"struct", io.Discard, structAttrs},
|
||||
{"struct file", outFile, structAttrs},
|
||||
} {
|
||||
ctx := context.Background()
|
||||
b.Run(bench.name, func(b *testing.B) {
|
||||
l := New(NewJSONHandler(bench.wc, nil)).With(bench.attrs...)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.LogAttrs(ctx, LevelInfo, "this is a typical log message",
|
||||
String("module", "github.com/google/go-cmp"),
|
||||
String("version", "v1.23.4"),
|
||||
Int("count", 23),
|
||||
Int("number", 123456),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSONEncoding(b *testing.B) {
|
||||
value := 3.14
|
||||
buf := buffer.New()
|
||||
defer buf.Free()
|
||||
b.Run("json.Marshal", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
by, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
buf.Write(by)
|
||||
*buf = (*buf)[:0]
|
||||
}
|
||||
})
|
||||
b.Run("Encoder.Encode", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := json.NewEncoder(buf).Encode(value); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
*buf = (*buf)[:0]
|
||||
}
|
||||
})
|
||||
_ = buf
|
||||
}
|
||||
200
src/log/slog/level.go
Normal file
200
src/log/slog/level.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// A Level is the importance or severity of a log event.
|
||||
// The higher the level, the more important or severe the event.
|
||||
type Level int
|
||||
|
||||
// Names for common levels.
|
||||
//
|
||||
// Level numbers are inherently arbitrary,
|
||||
// but we picked them to satisfy three constraints.
|
||||
// Any system can map them to another numbering scheme if it wishes.
|
||||
//
|
||||
// First, we wanted the default level to be Info, Since Levels are ints, Info is
|
||||
// the default value for int, zero.
|
||||
//
|
||||
// Second, we wanted to make it easy to use levels to specify logger verbosity.
|
||||
// Since a larger level means a more severe event, a logger that accepts events
|
||||
// with smaller (or more negative) level means a more verbose logger. Logger
|
||||
// verbosity is thus the negation of event severity, and the default verbosity
|
||||
// of 0 accepts all events at least as severe as INFO.
|
||||
//
|
||||
// Third, we wanted some room between levels to accommodate schemes with named
|
||||
// levels between ours. For example, Google Cloud Logging defines a Notice level
|
||||
// between Info and Warn. Since there are only a few of these intermediate
|
||||
// levels, the gap between the numbers need not be large. Our gap of 4 matches
|
||||
// OpenTelemetry's mapping. Subtracting 9 from an OpenTelemetry level in the
|
||||
// DEBUG, INFO, WARN and ERROR ranges converts it to the corresponding slog
|
||||
// Level range. OpenTelemetry also has the names TRACE and FATAL, which slog
|
||||
// does not. But those OpenTelemetry levels can still be represented as slog
|
||||
// Levels by using the appropriate integers.
|
||||
const (
|
||||
LevelDebug Level = -4
|
||||
LevelInfo Level = 0
|
||||
LevelWarn Level = 4
|
||||
LevelError Level = 8
|
||||
)
|
||||
|
||||
// String returns a name for the level.
|
||||
// If the level has a name, then that name
|
||||
// in uppercase is returned.
|
||||
// If the level is between named values, then
|
||||
// an integer is appended to the uppercased name.
|
||||
// Examples:
|
||||
//
|
||||
// LevelWarn.String() => "WARN"
|
||||
// (LevelInfo+2).String() => "INFO+2"
|
||||
func (l Level) String() string {
|
||||
str := func(base string, val Level) string {
|
||||
if val == 0 {
|
||||
return base
|
||||
}
|
||||
return fmt.Sprintf("%s%+d", base, val)
|
||||
}
|
||||
|
||||
switch {
|
||||
case l < LevelInfo:
|
||||
return str("DEBUG", l-LevelDebug)
|
||||
case l < LevelWarn:
|
||||
return str("INFO", l-LevelInfo)
|
||||
case l < LevelError:
|
||||
return str("WARN", l-LevelWarn)
|
||||
default:
|
||||
return str("ERROR", l-LevelError)
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON implements [encoding/json.Marshaler]
|
||||
// by quoting the output of [Level.String].
|
||||
func (l Level) MarshalJSON() ([]byte, error) {
|
||||
// AppendQuote is sufficient for JSON-encoding all Level strings.
|
||||
// They don't contain any runes that would produce invalid JSON
|
||||
// when escaped.
|
||||
return strconv.AppendQuote(nil, l.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [encoding/json.Unmarshaler]
|
||||
// It accepts any string produced by [Level.MarshalJSON],
|
||||
// ignoring case.
|
||||
// It also accepts numeric offsets that would result in a different string on
|
||||
// output. For example, "Error-8" would marshal as "INFO".
|
||||
func (l *Level) UnmarshalJSON(data []byte) error {
|
||||
s, err := strconv.Unquote(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return l.parse(s)
|
||||
}
|
||||
|
||||
// MarshalText implements [encoding.TextMarshaler]
|
||||
// by calling [Level.String].
|
||||
func (l Level) MarshalText() ([]byte, error) {
|
||||
return []byte(l.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements [encoding.TextUnmarshaler].
|
||||
// It accepts any string produced by [Level.MarshalText],
|
||||
// ignoring case.
|
||||
// It also accepts numeric offsets that would result in a different string on
|
||||
// output. For example, "Error-8" would marshal as "INFO".
|
||||
func (l *Level) UnmarshalText(data []byte) error {
|
||||
return l.parse(string(data))
|
||||
}
|
||||
|
||||
func (l *Level) parse(s string) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("slog: level string %q: %w", s, err)
|
||||
}
|
||||
}()
|
||||
|
||||
name := s
|
||||
offset := 0
|
||||
if i := strings.IndexAny(s, "+-"); i >= 0 {
|
||||
name = s[:i]
|
||||
offset, err = strconv.Atoi(s[i:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch strings.ToUpper(name) {
|
||||
case "DEBUG":
|
||||
*l = LevelDebug
|
||||
case "INFO":
|
||||
*l = LevelInfo
|
||||
case "WARN":
|
||||
*l = LevelWarn
|
||||
case "ERROR":
|
||||
*l = LevelError
|
||||
default:
|
||||
return errors.New("unknown name")
|
||||
}
|
||||
*l += Level(offset)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Level returns the receiver.
|
||||
// It implements [Leveler].
|
||||
func (l Level) Level() Level { return l }
|
||||
|
||||
// A LevelVar is a [Level] variable, to allow a [Handler] level to change
|
||||
// dynamically.
|
||||
// It implements [Leveler] as well as a Set method,
|
||||
// and it is safe for use by multiple goroutines.
|
||||
// The zero LevelVar corresponds to [LevelInfo].
|
||||
type LevelVar struct {
|
||||
val atomic.Int64
|
||||
}
|
||||
|
||||
// Level returns v's level.
|
||||
func (v *LevelVar) Level() Level {
|
||||
return Level(int(v.val.Load()))
|
||||
}
|
||||
|
||||
// Set sets v's level to l.
|
||||
func (v *LevelVar) Set(l Level) {
|
||||
v.val.Store(int64(l))
|
||||
}
|
||||
|
||||
func (v *LevelVar) String() string {
|
||||
return fmt.Sprintf("LevelVar(%s)", v.Level())
|
||||
}
|
||||
|
||||
// MarshalText implements [encoding.TextMarshaler]
|
||||
// by calling [Level.MarshalText].
|
||||
func (v *LevelVar) MarshalText() ([]byte, error) {
|
||||
return v.Level().MarshalText()
|
||||
}
|
||||
|
||||
// UnmarshalText implements [encoding.TextUnmarshaler]
|
||||
// by calling [Level.UnmarshalText].
|
||||
func (v *LevelVar) UnmarshalText(data []byte) error {
|
||||
var l Level
|
||||
if err := l.UnmarshalText(data); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Set(l)
|
||||
return nil
|
||||
}
|
||||
|
||||
// A Leveler provides a [Level] value.
|
||||
//
|
||||
// As Level itself implements Leveler, clients typically supply
|
||||
// a Level value wherever a Leveler is needed, such as in [HandlerOptions].
|
||||
// Clients who need to vary the level dynamically can provide a more complex
|
||||
// Leveler implementation such as *[LevelVar].
|
||||
type Leveler interface {
|
||||
Level() Level
|
||||
}
|
||||
187
src/log/slog/level_test.go
Normal file
187
src/log/slog/level_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLevelString(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in Level
|
||||
want string
|
||||
}{
|
||||
{0, "INFO"},
|
||||
{LevelError, "ERROR"},
|
||||
{LevelError + 2, "ERROR+2"},
|
||||
{LevelError - 2, "WARN+2"},
|
||||
{LevelWarn, "WARN"},
|
||||
{LevelWarn - 1, "INFO+3"},
|
||||
{LevelInfo, "INFO"},
|
||||
{LevelInfo + 1, "INFO+1"},
|
||||
{LevelInfo - 3, "DEBUG+1"},
|
||||
{LevelDebug, "DEBUG"},
|
||||
{LevelDebug - 2, "DEBUG-2"},
|
||||
} {
|
||||
got := test.in.String()
|
||||
if got != test.want {
|
||||
t.Errorf("%d: got %s, want %s", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelVar(t *testing.T) {
|
||||
var al LevelVar
|
||||
if got, want := al.Level(), LevelInfo; got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
al.Set(LevelWarn)
|
||||
if got, want := al.Level(), LevelWarn; got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
al.Set(LevelInfo)
|
||||
if got, want := al.Level(), LevelInfo; got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLevelMarshalJSON(t *testing.T) {
|
||||
want := LevelWarn - 3
|
||||
wantData := []byte(`"INFO+1"`)
|
||||
data, err := want.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(data, wantData) {
|
||||
t.Errorf("got %s, want %s", string(data), string(wantData))
|
||||
}
|
||||
var got Level
|
||||
if err := got.UnmarshalJSON(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelMarshalText(t *testing.T) {
|
||||
want := LevelWarn - 3
|
||||
wantData := []byte("INFO+1")
|
||||
data, err := want.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(data, wantData) {
|
||||
t.Errorf("got %s, want %s", string(data), string(wantData))
|
||||
}
|
||||
var got Level
|
||||
if err := got.UnmarshalText(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelParse(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in string
|
||||
want Level
|
||||
}{
|
||||
{"DEBUG", LevelDebug},
|
||||
{"INFO", LevelInfo},
|
||||
{"WARN", LevelWarn},
|
||||
{"ERROR", LevelError},
|
||||
{"debug", LevelDebug},
|
||||
{"iNfo", LevelInfo},
|
||||
{"INFO+87", LevelInfo + 87},
|
||||
{"Error-18", LevelError - 18},
|
||||
{"Error-8", LevelInfo},
|
||||
} {
|
||||
var got Level
|
||||
if err := got.parse(test.in); err != nil {
|
||||
t.Fatalf("%q: %v", test.in, err)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Errorf("%q: got %s, want %s", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelParseError(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in string
|
||||
want string // error string should contain this
|
||||
}{
|
||||
{"", "unknown name"},
|
||||
{"dbg", "unknown name"},
|
||||
{"INFO+", "invalid syntax"},
|
||||
{"INFO-", "invalid syntax"},
|
||||
{"ERROR+23x", "invalid syntax"},
|
||||
} {
|
||||
var l Level
|
||||
err := l.parse(test.in)
|
||||
if err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Errorf("%q: got %v, want string containing %q", test.in, err, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelFlag(t *testing.T) {
|
||||
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
lf := LevelInfo
|
||||
fs.TextVar(&lf, "level", lf, "set level")
|
||||
err := fs.Parse([]string{"-level", "WARN+3"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g, w := lf, LevelWarn+3; g != w {
|
||||
t.Errorf("got %v, want %v", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelVarMarshalText(t *testing.T) {
|
||||
var v LevelVar
|
||||
v.Set(LevelWarn)
|
||||
data, err := v.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var v2 LevelVar
|
||||
if err := v2.UnmarshalText(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g, w := v2.Level(), LevelWarn; g != w {
|
||||
t.Errorf("got %s, want %s", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelVarFlag(t *testing.T) {
|
||||
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
v := &LevelVar{}
|
||||
v.Set(LevelWarn + 3)
|
||||
fs.TextVar(v, "level", v, "set level")
|
||||
err := fs.Parse([]string{"-level", "WARN+3"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g, w := v.Level(), LevelWarn+3; g != w {
|
||||
t.Errorf("got %v, want %v", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelVarString(t *testing.T) {
|
||||
var v LevelVar
|
||||
v.Set(LevelError)
|
||||
got := v.String()
|
||||
want := "LevelVar(ERROR)"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
328
src/log/slog/logger.go
Normal file
328
src/log/slog/logger.go
Normal file
@@ -0,0 +1,328 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
loginternal "log/internal"
|
||||
"log/slog/internal"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var defaultLogger atomic.Pointer[Logger]
|
||||
|
||||
var logLoggerLevel LevelVar
|
||||
|
||||
// SetLogLoggerLevel controls the level for the bridge to the [log] package.
|
||||
//
|
||||
// Before [SetDefault] is called, slog top-level logging functions call the default [log.Logger].
|
||||
// In that mode, SetLogLoggerLevel sets the minimum level for those calls.
|
||||
// By default, the minimum level is Info, so calls to [Debug]
|
||||
// (as well as top-level logging calls at lower levels)
|
||||
// will not be passed to the log.Logger. After calling
|
||||
//
|
||||
// slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
//
|
||||
// calls to [Debug] will be passed to the log.Logger.
|
||||
//
|
||||
// After [SetDefault] is called, calls to the default [log.Logger] are passed to the
|
||||
// slog default handler. In that mode,
|
||||
// SetLogLoggerLevel sets the level at which those calls are logged.
|
||||
// That is, after calling
|
||||
//
|
||||
// slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
//
|
||||
// A call to [log.Printf] will result in output at level [LevelDebug].
|
||||
//
|
||||
// SetLogLoggerLevel returns the previous value.
|
||||
func SetLogLoggerLevel(level Level) (oldLevel Level) {
|
||||
oldLevel = logLoggerLevel.Level()
|
||||
logLoggerLevel.Set(level)
|
||||
return
|
||||
}
|
||||
|
||||
func init() {
|
||||
defaultLogger.Store(New(newDefaultHandler(loginternal.DefaultOutput)))
|
||||
}
|
||||
|
||||
// Default returns the default [Logger].
|
||||
func Default() *Logger { return defaultLogger.Load() }
|
||||
|
||||
// SetDefault makes l the default [Logger], which is used by
|
||||
// the top-level functions [Info], [Debug] and so on.
|
||||
// After this call, output from the log package's default Logger
|
||||
// (as with [log.Print], etc.) will be logged using l's Handler,
|
||||
// at a level controlled by [SetLogLoggerLevel].
|
||||
func SetDefault(l *Logger) {
|
||||
defaultLogger.Store(l)
|
||||
// If the default's handler is a defaultHandler, then don't use a handleWriter,
|
||||
// or we'll deadlock as they both try to acquire the log default mutex.
|
||||
// The defaultHandler will use whatever the log default writer is currently
|
||||
// set to, which is correct.
|
||||
// This can occur with SetDefault(Default()).
|
||||
// See TestSetDefault.
|
||||
if _, ok := l.Handler().(*defaultHandler); !ok {
|
||||
capturePC := log.Flags()&(log.Lshortfile|log.Llongfile) != 0
|
||||
log.SetOutput(&handlerWriter{l.Handler(), &logLoggerLevel, capturePC})
|
||||
log.SetFlags(0) // we want just the log message, no time or location
|
||||
}
|
||||
}
|
||||
|
||||
// handlerWriter is an io.Writer that calls a Handler.
|
||||
// It is used to link the default log.Logger to the default slog.Logger.
|
||||
type handlerWriter struct {
|
||||
h Handler
|
||||
level Leveler
|
||||
capturePC bool
|
||||
}
|
||||
|
||||
func (w *handlerWriter) Write(buf []byte) (int, error) {
|
||||
level := w.level.Level()
|
||||
if !w.h.Enabled(context.Background(), level) {
|
||||
return 0, nil
|
||||
}
|
||||
var pc uintptr
|
||||
if !internal.IgnorePC && w.capturePC {
|
||||
// skip [runtime.Callers, w.Write, Logger.Output, log.Print]
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(4, pcs[:])
|
||||
pc = pcs[0]
|
||||
}
|
||||
|
||||
// Remove final newline.
|
||||
origLen := len(buf) // Report that the entire buf was written.
|
||||
if len(buf) > 0 && buf[len(buf)-1] == '\n' {
|
||||
buf = buf[:len(buf)-1]
|
||||
}
|
||||
r := NewRecord(time.Now(), level, string(buf), pc)
|
||||
return origLen, w.h.Handle(context.Background(), r)
|
||||
}
|
||||
|
||||
// A Logger records structured information about each call to its
|
||||
// Log, Debug, Info, Warn, and Error methods.
|
||||
// For each call, it creates a [Record] and passes it to a [Handler].
|
||||
//
|
||||
// To create a new Logger, call [New] or a Logger method
|
||||
// that begins "With".
|
||||
type Logger struct {
|
||||
handler Handler // for structured logging
|
||||
}
|
||||
|
||||
func (l *Logger) clone() *Logger {
|
||||
c := *l
|
||||
return &c
|
||||
}
|
||||
|
||||
// Handler returns l's Handler.
|
||||
func (l *Logger) Handler() Handler { return l.handler }
|
||||
|
||||
// With returns a Logger that includes the given attributes
|
||||
// in each output operation. Arguments are converted to
|
||||
// attributes as if by [Logger.Log].
|
||||
func (l *Logger) With(args ...any) *Logger {
|
||||
if len(args) == 0 {
|
||||
return l
|
||||
}
|
||||
c := l.clone()
|
||||
c.handler = l.handler.WithAttrs(argsToAttrSlice(args))
|
||||
return c
|
||||
}
|
||||
|
||||
// WithGroup returns a Logger that starts a group, if name is non-empty.
|
||||
// The keys of all attributes added to the Logger will be qualified by the given
|
||||
// name. (How that qualification happens depends on the [Handler.WithGroup]
|
||||
// method of the Logger's Handler.)
|
||||
//
|
||||
// If name is empty, WithGroup returns the receiver.
|
||||
func (l *Logger) WithGroup(name string) *Logger {
|
||||
if name == "" {
|
||||
return l
|
||||
}
|
||||
c := l.clone()
|
||||
c.handler = l.handler.WithGroup(name)
|
||||
return c
|
||||
}
|
||||
|
||||
// New creates a new Logger with the given non-nil Handler.
|
||||
func New(h Handler) *Logger {
|
||||
if h == nil {
|
||||
panic("nil Handler")
|
||||
}
|
||||
return &Logger{handler: h}
|
||||
}
|
||||
|
||||
// With calls [Logger.With] on the default logger.
|
||||
func With(args ...any) *Logger {
|
||||
return Default().With(args...)
|
||||
}
|
||||
|
||||
// Enabled reports whether l emits log records at the given context and level.
|
||||
func (l *Logger) Enabled(ctx context.Context, level Level) bool {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return l.Handler().Enabled(ctx, level)
|
||||
}
|
||||
|
||||
// NewLogLogger returns a new [log.Logger] such that each call to its Output method
|
||||
// dispatches a Record to the specified handler. The logger acts as a bridge from
|
||||
// the older log API to newer structured logging handlers.
|
||||
func NewLogLogger(h Handler, level Level) *log.Logger {
|
||||
return log.New(&handlerWriter{h, level, true}, "", 0)
|
||||
}
|
||||
|
||||
// Log emits a log record with the current time and the given level and message.
|
||||
// The Record's Attrs consist of the Logger's attributes followed by
|
||||
// the Attrs specified by args.
|
||||
//
|
||||
// The attribute arguments are processed as follows:
|
||||
// - If an argument is an Attr, it is used as is.
|
||||
// - If an argument is a string and this is not the last argument,
|
||||
// the following argument is treated as the value and the two are combined
|
||||
// into an Attr.
|
||||
// - Otherwise, the argument is treated as a value with key "!BADKEY".
|
||||
func (l *Logger) Log(ctx context.Context, level Level, msg string, args ...any) {
|
||||
l.log(ctx, level, msg, args...)
|
||||
}
|
||||
|
||||
// LogAttrs is a more efficient version of [Logger.Log] that accepts only Attrs.
|
||||
func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) {
|
||||
l.logAttrs(ctx, level, msg, attrs...)
|
||||
}
|
||||
|
||||
// Debug logs at [LevelDebug].
|
||||
func (l *Logger) Debug(msg string, args ...any) {
|
||||
l.log(context.Background(), LevelDebug, msg, args...)
|
||||
}
|
||||
|
||||
// DebugContext logs at [LevelDebug] with the given context.
|
||||
func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any) {
|
||||
l.log(ctx, LevelDebug, msg, args...)
|
||||
}
|
||||
|
||||
// Info logs at [LevelInfo].
|
||||
func (l *Logger) Info(msg string, args ...any) {
|
||||
l.log(context.Background(), LevelInfo, msg, args...)
|
||||
}
|
||||
|
||||
// InfoContext logs at [LevelInfo] with the given context.
|
||||
func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any) {
|
||||
l.log(ctx, LevelInfo, msg, args...)
|
||||
}
|
||||
|
||||
// Warn logs at [LevelWarn].
|
||||
func (l *Logger) Warn(msg string, args ...any) {
|
||||
l.log(context.Background(), LevelWarn, msg, args...)
|
||||
}
|
||||
|
||||
// WarnContext logs at [LevelWarn] with the given context.
|
||||
func (l *Logger) WarnContext(ctx context.Context, msg string, args ...any) {
|
||||
l.log(ctx, LevelWarn, msg, args...)
|
||||
}
|
||||
|
||||
// Error logs at [LevelError].
|
||||
func (l *Logger) Error(msg string, args ...any) {
|
||||
l.log(context.Background(), LevelError, msg, args...)
|
||||
}
|
||||
|
||||
// ErrorContext logs at [LevelError] with the given context.
|
||||
func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any) {
|
||||
l.log(ctx, LevelError, msg, args...)
|
||||
}
|
||||
|
||||
// log is the low-level logging method for methods that take ...any.
|
||||
// It must always be called directly by an exported logging method
|
||||
// or function, because it uses a fixed call depth to obtain the pc.
|
||||
func (l *Logger) log(ctx context.Context, level Level, msg string, args ...any) {
|
||||
if !l.Enabled(ctx, level) {
|
||||
return
|
||||
}
|
||||
var pc uintptr
|
||||
if !internal.IgnorePC {
|
||||
var pcs [1]uintptr
|
||||
// skip [runtime.Callers, this function, this function's caller]
|
||||
runtime.Callers(3, pcs[:])
|
||||
pc = pcs[0]
|
||||
}
|
||||
r := NewRecord(time.Now(), level, msg, pc)
|
||||
r.Add(args...)
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
_ = l.Handler().Handle(ctx, r)
|
||||
}
|
||||
|
||||
// logAttrs is like [Logger.log], but for methods that take ...Attr.
|
||||
func (l *Logger) logAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) {
|
||||
if !l.Enabled(ctx, level) {
|
||||
return
|
||||
}
|
||||
var pc uintptr
|
||||
if !internal.IgnorePC {
|
||||
var pcs [1]uintptr
|
||||
// skip [runtime.Callers, this function, this function's caller]
|
||||
runtime.Callers(3, pcs[:])
|
||||
pc = pcs[0]
|
||||
}
|
||||
r := NewRecord(time.Now(), level, msg, pc)
|
||||
r.AddAttrs(attrs...)
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
_ = l.Handler().Handle(ctx, r)
|
||||
}
|
||||
|
||||
// Debug calls [Logger.Debug] on the default logger.
|
||||
func Debug(msg string, args ...any) {
|
||||
Default().log(context.Background(), LevelDebug, msg, args...)
|
||||
}
|
||||
|
||||
// DebugContext calls [Logger.DebugContext] on the default logger.
|
||||
func DebugContext(ctx context.Context, msg string, args ...any) {
|
||||
Default().log(ctx, LevelDebug, msg, args...)
|
||||
}
|
||||
|
||||
// Info calls [Logger.Info] on the default logger.
|
||||
func Info(msg string, args ...any) {
|
||||
Default().log(context.Background(), LevelInfo, msg, args...)
|
||||
}
|
||||
|
||||
// InfoContext calls [Logger.InfoContext] on the default logger.
|
||||
func InfoContext(ctx context.Context, msg string, args ...any) {
|
||||
Default().log(ctx, LevelInfo, msg, args...)
|
||||
}
|
||||
|
||||
// Warn calls [Logger.Warn] on the default logger.
|
||||
func Warn(msg string, args ...any) {
|
||||
Default().log(context.Background(), LevelWarn, msg, args...)
|
||||
}
|
||||
|
||||
// WarnContext calls [Logger.WarnContext] on the default logger.
|
||||
func WarnContext(ctx context.Context, msg string, args ...any) {
|
||||
Default().log(ctx, LevelWarn, msg, args...)
|
||||
}
|
||||
|
||||
// Error calls [Logger.Error] on the default logger.
|
||||
func Error(msg string, args ...any) {
|
||||
Default().log(context.Background(), LevelError, msg, args...)
|
||||
}
|
||||
|
||||
// ErrorContext calls [Logger.ErrorContext] on the default logger.
|
||||
func ErrorContext(ctx context.Context, msg string, args ...any) {
|
||||
Default().log(ctx, LevelError, msg, args...)
|
||||
}
|
||||
|
||||
// Log calls [Logger.Log] on the default logger.
|
||||
func Log(ctx context.Context, level Level, msg string, args ...any) {
|
||||
Default().log(ctx, level, msg, args...)
|
||||
}
|
||||
|
||||
// LogAttrs calls [Logger.LogAttrs] on the default logger.
|
||||
func LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) {
|
||||
Default().logAttrs(ctx, level, msg, attrs...)
|
||||
}
|
||||
717
src/log/slog/logger_test.go
Normal file
717
src/log/slog/logger_test.go
Normal file
@@ -0,0 +1,717 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"internal/race"
|
||||
"internal/testenv"
|
||||
"io"
|
||||
"log"
|
||||
loginternal "log/internal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// textTimeRE is a regexp to match log timestamps for Text handler.
|
||||
// This is RFC3339Nano with the fixed 3 digit sub-second precision.
|
||||
const textTimeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})`
|
||||
|
||||
// jsonTimeRE is a regexp to match log timestamps for Text handler.
|
||||
// This is RFC3339Nano with an arbitrary sub-second precision.
|
||||
const jsonTimeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})`
|
||||
|
||||
func TestLogTextHandler(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var buf bytes.Buffer
|
||||
|
||||
l := New(NewTextHandler(&buf, nil))
|
||||
|
||||
check := func(want string) {
|
||||
t.Helper()
|
||||
if want != "" {
|
||||
want = "time=" + textTimeRE + " " + want
|
||||
}
|
||||
checkLogOutput(t, buf.String(), want)
|
||||
buf.Reset()
|
||||
}
|
||||
|
||||
l.Info("msg", "a", 1, "b", 2)
|
||||
check(`level=INFO msg=msg a=1 b=2`)
|
||||
|
||||
// By default, debug messages are not printed.
|
||||
l.Debug("bg", Int("a", 1), "b", 2)
|
||||
check("")
|
||||
|
||||
l.Warn("w", Duration("dur", 3*time.Second))
|
||||
check(`level=WARN msg=w dur=3s`)
|
||||
|
||||
l.Error("bad", "a", 1)
|
||||
check(`level=ERROR msg=bad a=1`)
|
||||
|
||||
l.Log(ctx, LevelWarn+1, "w", Int("a", 1), String("b", "two"))
|
||||
check(`level=WARN\+1 msg=w a=1 b=two`)
|
||||
|
||||
l.LogAttrs(ctx, LevelInfo+1, "a b c", Int("a", 1), String("b", "two"))
|
||||
check(`level=INFO\+1 msg="a b c" a=1 b=two`)
|
||||
|
||||
l.Info("info", "a", []Attr{Int("i", 1)})
|
||||
check(`level=INFO msg=info a.i=1`)
|
||||
|
||||
l.Info("info", "a", GroupValue(Int("i", 1)))
|
||||
check(`level=INFO msg=info a.i=1`)
|
||||
}
|
||||
|
||||
func TestConnections(t *testing.T) {
|
||||
var logbuf, slogbuf bytes.Buffer
|
||||
|
||||
// Revert any changes to the default logger. This is important because other
|
||||
// tests might change the default logger using SetDefault. Also ensure we
|
||||
// restore the default logger at the end of the test.
|
||||
currentLogger := Default()
|
||||
currentLogWriter := log.Writer()
|
||||
currentLogFlags := log.Flags()
|
||||
SetDefault(New(newDefaultHandler(loginternal.DefaultOutput)))
|
||||
t.Cleanup(func() {
|
||||
SetDefault(currentLogger)
|
||||
log.SetOutput(currentLogWriter)
|
||||
log.SetFlags(currentLogFlags)
|
||||
})
|
||||
|
||||
// The default slog.Logger's handler uses the log package's default output.
|
||||
log.SetOutput(&logbuf)
|
||||
log.SetFlags(log.Lshortfile &^ log.LstdFlags)
|
||||
Info("msg", "a", 1)
|
||||
checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg a=1`)
|
||||
logbuf.Reset()
|
||||
Info("msg", "p", nil)
|
||||
checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg p=<nil>`)
|
||||
logbuf.Reset()
|
||||
var r *regexp.Regexp
|
||||
Info("msg", "r", r)
|
||||
checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg r=<nil>`)
|
||||
logbuf.Reset()
|
||||
Warn("msg", "b", 2)
|
||||
checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: WARN msg b=2`)
|
||||
logbuf.Reset()
|
||||
Error("msg", "err", io.EOF, "c", 3)
|
||||
checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: ERROR msg err=EOF c=3`)
|
||||
|
||||
// Levels below Info are not printed.
|
||||
logbuf.Reset()
|
||||
Debug("msg", "c", 3)
|
||||
checkLogOutput(t, logbuf.String(), "")
|
||||
|
||||
t.Run("wrap default handler", func(t *testing.T) {
|
||||
// It should be possible to wrap the default handler and get the right output.
|
||||
// This works because the default handler uses the pc in the Record
|
||||
// to get the source line, rather than a call depth.
|
||||
logger := New(wrappingHandler{Default().Handler()})
|
||||
logger.Info("msg", "d", 4)
|
||||
checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg d=4`)
|
||||
})
|
||||
|
||||
// Once slog.SetDefault is called, the direction is reversed: the default
|
||||
// log.Logger's output goes through the handler.
|
||||
SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{AddSource: true})))
|
||||
log.Print("msg2")
|
||||
checkLogOutput(t, slogbuf.String(), "time="+textTimeRE+` level=INFO source=.*logger_test.go:\d{3}"? msg=msg2`)
|
||||
|
||||
// The default log.Logger always outputs at Info level.
|
||||
slogbuf.Reset()
|
||||
SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{Level: LevelWarn})))
|
||||
log.Print("should not appear")
|
||||
if got := slogbuf.String(); got != "" {
|
||||
t.Errorf("got %q, want empty", got)
|
||||
}
|
||||
|
||||
// Setting log's output again breaks the connection.
|
||||
logbuf.Reset()
|
||||
slogbuf.Reset()
|
||||
log.SetOutput(&logbuf)
|
||||
log.SetFlags(log.Lshortfile &^ log.LstdFlags)
|
||||
log.Print("msg3")
|
||||
checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: msg3`)
|
||||
if got := slogbuf.String(); got != "" {
|
||||
t.Errorf("got %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
type wrappingHandler struct {
|
||||
h Handler
|
||||
}
|
||||
|
||||
func (h wrappingHandler) Enabled(ctx context.Context, level Level) bool {
|
||||
return h.h.Enabled(ctx, level)
|
||||
}
|
||||
func (h wrappingHandler) WithGroup(name string) Handler { return h.h.WithGroup(name) }
|
||||
func (h wrappingHandler) WithAttrs(as []Attr) Handler { return h.h.WithAttrs(as) }
|
||||
func (h wrappingHandler) Handle(ctx context.Context, r Record) error { return h.h.Handle(ctx, r) }
|
||||
|
||||
func TestAttrs(t *testing.T) {
|
||||
check := func(got []Attr, want ...Attr) {
|
||||
t.Helper()
|
||||
if !attrsEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
l1 := New(&captureHandler{}).With("a", 1)
|
||||
l2 := New(l1.Handler()).With("b", 2)
|
||||
l2.Info("m", "c", 3)
|
||||
h := l2.Handler().(*captureHandler)
|
||||
check(h.attrs, Int("a", 1), Int("b", 2))
|
||||
check(attrsSlice(h.r), Int("c", 3))
|
||||
}
|
||||
|
||||
func TestCallDepth(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
h := &captureHandler{}
|
||||
var startLine int
|
||||
|
||||
check := func(count int) {
|
||||
t.Helper()
|
||||
const wantFunc = "log/slog.TestCallDepth"
|
||||
const wantFile = "logger_test.go"
|
||||
wantLine := startLine + count*2
|
||||
got := h.r.source()
|
||||
gotFile := filepath.Base(got.File)
|
||||
if got.Function != wantFunc || gotFile != wantFile || got.Line != wantLine {
|
||||
t.Errorf("got (%s, %s, %d), want (%s, %s, %d)",
|
||||
got.Function, gotFile, got.Line, wantFunc, wantFile, wantLine)
|
||||
}
|
||||
}
|
||||
|
||||
defer SetDefault(Default()) // restore
|
||||
logger := New(h)
|
||||
SetDefault(logger)
|
||||
|
||||
// Calls to check must be one line apart.
|
||||
// Determine line where calls start.
|
||||
f, _ := runtime.CallersFrames([]uintptr{callerPC(2)}).Next()
|
||||
startLine = f.Line + 4
|
||||
// Do not change the number of lines between here and the call to check(0).
|
||||
|
||||
logger.Log(ctx, LevelInfo, "")
|
||||
check(0)
|
||||
logger.LogAttrs(ctx, LevelInfo, "")
|
||||
check(1)
|
||||
logger.Debug("")
|
||||
check(2)
|
||||
logger.Info("")
|
||||
check(3)
|
||||
logger.Warn("")
|
||||
check(4)
|
||||
logger.Error("")
|
||||
check(5)
|
||||
Debug("")
|
||||
check(6)
|
||||
Info("")
|
||||
check(7)
|
||||
Warn("")
|
||||
check(8)
|
||||
Error("")
|
||||
check(9)
|
||||
Log(ctx, LevelInfo, "")
|
||||
check(10)
|
||||
LogAttrs(ctx, LevelInfo, "")
|
||||
check(11)
|
||||
}
|
||||
|
||||
func TestAlloc(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dl := New(discardHandler{})
|
||||
defer SetDefault(Default()) // restore
|
||||
SetDefault(dl)
|
||||
|
||||
t.Run("Info", func(t *testing.T) {
|
||||
wantAllocs(t, 0, func() { Info("hello") })
|
||||
})
|
||||
t.Run("Error", func(t *testing.T) {
|
||||
wantAllocs(t, 0, func() { Error("hello") })
|
||||
})
|
||||
t.Run("logger.Info", func(t *testing.T) {
|
||||
wantAllocs(t, 0, func() { dl.Info("hello") })
|
||||
})
|
||||
t.Run("logger.Log", func(t *testing.T) {
|
||||
wantAllocs(t, 0, func() { dl.Log(ctx, LevelDebug, "hello") })
|
||||
})
|
||||
t.Run("2 pairs", func(t *testing.T) {
|
||||
s := "abc"
|
||||
i := 2000
|
||||
wantAllocs(t, 2, func() {
|
||||
dl.Info("hello",
|
||||
"n", i,
|
||||
"s", s,
|
||||
)
|
||||
})
|
||||
})
|
||||
t.Run("2 pairs disabled inline", func(t *testing.T) {
|
||||
l := New(discardHandler{disabled: true})
|
||||
s := "abc"
|
||||
i := 2000
|
||||
wantAllocs(t, 2, func() {
|
||||
l.Log(ctx, LevelInfo, "hello",
|
||||
"n", i,
|
||||
"s", s,
|
||||
)
|
||||
})
|
||||
})
|
||||
t.Run("2 pairs disabled", func(t *testing.T) {
|
||||
l := New(discardHandler{disabled: true})
|
||||
s := "abc"
|
||||
i := 2000
|
||||
wantAllocs(t, 0, func() {
|
||||
if l.Enabled(ctx, LevelInfo) {
|
||||
l.Log(ctx, LevelInfo, "hello",
|
||||
"n", i,
|
||||
"s", s,
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
t.Run("9 kvs", func(t *testing.T) {
|
||||
s := "abc"
|
||||
i := 2000
|
||||
d := time.Second
|
||||
wantAllocs(t, 10, func() {
|
||||
dl.Info("hello",
|
||||
"n", i, "s", s, "d", d,
|
||||
"n", i, "s", s, "d", d,
|
||||
"n", i, "s", s, "d", d)
|
||||
})
|
||||
})
|
||||
t.Run("pairs", func(t *testing.T) {
|
||||
wantAllocs(t, 0, func() { dl.Info("", "error", io.EOF) })
|
||||
})
|
||||
t.Run("attrs1", func(t *testing.T) {
|
||||
wantAllocs(t, 0, func() { dl.LogAttrs(ctx, LevelInfo, "", Int("a", 1)) })
|
||||
wantAllocs(t, 0, func() { dl.LogAttrs(ctx, LevelInfo, "", Any("error", io.EOF)) })
|
||||
})
|
||||
t.Run("attrs3", func(t *testing.T) {
|
||||
wantAllocs(t, 0, func() {
|
||||
dl.LogAttrs(ctx, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
|
||||
})
|
||||
})
|
||||
t.Run("attrs3 disabled", func(t *testing.T) {
|
||||
logger := New(discardHandler{disabled: true})
|
||||
wantAllocs(t, 0, func() {
|
||||
logger.LogAttrs(ctx, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
|
||||
})
|
||||
})
|
||||
t.Run("attrs6", func(t *testing.T) {
|
||||
wantAllocs(t, 1, func() {
|
||||
dl.LogAttrs(ctx, LevelInfo, "hello",
|
||||
Int("a", 1), String("b", "two"), Duration("c", time.Second),
|
||||
Int("d", 1), String("e", "two"), Duration("f", time.Second))
|
||||
})
|
||||
})
|
||||
t.Run("attrs9", func(t *testing.T) {
|
||||
wantAllocs(t, 1, func() {
|
||||
dl.LogAttrs(ctx, LevelInfo, "hello",
|
||||
Int("a", 1), String("b", "two"), Duration("c", time.Second),
|
||||
Int("d", 1), String("e", "two"), Duration("f", time.Second),
|
||||
Int("d", 1), String("e", "two"), Duration("f", time.Second))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetAttrs(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
args []any
|
||||
want []Attr
|
||||
}{
|
||||
{nil, nil},
|
||||
{[]any{"a", 1}, []Attr{Int("a", 1)}},
|
||||
{[]any{"a", 1, "b", "two"}, []Attr{Int("a", 1), String("b", "two")}},
|
||||
{[]any{"a"}, []Attr{String(badKey, "a")}},
|
||||
{[]any{"a", 1, "b"}, []Attr{Int("a", 1), String(badKey, "b")}},
|
||||
{[]any{"a", 1, 2, 3}, []Attr{Int("a", 1), Int(badKey, 2), Int(badKey, 3)}},
|
||||
} {
|
||||
r := NewRecord(time.Time{}, 0, "", 0)
|
||||
r.Add(test.args...)
|
||||
got := attrsSlice(r)
|
||||
if !attrsEqual(got, test.want) {
|
||||
t.Errorf("%v:\ngot %v\nwant %v", test.args, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefault(t *testing.T) {
|
||||
// Verify that setting the default to itself does not result in deadlock.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
defer func(w io.Writer) { log.SetOutput(w) }(log.Writer())
|
||||
log.SetOutput(io.Discard)
|
||||
go func() {
|
||||
Info("A")
|
||||
SetDefault(Default())
|
||||
Info("B")
|
||||
cancel()
|
||||
}()
|
||||
<-ctx.Done()
|
||||
if err := ctx.Err(); err != context.Canceled {
|
||||
t.Errorf("wanted canceled, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test defaultHandler minimum level without calling slog.SetDefault.
|
||||
func TestLogLoggerLevelForDefaultHandler(t *testing.T) {
|
||||
// Revert any changes to the default logger, flags, and level of log and slog.
|
||||
currentLogLoggerLevel := logLoggerLevel.Level()
|
||||
currentLogWriter := log.Writer()
|
||||
currentLogFlags := log.Flags()
|
||||
t.Cleanup(func() {
|
||||
logLoggerLevel.Set(currentLogLoggerLevel)
|
||||
log.SetOutput(currentLogWriter)
|
||||
log.SetFlags(currentLogFlags)
|
||||
})
|
||||
|
||||
var logBuf bytes.Buffer
|
||||
log.SetOutput(&logBuf)
|
||||
log.SetFlags(0)
|
||||
|
||||
for _, test := range []struct {
|
||||
logLevel Level
|
||||
logFn func(string, ...any)
|
||||
want string
|
||||
}{
|
||||
{LevelDebug, Debug, "DEBUG a"},
|
||||
{LevelDebug, Info, "INFO a"},
|
||||
{LevelInfo, Debug, ""},
|
||||
{LevelInfo, Info, "INFO a"},
|
||||
} {
|
||||
SetLogLoggerLevel(test.logLevel)
|
||||
test.logFn("a")
|
||||
checkLogOutput(t, logBuf.String(), test.want)
|
||||
logBuf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Test handlerWriter minimum level by calling slog.SetDefault.
|
||||
func TestLogLoggerLevelForHandlerWriter(t *testing.T) {
|
||||
removeTime := func(_ []string, a Attr) Attr {
|
||||
if a.Key == TimeKey {
|
||||
return Attr{}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Revert any changes to the default logger. This is important because other
|
||||
// tests might change the default logger using SetDefault. Also ensure we
|
||||
// restore the default logger at the end of the test.
|
||||
currentLogger := Default()
|
||||
currentLogLoggerLevel := logLoggerLevel.Level()
|
||||
currentLogWriter := log.Writer()
|
||||
currentFlags := log.Flags()
|
||||
t.Cleanup(func() {
|
||||
SetDefault(currentLogger)
|
||||
logLoggerLevel.Set(currentLogLoggerLevel)
|
||||
log.SetOutput(currentLogWriter)
|
||||
log.SetFlags(currentFlags)
|
||||
})
|
||||
|
||||
var logBuf bytes.Buffer
|
||||
log.SetOutput(&logBuf)
|
||||
log.SetFlags(0)
|
||||
SetLogLoggerLevel(LevelError)
|
||||
SetDefault(New(NewTextHandler(&logBuf, &HandlerOptions{ReplaceAttr: removeTime})))
|
||||
log.Print("error")
|
||||
checkLogOutput(t, logBuf.String(), `level=ERROR msg=error`)
|
||||
}
|
||||
|
||||
func TestLoggerError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
removeTime := func(_ []string, a Attr) Attr {
|
||||
if a.Key == TimeKey {
|
||||
return Attr{}
|
||||
}
|
||||
return a
|
||||
}
|
||||
l := New(NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeTime}))
|
||||
l.Error("msg", "err", io.EOF, "a", 1)
|
||||
checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF a=1`)
|
||||
buf.Reset()
|
||||
// use local var 'args' to defeat vet check
|
||||
args := []any{"err", io.EOF, "a"}
|
||||
l.Error("msg", args...)
|
||||
checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF !BADKEY=a`)
|
||||
}
|
||||
|
||||
func TestNewLogLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
h := NewTextHandler(&buf, nil)
|
||||
ll := NewLogLogger(h, LevelWarn)
|
||||
ll.Print("hello")
|
||||
checkLogOutput(t, buf.String(), "time="+textTimeRE+` level=WARN msg=hello`)
|
||||
}
|
||||
|
||||
func TestLoggerNoOps(t *testing.T) {
|
||||
l := Default()
|
||||
if l.With() != l {
|
||||
t.Error("wanted receiver, didn't get it")
|
||||
}
|
||||
if With() != l {
|
||||
t.Error("wanted receiver, didn't get it")
|
||||
}
|
||||
if l.WithGroup("") != l {
|
||||
t.Error("wanted receiver, didn't get it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext(t *testing.T) {
|
||||
// Verify that the context argument to log output methods is passed to the handler.
|
||||
// Also check the level.
|
||||
h := &captureHandler{}
|
||||
l := New(h)
|
||||
defer SetDefault(Default()) // restore
|
||||
SetDefault(l)
|
||||
|
||||
for _, test := range []struct {
|
||||
f func(context.Context, string, ...any)
|
||||
wantLevel Level
|
||||
}{
|
||||
{l.DebugContext, LevelDebug},
|
||||
{l.InfoContext, LevelInfo},
|
||||
{l.WarnContext, LevelWarn},
|
||||
{l.ErrorContext, LevelError},
|
||||
{DebugContext, LevelDebug},
|
||||
{InfoContext, LevelInfo},
|
||||
{WarnContext, LevelWarn},
|
||||
{ErrorContext, LevelError},
|
||||
} {
|
||||
h.clear()
|
||||
ctx := context.WithValue(context.Background(), "L", test.wantLevel)
|
||||
|
||||
test.f(ctx, "msg")
|
||||
if gv := h.ctx.Value("L"); gv != test.wantLevel || h.r.Level != test.wantLevel {
|
||||
t.Errorf("got context value %v, level %s; want %s for both", gv, h.r.Level, test.wantLevel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkLogOutput(t *testing.T, got, wantRegexp string) {
|
||||
t.Helper()
|
||||
got = clean(got)
|
||||
wantRegexp = "^" + wantRegexp + "$"
|
||||
matched, err := regexp.MatchString(wantRegexp, got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !matched {
|
||||
t.Errorf("\ngot %s\nwant %s", got, wantRegexp)
|
||||
}
|
||||
}
|
||||
|
||||
// clean prepares log output for comparison.
|
||||
func clean(s string) string {
|
||||
if len(s) > 0 && s[len(s)-1] == '\n' {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return strings.ReplaceAll(s, "\n", "~")
|
||||
}
|
||||
|
||||
type captureHandler struct {
|
||||
mu sync.Mutex
|
||||
ctx context.Context
|
||||
r Record
|
||||
attrs []Attr
|
||||
groups []string
|
||||
}
|
||||
|
||||
func (h *captureHandler) Handle(ctx context.Context, r Record) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.ctx = ctx
|
||||
h.r = r
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*captureHandler) Enabled(context.Context, Level) bool { return true }
|
||||
|
||||
func (c *captureHandler) WithAttrs(as []Attr) Handler {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
var c2 captureHandler
|
||||
c2.r = c.r
|
||||
c2.groups = c.groups
|
||||
c2.attrs = concat(c.attrs, as)
|
||||
return &c2
|
||||
}
|
||||
|
||||
func (c *captureHandler) WithGroup(name string) Handler {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
var c2 captureHandler
|
||||
c2.r = c.r
|
||||
c2.attrs = c.attrs
|
||||
c2.groups = append(slices.Clip(c.groups), name)
|
||||
return &c2
|
||||
}
|
||||
|
||||
func (c *captureHandler) clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.ctx = nil
|
||||
c.r = Record{}
|
||||
}
|
||||
|
||||
type discardHandler struct {
|
||||
disabled bool
|
||||
attrs []Attr
|
||||
}
|
||||
|
||||
func (d discardHandler) Enabled(context.Context, Level) bool { return !d.disabled }
|
||||
func (discardHandler) Handle(context.Context, Record) error { return nil }
|
||||
func (d discardHandler) WithAttrs(as []Attr) Handler {
|
||||
d.attrs = concat(d.attrs, as)
|
||||
return d
|
||||
}
|
||||
func (h discardHandler) WithGroup(name string) Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
// concat returns a new slice with the elements of s1 followed
|
||||
// by those of s2. The slice has no additional capacity.
|
||||
func concat[T any](s1, s2 []T) []T {
|
||||
s := make([]T, len(s1)+len(s2))
|
||||
copy(s, s1)
|
||||
copy(s[len(s1):], s2)
|
||||
return s
|
||||
}
|
||||
|
||||
// This is a simple benchmark. See the benchmarks subdirectory for more extensive ones.
|
||||
func BenchmarkNopLog(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
l := New(&captureHandler{})
|
||||
b.Run("no attrs", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.LogAttrs(ctx, LevelInfo, "msg")
|
||||
}
|
||||
})
|
||||
b.Run("attrs", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
|
||||
}
|
||||
})
|
||||
b.Run("attrs-parallel", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
|
||||
}
|
||||
})
|
||||
})
|
||||
b.Run("keys-values", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.Log(ctx, LevelInfo, "msg", "a", 1, "b", "two", "c", true)
|
||||
}
|
||||
})
|
||||
b.Run("WithContext", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.LogAttrs(ctx, LevelInfo, "msg2", Int("a", 1), String("b", "two"), Bool("c", true))
|
||||
}
|
||||
})
|
||||
b.Run("WithContext-parallel", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// callerPC returns the program counter at the given stack depth.
|
||||
func callerPC(depth int) uintptr {
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(depth, pcs[:])
|
||||
return pcs[0]
|
||||
}
|
||||
|
||||
func wantAllocs(t *testing.T, want int, f func()) {
|
||||
if race.Enabled {
|
||||
t.Skip("skipping test in race mode")
|
||||
}
|
||||
testenv.SkipIfOptimizationOff(t)
|
||||
t.Helper()
|
||||
got := int(testing.AllocsPerRun(5, f))
|
||||
if got != want {
|
||||
t.Errorf("got %d allocs, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// panicTextAndJsonMarshaler is a type that panics in MarshalText and MarshalJSON.
|
||||
type panicTextAndJsonMarshaler struct {
|
||||
msg any
|
||||
}
|
||||
|
||||
func (p panicTextAndJsonMarshaler) MarshalText() ([]byte, error) {
|
||||
panic(p.msg)
|
||||
}
|
||||
|
||||
func (p panicTextAndJsonMarshaler) MarshalJSON() ([]byte, error) {
|
||||
panic(p.msg)
|
||||
}
|
||||
|
||||
func TestPanics(t *testing.T) {
|
||||
// Revert any changes to the default logger. This is important because other
|
||||
// tests might change the default logger using SetDefault. Also ensure we
|
||||
// restore the default logger at the end of the test.
|
||||
currentLogger := Default()
|
||||
currentLogWriter := log.Writer()
|
||||
currentLogFlags := log.Flags()
|
||||
t.Cleanup(func() {
|
||||
SetDefault(currentLogger)
|
||||
log.SetOutput(currentLogWriter)
|
||||
log.SetFlags(currentLogFlags)
|
||||
})
|
||||
|
||||
var logBuf bytes.Buffer
|
||||
log.SetOutput(&logBuf)
|
||||
log.SetFlags(log.Lshortfile &^ log.LstdFlags)
|
||||
|
||||
SetDefault(New(newDefaultHandler(loginternal.DefaultOutput)))
|
||||
for _, pt := range []struct {
|
||||
in any
|
||||
out string
|
||||
}{
|
||||
{(*panicTextAndJsonMarshaler)(nil), `logger_test.go:\d+: INFO msg p=<nil>`},
|
||||
{panicTextAndJsonMarshaler{io.ErrUnexpectedEOF}, `logger_test.go:\d+: INFO msg p="!PANIC: unexpected EOF"`},
|
||||
{panicTextAndJsonMarshaler{"panicking"}, `logger_test.go:\d+: INFO msg p="!PANIC: panicking"`},
|
||||
{panicTextAndJsonMarshaler{42}, `logger_test.go:\d+: INFO msg p="!PANIC: 42"`},
|
||||
} {
|
||||
Info("msg", "p", pt.in)
|
||||
checkLogOutput(t, logBuf.String(), pt.out)
|
||||
logBuf.Reset()
|
||||
}
|
||||
|
||||
SetDefault(New(NewJSONHandler(&logBuf, nil)))
|
||||
for _, pt := range []struct {
|
||||
in any
|
||||
out string
|
||||
}{
|
||||
{(*panicTextAndJsonMarshaler)(nil), `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":null}`},
|
||||
{panicTextAndJsonMarshaler{io.ErrUnexpectedEOF}, `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":"!PANIC: unexpected EOF"}`},
|
||||
{panicTextAndJsonMarshaler{"panicking"}, `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":"!PANIC: panicking"}`},
|
||||
{panicTextAndJsonMarshaler{42}, `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":"!PANIC: 42"}`},
|
||||
} {
|
||||
Info("msg", "p", pt.in)
|
||||
checkLogOutput(t, logBuf.String(), pt.out)
|
||||
logBuf.Reset()
|
||||
}
|
||||
}
|
||||
226
src/log/slog/record.go
Normal file
226
src/log/slog/record.go
Normal file
@@ -0,0 +1,226 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
const nAttrsInline = 5
|
||||
|
||||
// A Record holds information about a log event.
|
||||
// Copies of a Record share state.
|
||||
// Do not modify a Record after handing out a copy to it.
|
||||
// Call [NewRecord] to create a new Record.
|
||||
// Use [Record.Clone] to create a copy with no shared state.
|
||||
type Record struct {
|
||||
// The time at which the output method (Log, Info, etc.) was called.
|
||||
Time time.Time
|
||||
|
||||
// The log message.
|
||||
Message string
|
||||
|
||||
// The level of the event.
|
||||
Level Level
|
||||
|
||||
// The program counter at the time the record was constructed, as determined
|
||||
// by runtime.Callers. If zero, no program counter is available.
|
||||
//
|
||||
// The only valid use for this value is as an argument to
|
||||
// [runtime.CallersFrames]. In particular, it must not be passed to
|
||||
// [runtime.FuncForPC].
|
||||
PC uintptr
|
||||
|
||||
// Allocation optimization: an inline array sized to hold
|
||||
// the majority of log calls (based on examination of open-source
|
||||
// code). It holds the start of the list of Attrs.
|
||||
front [nAttrsInline]Attr
|
||||
|
||||
// The number of Attrs in front.
|
||||
nFront int
|
||||
|
||||
// The list of Attrs except for those in front.
|
||||
// Invariants:
|
||||
// - len(back) > 0 iff nFront == len(front)
|
||||
// - Unused array elements are zero. Used to detect mistakes.
|
||||
back []Attr
|
||||
}
|
||||
|
||||
// NewRecord creates a [Record] from the given arguments.
|
||||
// Use [Record.AddAttrs] to add attributes to the Record.
|
||||
//
|
||||
// NewRecord is intended for logging APIs that want to support a [Handler] as
|
||||
// a backend.
|
||||
func NewRecord(t time.Time, level Level, msg string, pc uintptr) Record {
|
||||
return Record{
|
||||
Time: t,
|
||||
Message: msg,
|
||||
Level: level,
|
||||
PC: pc,
|
||||
}
|
||||
}
|
||||
|
||||
// Clone returns a copy of the record with no shared state.
|
||||
// The original record and the clone can both be modified
|
||||
// without interfering with each other.
|
||||
func (r Record) Clone() Record {
|
||||
r.back = slices.Clip(r.back) // prevent append from mutating shared array
|
||||
return r
|
||||
}
|
||||
|
||||
// NumAttrs returns the number of attributes in the [Record].
|
||||
func (r Record) NumAttrs() int {
|
||||
return r.nFront + len(r.back)
|
||||
}
|
||||
|
||||
// Attrs calls f on each Attr in the [Record].
|
||||
// Iteration stops if f returns false.
|
||||
func (r Record) Attrs(f func(Attr) bool) {
|
||||
for i := 0; i < r.nFront; i++ {
|
||||
if !f(r.front[i]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, a := range r.back {
|
||||
if !f(a) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddAttrs appends the given Attrs to the [Record]'s list of Attrs.
|
||||
// It omits empty groups.
|
||||
func (r *Record) AddAttrs(attrs ...Attr) {
|
||||
var i int
|
||||
for i = 0; i < len(attrs) && r.nFront < len(r.front); i++ {
|
||||
a := attrs[i]
|
||||
if a.Value.isEmptyGroup() {
|
||||
continue
|
||||
}
|
||||
r.front[r.nFront] = a
|
||||
r.nFront++
|
||||
}
|
||||
// Check if a copy was modified by slicing past the end
|
||||
// and seeing if the Attr there is non-zero.
|
||||
if cap(r.back) > len(r.back) {
|
||||
end := r.back[:len(r.back)+1][len(r.back)]
|
||||
if !end.isEmpty() {
|
||||
// Don't panic; copy and muddle through.
|
||||
r.back = slices.Clip(r.back)
|
||||
r.back = append(r.back, String("!BUG", "AddAttrs unsafely called on copy of Record made without using Record.Clone"))
|
||||
}
|
||||
}
|
||||
ne := countEmptyGroups(attrs[i:])
|
||||
r.back = slices.Grow(r.back, len(attrs[i:])-ne)
|
||||
for _, a := range attrs[i:] {
|
||||
if !a.Value.isEmptyGroup() {
|
||||
r.back = append(r.back, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add converts the args to Attrs as described in [Logger.Log],
|
||||
// then appends the Attrs to the [Record]'s list of Attrs.
|
||||
// It omits empty groups.
|
||||
func (r *Record) Add(args ...any) {
|
||||
var a Attr
|
||||
for len(args) > 0 {
|
||||
a, args = argsToAttr(args)
|
||||
if a.Value.isEmptyGroup() {
|
||||
continue
|
||||
}
|
||||
if r.nFront < len(r.front) {
|
||||
r.front[r.nFront] = a
|
||||
r.nFront++
|
||||
} else {
|
||||
if r.back == nil {
|
||||
r.back = make([]Attr, 0, countAttrs(args)+1)
|
||||
}
|
||||
r.back = append(r.back, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// countAttrs returns the number of Attrs that would be created from args.
|
||||
func countAttrs(args []any) int {
|
||||
n := 0
|
||||
for i := 0; i < len(args); i++ {
|
||||
n++
|
||||
if _, ok := args[i].(string); ok {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
const badKey = "!BADKEY"
|
||||
|
||||
// argsToAttr turns a prefix of the nonempty args slice into an Attr
|
||||
// and returns the unconsumed portion of the slice.
|
||||
// If args[0] is an Attr, it returns it.
|
||||
// If args[0] is a string, it treats the first two elements as
|
||||
// a key-value pair.
|
||||
// Otherwise, it treats args[0] as a value with a missing key.
|
||||
func argsToAttr(args []any) (Attr, []any) {
|
||||
switch x := args[0].(type) {
|
||||
case string:
|
||||
if len(args) == 1 {
|
||||
return String(badKey, x), nil
|
||||
}
|
||||
return Any(x, args[1]), args[2:]
|
||||
|
||||
case Attr:
|
||||
return x, args[1:]
|
||||
|
||||
default:
|
||||
return Any(badKey, x), args[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Source describes the location of a line of source code.
|
||||
type Source struct {
|
||||
// Function is the package path-qualified function name containing the
|
||||
// source line. If non-empty, this string uniquely identifies a single
|
||||
// function in the program. This may be the empty string if not known.
|
||||
Function string `json:"function"`
|
||||
// File and Line are the file name and line number (1-based) of the source
|
||||
// line. These may be the empty string and zero, respectively, if not known.
|
||||
File string `json:"file"`
|
||||
Line int `json:"line"`
|
||||
}
|
||||
|
||||
// group returns the non-zero fields of s as a slice of attrs.
|
||||
// It is similar to a LogValue method, but we don't want Source
|
||||
// to implement LogValuer because it would be resolved before
|
||||
// the ReplaceAttr function was called.
|
||||
func (s *Source) group() Value {
|
||||
var as []Attr
|
||||
if s.Function != "" {
|
||||
as = append(as, String("function", s.Function))
|
||||
}
|
||||
if s.File != "" {
|
||||
as = append(as, String("file", s.File))
|
||||
}
|
||||
if s.Line != 0 {
|
||||
as = append(as, Int("line", s.Line))
|
||||
}
|
||||
return GroupValue(as...)
|
||||
}
|
||||
|
||||
// source returns a Source for the log event.
|
||||
// If the Record was created without the necessary information,
|
||||
// or if the location is unavailable, it returns a non-nil *Source
|
||||
// with zero fields.
|
||||
func (r Record) source() *Source {
|
||||
fs := runtime.CallersFrames([]uintptr{r.PC})
|
||||
f, _ := fs.Next()
|
||||
return &Source{
|
||||
Function: f.Function,
|
||||
File: f.File,
|
||||
Line: f.Line,
|
||||
}
|
||||
}
|
||||
159
src/log/slog/record_test.go
Normal file
159
src/log/slog/record_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRecordAttrs(t *testing.T) {
|
||||
as := []Attr{Int("k1", 1), String("k2", "foo"), Int("k3", 3),
|
||||
Int64("k4", -1), Float64("f", 3.1), Uint64("u", 999)}
|
||||
r := newRecordWithAttrs(as)
|
||||
if g, w := r.NumAttrs(), len(as); g != w {
|
||||
t.Errorf("NumAttrs: got %d, want %d", g, w)
|
||||
}
|
||||
if got := attrsSlice(r); !attrsEqual(got, as) {
|
||||
t.Errorf("got %v, want %v", got, as)
|
||||
}
|
||||
|
||||
// Early return.
|
||||
// Hit both loops in Record.Attrs: front and back.
|
||||
for _, stop := range []int{2, 6} {
|
||||
var got []Attr
|
||||
r.Attrs(func(a Attr) bool {
|
||||
got = append(got, a)
|
||||
return len(got) < stop
|
||||
})
|
||||
want := as[:stop]
|
||||
if !attrsEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordSource(t *testing.T) {
|
||||
// Zero call depth => empty *Source.
|
||||
for _, test := range []struct {
|
||||
depth int
|
||||
wantFunction string
|
||||
wantFile string
|
||||
wantLinePositive bool
|
||||
}{
|
||||
{0, "", "", false},
|
||||
{-16, "", "", false},
|
||||
{1, "log/slog.TestRecordSource", "record_test.go", true}, // 1: caller of NewRecord
|
||||
{2, "testing.tRunner", "testing.go", true},
|
||||
} {
|
||||
var pc uintptr
|
||||
if test.depth > 0 {
|
||||
pc = callerPC(test.depth + 1)
|
||||
}
|
||||
r := NewRecord(time.Time{}, 0, "", pc)
|
||||
got := r.source()
|
||||
if i := strings.LastIndexByte(got.File, '/'); i >= 0 {
|
||||
got.File = got.File[i+1:]
|
||||
}
|
||||
if got.Function != test.wantFunction || got.File != test.wantFile || (got.Line > 0) != test.wantLinePositive {
|
||||
t.Errorf("depth %d: got (%q, %q, %d), want (%q, %q, %t)",
|
||||
test.depth,
|
||||
got.Function, got.File, got.Line,
|
||||
test.wantFunction, test.wantFile, test.wantLinePositive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasingAndClone(t *testing.T) {
|
||||
intAttrs := func(from, to int) []Attr {
|
||||
var as []Attr
|
||||
for i := from; i < to; i++ {
|
||||
as = append(as, Int("k", i))
|
||||
}
|
||||
return as
|
||||
}
|
||||
|
||||
check := func(r Record, want []Attr) {
|
||||
t.Helper()
|
||||
got := attrsSlice(r)
|
||||
if !attrsEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a record whose Attrs overflow the inline array,
|
||||
// creating a slice in r.back.
|
||||
r1 := NewRecord(time.Time{}, 0, "", 0)
|
||||
r1.AddAttrs(intAttrs(0, nAttrsInline+1)...)
|
||||
// Ensure that r1.back's capacity exceeds its length.
|
||||
b := make([]Attr, len(r1.back), len(r1.back)+1)
|
||||
copy(b, r1.back)
|
||||
r1.back = b
|
||||
// Make a copy that shares state.
|
||||
r2 := r1
|
||||
// Adding to both should insert a special Attr in the second.
|
||||
r1AttrsBefore := attrsSlice(r1)
|
||||
r1.AddAttrs(Int("p", 0))
|
||||
r2.AddAttrs(Int("p", 1))
|
||||
check(r1, append(slices.Clip(r1AttrsBefore), Int("p", 0)))
|
||||
r1Attrs := attrsSlice(r1)
|
||||
check(r2, append(slices.Clip(r1AttrsBefore),
|
||||
String("!BUG", "AddAttrs unsafely called on copy of Record made without using Record.Clone"), Int("p", 1)))
|
||||
|
||||
// Adding to a clone is fine.
|
||||
r2 = r1.Clone()
|
||||
check(r2, r1Attrs)
|
||||
r2.AddAttrs(Int("p", 2))
|
||||
check(r1, r1Attrs) // r1 is unchanged
|
||||
check(r2, append(slices.Clip(r1Attrs), Int("p", 2)))
|
||||
}
|
||||
|
||||
func newRecordWithAttrs(as []Attr) Record {
|
||||
r := NewRecord(time.Now(), LevelInfo, "", 0)
|
||||
r.AddAttrs(as...)
|
||||
return r
|
||||
}
|
||||
|
||||
func attrsSlice(r Record) []Attr {
|
||||
s := make([]Attr, 0, r.NumAttrs())
|
||||
r.Attrs(func(a Attr) bool { s = append(s, a); return true })
|
||||
return s
|
||||
}
|
||||
|
||||
func attrsEqual(as1, as2 []Attr) bool {
|
||||
return slices.EqualFunc(as1, as2, Attr.Equal)
|
||||
}
|
||||
|
||||
// Currently, pc(2) takes over 400ns, which is too expensive
|
||||
// to call it for every log message.
|
||||
func BenchmarkPC(b *testing.B) {
|
||||
for depth := 0; depth < 5; depth++ {
|
||||
b.Run(strconv.Itoa(depth), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var x uintptr
|
||||
for i := 0; i < b.N; i++ {
|
||||
x = callerPC(depth)
|
||||
}
|
||||
_ = x
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRecord(b *testing.B) {
|
||||
const nAttrs = nAttrsInline * 10
|
||||
var a Attr
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
r := NewRecord(time.Time{}, LevelInfo, "", 0)
|
||||
for j := 0; j < nAttrs; j++ {
|
||||
r.AddAttrs(Int("k", j))
|
||||
}
|
||||
r.Attrs(func(b Attr) bool { a = b; return true })
|
||||
}
|
||||
_ = a
|
||||
}
|
||||
104
src/log/slog/slogtest_test.go
Normal file
104
src/log/slog/slogtest_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/slogtest"
|
||||
)
|
||||
|
||||
func TestSlogtest(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
new func(io.Writer) slog.Handler
|
||||
parse func([]byte) (map[string]any, error)
|
||||
}{
|
||||
{"JSON", func(w io.Writer) slog.Handler { return slog.NewJSONHandler(w, nil) }, parseJSON},
|
||||
{"Text", func(w io.Writer) slog.Handler { return slog.NewTextHandler(w, nil) }, parseText},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
h := test.new(&buf)
|
||||
results := func() []map[string]any {
|
||||
ms, err := parseLines(buf.Bytes(), test.parse)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
if err := slogtest.TestHandler(h, results); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func parseLines(src []byte, parse func([]byte) (map[string]any, error)) ([]map[string]any, error) {
|
||||
var records []map[string]any
|
||||
for _, line := range bytes.Split(src, []byte{'\n'}) {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
m, err := parse(line)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", string(line), err)
|
||||
}
|
||||
records = append(records, m)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func parseJSON(bs []byte) (map[string]any, error) {
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(bs, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// parseText parses the output of a single call to TextHandler.Handle.
|
||||
// It can parse the output of the tests in this package,
|
||||
// but it doesn't handle quoted keys or values.
|
||||
// It doesn't need to handle all cases, because slogtest deliberately
|
||||
// uses simple inputs so handler writers can focus on testing
|
||||
// handler behavior, not parsing.
|
||||
func parseText(bs []byte) (map[string]any, error) {
|
||||
top := map[string]any{}
|
||||
s := string(bytes.TrimSpace(bs))
|
||||
for len(s) > 0 {
|
||||
kv, rest, _ := strings.Cut(s, " ") // assumes exactly one space between attrs
|
||||
k, value, found := strings.Cut(kv, "=")
|
||||
if !found {
|
||||
return nil, fmt.Errorf("no '=' in %q", kv)
|
||||
}
|
||||
keys := strings.Split(k, ".")
|
||||
// Populate a tree of maps for a dotted path such as "a.b.c=x".
|
||||
m := top
|
||||
for _, key := range keys[:len(keys)-1] {
|
||||
x, ok := m[key]
|
||||
var m2 map[string]any
|
||||
if !ok {
|
||||
m2 = map[string]any{}
|
||||
m[key] = m2
|
||||
} else {
|
||||
m2, ok = x.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("value for %q in composite key %q is not map[string]any", key, k)
|
||||
|
||||
}
|
||||
}
|
||||
m = m2
|
||||
}
|
||||
m[keys[len(keys)-1]] = value
|
||||
s = rest
|
||||
}
|
||||
return top, nil
|
||||
}
|
||||
163
src/log/slog/text_handler.go
Normal file
163
src/log/slog/text_handler.go
Normal file
@@ -0,0 +1,163 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// TextHandler is a [Handler] that writes Records to an [io.Writer] as a
|
||||
// sequence of key=value pairs separated by spaces and followed by a newline.
|
||||
type TextHandler struct {
|
||||
*commonHandler
|
||||
}
|
||||
|
||||
// NewTextHandler creates a [TextHandler] that writes to w,
|
||||
// using the given options.
|
||||
// If opts is nil, the default options are used.
|
||||
func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
|
||||
if opts == nil {
|
||||
opts = &HandlerOptions{}
|
||||
}
|
||||
return &TextHandler{
|
||||
&commonHandler{
|
||||
json: false,
|
||||
w: w,
|
||||
opts: *opts,
|
||||
mu: &sync.Mutex{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled reports whether the handler handles records at the given level.
|
||||
// The handler ignores records whose level is lower.
|
||||
func (h *TextHandler) Enabled(_ context.Context, level Level) bool {
|
||||
return h.commonHandler.enabled(level)
|
||||
}
|
||||
|
||||
// WithAttrs returns a new [TextHandler] whose attributes consists
|
||||
// of h's attributes followed by attrs.
|
||||
func (h *TextHandler) WithAttrs(attrs []Attr) Handler {
|
||||
return &TextHandler{commonHandler: h.commonHandler.withAttrs(attrs)}
|
||||
}
|
||||
|
||||
func (h *TextHandler) WithGroup(name string) Handler {
|
||||
return &TextHandler{commonHandler: h.commonHandler.withGroup(name)}
|
||||
}
|
||||
|
||||
// Handle formats its argument [Record] as a single line of space-separated
|
||||
// key=value items.
|
||||
//
|
||||
// If the Record's time is zero, the time is omitted.
|
||||
// Otherwise, the key is "time"
|
||||
// and the value is output in RFC3339 format with millisecond precision.
|
||||
//
|
||||
// If the Record's level is zero, the level is omitted.
|
||||
// Otherwise, the key is "level"
|
||||
// and the value of [Level.String] is output.
|
||||
//
|
||||
// If the AddSource option is set and source information is available,
|
||||
// the key is "source" and the value is output as FILE:LINE.
|
||||
//
|
||||
// The message's key is "msg".
|
||||
//
|
||||
// To modify these or other attributes, or remove them from the output, use
|
||||
// [HandlerOptions.ReplaceAttr].
|
||||
//
|
||||
// If a value implements [encoding.TextMarshaler], the result of MarshalText is
|
||||
// written. Otherwise, the result of [fmt.Sprint] is written.
|
||||
//
|
||||
// Keys and values are quoted with [strconv.Quote] if they contain Unicode space
|
||||
// characters, non-printing characters, '"' or '='.
|
||||
//
|
||||
// Keys inside groups consist of components (keys or group names) separated by
|
||||
// dots. No further escaping is performed.
|
||||
// Thus there is no way to determine from the key "a.b.c" whether there
|
||||
// are two groups "a" and "b" and a key "c", or a single group "a.b" and a key "c",
|
||||
// or single group "a" and a key "b.c".
|
||||
// If it is necessary to reconstruct the group structure of a key
|
||||
// even in the presence of dots inside components, use
|
||||
// [HandlerOptions.ReplaceAttr] to encode that information in the key.
|
||||
//
|
||||
// Each call to Handle results in a single serialized call to
|
||||
// io.Writer.Write.
|
||||
func (h *TextHandler) Handle(_ context.Context, r Record) error {
|
||||
return h.commonHandler.handle(r)
|
||||
}
|
||||
|
||||
func appendTextValue(s *handleState, v Value) error {
|
||||
switch v.Kind() {
|
||||
case KindString:
|
||||
s.appendString(v.str())
|
||||
case KindTime:
|
||||
s.appendTime(v.time())
|
||||
case KindAny:
|
||||
if tm, ok := v.any.(encoding.TextMarshaler); ok {
|
||||
data, err := tm.MarshalText()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: avoid the conversion to string.
|
||||
s.appendString(string(data))
|
||||
return nil
|
||||
}
|
||||
if bs, ok := byteSlice(v.any); ok {
|
||||
// As of Go 1.19, this only allocates for strings longer than 32 bytes.
|
||||
s.buf.WriteString(strconv.Quote(string(bs)))
|
||||
return nil
|
||||
}
|
||||
s.appendString(fmt.Sprintf("%+v", v.Any()))
|
||||
default:
|
||||
*s.buf = v.append(*s.buf)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// byteSlice returns its argument as a []byte if the argument's
|
||||
// underlying type is []byte, along with a second return value of true.
|
||||
// Otherwise it returns nil, false.
|
||||
func byteSlice(a any) ([]byte, bool) {
|
||||
if bs, ok := a.([]byte); ok {
|
||||
return bs, true
|
||||
}
|
||||
// Like Printf's %s, we allow both the slice type and the byte element type to be named.
|
||||
t := reflect.TypeOf(a)
|
||||
if t != nil && t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 {
|
||||
return reflect.ValueOf(a).Bytes(), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func needsQuoting(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return true
|
||||
}
|
||||
for i := 0; i < len(s); {
|
||||
b := s[i]
|
||||
if b < utf8.RuneSelf {
|
||||
// Quote anything except a backslash that would need quoting in a
|
||||
// JSON string, as well as space and '='
|
||||
if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) {
|
||||
return true
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) {
|
||||
return true
|
||||
}
|
||||
i += size
|
||||
}
|
||||
return false
|
||||
}
|
||||
176
src/log/slog/text_handler_test.go
Normal file
176
src/log/slog/text_handler_test.go
Normal file
@@ -0,0 +1,176 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"internal/testenv"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var testTime = time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
|
||||
func TestTextHandler(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
attr Attr
|
||||
wantKey, wantVal string
|
||||
}{
|
||||
{
|
||||
"unquoted",
|
||||
Int("a", 1),
|
||||
"a", "1",
|
||||
},
|
||||
{
|
||||
"quoted",
|
||||
String("x = y", `qu"o`),
|
||||
`"x = y"`, `"qu\"o"`,
|
||||
},
|
||||
{
|
||||
"String method",
|
||||
Any("name", name{"Ren", "Hoek"}),
|
||||
`name`, `"Hoek, Ren"`,
|
||||
},
|
||||
{
|
||||
"struct",
|
||||
Any("x", &struct{ A, b int }{A: 1, b: 2}),
|
||||
`x`, `"&{A:1 b:2}"`,
|
||||
},
|
||||
{
|
||||
"TextMarshaler",
|
||||
Any("t", text{"abc"}),
|
||||
`t`, `"text{\"abc\"}"`,
|
||||
},
|
||||
{
|
||||
"TextMarshaler error",
|
||||
Any("t", text{""}),
|
||||
`t`, `"!ERROR:text: empty string"`,
|
||||
},
|
||||
{
|
||||
"nil value",
|
||||
Any("a", nil),
|
||||
`a`, `<nil>`,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for _, opts := range []struct {
|
||||
name string
|
||||
opts HandlerOptions
|
||||
wantPrefix string
|
||||
modKey func(string) string
|
||||
}{
|
||||
{
|
||||
"none",
|
||||
HandlerOptions{},
|
||||
`time=2000-01-02T03:04:05.000Z level=INFO msg="a message"`,
|
||||
func(s string) string { return s },
|
||||
},
|
||||
{
|
||||
"replace",
|
||||
HandlerOptions{ReplaceAttr: upperCaseKey},
|
||||
`TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG="a message"`,
|
||||
strings.ToUpper,
|
||||
},
|
||||
} {
|
||||
t.Run(opts.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
h := NewTextHandler(&buf, &opts.opts)
|
||||
r := NewRecord(testTime, LevelInfo, "a message", 0)
|
||||
r.AddAttrs(test.attr)
|
||||
if err := h.Handle(context.Background(), r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := buf.String()
|
||||
// Remove final newline.
|
||||
got = got[:len(got)-1]
|
||||
want := opts.wantPrefix + " " + opts.modKey(test.wantKey) + "=" + test.wantVal
|
||||
if got != want {
|
||||
t.Errorf("\ngot %s\nwant %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// for testing fmt.Sprint
|
||||
type name struct {
|
||||
First, Last string
|
||||
}
|
||||
|
||||
func (n name) String() string { return n.Last + ", " + n.First }
|
||||
|
||||
// for testing TextMarshaler
|
||||
type text struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (t text) String() string { return t.s } // should be ignored
|
||||
|
||||
func (t text) MarshalText() ([]byte, error) {
|
||||
if t.s == "" {
|
||||
return nil, errors.New("text: empty string")
|
||||
}
|
||||
return []byte(fmt.Sprintf("text{%q}", t.s)), nil
|
||||
}
|
||||
|
||||
func TestTextHandlerPreformatted(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
var h Handler = NewTextHandler(&buf, nil)
|
||||
h = h.WithAttrs([]Attr{Duration("dur", time.Minute), Bool("b", true)})
|
||||
// Also test omitting time.
|
||||
r := NewRecord(time.Time{}, 0 /* 0 Level is INFO */, "m", 0)
|
||||
r.AddAttrs(Int("a", 1))
|
||||
if err := h.Handle(context.Background(), r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := strings.TrimSuffix(buf.String(), "\n")
|
||||
want := `level=INFO msg=m dur=1m0s b=true a=1`
|
||||
if got != want {
|
||||
t.Errorf("got %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextHandlerAlloc(t *testing.T) {
|
||||
testenv.SkipIfOptimizationOff(t)
|
||||
r := NewRecord(time.Now(), LevelInfo, "msg", 0)
|
||||
for i := 0; i < 10; i++ {
|
||||
r.AddAttrs(Int("x = y", i))
|
||||
}
|
||||
var h Handler = NewTextHandler(io.Discard, nil)
|
||||
wantAllocs(t, 0, func() { h.Handle(context.Background(), r) })
|
||||
|
||||
h = h.WithGroup("s")
|
||||
r.AddAttrs(Group("g", Int("a", 1)))
|
||||
wantAllocs(t, 0, func() { h.Handle(context.Background(), r) })
|
||||
}
|
||||
|
||||
func TestNeedsQuoting(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{"", true},
|
||||
{"ab", false},
|
||||
{"a=b", true},
|
||||
{`"ab"`, true},
|
||||
{"\a\b", true},
|
||||
{"a\tb", true},
|
||||
{"µåπ", false},
|
||||
{"a b", true},
|
||||
{"badutf8\xF6", true},
|
||||
} {
|
||||
got := needsQuoting(test.in)
|
||||
if got != test.want {
|
||||
t.Errorf("%q: got %t, want %t", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
540
src/log/slog/value.go
Normal file
540
src/log/slog/value.go
Normal file
@@ -0,0 +1,540 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// A Value can represent any Go value, but unlike type any,
|
||||
// it can represent most small values without an allocation.
|
||||
// The zero Value corresponds to nil.
|
||||
type Value struct {
|
||||
_ [0]func() // disallow ==
|
||||
// num holds the value for Kinds Int64, Uint64, Float64, Bool and Duration,
|
||||
// the string length for KindString, and nanoseconds since the epoch for KindTime.
|
||||
num uint64
|
||||
// If any is of type Kind, then the value is in num as described above.
|
||||
// If any is of type *time.Location, then the Kind is Time and time.Time value
|
||||
// can be constructed from the Unix nanos in num and the location (monotonic time
|
||||
// is not preserved).
|
||||
// If any is of type stringptr, then the Kind is String and the string value
|
||||
// consists of the length in num and the pointer in any.
|
||||
// Otherwise, the Kind is Any and any is the value.
|
||||
// (This implies that Attrs cannot store values of type Kind, *time.Location
|
||||
// or stringptr.)
|
||||
any any
|
||||
}
|
||||
|
||||
type (
|
||||
stringptr *byte // used in Value.any when the Value is a string
|
||||
groupptr *Attr // used in Value.any when the Value is a []Attr
|
||||
)
|
||||
|
||||
// Kind is the kind of a [Value].
|
||||
type Kind int
|
||||
|
||||
// The following list is sorted alphabetically, but it's also important that
|
||||
// KindAny is 0 so that a zero Value represents nil.
|
||||
|
||||
const (
|
||||
KindAny Kind = iota
|
||||
KindBool
|
||||
KindDuration
|
||||
KindFloat64
|
||||
KindInt64
|
||||
KindString
|
||||
KindTime
|
||||
KindUint64
|
||||
KindGroup
|
||||
KindLogValuer
|
||||
)
|
||||
|
||||
var kindStrings = []string{
|
||||
"Any",
|
||||
"Bool",
|
||||
"Duration",
|
||||
"Float64",
|
||||
"Int64",
|
||||
"String",
|
||||
"Time",
|
||||
"Uint64",
|
||||
"Group",
|
||||
"LogValuer",
|
||||
}
|
||||
|
||||
func (k Kind) String() string {
|
||||
if k >= 0 && int(k) < len(kindStrings) {
|
||||
return kindStrings[k]
|
||||
}
|
||||
return "<unknown slog.Kind>"
|
||||
}
|
||||
|
||||
// Unexported version of Kind, just so we can store Kinds in Values.
|
||||
// (No user-provided value has this type.)
|
||||
type kind Kind
|
||||
|
||||
// Kind returns v's Kind.
|
||||
func (v Value) Kind() Kind {
|
||||
switch x := v.any.(type) {
|
||||
case Kind:
|
||||
return x
|
||||
case stringptr:
|
||||
return KindString
|
||||
case timeLocation, timeTime:
|
||||
return KindTime
|
||||
case groupptr:
|
||||
return KindGroup
|
||||
case LogValuer:
|
||||
return KindLogValuer
|
||||
case kind: // a kind is just a wrapper for a Kind
|
||||
return KindAny
|
||||
default:
|
||||
return KindAny
|
||||
}
|
||||
}
|
||||
|
||||
//////////////// Constructors
|
||||
|
||||
// StringValue returns a new [Value] for a string.
|
||||
func StringValue(value string) Value {
|
||||
return Value{num: uint64(len(value)), any: stringptr(unsafe.StringData(value))}
|
||||
}
|
||||
|
||||
// IntValue returns a [Value] for an int.
|
||||
func IntValue(v int) Value {
|
||||
return Int64Value(int64(v))
|
||||
}
|
||||
|
||||
// Int64Value returns a [Value] for an int64.
|
||||
func Int64Value(v int64) Value {
|
||||
return Value{num: uint64(v), any: KindInt64}
|
||||
}
|
||||
|
||||
// Uint64Value returns a [Value] for a uint64.
|
||||
func Uint64Value(v uint64) Value {
|
||||
return Value{num: v, any: KindUint64}
|
||||
}
|
||||
|
||||
// Float64Value returns a [Value] for a floating-point number.
|
||||
func Float64Value(v float64) Value {
|
||||
return Value{num: math.Float64bits(v), any: KindFloat64}
|
||||
}
|
||||
|
||||
// BoolValue returns a [Value] for a bool.
|
||||
func BoolValue(v bool) Value {
|
||||
u := uint64(0)
|
||||
if v {
|
||||
u = 1
|
||||
}
|
||||
return Value{num: u, any: KindBool}
|
||||
}
|
||||
|
||||
type (
|
||||
// Unexported version of *time.Location, just so we can store *time.Locations in
|
||||
// Values. (No user-provided value has this type.)
|
||||
timeLocation *time.Location
|
||||
|
||||
// timeTime is for times where UnixNano is undefined.
|
||||
timeTime time.Time
|
||||
)
|
||||
|
||||
// TimeValue returns a [Value] for a [time.Time].
|
||||
// It discards the monotonic portion.
|
||||
func TimeValue(v time.Time) Value {
|
||||
if v.IsZero() {
|
||||
// UnixNano on the zero time is undefined, so represent the zero time
|
||||
// with a nil *time.Location instead. time.Time.Location method never
|
||||
// returns nil, so a Value with any == timeLocation(nil) cannot be
|
||||
// mistaken for any other Value, time.Time or otherwise.
|
||||
return Value{any: timeLocation(nil)}
|
||||
}
|
||||
nsec := v.UnixNano()
|
||||
t := time.Unix(0, nsec)
|
||||
if v.Equal(t) {
|
||||
// UnixNano correctly represents the time, so use a zero-alloc representation.
|
||||
return Value{num: uint64(nsec), any: timeLocation(v.Location())}
|
||||
}
|
||||
// Fall back to the general form.
|
||||
// Strip the monotonic portion to match the other representation.
|
||||
return Value{any: timeTime(v.Round(0))}
|
||||
}
|
||||
|
||||
// DurationValue returns a [Value] for a [time.Duration].
|
||||
func DurationValue(v time.Duration) Value {
|
||||
return Value{num: uint64(v.Nanoseconds()), any: KindDuration}
|
||||
}
|
||||
|
||||
// GroupValue returns a new [Value] for a list of Attrs.
|
||||
// The caller must not subsequently mutate the argument slice.
|
||||
func GroupValue(as ...Attr) Value {
|
||||
// Remove empty groups.
|
||||
// It is simpler overall to do this at construction than
|
||||
// to check each Group recursively for emptiness.
|
||||
if n := countEmptyGroups(as); n > 0 {
|
||||
as2 := make([]Attr, 0, len(as)-n)
|
||||
for _, a := range as {
|
||||
if !a.Value.isEmptyGroup() {
|
||||
as2 = append(as2, a)
|
||||
}
|
||||
}
|
||||
as = as2
|
||||
}
|
||||
return Value{num: uint64(len(as)), any: groupptr(unsafe.SliceData(as))}
|
||||
}
|
||||
|
||||
// countEmptyGroups returns the number of empty group values in its argument.
|
||||
func countEmptyGroups(as []Attr) int {
|
||||
n := 0
|
||||
for _, a := range as {
|
||||
if a.Value.isEmptyGroup() {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// AnyValue returns a [Value] for the supplied value.
|
||||
//
|
||||
// If the supplied value is of type Value, it is returned
|
||||
// unmodified.
|
||||
//
|
||||
// Given a value of one of Go's predeclared string, bool, or
|
||||
// (non-complex) numeric types, AnyValue returns a Value of kind
|
||||
// [KindString], [KindBool], [KindUint64], [KindInt64], or [KindFloat64].
|
||||
// The width of the original numeric type is not preserved.
|
||||
//
|
||||
// Given a [time.Time] or [time.Duration] value, AnyValue returns a Value of kind
|
||||
// [KindTime] or [KindDuration]. The monotonic time is not preserved.
|
||||
//
|
||||
// For nil, or values of all other types, including named types whose
|
||||
// underlying type is numeric, AnyValue returns a value of kind [KindAny].
|
||||
func AnyValue(v any) Value {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return StringValue(v)
|
||||
case int:
|
||||
return Int64Value(int64(v))
|
||||
case uint:
|
||||
return Uint64Value(uint64(v))
|
||||
case int64:
|
||||
return Int64Value(v)
|
||||
case uint64:
|
||||
return Uint64Value(v)
|
||||
case bool:
|
||||
return BoolValue(v)
|
||||
case time.Duration:
|
||||
return DurationValue(v)
|
||||
case time.Time:
|
||||
return TimeValue(v)
|
||||
case uint8:
|
||||
return Uint64Value(uint64(v))
|
||||
case uint16:
|
||||
return Uint64Value(uint64(v))
|
||||
case uint32:
|
||||
return Uint64Value(uint64(v))
|
||||
case uintptr:
|
||||
return Uint64Value(uint64(v))
|
||||
case int8:
|
||||
return Int64Value(int64(v))
|
||||
case int16:
|
||||
return Int64Value(int64(v))
|
||||
case int32:
|
||||
return Int64Value(int64(v))
|
||||
case float64:
|
||||
return Float64Value(v)
|
||||
case float32:
|
||||
return Float64Value(float64(v))
|
||||
case []Attr:
|
||||
return GroupValue(v...)
|
||||
case Kind:
|
||||
return Value{any: kind(v)}
|
||||
case Value:
|
||||
return v
|
||||
default:
|
||||
return Value{any: v}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////// Accessors
|
||||
|
||||
// Any returns v's value as an any.
|
||||
func (v Value) Any() any {
|
||||
switch v.Kind() {
|
||||
case KindAny:
|
||||
if k, ok := v.any.(kind); ok {
|
||||
return Kind(k)
|
||||
}
|
||||
return v.any
|
||||
case KindLogValuer:
|
||||
return v.any
|
||||
case KindGroup:
|
||||
return v.group()
|
||||
case KindInt64:
|
||||
return int64(v.num)
|
||||
case KindUint64:
|
||||
return v.num
|
||||
case KindFloat64:
|
||||
return v.float()
|
||||
case KindString:
|
||||
return v.str()
|
||||
case KindBool:
|
||||
return v.bool()
|
||||
case KindDuration:
|
||||
return v.duration()
|
||||
case KindTime:
|
||||
return v.time()
|
||||
default:
|
||||
panic(fmt.Sprintf("bad kind: %s", v.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// String returns Value's value as a string, formatted like [fmt.Sprint]. Unlike
|
||||
// the methods Int64, Float64, and so on, which panic if v is of the
|
||||
// wrong kind, String never panics.
|
||||
func (v Value) String() string {
|
||||
if sp, ok := v.any.(stringptr); ok {
|
||||
return unsafe.String(sp, v.num)
|
||||
}
|
||||
var buf []byte
|
||||
return string(v.append(buf))
|
||||
}
|
||||
|
||||
func (v Value) str() string {
|
||||
return unsafe.String(v.any.(stringptr), v.num)
|
||||
}
|
||||
|
||||
// Int64 returns v's value as an int64. It panics
|
||||
// if v is not a signed integer.
|
||||
func (v Value) Int64() int64 {
|
||||
if g, w := v.Kind(), KindInt64; g != w {
|
||||
panic(fmt.Sprintf("Value kind is %s, not %s", g, w))
|
||||
}
|
||||
return int64(v.num)
|
||||
}
|
||||
|
||||
// Uint64 returns v's value as a uint64. It panics
|
||||
// if v is not an unsigned integer.
|
||||
func (v Value) Uint64() uint64 {
|
||||
if g, w := v.Kind(), KindUint64; g != w {
|
||||
panic(fmt.Sprintf("Value kind is %s, not %s", g, w))
|
||||
}
|
||||
return v.num
|
||||
}
|
||||
|
||||
// Bool returns v's value as a bool. It panics
|
||||
// if v is not a bool.
|
||||
func (v Value) Bool() bool {
|
||||
if g, w := v.Kind(), KindBool; g != w {
|
||||
panic(fmt.Sprintf("Value kind is %s, not %s", g, w))
|
||||
}
|
||||
return v.bool()
|
||||
}
|
||||
|
||||
func (v Value) bool() bool {
|
||||
return v.num == 1
|
||||
}
|
||||
|
||||
// Duration returns v's value as a [time.Duration]. It panics
|
||||
// if v is not a time.Duration.
|
||||
func (v Value) Duration() time.Duration {
|
||||
if g, w := v.Kind(), KindDuration; g != w {
|
||||
panic(fmt.Sprintf("Value kind is %s, not %s", g, w))
|
||||
}
|
||||
|
||||
return v.duration()
|
||||
}
|
||||
|
||||
func (v Value) duration() time.Duration {
|
||||
return time.Duration(int64(v.num))
|
||||
}
|
||||
|
||||
// Float64 returns v's value as a float64. It panics
|
||||
// if v is not a float64.
|
||||
func (v Value) Float64() float64 {
|
||||
if g, w := v.Kind(), KindFloat64; g != w {
|
||||
panic(fmt.Sprintf("Value kind is %s, not %s", g, w))
|
||||
}
|
||||
|
||||
return v.float()
|
||||
}
|
||||
|
||||
func (v Value) float() float64 {
|
||||
return math.Float64frombits(v.num)
|
||||
}
|
||||
|
||||
// Time returns v's value as a [time.Time]. It panics
|
||||
// if v is not a time.Time.
|
||||
func (v Value) Time() time.Time {
|
||||
if g, w := v.Kind(), KindTime; g != w {
|
||||
panic(fmt.Sprintf("Value kind is %s, not %s", g, w))
|
||||
}
|
||||
return v.time()
|
||||
}
|
||||
|
||||
// See TimeValue to understand how times are represented.
|
||||
func (v Value) time() time.Time {
|
||||
switch a := v.any.(type) {
|
||||
case timeLocation:
|
||||
if a == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(0, int64(v.num)).In(a)
|
||||
case timeTime:
|
||||
return time.Time(a)
|
||||
default:
|
||||
panic(fmt.Sprintf("bad time type %T", v.any))
|
||||
}
|
||||
}
|
||||
|
||||
// LogValuer returns v's value as a LogValuer. It panics
|
||||
// if v is not a LogValuer.
|
||||
func (v Value) LogValuer() LogValuer {
|
||||
return v.any.(LogValuer)
|
||||
}
|
||||
|
||||
// Group returns v's value as a []Attr.
|
||||
// It panics if v's [Kind] is not [KindGroup].
|
||||
func (v Value) Group() []Attr {
|
||||
if sp, ok := v.any.(groupptr); ok {
|
||||
return unsafe.Slice((*Attr)(sp), v.num)
|
||||
}
|
||||
panic("Group: bad kind")
|
||||
}
|
||||
|
||||
func (v Value) group() []Attr {
|
||||
return unsafe.Slice((*Attr)(v.any.(groupptr)), v.num)
|
||||
}
|
||||
|
||||
//////////////// Other
|
||||
|
||||
// Equal reports whether v and w represent the same Go value.
|
||||
func (v Value) Equal(w Value) bool {
|
||||
k1 := v.Kind()
|
||||
k2 := w.Kind()
|
||||
if k1 != k2 {
|
||||
return false
|
||||
}
|
||||
switch k1 {
|
||||
case KindInt64, KindUint64, KindBool, KindDuration:
|
||||
return v.num == w.num
|
||||
case KindString:
|
||||
return v.str() == w.str()
|
||||
case KindFloat64:
|
||||
return v.float() == w.float()
|
||||
case KindTime:
|
||||
return v.time().Equal(w.time())
|
||||
case KindAny, KindLogValuer:
|
||||
return v.any == w.any // may panic if non-comparable
|
||||
case KindGroup:
|
||||
return slices.EqualFunc(v.group(), w.group(), Attr.Equal)
|
||||
default:
|
||||
panic(fmt.Sprintf("bad kind: %s", k1))
|
||||
}
|
||||
}
|
||||
|
||||
// isEmptyGroup reports whether v is a group that has no attributes.
|
||||
func (v Value) isEmptyGroup() bool {
|
||||
if v.Kind() != KindGroup {
|
||||
return false
|
||||
}
|
||||
// We do not need to recursively examine the group's Attrs for emptiness,
|
||||
// because GroupValue removed them when the group was constructed, and
|
||||
// groups are immutable.
|
||||
return len(v.group()) == 0
|
||||
}
|
||||
|
||||
// append appends a text representation of v to dst.
|
||||
// v is formatted as with fmt.Sprint.
|
||||
func (v Value) append(dst []byte) []byte {
|
||||
switch v.Kind() {
|
||||
case KindString:
|
||||
return append(dst, v.str()...)
|
||||
case KindInt64:
|
||||
return strconv.AppendInt(dst, int64(v.num), 10)
|
||||
case KindUint64:
|
||||
return strconv.AppendUint(dst, v.num, 10)
|
||||
case KindFloat64:
|
||||
return strconv.AppendFloat(dst, v.float(), 'g', -1, 64)
|
||||
case KindBool:
|
||||
return strconv.AppendBool(dst, v.bool())
|
||||
case KindDuration:
|
||||
return append(dst, v.duration().String()...)
|
||||
case KindTime:
|
||||
return append(dst, v.time().String()...)
|
||||
case KindGroup:
|
||||
return fmt.Append(dst, v.group())
|
||||
case KindAny, KindLogValuer:
|
||||
return fmt.Append(dst, v.any)
|
||||
default:
|
||||
panic(fmt.Sprintf("bad kind: %s", v.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// A LogValuer is any Go value that can convert itself into a Value for logging.
|
||||
//
|
||||
// This mechanism may be used to defer expensive operations until they are
|
||||
// needed, or to expand a single value into a sequence of components.
|
||||
type LogValuer interface {
|
||||
LogValue() Value
|
||||
}
|
||||
|
||||
const maxLogValues = 100
|
||||
|
||||
// Resolve repeatedly calls LogValue on v while it implements [LogValuer],
|
||||
// and returns the result.
|
||||
// If v resolves to a group, the group's attributes' values are not recursively
|
||||
// resolved.
|
||||
// If the number of LogValue calls exceeds a threshold, a Value containing an
|
||||
// error is returned.
|
||||
// Resolve's return value is guaranteed not to be of Kind [KindLogValuer].
|
||||
func (v Value) Resolve() (rv Value) {
|
||||
orig := v
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rv = AnyValue(fmt.Errorf("LogValue panicked\n%s", stack(3, 5)))
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < maxLogValues; i++ {
|
||||
if v.Kind() != KindLogValuer {
|
||||
return v
|
||||
}
|
||||
v = v.LogValuer().LogValue()
|
||||
}
|
||||
err := fmt.Errorf("LogValue called too many times on Value of type %T", orig.Any())
|
||||
return AnyValue(err)
|
||||
}
|
||||
|
||||
func stack(skip, nFrames int) string {
|
||||
pcs := make([]uintptr, nFrames+1)
|
||||
n := runtime.Callers(skip+1, pcs)
|
||||
if n == 0 {
|
||||
return "(no stack)"
|
||||
}
|
||||
frames := runtime.CallersFrames(pcs[:n])
|
||||
var b strings.Builder
|
||||
i := 0
|
||||
for {
|
||||
frame, more := frames.Next()
|
||||
fmt.Fprintf(&b, "called from %s (%s:%d)\n", frame.Function, frame.File, frame.Line)
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
i++
|
||||
if i >= nFrames {
|
||||
fmt.Fprintf(&b, "(rest of stack elided)\n")
|
||||
break
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
215
src/log/slog/value_access_benchmark_test.go
Normal file
215
src/log/slog/value_access_benchmark_test.go
Normal file
@@ -0,0 +1,215 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Benchmark for accessing Value values.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The "As" form is the slowest.
|
||||
// The switch-panic and visitor times are almost the same.
|
||||
// BenchmarkDispatch/switch-checked-8 8669427 137.7 ns/op
|
||||
// BenchmarkDispatch/As-8 8212087 145.3 ns/op
|
||||
// BenchmarkDispatch/Visit-8 8926146 135.3 ns/op
|
||||
func BenchmarkDispatch(b *testing.B) {
|
||||
vs := []Value{
|
||||
Int64Value(32768),
|
||||
Uint64Value(0xfacecafe),
|
||||
StringValue("anything"),
|
||||
BoolValue(true),
|
||||
Float64Value(1.2345),
|
||||
DurationValue(time.Second),
|
||||
AnyValue(b),
|
||||
}
|
||||
var (
|
||||
ii int64
|
||||
s string
|
||||
bb bool
|
||||
u uint64
|
||||
d time.Duration
|
||||
f float64
|
||||
a any
|
||||
)
|
||||
b.Run("switch-checked", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, v := range vs {
|
||||
switch v.Kind() {
|
||||
case KindString:
|
||||
s = v.String()
|
||||
case KindInt64:
|
||||
ii = v.Int64()
|
||||
case KindUint64:
|
||||
u = v.Uint64()
|
||||
case KindFloat64:
|
||||
f = v.Float64()
|
||||
case KindBool:
|
||||
bb = v.Bool()
|
||||
case KindDuration:
|
||||
d = v.Duration()
|
||||
case KindAny:
|
||||
a = v.Any()
|
||||
default:
|
||||
panic("bad kind")
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = ii
|
||||
_ = s
|
||||
_ = bb
|
||||
_ = u
|
||||
_ = d
|
||||
_ = f
|
||||
_ = a
|
||||
|
||||
})
|
||||
b.Run("As", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, kv := range vs {
|
||||
if v, ok := kv.AsString(); ok {
|
||||
s = v
|
||||
} else if v, ok := kv.AsInt64(); ok {
|
||||
ii = v
|
||||
} else if v, ok := kv.AsUint64(); ok {
|
||||
u = v
|
||||
} else if v, ok := kv.AsFloat64(); ok {
|
||||
f = v
|
||||
} else if v, ok := kv.AsBool(); ok {
|
||||
bb = v
|
||||
} else if v, ok := kv.AsDuration(); ok {
|
||||
d = v
|
||||
} else if v, ok := kv.AsAny(); ok {
|
||||
a = v
|
||||
} else {
|
||||
panic("bad kind")
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = ii
|
||||
_ = s
|
||||
_ = bb
|
||||
_ = u
|
||||
_ = d
|
||||
_ = f
|
||||
_ = a
|
||||
})
|
||||
|
||||
b.Run("Visit", func(b *testing.B) {
|
||||
v := &setVisitor{}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, kv := range vs {
|
||||
kv.Visit(v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type setVisitor struct {
|
||||
i int64
|
||||
s string
|
||||
b bool
|
||||
u uint64
|
||||
d time.Duration
|
||||
f float64
|
||||
a any
|
||||
}
|
||||
|
||||
func (v *setVisitor) String(s string) { v.s = s }
|
||||
func (v *setVisitor) Int64(i int64) { v.i = i }
|
||||
func (v *setVisitor) Uint64(x uint64) { v.u = x }
|
||||
func (v *setVisitor) Float64(x float64) { v.f = x }
|
||||
func (v *setVisitor) Bool(x bool) { v.b = x }
|
||||
func (v *setVisitor) Duration(x time.Duration) { v.d = x }
|
||||
func (v *setVisitor) Any(x any) { v.a = x }
|
||||
|
||||
// When dispatching on all types, the "As" functions are slightly slower
|
||||
// than switching on the kind and then calling a function that checks
|
||||
// the kind again. See BenchmarkDispatch above.
|
||||
|
||||
func (a Value) AsString() (string, bool) {
|
||||
if a.Kind() == KindString {
|
||||
return a.str(), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (a Value) AsInt64() (int64, bool) {
|
||||
if a.Kind() == KindInt64 {
|
||||
return int64(a.num), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (a Value) AsUint64() (uint64, bool) {
|
||||
if a.Kind() == KindUint64 {
|
||||
return a.num, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (a Value) AsFloat64() (float64, bool) {
|
||||
if a.Kind() == KindFloat64 {
|
||||
return a.float(), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (a Value) AsBool() (bool, bool) {
|
||||
if a.Kind() == KindBool {
|
||||
return a.bool(), true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func (a Value) AsDuration() (time.Duration, bool) {
|
||||
if a.Kind() == KindDuration {
|
||||
return a.duration(), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (a Value) AsAny() (any, bool) {
|
||||
if a.Kind() == KindAny {
|
||||
return a.any, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Problem: adding a type means adding a method, which is a breaking change.
|
||||
// Using an unexported method to force embedding will make programs compile,
|
||||
// But they will panic at runtime when we call the new method.
|
||||
type Visitor interface {
|
||||
String(string)
|
||||
Int64(int64)
|
||||
Uint64(uint64)
|
||||
Float64(float64)
|
||||
Bool(bool)
|
||||
Duration(time.Duration)
|
||||
Any(any)
|
||||
}
|
||||
|
||||
func (a Value) Visit(v Visitor) {
|
||||
switch a.Kind() {
|
||||
case KindString:
|
||||
v.String(a.str())
|
||||
case KindInt64:
|
||||
v.Int64(int64(a.num))
|
||||
case KindUint64:
|
||||
v.Uint64(a.num)
|
||||
case KindBool:
|
||||
v.Bool(a.bool())
|
||||
case KindFloat64:
|
||||
v.Float64(a.float())
|
||||
case KindDuration:
|
||||
v.Duration(a.duration())
|
||||
case KindAny:
|
||||
v.Any(a.any)
|
||||
default:
|
||||
panic("bad kind")
|
||||
}
|
||||
}
|
||||
296
src/log/slog/value_test.go
Normal file
296
src/log/slog/value_test.go
Normal file
@@ -0,0 +1,296 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package slog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func TestKindString(t *testing.T) {
|
||||
if got, want := KindGroup.String(), "Group"; got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueEqual(t *testing.T) {
|
||||
var x, y int
|
||||
vals := []Value{
|
||||
{},
|
||||
Int64Value(1),
|
||||
Int64Value(2),
|
||||
Float64Value(3.5),
|
||||
Float64Value(3.7),
|
||||
BoolValue(true),
|
||||
BoolValue(false),
|
||||
TimeValue(testTime),
|
||||
TimeValue(time.Time{}),
|
||||
TimeValue(time.Date(2001, 1, 2, 3, 4, 5, 0, time.UTC)),
|
||||
TimeValue(time.Date(2300, 1, 1, 0, 0, 0, 0, time.UTC)), // overflows nanoseconds
|
||||
TimeValue(time.Date(1715, 6, 13, 0, 25, 26, 290448384, time.UTC)), // overflowed value
|
||||
AnyValue(&x),
|
||||
AnyValue(&y),
|
||||
GroupValue(Bool("b", true), Int("i", 3)),
|
||||
GroupValue(Bool("b", true), Int("i", 4)),
|
||||
GroupValue(Bool("b", true), Int("j", 4)),
|
||||
DurationValue(3 * time.Second),
|
||||
DurationValue(2 * time.Second),
|
||||
StringValue("foo"),
|
||||
StringValue("fuu"),
|
||||
}
|
||||
for i, v1 := range vals {
|
||||
for j, v2 := range vals {
|
||||
got := v1.Equal(v2)
|
||||
want := i == j
|
||||
if got != want {
|
||||
t.Errorf("%v.Equal(%v): got %t, want %t", v1, v2, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func panics(f func()) (b bool) {
|
||||
defer func() {
|
||||
if x := recover(); x != nil {
|
||||
b = true
|
||||
}
|
||||
}()
|
||||
f()
|
||||
return false
|
||||
}
|
||||
|
||||
func TestValueString(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
v Value
|
||||
want string
|
||||
}{
|
||||
{Int64Value(-3), "-3"},
|
||||
{Uint64Value(1), "1"},
|
||||
{Float64Value(.15), "0.15"},
|
||||
{BoolValue(true), "true"},
|
||||
{StringValue("foo"), "foo"},
|
||||
{TimeValue(testTime), "2000-01-02 03:04:05 +0000 UTC"},
|
||||
{AnyValue(time.Duration(3 * time.Second)), "3s"},
|
||||
{GroupValue(Int("a", 1), Bool("b", true)), "[a=1 b=true]"},
|
||||
} {
|
||||
if got := test.v.String(); got != test.want {
|
||||
t.Errorf("%#v:\ngot %q\nwant %q", test.v, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueNoAlloc(t *testing.T) {
|
||||
// Assign values just to make sure the compiler doesn't optimize away the statements.
|
||||
var (
|
||||
i int64
|
||||
u uint64
|
||||
f float64
|
||||
b bool
|
||||
s string
|
||||
x any
|
||||
p = &i
|
||||
d time.Duration
|
||||
tm time.Time
|
||||
)
|
||||
a := int(testing.AllocsPerRun(5, func() {
|
||||
i = Int64Value(1).Int64()
|
||||
u = Uint64Value(1).Uint64()
|
||||
f = Float64Value(1).Float64()
|
||||
b = BoolValue(true).Bool()
|
||||
s = StringValue("foo").String()
|
||||
d = DurationValue(d).Duration()
|
||||
tm = TimeValue(testTime).Time()
|
||||
x = AnyValue(p).Any()
|
||||
}))
|
||||
if a != 0 {
|
||||
t.Errorf("got %d allocs, want zero", a)
|
||||
}
|
||||
_ = u
|
||||
_ = f
|
||||
_ = b
|
||||
_ = s
|
||||
_ = x
|
||||
_ = tm
|
||||
}
|
||||
|
||||
func TestAnyLevelAlloc(t *testing.T) {
|
||||
// Because typical Levels are small integers,
|
||||
// they are zero-alloc.
|
||||
var a Value
|
||||
x := LevelDebug + 100
|
||||
wantAllocs(t, 0, func() { a = AnyValue(x) })
|
||||
_ = a
|
||||
}
|
||||
|
||||
func TestAnyValue(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in any
|
||||
want Value
|
||||
}{
|
||||
{1, IntValue(1)},
|
||||
{1.5, Float64Value(1.5)},
|
||||
{float32(2.5), Float64Value(2.5)},
|
||||
{"s", StringValue("s")},
|
||||
{true, BoolValue(true)},
|
||||
{testTime, TimeValue(testTime)},
|
||||
{time.Hour, DurationValue(time.Hour)},
|
||||
{[]Attr{Int("i", 3)}, GroupValue(Int("i", 3))},
|
||||
{IntValue(4), IntValue(4)},
|
||||
{uint(2), Uint64Value(2)},
|
||||
{uint8(3), Uint64Value(3)},
|
||||
{uint16(4), Uint64Value(4)},
|
||||
{uint32(5), Uint64Value(5)},
|
||||
{uint64(6), Uint64Value(6)},
|
||||
{uintptr(7), Uint64Value(7)},
|
||||
{int8(8), Int64Value(8)},
|
||||
{int16(9), Int64Value(9)},
|
||||
{int32(10), Int64Value(10)},
|
||||
{int64(11), Int64Value(11)},
|
||||
} {
|
||||
got := AnyValue(test.in)
|
||||
if !got.Equal(test.want) {
|
||||
t.Errorf("%v (%[1]T): got %v (kind %s), want %v (kind %s)",
|
||||
test.in, got, got.Kind(), test.want, test.want.Kind())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueAny(t *testing.T) {
|
||||
for _, want := range []any{
|
||||
nil,
|
||||
LevelDebug + 100,
|
||||
time.UTC, // time.Locations treated specially...
|
||||
KindBool, // ...as are Kinds
|
||||
[]Attr{Int("a", 1)},
|
||||
int64(2),
|
||||
uint64(3),
|
||||
true,
|
||||
time.Minute,
|
||||
time.Time{},
|
||||
3.14,
|
||||
"foo",
|
||||
} {
|
||||
v := AnyValue(want)
|
||||
got := v.Any()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogValue(t *testing.T) {
|
||||
want := "replaced"
|
||||
r := &replace{StringValue(want)}
|
||||
v := AnyValue(r)
|
||||
if g, w := v.Kind(), KindLogValuer; g != w {
|
||||
t.Errorf("got %s, want %s", g, w)
|
||||
}
|
||||
got := v.LogValuer().LogValue().Any()
|
||||
if got != want {
|
||||
t.Errorf("got %#v, want %#v", got, want)
|
||||
}
|
||||
|
||||
// Test Resolve.
|
||||
got = v.Resolve().Any()
|
||||
if got != want {
|
||||
t.Errorf("got %#v, want %#v", got, want)
|
||||
}
|
||||
|
||||
// Test Resolve max iteration.
|
||||
r.v = AnyValue(r) // create a cycle
|
||||
got = AnyValue(r).Resolve().Any()
|
||||
if _, ok := got.(error); !ok {
|
||||
t.Errorf("expected error, got %T", got)
|
||||
}
|
||||
|
||||
// Groups are not recursively resolved.
|
||||
c := Any("c", &replace{StringValue("d")})
|
||||
v = AnyValue(&replace{GroupValue(Int("a", 1), Group("b", c))})
|
||||
got2 := v.Resolve().Any().([]Attr)
|
||||
want2 := []Attr{Int("a", 1), Group("b", c)}
|
||||
if !attrsEqual(got2, want2) {
|
||||
t.Errorf("got %v, want %v", got2, want2)
|
||||
}
|
||||
|
||||
// Verify that panics in Resolve are caught and turn into errors.
|
||||
v = AnyValue(panickingLogValue{})
|
||||
got = v.Resolve().Any()
|
||||
gotErr, ok := got.(error)
|
||||
if !ok {
|
||||
t.Errorf("expected error, got %T", got)
|
||||
}
|
||||
// The error should provide some context information.
|
||||
// We'll just check that this function name appears in it.
|
||||
if got, want := gotErr.Error(), "TestLogValue"; !strings.Contains(got, want) {
|
||||
t.Errorf("got %q, want substring %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueTime(t *testing.T) {
|
||||
// Validate that all representations of times work correctly.
|
||||
for _, tm := range []time.Time{
|
||||
time.Time{},
|
||||
time.Unix(0, 1e15), // UnixNanos is defined
|
||||
time.Date(2300, 1, 1, 0, 0, 0, 0, time.UTC), // overflows UnixNanos
|
||||
} {
|
||||
got := TimeValue(tm).Time()
|
||||
if !got.Equal(tm) {
|
||||
t.Errorf("got %s (%#[1]v), want %s (%#[2]v)", got, tm)
|
||||
}
|
||||
if g, w := got.Location(), tm.Location(); g != w {
|
||||
t.Errorf("%s: location: got %v, want %v", tm, g, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyGroup(t *testing.T) {
|
||||
g := GroupValue(
|
||||
Int("a", 1),
|
||||
Group("g1", Group("g2")),
|
||||
Group("g3", Group("g4", Int("b", 2))))
|
||||
got := g.Group()
|
||||
want := []Attr{Int("a", 1), Group("g3", Group("g4", Int("b", 2)))}
|
||||
if !attrsEqual(got, want) {
|
||||
t.Errorf("\ngot %v\nwant %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
type replace struct {
|
||||
v Value
|
||||
}
|
||||
|
||||
func (r *replace) LogValue() Value { return r.v }
|
||||
|
||||
type panickingLogValue struct{}
|
||||
|
||||
func (panickingLogValue) LogValue() Value { panic("bad") }
|
||||
|
||||
// A Value with "unsafe" strings is significantly faster:
|
||||
// safe: 1785 ns/op, 0 allocs
|
||||
// unsafe: 690 ns/op, 0 allocs
|
||||
|
||||
// Run this with and without -tags unsafe_kvs to compare.
|
||||
func BenchmarkUnsafeStrings(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
dst := make([]Value, 100)
|
||||
src := make([]Value, len(dst))
|
||||
b.Logf("Value size = %d", unsafe.Sizeof(Value{}))
|
||||
for i := range src {
|
||||
src[i] = StringValue(fmt.Sprintf("string#%d", i))
|
||||
}
|
||||
b.ResetTimer()
|
||||
var d string
|
||||
for i := 0; i < b.N; i++ {
|
||||
copy(dst, src)
|
||||
for _, a := range dst {
|
||||
d = a.String()
|
||||
}
|
||||
}
|
||||
_ = d
|
||||
}
|
||||
24
src/log/syslog/doc.go
Normal file
24
src/log/syslog/doc.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package syslog provides a simple interface to the system log
|
||||
// service. It can send messages to the syslog daemon using UNIX
|
||||
// domain sockets, UDP or TCP.
|
||||
//
|
||||
// Only one call to Dial is necessary. On write failures,
|
||||
// the syslog client will attempt to reconnect to the server
|
||||
// and write again.
|
||||
//
|
||||
// The syslog package is frozen and is not accepting new features.
|
||||
// Some external packages provide more functionality. See:
|
||||
//
|
||||
// https://godoc.org/?q=syslog
|
||||
package syslog
|
||||
|
||||
// BUG(brainman): This package is not implemented on Windows. As the
|
||||
// syslog package is frozen, Windows users are encouraged to
|
||||
// use a package outside of the standard library. For background,
|
||||
// see https://golang.org/issue/1108.
|
||||
|
||||
// BUG(akumar): This package is not implemented on Plan 9.
|
||||
23
src/log/syslog/example_test.go
Normal file
23
src/log/syslog/example_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows && !plan9
|
||||
|
||||
package syslog_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/syslog"
|
||||
)
|
||||
|
||||
func ExampleDial() {
|
||||
sysLog, err := syslog.Dial("tcp", "localhost:1234",
|
||||
syslog.LOG_WARNING|syslog.LOG_DAEMON, "demotag")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Fprintf(sysLog, "This is a daemon warning with demotag.")
|
||||
sysLog.Emerg("And this is a daemon emergency with demotag.")
|
||||
}
|
||||
318
src/log/syslog/syslog.go
Normal file
318
src/log/syslog/syslog.go
Normal file
@@ -0,0 +1,318 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows && !plan9
|
||||
|
||||
package syslog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The Priority is a combination of the syslog facility and
|
||||
// severity. For example, [LOG_ALERT] | [LOG_FTP] sends an alert severity
|
||||
// message from the FTP facility. The default severity is [LOG_EMERG];
|
||||
// the default facility is [LOG_KERN].
|
||||
type Priority int
|
||||
|
||||
const severityMask = 0x07
|
||||
const facilityMask = 0xf8
|
||||
|
||||
const (
|
||||
// Severity.
|
||||
|
||||
// From /usr/include/sys/syslog.h.
|
||||
// These are the same on Linux, BSD, and OS X.
|
||||
LOG_EMERG Priority = iota
|
||||
LOG_ALERT
|
||||
LOG_CRIT
|
||||
LOG_ERR
|
||||
LOG_WARNING
|
||||
LOG_NOTICE
|
||||
LOG_INFO
|
||||
LOG_DEBUG
|
||||
)
|
||||
|
||||
const (
|
||||
// Facility.
|
||||
|
||||
// From /usr/include/sys/syslog.h.
|
||||
// These are the same up to LOG_FTP on Linux, BSD, and OS X.
|
||||
LOG_KERN Priority = iota << 3
|
||||
LOG_USER
|
||||
LOG_MAIL
|
||||
LOG_DAEMON
|
||||
LOG_AUTH
|
||||
LOG_SYSLOG
|
||||
LOG_LPR
|
||||
LOG_NEWS
|
||||
LOG_UUCP
|
||||
LOG_CRON
|
||||
LOG_AUTHPRIV
|
||||
LOG_FTP
|
||||
_ // unused
|
||||
_ // unused
|
||||
_ // unused
|
||||
_ // unused
|
||||
LOG_LOCAL0
|
||||
LOG_LOCAL1
|
||||
LOG_LOCAL2
|
||||
LOG_LOCAL3
|
||||
LOG_LOCAL4
|
||||
LOG_LOCAL5
|
||||
LOG_LOCAL6
|
||||
LOG_LOCAL7
|
||||
)
|
||||
|
||||
// A Writer is a connection to a syslog server.
|
||||
type Writer struct {
|
||||
priority Priority
|
||||
tag string
|
||||
hostname string
|
||||
network string
|
||||
raddr string
|
||||
|
||||
mu sync.Mutex // guards conn
|
||||
conn serverConn
|
||||
}
|
||||
|
||||
// This interface and the separate syslog_unix.go file exist for
|
||||
// Solaris support as implemented by gccgo. On Solaris you cannot
|
||||
// simply open a TCP connection to the syslog daemon. The gccgo
|
||||
// sources have a syslog_solaris.go file that implements unixSyslog to
|
||||
// return a type that satisfies this interface and simply calls the C
|
||||
// library syslog function.
|
||||
type serverConn interface {
|
||||
writeString(p Priority, hostname, tag, s, nl string) error
|
||||
close() error
|
||||
}
|
||||
|
||||
type netConn struct {
|
||||
local bool
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// New establishes a new connection to the system log daemon. Each
|
||||
// write to the returned writer sends a log message with the given
|
||||
// priority (a combination of the syslog facility and severity) and
|
||||
// prefix tag. If tag is empty, the [os.Args][0] is used.
|
||||
func New(priority Priority, tag string) (*Writer, error) {
|
||||
return Dial("", "", priority, tag)
|
||||
}
|
||||
|
||||
// Dial establishes a connection to a log daemon by connecting to
|
||||
// address raddr on the specified network. Each write to the returned
|
||||
// writer sends a log message with the facility and severity
|
||||
// (from priority) and tag. If tag is empty, the [os.Args][0] is used.
|
||||
// If network is empty, Dial will connect to the local syslog server.
|
||||
// Otherwise, see the documentation for net.Dial for valid values
|
||||
// of network and raddr.
|
||||
func Dial(network, raddr string, priority Priority, tag string) (*Writer, error) {
|
||||
if priority < 0 || priority > LOG_LOCAL7|LOG_DEBUG {
|
||||
return nil, errors.New("log/syslog: invalid priority")
|
||||
}
|
||||
|
||||
if tag == "" {
|
||||
tag = os.Args[0]
|
||||
}
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
w := &Writer{
|
||||
priority: priority,
|
||||
tag: tag,
|
||||
hostname: hostname,
|
||||
network: network,
|
||||
raddr: raddr,
|
||||
}
|
||||
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
err := w.connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w, err
|
||||
}
|
||||
|
||||
// connect makes a connection to the syslog server.
|
||||
// It must be called with w.mu held.
|
||||
func (w *Writer) connect() (err error) {
|
||||
if w.conn != nil {
|
||||
// ignore err from close, it makes sense to continue anyway
|
||||
w.conn.close()
|
||||
w.conn = nil
|
||||
}
|
||||
|
||||
if w.network == "" {
|
||||
w.conn, err = unixSyslog()
|
||||
if w.hostname == "" {
|
||||
w.hostname = "localhost"
|
||||
}
|
||||
} else {
|
||||
var c net.Conn
|
||||
c, err = net.Dial(w.network, w.raddr)
|
||||
if err == nil {
|
||||
w.conn = &netConn{
|
||||
conn: c,
|
||||
local: w.network == "unixgram" || w.network == "unix",
|
||||
}
|
||||
if w.hostname == "" {
|
||||
w.hostname = c.LocalAddr().String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Write sends a log message to the syslog daemon.
|
||||
func (w *Writer) Write(b []byte) (int, error) {
|
||||
return w.writeAndRetry(w.priority, string(b))
|
||||
}
|
||||
|
||||
// Close closes a connection to the syslog daemon.
|
||||
func (w *Writer) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.conn != nil {
|
||||
err := w.conn.close()
|
||||
w.conn = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Emerg logs a message with severity [LOG_EMERG], ignoring the severity
|
||||
// passed to New.
|
||||
func (w *Writer) Emerg(m string) error {
|
||||
_, err := w.writeAndRetry(LOG_EMERG, m)
|
||||
return err
|
||||
}
|
||||
|
||||
// Alert logs a message with severity [LOG_ALERT], ignoring the severity
|
||||
// passed to New.
|
||||
func (w *Writer) Alert(m string) error {
|
||||
_, err := w.writeAndRetry(LOG_ALERT, m)
|
||||
return err
|
||||
}
|
||||
|
||||
// Crit logs a message with severity [LOG_CRIT], ignoring the severity
|
||||
// passed to New.
|
||||
func (w *Writer) Crit(m string) error {
|
||||
_, err := w.writeAndRetry(LOG_CRIT, m)
|
||||
return err
|
||||
}
|
||||
|
||||
// Err logs a message with severity [LOG_ERR], ignoring the severity
|
||||
// passed to New.
|
||||
func (w *Writer) Err(m string) error {
|
||||
_, err := w.writeAndRetry(LOG_ERR, m)
|
||||
return err
|
||||
}
|
||||
|
||||
// Warning logs a message with severity [LOG_WARNING], ignoring the
|
||||
// severity passed to New.
|
||||
func (w *Writer) Warning(m string) error {
|
||||
_, err := w.writeAndRetry(LOG_WARNING, m)
|
||||
return err
|
||||
}
|
||||
|
||||
// Notice logs a message with severity [LOG_NOTICE], ignoring the
|
||||
// severity passed to New.
|
||||
func (w *Writer) Notice(m string) error {
|
||||
_, err := w.writeAndRetry(LOG_NOTICE, m)
|
||||
return err
|
||||
}
|
||||
|
||||
// Info logs a message with severity [LOG_INFO], ignoring the severity
|
||||
// passed to New.
|
||||
func (w *Writer) Info(m string) error {
|
||||
_, err := w.writeAndRetry(LOG_INFO, m)
|
||||
return err
|
||||
}
|
||||
|
||||
// Debug logs a message with severity [LOG_DEBUG], ignoring the severity
|
||||
// passed to New.
|
||||
func (w *Writer) Debug(m string) error {
|
||||
_, err := w.writeAndRetry(LOG_DEBUG, m)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *Writer) writeAndRetry(p Priority, s string) (int, error) {
|
||||
pr := (w.priority & facilityMask) | (p & severityMask)
|
||||
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.conn != nil {
|
||||
if n, err := w.write(pr, s); err == nil {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
if err := w.connect(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return w.write(pr, s)
|
||||
}
|
||||
|
||||
// write generates and writes a syslog formatted string. The
|
||||
// format is as follows: <PRI>TIMESTAMP HOSTNAME TAG[PID]: MSG
|
||||
func (w *Writer) write(p Priority, msg string) (int, error) {
|
||||
// ensure it ends in a \n
|
||||
nl := ""
|
||||
if !strings.HasSuffix(msg, "\n") {
|
||||
nl = "\n"
|
||||
}
|
||||
|
||||
err := w.conn.writeString(p, w.hostname, w.tag, msg, nl)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Note: return the length of the input, not the number of
|
||||
// bytes printed by Fprintf, because this must behave like
|
||||
// an io.Writer.
|
||||
return len(msg), nil
|
||||
}
|
||||
|
||||
func (n *netConn) writeString(p Priority, hostname, tag, msg, nl string) error {
|
||||
if n.local {
|
||||
// Compared to the network form below, the changes are:
|
||||
// 1. Use time.Stamp instead of time.RFC3339.
|
||||
// 2. Drop the hostname field from the Fprintf.
|
||||
timestamp := time.Now().Format(time.Stamp)
|
||||
_, err := fmt.Fprintf(n.conn, "<%d>%s %s[%d]: %s%s",
|
||||
p, timestamp,
|
||||
tag, os.Getpid(), msg, nl)
|
||||
return err
|
||||
}
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
_, err := fmt.Fprintf(n.conn, "<%d>%s %s %s[%d]: %s%s",
|
||||
p, timestamp, hostname,
|
||||
tag, os.Getpid(), msg, nl)
|
||||
return err
|
||||
}
|
||||
|
||||
func (n *netConn) close() error {
|
||||
return n.conn.Close()
|
||||
}
|
||||
|
||||
// NewLogger creates a [log.Logger] whose output is written to the
|
||||
// system log service with the specified priority, a combination of
|
||||
// the syslog facility and severity. The logFlag argument is the flag
|
||||
// set passed through to [log.New] to create the Logger.
|
||||
func NewLogger(p Priority, logFlag int) (*log.Logger, error) {
|
||||
s, err := New(p, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return log.New(s, "", logFlag), nil
|
||||
}
|
||||
432
src/log/syslog/syslog_test.go
Normal file
432
src/log/syslog/syslog_test.go
Normal file
@@ -0,0 +1,432 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows && !plan9 && !js && !wasip1
|
||||
|
||||
package syslog
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func runPktSyslog(c net.PacketConn, done chan<- string) {
|
||||
var buf [4096]byte
|
||||
var rcvd string
|
||||
ct := 0
|
||||
for {
|
||||
var n int
|
||||
var err error
|
||||
|
||||
c.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
n, _, err = c.ReadFrom(buf[:])
|
||||
rcvd += string(buf[:n])
|
||||
if err != nil {
|
||||
if oe, ok := err.(*net.OpError); ok {
|
||||
if ct < 3 && oe.Temporary() {
|
||||
ct++
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Close()
|
||||
done <- rcvd
|
||||
}
|
||||
|
||||
var crashy = false
|
||||
|
||||
func testableNetwork(network string) bool {
|
||||
switch network {
|
||||
case "unix", "unixgram":
|
||||
switch runtime.GOOS {
|
||||
case "ios", "android":
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func runStreamSyslog(l net.Listener, done chan<- string, wg *sync.WaitGroup) {
|
||||
for {
|
||||
var c net.Conn
|
||||
var err error
|
||||
if c, err = l.Accept(); err != nil {
|
||||
return
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(c net.Conn) {
|
||||
defer wg.Done()
|
||||
c.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
b := bufio.NewReader(c)
|
||||
for ct := 1; !crashy || ct&7 != 0; ct++ {
|
||||
s, err := b.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
done <- s
|
||||
}
|
||||
c.Close()
|
||||
}(c)
|
||||
}
|
||||
}
|
||||
|
||||
func startServer(t *testing.T, n, la string, done chan<- string) (addr string, sock io.Closer, wg *sync.WaitGroup) {
|
||||
if n == "udp" || n == "tcp" {
|
||||
la = "127.0.0.1:0"
|
||||
} else {
|
||||
// unix and unixgram: choose an address if none given.
|
||||
if la == "" {
|
||||
// The address must be short to fit in the sun_path field of the
|
||||
// sockaddr_un passed to the underlying system calls, so we use
|
||||
// os.MkdirTemp instead of t.TempDir: t.TempDir generally includes all or
|
||||
// part of the test name in the directory, which can be much more verbose
|
||||
// and risks running up against the limit.
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Errorf("failed to remove socket temp directory: %v", err)
|
||||
}
|
||||
})
|
||||
la = filepath.Join(dir, "sock")
|
||||
}
|
||||
}
|
||||
|
||||
wg = new(sync.WaitGroup)
|
||||
if n == "udp" || n == "unixgram" {
|
||||
l, e := net.ListenPacket(n, la)
|
||||
if e != nil {
|
||||
t.Helper()
|
||||
t.Fatalf("startServer failed: %v", e)
|
||||
}
|
||||
addr = l.LocalAddr().String()
|
||||
sock = l
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
runPktSyslog(l, done)
|
||||
}()
|
||||
} else {
|
||||
l, e := net.Listen(n, la)
|
||||
if e != nil {
|
||||
t.Helper()
|
||||
t.Fatalf("startServer failed: %v", e)
|
||||
}
|
||||
addr = l.Addr().String()
|
||||
sock = l
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
runStreamSyslog(l, done, wg)
|
||||
}()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestWithSimulated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
msg := "Test 123"
|
||||
for _, tr := range []string{"unix", "unixgram", "udp", "tcp"} {
|
||||
if !testableNetwork(tr) {
|
||||
continue
|
||||
}
|
||||
|
||||
tr := tr
|
||||
t.Run(tr, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
done := make(chan string)
|
||||
addr, sock, srvWG := startServer(t, tr, "", done)
|
||||
defer srvWG.Wait()
|
||||
defer sock.Close()
|
||||
if tr == "unix" || tr == "unixgram" {
|
||||
defer os.Remove(addr)
|
||||
}
|
||||
s, err := Dial(tr, addr, LOG_INFO|LOG_USER, "syslog_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial() failed: %v", err)
|
||||
}
|
||||
err = s.Info(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("log failed: %v", err)
|
||||
}
|
||||
check(t, msg, <-done, tr)
|
||||
s.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
net := "unix"
|
||||
if !testableNetwork(net) {
|
||||
t.Skipf("skipping on %s/%s; 'unix' is not supported", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
done := make(chan string)
|
||||
addr, sock, srvWG := startServer(t, net, "", done)
|
||||
defer srvWG.Wait()
|
||||
defer os.Remove(addr)
|
||||
defer sock.Close()
|
||||
|
||||
s, err := Dial(net, addr, LOG_INFO|LOG_USER, "syslog_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial() failed: %v", err)
|
||||
}
|
||||
msg := "Moo 2"
|
||||
err = s.Info(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("log failed: %v", err)
|
||||
}
|
||||
check(t, msg, <-done, net)
|
||||
|
||||
// restart the server
|
||||
if err := os.Remove(addr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, sock2, srvWG2 := startServer(t, net, addr, done)
|
||||
defer srvWG2.Wait()
|
||||
defer sock2.Close()
|
||||
|
||||
// and try retransmitting
|
||||
msg = "Moo 3"
|
||||
err = s.Info(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("log failed: %v", err)
|
||||
}
|
||||
check(t, msg, <-done, net)
|
||||
|
||||
s.Close()
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
if LOG_LOCAL7 != 23<<3 {
|
||||
t.Fatalf("LOG_LOCAL7 has wrong value")
|
||||
}
|
||||
if testing.Short() {
|
||||
// Depends on syslog daemon running, and sometimes it's not.
|
||||
t.Skip("skipping syslog test during -short")
|
||||
}
|
||||
|
||||
s, err := New(LOG_INFO|LOG_USER, "the_tag")
|
||||
if err != nil {
|
||||
if err.Error() == "Unix syslog delivery error" {
|
||||
t.Skip("skipping: syslogd not running")
|
||||
}
|
||||
t.Fatalf("New() failed: %s", err)
|
||||
}
|
||||
// Don't send any messages.
|
||||
s.Close()
|
||||
}
|
||||
|
||||
func TestNewLogger(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping syslog test during -short")
|
||||
}
|
||||
f, err := NewLogger(LOG_USER|LOG_INFO, 0)
|
||||
if f == nil {
|
||||
if err.Error() == "Unix syslog delivery error" {
|
||||
t.Skip("skipping: syslogd not running")
|
||||
}
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDial(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping syslog test during -short")
|
||||
}
|
||||
f, err := Dial("", "", (LOG_LOCAL7|LOG_DEBUG)+1, "syslog_test")
|
||||
if f != nil {
|
||||
t.Fatalf("Should have trapped bad priority")
|
||||
}
|
||||
f, err = Dial("", "", -1, "syslog_test")
|
||||
if f != nil {
|
||||
t.Fatalf("Should have trapped bad priority")
|
||||
}
|
||||
l, err := Dial("", "", LOG_USER|LOG_ERR, "syslog_test")
|
||||
if err != nil {
|
||||
if err.Error() == "Unix syslog delivery error" {
|
||||
t.Skip("skipping: syslogd not running")
|
||||
}
|
||||
t.Fatalf("Dial() failed: %s", err)
|
||||
}
|
||||
l.Close()
|
||||
}
|
||||
|
||||
func check(t *testing.T, in, out, transport string) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
t.Errorf("Error retrieving hostname: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if transport == "unixgram" || transport == "unix" {
|
||||
var month, date, ts string
|
||||
var pid int
|
||||
tmpl := fmt.Sprintf("<%d>%%s %%s %%s syslog_test[%%d]: %s\n", LOG_USER+LOG_INFO, in)
|
||||
n, err := fmt.Sscanf(out, tmpl, &month, &date, &ts, &pid)
|
||||
if n != 4 || err != nil {
|
||||
t.Errorf("Got %q, does not match template %q (%d %s)", out, tmpl, n, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Non-UNIX domain transports.
|
||||
var parsedHostname, timestamp string
|
||||
var pid int
|
||||
tmpl := fmt.Sprintf("<%d>%%s %%s syslog_test[%%d]: %s\n", LOG_USER+LOG_INFO, in)
|
||||
n, err := fmt.Sscanf(out, tmpl, ×tamp, &parsedHostname, &pid)
|
||||
if n != 3 || err != nil {
|
||||
t.Errorf("Got %q, does not match template %q (%d %s)", out, tmpl, n, err)
|
||||
}
|
||||
if hostname != parsedHostname {
|
||||
t.Errorf("Hostname got %q want %q in %q", parsedHostname, hostname, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
pri Priority
|
||||
pre string
|
||||
msg string
|
||||
exp string
|
||||
}{
|
||||
{LOG_USER | LOG_ERR, "syslog_test", "", "%s %s syslog_test[%d]: \n"},
|
||||
{LOG_USER | LOG_ERR, "syslog_test", "write test", "%s %s syslog_test[%d]: write test\n"},
|
||||
// Write should not add \n if there already is one
|
||||
{LOG_USER | LOG_ERR, "syslog_test", "write test 2\n", "%s %s syslog_test[%d]: write test 2\n"},
|
||||
}
|
||||
|
||||
if hostname, err := os.Hostname(); err != nil {
|
||||
t.Fatalf("Error retrieving hostname")
|
||||
} else {
|
||||
for _, test := range tests {
|
||||
done := make(chan string)
|
||||
addr, sock, srvWG := startServer(t, "udp", "", done)
|
||||
defer srvWG.Wait()
|
||||
defer sock.Close()
|
||||
l, err := Dial("udp", addr, test.pri, test.pre)
|
||||
if err != nil {
|
||||
t.Fatalf("syslog.Dial() failed: %v", err)
|
||||
}
|
||||
defer l.Close()
|
||||
_, err = io.WriteString(l, test.msg)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteString() failed: %v", err)
|
||||
}
|
||||
rcvd := <-done
|
||||
test.exp = fmt.Sprintf("<%d>", test.pri) + test.exp
|
||||
var parsedHostname, timestamp string
|
||||
var pid int
|
||||
if n, err := fmt.Sscanf(rcvd, test.exp, ×tamp, &parsedHostname, &pid); n != 3 || err != nil || hostname != parsedHostname {
|
||||
t.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, test.exp, n, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentWrite(t *testing.T) {
|
||||
addr, sock, srvWG := startServer(t, "udp", "", make(chan string, 1))
|
||||
defer srvWG.Wait()
|
||||
defer sock.Close()
|
||||
w, err := Dial("udp", addr, LOG_USER|LOG_ERR, "how's it going?")
|
||||
if err != nil {
|
||||
t.Fatalf("syslog.Dial() failed: %v", err)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := w.Info("test")
|
||||
if err != nil {
|
||||
t.Errorf("Info() failed: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestConcurrentReconnect(t *testing.T) {
|
||||
crashy = true
|
||||
defer func() { crashy = false }()
|
||||
|
||||
const N = 10
|
||||
const M = 100
|
||||
net := "unix"
|
||||
if !testableNetwork(net) {
|
||||
net = "tcp"
|
||||
if !testableNetwork(net) {
|
||||
t.Skipf("skipping on %s/%s; neither 'unix' or 'tcp' is supported", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
done := make(chan string, N*M)
|
||||
addr, sock, srvWG := startServer(t, net, "", done)
|
||||
if net == "unix" {
|
||||
defer os.Remove(addr)
|
||||
}
|
||||
|
||||
// count all the messages arriving
|
||||
count := make(chan int, 1)
|
||||
go func() {
|
||||
ct := 0
|
||||
for range done {
|
||||
ct++
|
||||
// we are looking for 500 out of 1000 events
|
||||
// here because lots of log messages are lost
|
||||
// in buffers (kernel and/or bufio)
|
||||
if ct > N*M/2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
count <- ct
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(N)
|
||||
for i := 0; i < N; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
w, err := Dial(net, addr, LOG_USER|LOG_ERR, "tag")
|
||||
if err != nil {
|
||||
t.Errorf("syslog.Dial() failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer w.Close()
|
||||
for i := 0; i < M; i++ {
|
||||
err := w.Info("test")
|
||||
if err != nil {
|
||||
t.Errorf("Info() failed: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
sock.Close()
|
||||
srvWG.Wait()
|
||||
close(done)
|
||||
|
||||
select {
|
||||
case <-count:
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("timeout in concurrent reconnect")
|
||||
}
|
||||
}
|
||||
29
src/log/syslog/syslog_unix.go
Normal file
29
src/log/syslog/syslog_unix.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows && !plan9
|
||||
|
||||
package syslog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
// unixSyslog opens a connection to the syslog daemon running on the
|
||||
// local machine using a Unix domain socket.
|
||||
|
||||
func unixSyslog() (conn serverConn, err error) {
|
||||
logTypes := []string{"unixgram", "unix"}
|
||||
logPaths := []string{"/dev/log", "/var/run/syslog", "/var/run/log"}
|
||||
for _, network := range logTypes {
|
||||
for _, path := range logPaths {
|
||||
conn, err := net.Dial(network, path)
|
||||
if err == nil {
|
||||
return &netConn{conn: conn, local: true}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New("Unix syslog delivery error")
|
||||
}
|
||||
Reference in New Issue
Block a user