loggers/common.go
2024-07-20 16:27:46 -07:00

116 lines
3.1 KiB
Go

package loggers
import (
"bytes"
"io"
"log/slog"
"os"
"path/filepath"
"runtime"
"strconv"
"time"
"go.elara.ws/loggers/internal/colors"
"golang.org/x/term"
)
// Options represents common options for slog handlers
// in this package.
type Options struct {
// TimeFormat represents the format of the timestamp
// provided by the [Pretty] logger.
TimeFormat string
// Level is the log level above which the handler will
// handle records.
Level slog.Level
// ShowCaller indicates whether the caller that created the
// record should be provided in the output
ShowCaller bool
// ForceColors prevents checking whether a handler outputs
// to a valid tty and always enables colors if set to true.
ForceColors bool
}
// groupOrAttr represents a group name or [log/slog.Attr].
type groupOrAttr struct {
attr slog.Attr
group string
}
// writeGroup writes all the attributes in a group to buf.
func writeGroup(colorize bool, buf *bytes.Buffer, group slog.Attr) {
attrs := group.Value.Group()
for i, attr := range attrs {
attr.Key = group.Key + "." + attr.Key
writeAttr(colorize, buf, attr)
if i < len(attrs)-1 {
buf.WriteByte(' ')
}
}
}
// writeAttr writes a single attribute to buf.
//
// If the attribute is a [log/slog.Group], it calls [writeGroup].
// If the value of the attribute is an error, it will color it red.
func writeAttr(colorize bool, buf *bytes.Buffer, attr slog.Attr) {
attr.Value = attr.Value.Resolve()
if attr.Equal(slog.Attr{}) {
return
}
if attr.Value.Kind() == slog.KindGroup {
writeGroup(colorize, buf, attr)
return
}
if _, ok := attr.Value.Any().(error); ok {
colors.WriteString(colorize, buf, colors.Red, attr.Key+"=")
colors.WriteCode(colorize, buf, colors.LightRed)
} else {
colors.WriteString(colorize, buf, colors.Cyan, attr.Key+"=")
colors.WriteCode(colorize, buf, colors.White)
}
abuf := buf.AvailableBuffer()
switch attr.Value.Kind() {
case slog.KindInt64:
abuf = strconv.AppendInt(abuf, attr.Value.Int64(), 10)
case slog.KindUint64:
abuf = strconv.AppendUint(abuf, attr.Value.Uint64(), 10)
case slog.KindFloat64:
abuf = strconv.AppendFloat(abuf, attr.Value.Float64(), 'g', -1, 64)
case slog.KindBool:
abuf = strconv.AppendBool(abuf, attr.Value.Bool())
case slog.KindDuration:
abuf = append(abuf, attr.Value.Duration().String()...)
case slog.KindTime:
abuf = attr.Value.Time().AppendFormat(abuf, time.RFC3339)
default:
abuf = strconv.AppendQuote(abuf, attr.Value.String())
}
buf.Write(abuf)
colors.WriteCode(colorize, buf, colors.Reset)
}
// writeCaller extracts the caller from the given program counter
// and writes it to buf, enclosed in square brackets.
func writeCaller(colorize bool, buf *bytes.Buffer, pc uintptr) {
frame, _ := runtime.CallersFrames([]uintptr{pc}).Next()
text := "[" + filepath.Base(frame.File) + ":" + strconv.Itoa(frame.Line) + "]"
buf.WriteByte(' ')
colors.WriteString(colorize, buf, colors.Bold+colors.LightBlue, text)
}
// isTerm checks if wr corresponds to a valid tty.
func isTerm(wr io.Writer) bool {
if fl, ok := wr.(*os.File); ok {
return term.IsTerminal(int(fl.Fd()))
}
return false
}