Skip to content

Commit

Permalink
slog: catch panics during formatting
Browse files Browse the repository at this point in the history
This is a port of https://go.dev/cl/514135 to exp/slog.

Fixes #64034.

Change-Id: Icd632a04aba4329d50be8a6d18cb5e646d27dbf8
Reviewed-on: https://go-review.googlesource.com/c/exp/+/541435
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
  • Loading branch information
jba committed Nov 10, 2023
1 parent 2478ac8 commit 9a3e603
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 4 deletions.
18 changes: 18 additions & 0 deletions slog/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"fmt"
"io"
"reflect"
"strconv"
"sync"
"time"
Expand Down Expand Up @@ -504,6 +505,23 @@ func (s *handleState) appendString(str string) {
}

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)
Expand Down
75 changes: 71 additions & 4 deletions slog/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ import (
"golang.org/x/exp/slices"
)

const timeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})`
// 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) {
var buf bytes.Buffer
Expand All @@ -30,7 +36,7 @@ func TestLogTextHandler(t *testing.T) {
check := func(want string) {
t.Helper()
if want != "" {
want = "time=" + timeRE + " " + want
want = "time=" + textTimeRE + " " + want
}
checkLogOutput(t, buf.String(), want)
buf.Reset()
Expand Down Expand Up @@ -112,7 +118,7 @@ func TestConnections(t *testing.T) {
// log.Logger's output goes through the handler.
SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{AddSource: true})))
log.Print("msg2")
checkLogOutput(t, slogbuf.String(), "time="+timeRE+` level=INFO source=.*logger_test.go:\d{3} msg=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()
Expand Down Expand Up @@ -371,7 +377,7 @@ func TestNewLogLogger(t *testing.T) {
h := NewTextHandler(&buf, nil)
ll := NewLogLogger(h, LevelWarn)
ll.Print("hello")
checkLogOutput(t, buf.String(), "time="+timeRE+` level=WARN msg=hello`)
checkLogOutput(t, buf.String(), "time="+textTimeRE+` level=WARN msg=hello`)
}

func checkLogOutput(t *testing.T, got, wantRegexp string) {
Expand Down Expand Up @@ -507,3 +513,64 @@ func callerPC(depth int) uintptr {
runtime.Callers(depth, pcs[:])
return pcs[0]
}

// 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(log.Output)))
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()
}
}

0 comments on commit 9a3e603

Please sign in to comment.