diamond-orm/internal/logging/custom_handler.go

208 lines
3.9 KiB
Go

package logging
import (
"bytes"
"context"
"io"
"log/slog"
"runtime"
"slices"
"strings"
"sync"
"text/template"
"time"
)
type FormattedHandler struct {
mu *sync.Mutex
out io.Writer
opts Options
attrs map[string]slog.Value
groups []string
groupLvl int
}
type Options struct {
Level slog.Leveler
Format string
ReplaceAttr func(groups []string, attr slog.Attr) slog.Attr
}
type locData struct {
FileName string
Function string
Line int
}
func NewFormattedHandler(out io.Writer, options Options) *FormattedHandler {
h := &FormattedHandler{
opts: options,
out: out,
mu: &sync.Mutex{},
groups: make([]string, 0),
}
if h.opts.Format == "" {
h.opts.Format = "{{.Time}} [{{.Level}}]"
}
if h.opts.Level == nil {
h.opts.Level = slog.LevelInfo
}
return h
}
func (f *FormattedHandler) Enabled(ctx context.Context, level slog.Level) bool {
return level >= f.opts.Level.Level()
}
func (f *FormattedHandler) Handle(ctx context.Context, r slog.Record) error {
bufp := allocBuf()
buf := *bufp
defer func() {
*bufp = buf
freeBuf(bufp)
}()
rep := f.opts.ReplaceAttr
key := slog.LevelKey
val := r.Level
if rep == nil {
r.AddAttrs(slog.String(key, val.String()))
} else {
nattr := slog.Any(key, val)
nattr.Value = rep(f.groups, nattr).Value
r.AddAttrs(nattr)
}
f.mu.Lock()
defer f.mu.Unlock()
tctx, tmpl := f.newFmtCtx(r)
wr := bytes.NewBuffer(buf)
parsed, err := tmpl.Parse(f.opts.Format)
if err != nil {
return err
}
err = parsed.Execute(wr, tctx)
if err != nil {
return err
}
wr.WriteByte('\n')
_, err = f.out.Write(wr.Bytes())
return err
}
func (f *FormattedHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
if len(attrs) == 0 {
return f
}
nf := f.clone()
bufp := allocBuf()
buf := *bufp
defer func() {
*bufp = buf
freeBuf(bufp)
}()
s := f.newState(bytes.NewBuffer(buf))
defer s.free()
pos := s.buf.Len()
s.startGroups()
if !s.appendAttrs(attrs) {
s.buf.Truncate(pos)
} else {
nf.groupLvl = len(nf.groups)
}
return nf
}
func (f *FormattedHandler) WithGroup(name string) slog.Handler {
if name == "" {
return f
}
f2 := f.clone()
f2.groups = append(f2.groups, name)
return f2
}
func (f *FormattedHandler) clone() *FormattedHandler {
return &FormattedHandler{
opts: f.opts,
groups: slices.Clip(f.groups),
out: f.out,
mu: f.mu,
groupLvl: f.groupLvl,
}
}
type tmplData struct {
Level string
Message string
RawTime time.Time
Time string
PC uintptr
Location locData
Record slog.Record
}
func hasBuiltInKey(a slog.Attr) bool {
return a.Key == slog.MessageKey ||
a.Key == slog.TimeKey ||
a.Key == slog.SourceKey
}
func (f *FormattedHandler) newFmtCtx(r slog.Record) (ctx *tmplData, tmpl *template.Template) {
tmpl = template.New("log")
ctx = &tmplData{
Message: r.Message,
RawTime: r.Time,
PC: r.PC,
Location: locData{},
}
if !r.Time.IsZero() {
ctx.Time = r.Time.Format(time.RFC3339Nano)
}
r.Attrs(func(a slog.Attr) bool {
if a.Key == slog.LevelKey {
str := strings.ToUpper(a.Value.String())
if rep := f.opts.ReplaceAttr; rep != nil {
str = strings.ToUpper(a.Value.String())
}
ctx.Level = str
}
return true
})
if r.PC != 0 {
frames := runtime.CallersFrames([]uintptr{r.PC})
frame, _ := frames.Next()
ctx.Location.FileName = frame.File
ctx.Location.Function = frame.Function
ctx.Location.Line = frame.Line
}
fm := make(map[string]any)
fm["rest"] = func() string {
bb := new(bytes.Buffer)
s := f.newState(bb)
defer s.free()
s.begin(r)
return s.buf.String()
}
tmpl = tmpl.Funcs(fm)
return
}
var bufPool = sync.Pool{
New: func() any {
b := make([]byte, 0, 4096)
return &b
},
}
func allocBuf() *[]byte {
return bufPool.Get().(*[]byte)
}
func freeBuf(b *[]byte) {
const maxBufferSize = 16 << 10
if cap(*b) <= maxBufferSize {
*b = (*b)[:0]
bufPool.Put(b)
}
}