Skip to content

Commit

Permalink
slogutil: jsonhybrid
Browse files Browse the repository at this point in the history
  • Loading branch information
Mizzick committed Oct 22, 2024
1 parent d9aaf55 commit 1eb6fbc
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 76 deletions.
2 changes: 0 additions & 2 deletions logutil/slogutil/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const (
FormatDefault Format = "default"
FormatJSON Format = "json"
FormatJSONHybrid Format = "jsonhybrid"
FormatJSONL Format = "jsonl"
FormatText Format = "text"
)

Expand All @@ -21,7 +20,6 @@ func NewFormat(s string) (f Format, err error) {
FormatDefault,
FormatJSON,
FormatJSONHybrid,
FormatJSONL,
FormatText:
return f, nil
default:
Expand Down
54 changes: 45 additions & 9 deletions logutil/slogutil/jsonhybrid.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,57 @@ const (

// NewJSONHybridHandler creates a new properly initialized *JSONHybridHandler.
// opts are used for the underlying JSON handler.
func NewJSONHybridHandler(w io.Writer, opts *slog.HandlerOptions) (h *JSONHybridHandler) {
func NewJSONHybridHandler(
w io.Writer,
opts *slog.HandlerOptions,
removeTime bool,
) (h *JSONHybridHandler) {
var replaceAttr func(groups []string, a slog.Attr) slog.Attr
if removeTime {
replaceAttr = func(groups []string, a slog.Attr) (res slog.Attr) {
return replaceAttrKey(groups, RemoveTime(groups, a))
}
} else {
replaceAttr = replaceAttrKey
}

handlerOpts := &slog.HandlerOptions{
ReplaceAttr: replaceAttr,
}

return &JSONHybridHandler{
json: slog.NewJSONHandler(w, opts),
attrPool: syncutil.NewSlicePool[slog.Attr](initAttrsLenEst),
bufTextPool: syncutil.NewPool(func() (bufTextHdlr *bufferedTextHandler) {
return newBufferedTextHandler(initLineLenEst)
return newBufferedTextHandler(initLineLenEst, handlerOpts)
}),
textAttrs: nil,
}
}

// replaceAttrKey is a [slog.HandlerOptions.ReplaceAttr] function that removes
// "msg" and "source" attributes. It also adds [LevelTrace] custom name for
// level attribute.
func replaceAttrKey(groups []string, a slog.Attr) (res slog.Attr) {
if len(groups) > 0 {
return a
}

switch a.Key {
case slog.MessageKey, slog.SourceKey:
return slog.Attr{}
case slog.LevelKey:
lvl := a.Value.Any().(slog.Level)
if lvl == LevelTrace {
a.Value = traceAttrValue
}

return a
default:
return a
}
}

// type check
var _ slog.Handler = (*JSONHybridHandler)(nil)

Expand All @@ -69,11 +109,7 @@ func (h *JSONHybridHandler) Handle(ctx context.Context, r slog.Record) (err erro
bufTextHdlr.reset()

_, _ = bufTextHdlr.buffer.WriteString(r.Message)

numAttrs := r.NumAttrs() + len(h.textAttrs)
if numAttrs > 0 {
_, _ = bufTextHdlr.buffer.WriteString("; attrs: ")
}
_, _ = bufTextHdlr.buffer.WriteString("; attrs: ")

textAttrsPtr := h.attrPool.Get()
defer h.attrPool.Put(textAttrsPtr)
Expand All @@ -85,7 +121,7 @@ func (h *JSONHybridHandler) Handle(ctx context.Context, r slog.Record) (err erro
return true
})

textRec := slog.NewRecord(time.Time{}, r.Level, "", 0)
textRec := slog.NewRecord(r.Time, r.Level, "", 0)
textRec.AddAttrs(h.textAttrs...)
textRec.AddAttrs(*textAttrsPtr...)

Expand All @@ -99,7 +135,7 @@ func (h *JSONHybridHandler) Handle(ctx context.Context, r slog.Record) (err erro
// Remove newline.
msgForJSON = msgForJSON[:len(msgForJSON)-1]

return h.json.Handle(ctx, slog.NewRecord(r.Time, r.Level, msgForJSON, r.PC))
return h.json.Handle(ctx, slog.NewRecord(time.Time{}, r.Level, msgForJSON, r.PC))
}

// WithAttrs implements the [slog.Handler] interface for *JSONHybridHandler.
Expand Down
14 changes: 7 additions & 7 deletions logutil/slogutil/jsonhybrid_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func ExampleJSONHybridHandler() {
Level: slog.LevelDebug,
// Use slogutil.RemoveTime to make the example reproducible.
ReplaceAttr: slogutil.RemoveTime,
})
}, true)
l := slog.New(h)

