Skip to content

Commit

Permalink
slogutil: imp code
Browse files Browse the repository at this point in the history
  • Loading branch information
Mizzick committed Oct 23, 2024
1 parent e30fd00 commit 75f05de
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 76 deletions.
111 changes: 44 additions & 67 deletions logutil/slogutil/jsonhybrid.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,98 +2,65 @@ package slogutil

import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"slices"
"time"
"sync"

"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/syncutil"
)

// JSONHybridHandler is a hybrid JSON-and-text [slog.Handler] more suitable for
// stricter environments. It guarantees that the only properties present in the
// resulting objects are "level", "msg", "time", and "source", depending on the
// options. All other attributes are packed into the "msg" property using the
// same format as [slogutil.TextHandler].
// resulting objects are "severity" and "message". All other attributes are
// packed into the "message" property using the same format as
// [slog.TextHandler].
//
// NOTE: [JSONHybridHandler.WithGroup] is not currently supported and panics.
//
// Example of output:
//
// {"severity":"NORMAL","message":"time=2024-10-22T12:09:59.525+03:00 level=INFO msg=listening prefix=websvc server=http://127.0.0.1:8181"}
type JSONHybridHandler struct {
json *slog.JSONHandler
attrPool *syncutil.Pool[[]slog.Attr]
level slog.Leveler
writer io.Writer
bufTextPool *syncutil.Pool[bufferedTextHandler]
mu *sync.Mutex
textAttrs []slog.Attr
}

const (
// initAttrsLenEst is the estimation used to set the initial length of
// attribute slices.
initAttrsLenEst = 2

// initLineLenEst is the estimation used to set the initial sizes of
// log-line buffers.
initLineLenEst = 256
)
// initLineLenEst is the estimation used to set the initial sizes of log-line
// buffers.
const initLineLenEst = 256

// NewJSONHybridHandler creates a new properly initialized *JSONHybridHandler.
// opts are used for the underlying JSON handler.
// opts are used for the underlying text handler.
func NewJSONHybridHandler(w io.Writer, opts *slog.HandlerOptions) (h *JSONHybridHandler) {
lvl := slog.LevelInfo
if opts != nil && opts.Level != nil {
lvl = opts.Level.Level()
}

return &JSONHybridHandler{
json: slog.NewJSONHandler(w, &slog.HandlerOptions{
Level: lvl,
ReplaceAttr: renameAttrs,
}),
attrPool: syncutil.NewSlicePool[slog.Attr](initAttrsLenEst),
level: lvl,
writer: w,
bufTextPool: syncutil.NewPool(func() (bufTextHdlr *bufferedTextHandler) {
return newBufferedTextHandler(initLineLenEst, opts)
}),
mu: &sync.Mutex{},
textAttrs: nil,
}
}

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

// renameAttrs is a [slog.HandlerOptions.ReplaceAttr] function that renames the
// [slog.LevelKey] key to [keySeverity], and the [slog.MessageKey] key to
// [keyMessage]. It also sets the level value to "NORMAL" for levels less than
// [LevelError].
func renameAttrs(groups []string, a slog.Attr) (res slog.Attr) {
if len(groups) > 0 {
return a
}

switch a.Key {
case KeyLevel:
lvl := a.Value.Any().(slog.Level)
if lvl < LevelError {
a.Value = valueLevelNormal
}

a.Key = keySeverity
case KeyMessage:
a.Key = keyMessage
}

return a
}

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

