208 lines
3.9 KiB
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)
|
|
}
|
|
}
|