l.Debug("debug with no attributes")
Expand All @@ -27,10 +27,10 @@ func ExampleJSONHybridHandler() {
l.Info("new info with attributes", "number", 123)

// Output:
// {"level":"DEBUG","msg":"debug with no attributes"}
// {"level":"DEBUG","msg":"debug with attributes; attrs: number=123"}
// {"level":"INFO","msg":"info with no attributes"}
// {"level":"INFO","msg":"info with attributes; attrs: number=123"}
// {"level":"INFO","msg":"new info with no attributes; attrs: attr=abc"}
// {"level":"INFO","msg":"new info with attributes; attrs: attr=abc number=123"}
// {"level":"DEBUG","msg":"debug with no attributes; attrs: level=DEBUG"}
// {"level":"DEBUG","msg":"debug with attributes; attrs: level=DEBUG number=123"}
// {"level":"INFO","msg":"info with no attributes; attrs: level=INFO"}
// {"level":"INFO","msg":"info with attributes; attrs: level=INFO number=123"}
// {"level":"INFO","msg":"new info with no attributes; attrs: level=INFO attr=abc"}
// {"level":"INFO","msg":"new info with attributes; attrs: level=INFO attr=abc number=123"}
}
17 changes: 8 additions & 9 deletions logutil/slogutil/jsonhybrid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestJSONHybridHandler_Handle(t *testing.T) {
var (
hybridHdlr = slogutil.NewJSONHybridHandler(hybridOutput, &slog.HandlerOptions{
ReplaceAttr: slogutil.RemoveTime,
})
}, true)
textHdlr = slog.NewTextHandler(textOutput, &slog.HandlerOptions{
ReplaceAttr: slogutil.RemoveTime,
})
Expand Down Expand Up @@ -64,7 +64,7 @@ func TestJSONHybridHandler_Handle(t *testing.T) {

for i := 0; i < numGoroutine; i++ {
textString := textOutputStrings[i]
expectedString := strings.Replace(textString, `level=INFO msg="test message" `, "", 1)
expectedString := strings.Replace(textString, `msg="test message" `, "", 1)

jsonString := hybridOutputStrings[i]
gotString := strings.Replace(jsonString, `{"level":"INFO","msg":"test message; attrs: `, "", 1)
Expand All @@ -75,7 +75,7 @@ func TestJSONHybridHandler_Handle(t *testing.T) {
}

func BenchmarkJSONHybridHandler_Handle(b *testing.B) {
h := slogutil.NewJSONHybridHandler(io.Discard, nil)
h := slogutil.NewJSONHybridHandler(io.Discard, nil, false)

ctx := context.Background()
r := slog.NewRecord(time.Now(), slog.LevelInfo, "test message", 0)
Expand All @@ -92,10 +92,9 @@ func BenchmarkJSONHybridHandler_Handle(b *testing.B) {

require.NoError(b, errSink)

// Most recent results, on a ThinkPad X13 with a Ryzen Pro 7 CPU:
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/golibs/logutil/slogutil
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkJSONHybridHandler_Handle-16 1035621 1246 ns/op 48 B/op 1 allocs/op
// goos: darwin
// goarch: arm64
// pkg: github.com/AdguardTeam/golibs/logutil/slogutil
// cpu: Apple M1 Pro
// BenchmarkJSONHybridHandler_Handle-8 1596511 742.7 ns/op 104 B/op 2 allocs/op
}
27 changes: 26 additions & 1 deletion logutil/slogutil/legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,37 @@ func NewAdGuardLegacyHandler(lvl slog.Leveler) (h *AdGuardLegacyHandler) {
level: lvl,
attrPool: syncutil.NewSlicePool[slog.Attr](initAttrsLenEst),
bufTextPool: syncutil.NewPool(func() (bufTextHdlr *bufferedTextHandler) {
return newBufferedTextHandler(initLineLenEst)
return newBufferedTextHandler(initLineLenEst, legacyTextHandlerOpts)
}),
attrs: nil,
}
}

// legacyTextHandlerOpts are the options used by buffered text handlers of
// [FormatAdGuardLegacy] handlers.
var legacyTextHandlerOpts = &slog.HandlerOptions{
ReplaceAttr: legacyRemoveTopLevel,
}

// legacyRemoveTopLevel is a [slog.HandlerOptions.ReplaceAttr] function that removes
// "level", "msg", "time", and "source" attributes.
func legacyRemoveTopLevel(groups []string, a slog.Attr) (res slog.Attr) {
if len(groups) > 0 {
return a
}

switch a.Key {
case
slog.LevelKey,
slog.MessageKey,
slog.TimeKey,
slog.SourceKey:
return slog.Attr{}
default:
return a
}
}

// type check
var _ slog.Handler = (*AdGuardLegacyHandler)(nil)

Expand Down
54 changes: 6 additions & 48 deletions logutil/slogutil/slogutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ const (
KeyTime = slog.TimeKey
KeyLevel = slog.LevelKey

// keySeverity is the key for the level attribute in [FormatJSONL].
// keySeverity is the key for the level attribute in [FormatJSONHybrid].
keySeverity = "severity"

// keyMessage is the key for the message attribute in [FormatJSONL].
// keyMessage is the key for the message attribute in [FormatJSONHybrid].
keyMessage = "message"
)

Expand Down Expand Up @@ -82,13 +82,8 @@ func New(c *Config) (l *slog.Logger) {
case FormatJSONHybrid:
h = NewJSONHybridHandler(output, &slog.HandlerOptions{
Level: lvl,
ReplaceAttr: replaceAttr,
})
case FormatJSONL:
h = NewJSONHybridHandler(output, &slog.HandlerOptions{
Level: lvl,
ReplaceAttr: newJSONLReplaceAttr(!c.AddTimestamp),
})
ReplaceAttr: renameAttrs,
}, !c.AddTimestamp)
case FormatText:
h = slog.NewTextHandler(output, &slog.HandlerOptions{
Level: lvl,
Expand Down Expand Up @@ -150,18 +145,6 @@ func ReplaceLevel(groups []string, a slog.Attr) (res slog.Attr) {
return a
}

// newJSONLReplaceAttr is a function that returns
// [slog.HandlerOptions.ReplaceAttr] function for [FormatJSONL] format.
func newJSONLReplaceAttr(removeTime bool) func(groups []string, a slog.Attr) (res slog.Attr) {
if !removeTime {
return renameAttrs
}

return func(groups []string, a slog.Attr) (res slog.Attr) {
return renameAttrs(groups, RemoveTime(groups, a))
}
}

// normalAttrValue is a NORMAL value under the [slog.LevelKey] key.
var normalAttrValue = slog.StringValue("NORMAL")

Expand Down Expand Up @@ -241,37 +224,12 @@ type bufferedTextHandler struct {

// newBufferedTextHandler returns a new bufferedTextHandler with the given
// buffer length.
func newBufferedTextHandler(l int) (h *bufferedTextHandler) {
func newBufferedTextHandler(l int, handlerOpts *slog.HandlerOptions) (h *bufferedTextHandler) {
buf := bytes.NewBuffer(make([]byte, 0, l))

return &bufferedTextHandler{
buffer: buf,
handler: slog.NewTextHandler(buf, textHandlerOpts),
}
}

// textHandlerOpts are the options used by buffered text handlers of JSON hybrid
// handlers.
var textHandlerOpts = &slog.HandlerOptions{
ReplaceAttr: removeTopLevel,
}

// removeTopLevel is a [slog.HandlerOptions.ReplaceAttr] function that removes
// "level", "msg", "time", and "source" attributes.
func removeTopLevel(groups []string, a slog.Attr) (res slog.Attr) {
if len(groups) > 0 {
return a
}

switch a.Key {
case
slog.LevelKey,
slog.MessageKey,
slog.TimeKey,
slog.SourceKey:
return slog.Attr{}
default:
return a
handler: slog.NewTextHandler(buf, handlerOpts),
}
}

Expand Down

0 comments on commit 1eb6fbc

Please sign in to comment.