diff --git a/src/log/slog/encoder.go b/src/log/slog/encoder.go new file mode 100644 index 00000000000000..3581dc34c15ccb --- /dev/null +++ b/src/log/slog/encoder.go @@ -0,0 +1,12 @@ +// 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 + +type encoder interface { + reset() + free() + encode(v any) error + bytes() []byte +} diff --git a/src/log/slog/handler.go b/src/log/slog/handler.go index 26eb4b82fc8b57..acbb40664b9b57 100644 --- a/src/log/slog/handler.go +++ b/src/log/slog/handler.go @@ -380,12 +380,13 @@ func (h *commonHandler) attrSep() string { // 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 + 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 + anyEncoder encoder } var groupPool = sync.Pool{New: func() any { @@ -416,6 +417,10 @@ func (s *handleState) free() { *gs = (*gs)[:0] groupPool.Put(gs) } + if s.anyEncoder != nil { + s.anyEncoder.free() + s.anyEncoder = nil + } s.prefix.Free() } @@ -558,7 +563,7 @@ func (s *handleState) appendTwoStrings(x, y string) { buf = appendEscapedJSONString(buf, x) buf = appendEscapedJSONString(buf, y) buf.WriteByte('"') - case !needsQuoting(x) && !needsQuoting(y): + case !needsQuotingString(x) && !needsQuotingString(y): buf.WriteString(x) buf.WriteString(y) default: @@ -574,7 +579,7 @@ func (s *handleState) appendString(str string) { s.buf.WriteByte('"') } else { // text - if needsQuoting(str) { + if needsQuotingString(str) { *s.buf = strconv.AppendQuote(*s.buf, str) } else { s.buf.WriteString(str) diff --git a/src/log/slog/internal/benchmarks/benchmarks.go b/src/log/slog/internal/benchmarks/benchmarks.go index 3a28523beb44ae..2c5dfca4aad247 100644 --- a/src/log/slog/internal/benchmarks/benchmarks.go +++ b/src/log/slog/internal/benchmarks/benchmarks.go @@ -37,6 +37,11 @@ var ( testInt = 32768 testDuration = 23 * time.Second testError = errors.New("fail") + testEvent = &event{ + ID: "testing_event_#01", + Description: "Some text for this description to simulate a real scenario.", + CreatedAt: time.Date(2025, time.July, 5, 0, 0, 0, 0, time.UTC), + } ) var testAttrs = []slog.Attr{ @@ -48,3 +53,9 @@ var testAttrs = []slog.Attr{ } 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" + +type event struct { + ID string `json:"id"` + Description string `json:"description"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/src/log/slog/internal/benchmarks/benchmarks_test.go b/src/log/slog/internal/benchmarks/benchmarks_test.go index 18643b73e6c8a0..400160105236d0 100644 --- a/src/log/slog/internal/benchmarks/benchmarks_test.go +++ b/src/log/slog/internal/benchmarks/benchmarks_test.go @@ -85,7 +85,7 @@ func BenchmarkAttrs(b *testing.B) { slog.Int("status", testInt), slog.Duration("duration", testDuration), slog.Time("time", testTime), - slog.Any("error", testError), + slog.Any("event", testEvent), ) }, }, @@ -133,7 +133,7 @@ func BenchmarkAttrs(b *testing.B) { slog.Int("status", testInt), slog.Duration("duration", testDuration), slog.Time("time", testTime), - slog.Any("error", testError), + slog.Any("event", testEvent), ) }, }, diff --git a/src/log/slog/json_handler.go b/src/log/slog/json_handler.go index f139c5413912bf..6a54984928d998 100644 --- a/src/log/slog/json_handler.go +++ b/src/log/slog/json_handler.go @@ -11,7 +11,6 @@ import ( "errors" "fmt" "io" - "log/slog/internal/buffer" "strconv" "sync" "time" @@ -113,7 +112,7 @@ func appendJSONValue(s *handleState, v Value) error { // 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 { + if err := appendJSONMarshal(s, v.Float64()); err != nil { return err } case KindBool: @@ -129,7 +128,7 @@ func appendJSONValue(s *handleState, v Value) error { if err, ok := a.(error); ok && !jm { s.appendString(err.Error()) } else { - return appendJSONMarshal(s.buf, a) + return appendJSONMarshal(s, a) } default: panic(fmt.Sprintf("bad kind: %s", v.Kind())) @@ -137,19 +136,63 @@ func appendJSONValue(s *handleState, v Value) error { 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 { +func appendJSONMarshal(s *handleState, v any) error { + // Aquire a new encoder if not asigned. + if s.anyEncoder == nil { + s.anyEncoder = newJSONMarshalEncoder() + } + + enc := s.anyEncoder + enc.reset() // reset the encoder, if was reused + if err := enc.encode(v); err != nil { return err } - bs := bb.Bytes() - buf.Write(bs[:len(bs)-1]) // remove final newline + + bs := enc.bytes() + s.buf.Write(bs[:len(bs)-1]) // remove final newline return nil } +type jsonMarshalEncoder struct { + buf *bytes.Buffer + enc *json.Encoder +} + +func newJSONMarshalEncoder() encoder { + if enc := jsonEncoderPool.Get(); enc != nil { + return enc.(*jsonMarshalEncoder) + } + + buf := &bytes.Buffer{} + + // Use a json.Encoder to avoid escaping HTML. + jsonEnc := json.NewEncoder(buf) + jsonEnc.SetEscapeHTML(false) + + return &jsonMarshalEncoder{buf: buf, enc: jsonEnc} +} + +func (e *jsonMarshalEncoder) reset() { + e.buf.Reset() +} + +func (e *jsonMarshalEncoder) encode(v any) error { + return e.enc.Encode(v) +} + +func (e *jsonMarshalEncoder) bytes() []byte { + return e.buf.Bytes() +} + +func (e *jsonMarshalEncoder) free() { + const maxBufferSize = 8 << 10 + if e.buf.Cap() <= maxBufferSize { + jsonEncoderPool.Put(e) + } +} + +var jsonEncoderPool = sync.Pool{} + // appendEscapedJSONString escapes s for JSON and appends it to buf. // It does not surround the string in quotation marks. // diff --git a/src/log/slog/text_handler.go b/src/log/slog/text_handler.go index 5a0d0a4a7ed42b..b8e13d55b4095a 100644 --- a/src/log/slog/text_handler.go +++ b/src/log/slog/text_handler.go @@ -5,6 +5,7 @@ package slog import ( + "bytes" "context" "encoding" "fmt" @@ -114,7 +115,21 @@ func appendTextValue(s *handleState, v Value) error { s.buf.WriteString(strconv.Quote(string(bs))) return nil } - s.appendString(fmt.Sprintf("%+v", v.Any())) + if s.anyEncoder == nil { + s.anyEncoder = newTextMarshalEncoder() + } + + enc := s.anyEncoder + enc.reset() + enc.encode(v.Any()) + bs := enc.bytes() + if needsQuotingBytes(bs) { + s.buf.WriteByte('"') + s.buf.Write(bs) + s.buf.WriteByte('"') + return nil + } + s.buf.Write(bs) default: *s.buf = v.append(*s.buf) } @@ -136,12 +151,60 @@ func byteSlice(a any) ([]byte, bool) { return nil, false } -func needsQuoting(s string) bool { - if len(s) == 0 { +type textMarshalEncoder struct { + buf *bytes.Buffer +} + +func newTextMarshalEncoder() encoder { + if enc := textEncoderPool.Get(); enc != nil { + return enc.(*textMarshalEncoder) + } + + buf := &bytes.Buffer{} + + return &textMarshalEncoder{buf: buf} +} + +func (e *textMarshalEncoder) reset() { + e.buf.Reset() +} + +func (e *textMarshalEncoder) encode(v any) error { + fmt.Fprintf(e.buf, "%+v", v) + return nil +} + +func (e *textMarshalEncoder) bytes() []byte { + return e.buf.Bytes() +} + +func (e *textMarshalEncoder) free() { + const maxBufferSize = 4 << 10 + if e.buf.Cap() <= maxBufferSize { + textEncoderPool.Put(e) + } +} + +var textEncoderPool = sync.Pool{} + +func needsQuotingString(s string) bool { + return needsQuoting(s, func(i int) (rune, int) { + return utf8.DecodeRuneInString(s[i:]) + }) +} + +func needsQuotingBytes(b []byte) bool { + return needsQuoting(b, func(i int) (rune, int) { + return utf8.DecodeRune(b[i:]) + }) +} + +func needsQuoting[T string | []byte](text T, decodeRune func(i int) (rune, int)) bool { + if len(text) == 0 { return true } - for i := 0; i < len(s); { - b := s[i] + for i := 0; i < len(text); { + b := text[i] if b < utf8.RuneSelf { // Quote anything except a backslash that would need quoting in a // JSON string, as well as space and '=' @@ -151,7 +214,7 @@ func needsQuoting(s string) bool { i++ continue } - r, size := utf8.DecodeRuneInString(s[i:]) + r, size := decodeRune(i) if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) { return true } diff --git a/src/log/slog/text_handler_test.go b/src/log/slog/text_handler_test.go index 591c243b11f01d..61234d5cafca60 100644 --- a/src/log/slog/text_handler_test.go +++ b/src/log/slog/text_handler_test.go @@ -168,9 +168,14 @@ func TestNeedsQuoting(t *testing.T) { {"a b", true}, {"badutf8\xF6", true}, } { - got := needsQuoting(test.in) + got := needsQuotingString(test.in) if got != test.want { - t.Errorf("%q: got %t, want %t", test.in, got, test.want) + t.Errorf("needsQuotingString: %q: got %t, want %t", test.in, got, test.want) + } + + got = needsQuotingBytes([]byte(test.in)) + if got != test.want { + t.Errorf("needsQuotingBytes: %q: got %t, want %t", test.in, got, test.want) } } }