zen/core/zlog/zlog.go

254 lines
6.2 KiB
Go
Raw Normal View History

2024-08-22 17:26:53 +07:00
package zlog
2024-08-23 10:18:27 +07:00
import (
"context"
2024-08-27 15:22:42 +07:00
"errors"
2024-08-23 10:18:27 +07:00
"io"
"log/slog"
2024-08-27 15:22:42 +07:00
"os"
"testing"
2024-08-23 12:29:47 +07:00
"time"
2024-08-22 17:26:53 +07:00
2024-08-23 10:18:27 +07:00
"github.com/fatih/color"
2024-08-27 15:22:42 +07:00
"github.com/mattn/go-isatty"
2024-08-23 12:29:47 +07:00
"github.com/tidwall/pretty"
2024-08-28 11:01:20 +07:00
"gitlab.bareksa.com/backend/zen/core/zcaller"
2024-08-27 15:22:42 +07:00
"gitlab.bareksa.com/backend/zen/internal/bufferpool"
2024-08-23 10:18:27 +07:00
)
2024-08-27 15:22:42 +07:00
type ZLog struct {
bufPool *bufferpool.Pool
2024-08-23 12:29:47 +07:00
jsonPool *jsonHandlerPool
withAttrs []slog.Attr
withGroup []string
2024-08-27 15:22:42 +07:00
opts *ZLogOptions
2024-08-23 12:29:47 +07:00
writer WriteLocker
2024-08-23 10:18:27 +07:00
}
2024-08-27 15:22:42 +07:00
type ZLogOptions struct {
// HandlerOptions handles the options for the underlying slog.Handler.
//
// If nil, default options are used.
HandlerOptions *slog.HandlerOptions
// Pretty enables pretty-printing of log messages.
2024-09-03 13:38:45 +07:00
//
// If nil, it is automatically detected based on the output writer.
Pretty *bool
2024-08-27 15:22:42 +07:00
// Color enables colorized output.
2024-09-03 13:38:45 +07:00
//
// If nil, it is automatically detected based on the output writer.
Color *bool
2024-08-27 15:22:42 +07:00
// NotificationHandler is a handler that is called when a log message is
// intended to be sent to a notification service.
//
// If nil, no notification is sent.
NotificationHandler NotificationHandler
}
// New creates a new Logger that writes to w.
//
// if opts is nil, default options are used.
func New(w io.Writer, opts *ZLogOptions) *ZLog {
if w == nil {
panic(errors.New("zlog: New: w is nil"))
}
if opts == nil {
2024-09-03 13:38:45 +07:00
opts = &ZLogOptions{}
}
if opts.Pretty == nil {
opts.Pretty = detectPretty(w)
2024-08-27 15:22:42 +07:00
}
2024-09-03 13:38:45 +07:00
if opts.Color == nil {
opts.Color = detectPretty(w)
}
if opts.HandlerOptions == nil {
opts.HandlerOptions = defaultHandlerOptions
}
opts.HandlerOptions.ReplaceAttr = wrapPrettyReplaceAttr(*opts.Pretty, opts.HandlerOptions.ReplaceAttr)
2024-08-27 15:22:42 +07:00
wl := WrapLocker(w)
return &ZLog{
bufPool: bufferpool.New(bufferpool.SharedCap, bufferpool.BufCap),
jsonPool: newJsonHandlerPool(opts.HandlerOptions),
withAttrs: []slog.Attr{},
withGroup: []string{},
opts: opts,
writer: wl,
}
}
2024-08-23 10:18:27 +07:00
// Clone clones the Logger, replacing the output writer with w.
//
// If w is nil, the clone shares the original Logger's output writer.
2024-08-27 15:22:42 +07:00
func (log *ZLog) Clone(w io.Writer) *ZLog {
2024-08-23 10:18:27 +07:00
var output WriteLocker
if w == nil {
output = log.writer
} else {
output = WrapLocker(w)
}
withAttrs := make([]slog.Attr, len(log.withAttrs))
copy(withAttrs, log.withAttrs)
withGroup := make([]string, len(log.withGroup))
copy(withGroup, log.withGroup)
2024-08-27 15:22:42 +07:00
return &ZLog{
2024-08-23 12:29:47 +07:00
withAttrs: withAttrs,
withGroup: withGroup,
opts: log.opts,
2024-08-27 15:22:42 +07:00
bufPool: bufferpool.New(bufferpool.SharedCap, bufferpool.BufCap),
jsonPool: newJsonHandlerPool(log.opts.HandlerOptions),
2024-08-23 12:29:47 +07:00
writer: output,
2024-08-23 10:18:27 +07:00
}
}
// Enabled implements the slog.Handler interface.
2024-08-27 15:22:42 +07:00
func (lo *ZLog) Enabled(ctx context.Context, lvl slog.Level) bool {
return lvl >= lo.opts.HandlerOptions.Level.Level()
2024-08-23 10:18:27 +07:00
}
// Handle implements the slog.Handler interface.
2024-08-27 15:22:42 +07:00
func (lo *ZLog) Handle(ctx context.Context, record slog.Record) error {
2024-09-03 13:38:45 +07:00
if !*lo.opts.Pretty {
2024-08-23 10:18:27 +07:00
j := lo.jsonPool.Get()
defer j.Put()
2024-09-03 13:38:45 +07:00
_ = j.Handle(ctx, record)
lo.writer.Lock()
defer lo.writer.Unlock()
_, err := j.buf.WriteTo(lo.writer)
return err
2024-08-23 10:18:27 +07:00
}
var levelColor *color.Color
switch {
case record.Level >= slog.LevelError:
levelColor = color.New(color.FgRed)
case record.Level >= slog.LevelWarn:
levelColor = color.New(color.FgYellow)
case record.Level >= slog.LevelInfo:
levelColor = color.New(color.FgGreen)
default:
levelColor = color.New(color.FgWhite)
}
buf := lo.bufPool.Get()
2024-08-27 15:22:42 +07:00
defer buf.Close()
2024-08-23 10:18:27 +07:00
2024-08-27 15:22:42 +07:00
if record.PC != 0 && lo.opts.HandlerOptions.AddSource {
2024-08-28 11:01:20 +07:00
f := zcaller.Caller(record.PC).Frame()
2024-08-23 12:29:47 +07:00
levelColor.Fprint(buf, f.ShortFile())
levelColor.Fprint(buf, ":")
levelColor.Fprint(buf, f.Line)
levelColor.Fprint(buf, " -- ")
levelColor.Fprint(buf, f.ShortFunction())
buf.WriteByte('\n')
}
if !record.Time.IsZero() {
const format = `[` + time.DateTime + `] `
b := record.Time.AppendFormat(nil, format)
buf.Write(b)
}
buf.WriteByte('[')
levelColor.Add(color.Bold).Fprint(buf, record.Level.String())
buf.WriteString("] ")
if record.Message != "" {
buf.WriteString(record.Message)
2024-09-03 13:38:45 +07:00
// levelColor.Fprintf(buf, record.Message)
2024-08-23 12:29:47 +07:00
}
buf.WriteByte('\n')
jHandler := lo.jsonPool.Get()
defer jHandler.Put()
_ = jHandler.Handle(ctx, record)
if jHandler.buf.Len() > 3 { // Skip empty objects like "{}\n"
jsonData := jHandler.buf.Bytes()
jsonData = pretty.Pretty(jsonData)
2024-09-03 13:38:45 +07:00
if *lo.opts.Color {
2024-08-23 12:29:47 +07:00
jsonData = pretty.Color(jsonData, nil)
}
buf.Write(jsonData)
}
buf.WriteByte('\n')
lo.writer.Lock()
defer lo.writer.Unlock()
_, err := buf.WriteTo(lo.writer)
return err
2024-08-23 10:18:27 +07:00
}
// WithAttrs implements the slog.Handler interface.
2024-08-27 15:22:42 +07:00
func (lo *ZLog) WithAttrs(attrs []slog.Attr) slog.Handler {
2024-08-23 10:18:27 +07:00
l := lo.Clone(nil)
l.withAttrs = append(l.withAttrs, attrs...)
return l
}
// WithGroup implements the slog.Handler interface.
2024-08-27 15:22:42 +07:00
func (lo *ZLog) WithGroup(name string) slog.Handler {
2024-08-23 10:18:27 +07:00
l := lo.Clone(nil)
l.withGroup = append(l.withGroup, name)
return l
}
type replaceAttrFunc = func([]string, slog.Attr) slog.Attr
2024-08-27 15:22:42 +07:00
func wrapPrettyReplaceAttr(pretty bool, f replaceAttrFunc) replaceAttrFunc {
if f == nil {
f = func(s []string, a slog.Attr) slog.Attr { return a }
}
2024-09-03 13:38:45 +07:00
return func(groups []string, a slog.Attr) slog.Attr {
if len(groups) > 0 {
return f(groups, a)
2024-08-27 15:22:42 +07:00
}
2024-09-03 13:38:45 +07:00
if !pretty {
switch a.Key {
case slog.MessageKey:
return slog.Attr{Key: "message", Value: a.Value}
case slog.SourceKey:
if s, ok := a.Value.Any().(*slog.Source); ok {
return slog.Attr{Key: "caller", Value: slogSource{Source: s}.LogValue()}
}
return slog.Attr{Key: "caller", Value: a.Value}
}
return f(groups, a)
2024-08-23 10:18:27 +07:00
}
switch a.Key {
case slog.TimeKey, slog.LevelKey, slog.SourceKey, slog.MessageKey:
return slog.Attr{}
}
2024-09-03 13:38:45 +07:00
return f(groups, a)
2024-08-23 10:18:27 +07:00
}
2024-08-22 17:26:53 +07:00
}
2024-08-27 15:22:42 +07:00
func defaultLevel() slog.Leveler {
if testing.Testing() {
return slog.Level(999) // Disable logging in tests
}
return slog.LevelInfo
}
var defaultOpts = &ZLogOptions{
2024-09-03 13:38:45 +07:00
Pretty: detectPretty(os.Stderr),
Color: detectPretty(os.Stderr),
2024-08-27 15:22:42 +07:00
HandlerOptions: defaultHandlerOptions,
}
var defaultHandlerOptions = &slog.HandlerOptions{
2024-09-03 13:38:45 +07:00
AddSource: true,
Level: defaultLevel(),
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { return a },
}
func detectPretty(w io.Writer) *bool {
var b bool
if fd, ok := w.(interface{ Fd() uintptr }); ok {
b = isatty.IsTerminal(fd.Fd())
}
return &b
2024-08-27 15:22:42 +07:00
}