// Enabled implements the [slog.Handler] interface for *JSONHybridHandler.
func (h *JSONHybridHandler) Enabled(ctx context.Context, level slog.Level) (ok bool) {
return h.json.Enabled(ctx, level)
func (h *JSONHybridHandler) Enabled(_ context.Context, level slog.Level) (ok bool) {
return level >= h.level.Level()
}

// Handle implements the [slog.Handler] interface for *JSONHybridHandler.
Expand All @@ -103,21 +70,9 @@ func (h *JSONHybridHandler) Handle(ctx context.Context, r slog.Record) (err erro

bufTextHdlr.reset()

textAttrsPtr := h.attrPool.Get()
defer h.attrPool.Put(textAttrsPtr)

*textAttrsPtr = (*textAttrsPtr)[:0]
r.Attrs(func(a slog.Attr) (cont bool) {
*textAttrsPtr = append(*textAttrsPtr, a)

return true
})
r.AddAttrs(h.textAttrs...)

textRec := slog.NewRecord(r.Time, r.Level, r.Message, 0)
textRec.AddAttrs(h.textAttrs...)
textRec.AddAttrs(*textAttrsPtr...)

err = bufTextHdlr.handler.Handle(ctx, textRec)
err = bufTextHdlr.handler.Handle(ctx, r)
if err != nil {
return fmt.Errorf("handling text for msg: %w", err)
}
Expand All @@ -127,15 +82,37 @@ 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(time.Time{}, r.Level, msgForJSON, r.PC))
var severity string
if r.Level < slog.LevelError {
severity = "NORMAL"
} else {
severity = "ERROR"
}

data := struct {
Severity string `json:"severity"`
Message string `json:"message"`
}{
Severity: severity,
Message: msgForJSON,
}

enc := json.NewEncoder(h.writer)
enc.SetEscapeHTML(false)

h.mu.Lock()
defer h.mu.Unlock()

return enc.Encode(data)
}

// WithAttrs implements the [slog.Handler] interface for *JSONHybridHandler.
func (h *JSONHybridHandler) WithAttrs(attrs []slog.Attr) (res slog.Handler) {
return &JSONHybridHandler{
json: h.json,
attrPool: h.attrPool,
level: h.level,
writer: h.writer,
bufTextPool: h.bufTextPool,
mu: h.mu,
textAttrs: append(slices.Clip(h.textAttrs), attrs...),
}
}
Expand Down
4 changes: 2 additions & 2 deletions logutil/slogutil/jsonhybrid_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func ExampleJSONHybridHandler() {
// {"severity":"NORMAL","message":"level=INFO msg=\"info with no attributes\""}
// {"severity":"NORMAL","message":"level=INFO msg=\"info with attributes\" number=123"}
// {"severity":"NORMAL","message":"level=INFO msg=\"new info with no attributes\" attr=abc"}
// {"severity":"NORMAL","message":"level=INFO msg=\"new info with attributes\" attr=abc number=123"}
// {"severity":"NORMAL","message":"level=INFO msg=\"new info with attributes\" number=123 attr=abc"}
// {"severity":"ERROR","message":"level=ERROR msg=\"error with no attributes\" attr=abc"}
// {"severity":"ERROR","message":"level=ERROR msg=\"error with attributes\" attr=abc number=123"}
// {"severity":"ERROR","message":"level=ERROR msg=\"error with attributes\" number=123 attr=abc"}
}
2 changes: 1 addition & 1 deletion logutil/slogutil/jsonhybrid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,5 @@ func BenchmarkJSONHybridHandler_Handle(b *testing.B) {
// goarch: arm64
// pkg: github.com/AdguardTeam/golibs/logutil/slogutil
// cpu: Apple M1 Pro
// BenchmarkJSONHybridHandler_Handle-8 1364242 959.7 ns/op 96 B/op 1 allocs/op
// BenchmarkJSONHybridHandler_Handle-8 1929268 603.5 ns/op 128 B/op 2 allocs/op
}
4 changes: 4 additions & 0 deletions logutil/slogutil/legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import (
"github.com/AdguardTeam/golibs/syncutil"
)

// initAttrsLenEst is the estimation used to set the initial length of
// attribute slices.
const initAttrsLenEst = 2

// AdGuardLegacyHandler is a text [slog.Handler] that uses package
// github.com/AdguardTeam/golibs/log for output. It is a legacy handler that
// will be removed in a future version.
Expand Down
6 changes: 0 additions & 6 deletions logutil/slogutil/slogutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@ const (
KeySource = slog.SourceKey
KeyTime = slog.TimeKey
KeyLevel = slog.LevelKey

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

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

// Config contains the configuration for a logger.
Expand Down

0 comments on commit 75f05de

Please sign in to comment.