package zlog import ( "context" "errors" "io" "log/slog" "os" "testing" "time" "github.com/fatih/color" "github.com/mattn/go-isatty" "github.com/tidwall/pretty" "gitlab.bareksa.com/backend/zen/core/zcaller" "gitlab.bareksa.com/backend/zen/internal/bufferpool" ) type ZLog struct { bufPool *bufferpool.Pool jsonPool *jsonHandlerPool withAttrs []slog.Attr withGroup []string opts *ZLogOptions writer WriteLocker } 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. // // If nil, it is automatically detected based on the output writer. Pretty *bool // Color enables colorized output. // // If nil, it is automatically detected based on the output writer. Color *bool // 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 { opts = &ZLogOptions{} } if opts.Pretty == nil { opts.Pretty = detectPretty(w) } if opts.Color == nil { opts.Color = detectPretty(w) } if opts.HandlerOptions == nil { opts.HandlerOptions = defaultHandlerOptions } opts.HandlerOptions.ReplaceAttr = wrapPrettyReplaceAttr(*opts.Pretty, opts.HandlerOptions.ReplaceAttr) 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, } } // Clone clones the Logger, replacing the output writer with w. // // If w is nil, the clone shares the original Logger's output writer. func (log *ZLog) Clone(w io.Writer) *ZLog { 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) return &ZLog{ withAttrs: withAttrs, withGroup: withGroup, opts: log.opts, bufPool: bufferpool.New(bufferpool.SharedCap, bufferpool.BufCap), jsonPool: newJsonHandlerPool(log.opts.HandlerOptions), writer: output, } } // Enabled implements the slog.Handler interface. func (lo *ZLog) Enabled(ctx context.Context, lvl slog.Level) bool { return lvl >= lo.opts.HandlerOptions.Level.Level() } // Handle implements the slog.Handler interface. func (lo *ZLog) Handle(ctx context.Context, record slog.Record) error { if !*lo.opts.Pretty { j := lo.jsonPool.Get() defer j.Put() _ = j.Handle(ctx, record) lo.writer.Lock() defer lo.writer.Unlock() _, err := j.buf.WriteTo(lo.writer) return err } 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() defer buf.Close() if record.PC != 0 && lo.opts.HandlerOptions.AddSource { f := zcaller.Caller(record.PC).Frame() 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) // levelColor.Fprintf(buf, record.Message) } 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) if *lo.opts.Color { 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 } // WithAttrs implements the slog.Handler interface. func (lo *ZLog) WithAttrs(attrs []slog.Attr) slog.Handler { l := lo.Clone(nil) l.withAttrs = append(l.withAttrs, attrs...) return l } // WithGroup implements the slog.Handler interface. func (lo *ZLog) WithGroup(name string) slog.Handler { l := lo.Clone(nil) l.withGroup = append(l.withGroup, name) return l } type replaceAttrFunc = func([]string, slog.Attr) slog.Attr func wrapPrettyReplaceAttr(pretty bool, f replaceAttrFunc) replaceAttrFunc { if f == nil { f = func(s []string, a slog.Attr) slog.Attr { return a } } return func(groups []string, a slog.Attr) slog.Attr { if len(groups) > 0 { return f(groups, a) } 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) } switch a.Key { case slog.TimeKey, slog.LevelKey, slog.SourceKey, slog.MessageKey: return slog.Attr{} } return f(groups, a) } } func defaultLevel() slog.Leveler { if testing.Testing() { return slog.Level(999) // Disable logging in tests } return slog.LevelInfo } var defaultOpts = &ZLogOptions{ Pretty: detectPretty(os.Stderr), Color: detectPretty(os.Stderr), HandlerOptions: defaultHandlerOptions, } var defaultHandlerOptions = &slog.HandlerOptions{ 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 }