Skip to content

Commit 9a3e603

Browse files
committed
slog: catch panics during formatting
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 <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 2478ac8 commit 9a3e603

File tree

2 files changed

+89
-4
lines changed

2 files changed

+89
-4
lines changed

slog/handler.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"fmt"
1010
"io"
11+
"reflect"
1112
"strconv"
1213
"sync"
1314
"time"
@@ -504,6 +505,23 @@ func (s *handleState) appendString(str string) {
504505
}
505506

506507
func (s *handleState) appendValue(v Value) {
508+
defer func() {
509+
if r := recover(); r != nil {
510+
// If it panics with a nil pointer, the most likely cases are
511+
// an encoding.TextMarshaler or error fails to guard against nil,
512+
// in which case "<nil>" seems to be the feasible choice.
513+
//
514+
// Adapted from the code in fmt/print.go.
515+
if v := reflect.ValueOf(v.any); v.Kind() == reflect.Pointer && v.IsNil() {
516+
s.appendString("<nil>")
517+
return
518+
}
519+
520+
// Otherwise just print the original panic message.
521+
s.appendString(fmt.Sprintf("!PANIC: %v", r))
522+
}
523+
}()
524+
507525
var err error
508526
if s.h.json {
509527
err = appendJSONValue(s, v)

slog/logger_test.go

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ import (
2020
"golang.org/x/exp/slices"
2121
)
2222

23-
const timeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})`
23+
// textTimeRE is a regexp to match log timestamps for Text handler.
24+
// This is RFC3339Nano with the fixed 3 digit sub-second precision.
25+
const textTimeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})`
26+
27+
// jsonTimeRE is a regexp to match log timestamps for Text handler.
28+
// This is RFC3339Nano with an arbitrary sub-second precision.
29+
const jsonTimeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})`
2430

2531
func TestLogTextHandler(t *testing.T) {
2632
var buf bytes.Buffer
@@ -30,7 +36,7 @@ func TestLogTextHandler(t *testing.T) {
3036
check := func(want string) {
3137
t.Helper()
3238
if want != "" {
33-
want = "time=" + timeRE + " " + want
39+
want = "time=" + textTimeRE + " " + want
3440
}
3541
checkLogOutput(t, buf.String(), want)
3642
buf.Reset()
@@ -112,7 +118,7 @@ func TestConnections(t *testing.T) {
112118
// log.Logger's output goes through the handler.
113119
SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{AddSource: true})))
114120
log.Print("msg2")
115-
checkLogOutput(t, slogbuf.String(), "time="+timeRE+` level=INFO source=.*logger_test.go:\d{3} msg=msg2`)
121+
checkLogOutput(t, slogbuf.String(), "time="+textTimeRE+` level=INFO source=.*logger_test.go:\d{3} msg=msg2`)
116122

117123
// The default log.Logger always outputs at Info level.
118124
slogbuf.Reset()
@@ -371,7 +377,7 @@ func TestNewLogLogger(t *testing.T) {
371377
h := NewTextHandler(&buf, nil)
372378
ll := NewLogLogger(h, LevelWarn)
373379
ll.Print("hello")
374-
checkLogOutput(t, buf.String(), "time="+timeRE+` level=WARN msg=hello`)
380+
checkLogOutput(t, buf.String(), "time="+textTimeRE+` level=WARN msg=hello`)
375381
}
376382

377383
func checkLogOutput(t *testing.T, got, wantRegexp string) {
@@ -507,3 +513,64 @@ func callerPC(depth int) uintptr {
507513
runtime.Callers(depth, pcs[:])
508514
return pcs[0]
509515
}
516+
517+
// panicTextAndJsonMarshaler is a type that panics in MarshalText and MarshalJSON.
518+
type panicTextAndJsonMarshaler struct {
519+
msg any
520+
}
521+
522+
func (p panicTextAndJsonMarshaler) MarshalText() ([]byte, error) {
523+
panic(p.msg)
524+
}
525+
526+
func (p panicTextAndJsonMarshaler) MarshalJSON() ([]byte, error) {
527+
panic(p.msg)
528+
}
529+
530+
func TestPanics(t *testing.T) {
531+
// Revert any changes to the default logger. This is important because other
532+
// tests might change the default logger using SetDefault. Also ensure we
533+
// restore the default logger at the end of the test.
534+
currentLogger := Default()
535+
currentLogWriter := log.Writer()
536+
currentLogFlags := log.Flags()
537+
t.Cleanup(func() {
538+
SetDefault(currentLogger)
539+
log.SetOutput(currentLogWriter)
540+
log.SetFlags(currentLogFlags)
541+
})
542+
543+
var logBuf bytes.Buffer
544+
log.SetOutput(&logBuf)
545+
log.SetFlags(log.Lshortfile &^ log.LstdFlags)
546+
547+
SetDefault(New(newDefaultHandler(log.Output)))
548+
for _, pt := range []struct {
549+
in any
550+
out string
551+
}{
552+
{(*panicTextAndJsonMarshaler)(nil), `logger_test.go:\d+: INFO msg p=<nil>`},
553+
{panicTextAndJsonMarshaler{io.ErrUnexpectedEOF}, `logger_test.go:\d+: INFO msg p="!PANIC: unexpected EOF"`},
554+
{panicTextAndJsonMarshaler{"panicking"}, `logger_test.go:\d+: INFO msg p="!PANIC: panicking"`},
555+
{panicTextAndJsonMarshaler{42}, `logger_test.go:\d+: INFO msg p="!PANIC: 42"`},
556+
} {
557+
Info("msg", "p", pt.in)
558+
checkLogOutput(t, logBuf.String(), pt.out)
559+
logBuf.Reset()
560+
}
561+
562+
SetDefault(New(NewJSONHandler(&logBuf, nil)))
563+
for _, pt := range []struct {
564+
in any
565+
out string
566+
}{
567+
{(*panicTextAndJsonMarshaler)(nil), `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":null}`},
568+
{panicTextAndJsonMarshaler{io.ErrUnexpectedEOF}, `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":"!PANIC: unexpected EOF"}`},
569+
{panicTextAndJsonMarshaler{"panicking"}, `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":"!PANIC: panicking"}`},
570+
{panicTextAndJsonMarshaler{42}, `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":"!PANIC: 42"}`},
571+
} {
572+
Info("msg", "p", pt.in)
573+
checkLogOutput(t, logBuf.String(), pt.out)
574+
logBuf.Reset()
575+
}
576+
}

0 commit comments

Comments
 (0)