package logger import ( "context" "io" "log/slog" "os" "strings" "sync" "time" "arimelody.space/melody-vod-manager/colour" "github.com/mattn/go-isatty" ) type ( Options struct { Handlers []slog.Handler Level slog.Leveler IsTerminal bool } SlogHandler struct { attrs []slog.Attr group string mutex *sync.Mutex options Options writer io.Writer } ) const groupPadding = 11 const levelPadding = 5 func NewSlogHandler(writer io.Writer, options Options) *SlogHandler { writerFile, writerIsFile := writer.(*os.File) if writerIsFile { options.IsTerminal = isatty.IsTerminal(writerFile.Fd()) } return &SlogHandler{ options: options, attrs: []slog.Attr{}, group: "OTHER", mutex: &sync.Mutex{}, writer: writer, } } func (log *SlogHandler) appendRecord(buf *strings.Builder, record slog.Record) { log.appendTime(buf, record.Time) log.appendGroup(buf, log.group) log.appendLevel(buf, record.Level) log.appendMessage(buf, record.Message) for _, attr := range log.attrs { log.appendAttr(buf, attr) } record.Attrs(func(attr slog.Attr) bool { log.appendAttr(buf, attr) return true }) } func (log *SlogHandler) appendTime(buf *strings.Builder, t time.Time) { if !t.IsZero() { if log.options.IsTerminal { buf.WriteString(colour.Dim + colour.Gray) } buf.WriteRune('[') buf.WriteString(t.Format(time.UnixDate)) buf.WriteRune(']') if log.options.IsTerminal { buf.WriteString(colour.Reset) } buf.WriteRune(' ') } } func (log *SlogHandler) appendGroup(buf *strings.Builder, group string) { if log.options.IsTerminal { buf.WriteString(colour.Dim + colour.Gray) } log.appendPadLeft(buf, groupPadding, group) if log.options.IsTerminal { buf.WriteString(colour.Reset) } buf.WriteRune(' ') } func (log *SlogHandler) appendPadLeft(buf *strings.Builder, padding int, content string) { padding -= len(content) for i := padding; i > 0; i-- { buf.WriteRune(' ') } buf.WriteString(content) } func (log *SlogHandler) appendPadRight(buf *strings.Builder, padding int, content string) { buf.WriteString(content) padding -= len(content) for i := padding; i > 0; i-- { buf.WriteRune(' ') } } func (log *SlogHandler) appendLevel(buf *strings.Builder, level slog.Level) { if log.options.IsTerminal { levelColour := colour.Reset switch level { case slog.LevelDebug: levelColour = colour.Purple case slog.LevelError: levelColour = colour.Red case slog.LevelWarn: levelColour = colour.Yellow case slog.LevelInfo: levelColour = colour.Cyan } buf.WriteString(levelColour) } log.appendPadRight(buf, levelPadding, level.String()) if log.options.IsTerminal { buf.WriteString(colour.Reset) } buf.WriteRune(' ') } func (log *SlogHandler) appendMessage(buf *strings.Builder, message string) { if message != "" { buf.WriteString(message) buf.WriteRune(' ') } } func (log *SlogHandler) appendAttr(buf *strings.Builder, attr slog.Attr) { buf.WriteString(attr.String()) buf.WriteRune(' ') } func (log *SlogHandler) Enabled(ctx context.Context, level slog.Level) bool { return level >= log.options.Level.Level() } func (log *SlogHandler) Handle(ctx context.Context, record slog.Record) error { buf := &strings.Builder{} log.appendRecord(buf, record) buf.WriteRune('\n') log.mutex.Lock() defer log.mutex.Unlock() if _, err := log.writer.Write([]byte(buf.String())); err != nil { return err } for _, handler := range log.options.Handlers { if err := handler.Handle(ctx, record); err != nil { return err } } return nil } func (log *SlogHandler) WithGroup(group string) slog.Handler { log2 := *log log2.group = group log2.options.Handlers = make([]slog.Handler, len(log.options.Handlers)) for i, handler := range log.options.Handlers { log.options.Handlers[i] = handler.WithGroup(group) } return &log2 } func (log *SlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { log2 := *log log2.attrs = make([]slog.Attr, 0, len(log.attrs)+len(attrs)) log2.attrs = append(log2.attrs, log.attrs...) log2.attrs = append(log2.attrs, attrs...) log2.options.Handlers = make([]slog.Handler, len(log.options.Handlers)) for i, handler := range log.options.Handlers { log.options.Handlers[i] = handler.WithAttrs(attrs) } return &log2 }