mirror of
https://github.com/hanzoai/log.git
synced 2026-10-30 18:29:00 +00:00
The module declares go 1.27.1. go fix applied the modernizers; omitzero is not applied, since it changes what the JSON says. Co-authored-by: Hanzo Dev <[email protected]>
867 lines
21 KiB
Go
867 lines
21 KiB
Go
// Copyright (C) 2019-2026, Hanzo AI Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package log
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"maps"
|
|
"math"
|
|
"math/big"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/mattn/go-colorable"
|
|
"github.com/mattn/go-isatty"
|
|
)
|
|
|
|
// slog.Level constants for slog-based handlers
|
|
const (
|
|
levelMaxVerbosity slog.Level = math.MinInt
|
|
slogLevelTrace slog.Level = -8
|
|
slogLevelDebug = slog.LevelDebug
|
|
slogLevelInfo = slog.LevelInfo
|
|
slogLevelWarn = slog.LevelWarn
|
|
slogLevelError = slog.LevelError
|
|
slogLevelCrit slog.Level = 12
|
|
|
|
// Exported slog.Level constants for use with Logger.Enabled()
|
|
SlogLevelTrace slog.Level = slogLevelTrace
|
|
SlogLevelDebug = slogLevelDebug
|
|
SlogLevelInfo = slogLevelInfo
|
|
SlogLevelWarn = slogLevelWarn
|
|
SlogLevelError = slogLevelError
|
|
SlogLevelCrit = slogLevelCrit
|
|
)
|
|
|
|
// Level aliases for geth/logger compatibility.
|
|
// These are logger.Level type (int8) for use with Logger.Enabled().
|
|
const (
|
|
LevelTrace Level = TraceLevel
|
|
LevelDebug Level = DebugLevel
|
|
LevelInfo Level = InfoLevel
|
|
LevelWarn Level = WarnLevel
|
|
LevelError Level = ErrorLevel
|
|
LevelCrit Level = FatalLevel
|
|
|
|
// Aliases for backward compatibility
|
|
LvlTrace = LevelTrace
|
|
LvlInfo = LevelInfo
|
|
LvlDebug = LevelDebug
|
|
)
|
|
|
|
const (
|
|
legacyLevelCrit = iota
|
|
legacyLevelError
|
|
legacyLevelWarn
|
|
legacyLevelInfo
|
|
legacyLevelDebug
|
|
legacyLevelTrace
|
|
)
|
|
|
|
const (
|
|
timeFormat = "2006-01-02T15:04:05-0700"
|
|
floatFormat = 'f'
|
|
termMsgJust = 40
|
|
termCtxMaxPadding = 40
|
|
)
|
|
|
|
var spaces = []byte(" ")
|
|
|
|
// SlogLogger is a Logger interface that wraps slog for geth compatibility.
|
|
type SlogLogger interface {
|
|
With(ctx ...any) SlogLogger
|
|
New(ctx ...any) SlogLogger
|
|
Log(level slog.Level, msg string, ctx ...any)
|
|
Trace(msg string, ctx ...any)
|
|
Debug(msg string, ctx ...any)
|
|
Info(msg string, ctx ...any)
|
|
Warn(msg string, ctx ...any)
|
|
Error(msg string, ctx ...any)
|
|
Crit(msg string, ctx ...any)
|
|
Write(level slog.Level, msg string, attrs ...any)
|
|
Enabled(ctx context.Context, level slog.Level) bool
|
|
Handler() slog.Handler
|
|
}
|
|
|
|
type slogLogger struct {
|
|
inner *slog.Logger
|
|
}
|
|
|
|
var defaultSlogLogger atomic.Pointer[slogLogger]
|
|
|
|
func init() {
|
|
defaultSlogLogger.Store(&slogLogger{inner: slog.Default()})
|
|
}
|
|
|
|
// SlogRoot returns the root slog-based logger.
|
|
func SlogRoot() SlogLogger {
|
|
return defaultSlogLogger.Load()
|
|
}
|
|
|
|
// SetSlogDefault sets the default slog-based logger.
|
|
func SetSlogDefault(l SlogLogger) {
|
|
if lg, ok := l.(*slogLogger); ok {
|
|
defaultSlogLogger.Store(lg)
|
|
slog.SetDefault(lg.inner)
|
|
}
|
|
}
|
|
|
|
// NewLogger creates a new SlogLogger with the given handler.
|
|
func NewLogger(h slog.Handler) SlogLogger {
|
|
return &slogLogger{inner: slog.New(h)}
|
|
}
|
|
|
|
// NewLoggerFromHandler creates a new SlogLogger from a slog.Handler.
|
|
func NewLoggerFromHandler(h slog.Handler) SlogLogger {
|
|
return &slogLogger{inner: slog.New(h)}
|
|
}
|
|
|
|
func (l *slogLogger) Write(level slog.Level, msg string, attrs ...any) {
|
|
if !l.inner.Enabled(context.Background(), level) {
|
|
return
|
|
}
|
|
var pcs [1]uintptr
|
|
runtime.Callers(3, pcs[:])
|
|
if len(attrs)%2 != 0 {
|
|
attrs = append(attrs, nil, "LOG_ERROR", "Normalized odd number of arguments by adding nil")
|
|
}
|
|
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
|
|
r.Add(attrs...)
|
|
_ = l.inner.Handler().Handle(context.Background(), r)
|
|
}
|
|
|
|
func (l *slogLogger) Log(level slog.Level, msg string, attrs ...any) {
|
|
l.Write(level, msg, attrs...)
|
|
}
|
|
|
|
func (l *slogLogger) With(ctx ...any) SlogLogger {
|
|
return &slogLogger{l.inner.With(ctx...)}
|
|
}
|
|
|
|
func (l *slogLogger) New(ctx ...any) SlogLogger {
|
|
return l.With(ctx...)
|
|
}
|
|
|
|
func (l *slogLogger) Enabled(ctx context.Context, level slog.Level) bool {
|
|
return l.inner.Enabled(ctx, level)
|
|
}
|
|
|
|
func (l *slogLogger) Trace(msg string, ctx ...any) {
|
|
l.Write(slogLevelTrace, msg, ctx...)
|
|
}
|
|
|
|
func (l *slogLogger) Debug(msg string, ctx ...any) {
|
|
l.Write(slog.LevelDebug, msg, ctx...)
|
|
}
|
|
|
|
func (l *slogLogger) Info(msg string, ctx ...any) {
|
|
l.Write(slog.LevelInfo, msg, ctx...)
|
|
}
|
|
|
|
func (l *slogLogger) Warn(msg string, ctx ...any) {
|
|
l.Write(slog.LevelWarn, msg, ctx...)
|
|
}
|
|
|
|
func (l *slogLogger) Error(msg string, ctx ...any) {
|
|
l.Write(slog.LevelError, msg, ctx...)
|
|
}
|
|
|
|
func (l *slogLogger) Crit(msg string, ctx ...any) {
|
|
l.Write(slogLevelCrit, msg, ctx...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func (l *slogLogger) Handler() slog.Handler {
|
|
return l.inner.Handler()
|
|
}
|
|
|
|
// FromLegacyLevel converts old geth verbosity level to slog.Level.
|
|
func FromLegacyLevel(lvl int) slog.Level {
|
|
switch lvl {
|
|
case legacyLevelCrit:
|
|
return slogLevelCrit
|
|
case legacyLevelError:
|
|
return slog.LevelError
|
|
case legacyLevelWarn:
|
|
return slog.LevelWarn
|
|
case legacyLevelInfo:
|
|
return slog.LevelInfo
|
|
case legacyLevelDebug:
|
|
return slog.LevelDebug
|
|
case legacyLevelTrace:
|
|
return slogLevelTrace
|
|
}
|
|
if lvl > legacyLevelTrace {
|
|
return slogLevelTrace
|
|
}
|
|
return slogLevelCrit
|
|
}
|
|
|
|
// LvlFromString returns the appropriate level from a string name.
|
|
func LvlFromString(lvlString string) (slog.Level, error) {
|
|
switch strings.ToLower(lvlString) {
|
|
case "trace", "trce":
|
|
return slogLevelTrace, nil
|
|
case "debug", "dbug":
|
|
return slogLevelDebug, nil
|
|
case "info":
|
|
return slogLevelInfo, nil
|
|
case "warn":
|
|
return slogLevelWarn, nil
|
|
case "error", "eror":
|
|
return slogLevelError, nil
|
|
case "crit":
|
|
return slogLevelCrit, nil
|
|
default:
|
|
return slogLevelDebug, fmt.Errorf("unknown level: %v", lvlString)
|
|
}
|
|
}
|
|
|
|
// LevelAlignedString returns a 5-character string containing the name of a level.
|
|
func LevelAlignedString(l slog.Level) string {
|
|
switch l {
|
|
case slogLevelTrace:
|
|
return "TRACE"
|
|
case slog.LevelDebug:
|
|
return "DEBUG"
|
|
case slog.LevelInfo:
|
|
return "INFO "
|
|
case slog.LevelWarn:
|
|
return "WARN "
|
|
case slog.LevelError:
|
|
return "ERROR"
|
|
case slogLevelCrit:
|
|
return "CRIT "
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// LevelString returns a string containing the name of a level.
|
|
func LevelString(l slog.Level) string {
|
|
switch l {
|
|
case slogLevelTrace:
|
|
return "trace"
|
|
case slog.LevelDebug:
|
|
return "debug"
|
|
case slog.LevelInfo:
|
|
return "info"
|
|
case slog.LevelWarn:
|
|
return "warn"
|
|
case slog.LevelError:
|
|
return "error"
|
|
case slogLevelCrit:
|
|
return "crit"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// GlogHandler is a log handler that mimics glog behavior.
|
|
type GlogHandler struct {
|
|
origin slog.Handler
|
|
verbosity slog.Level
|
|
vmodule map[string]slog.Level
|
|
patternCache map[string]*regexp.Regexp // cached compiled patterns
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewGlogHandler creates a GlogHandler wrapping the given handler.
|
|
func NewGlogHandler(h slog.Handler) *GlogHandler {
|
|
return &GlogHandler{
|
|
origin: h,
|
|
verbosity: slogLevelInfo,
|
|
vmodule: make(map[string]slog.Level),
|
|
patternCache: make(map[string]*regexp.Regexp),
|
|
}
|
|
}
|
|
|
|
// Verbosity sets the global verbosity level.
|
|
func (h *GlogHandler) Verbosity(level slog.Level) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.verbosity = level
|
|
}
|
|
|
|
// Vmodule sets per-module verbosity patterns.
|
|
func (h *GlogHandler) Vmodule(pattern string) error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
h.vmodule = make(map[string]slog.Level)
|
|
h.patternCache = make(map[string]*regexp.Regexp)
|
|
if pattern == "" {
|
|
return nil
|
|
}
|
|
|
|
for rule := range strings.SplitSeq(pattern, ",") {
|
|
parts := strings.Split(rule, "=")
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
module := strings.TrimSpace(parts[0])
|
|
levelStr := strings.TrimSpace(parts[1])
|
|
level, err := LvlFromString(levelStr)
|
|
if err != nil {
|
|
var lvl int
|
|
if _, parseErr := fmt.Sscanf(levelStr, "%d", &lvl); parseErr == nil {
|
|
level = FromLegacyLevel(lvl)
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
h.vmodule[module] = level
|
|
// Precompile pattern regex
|
|
if re, err := compilePattern(module); err == nil {
|
|
h.patternCache[module] = re
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *GlogHandler) Handle(ctx context.Context, r slog.Record) error {
|
|
h.mu.RLock()
|
|
verbosity := h.verbosity
|
|
vmodule := h.vmodule
|
|
patternCache := h.patternCache
|
|
h.mu.RUnlock()
|
|
|
|
if len(vmodule) > 0 {
|
|
_, file, _, ok := runtime.Caller(6)
|
|
if ok {
|
|
for pattern, level := range vmodule {
|
|
re := patternCache[pattern]
|
|
if re != nil && re.MatchString(file) && r.Level >= level {
|
|
return h.origin.Handle(ctx, r)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if r.Level >= verbosity {
|
|
return h.origin.Handle(ctx, r)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// compilePattern converts a vmodule pattern to a compiled regex.
|
|
func compilePattern(pattern string) (*regexp.Regexp, error) {
|
|
escaped := strings.ReplaceAll(pattern, ".", "\\.")
|
|
escaped = strings.ReplaceAll(escaped, "*", ".*")
|
|
escaped = strings.ReplaceAll(escaped, "/", "\\/")
|
|
return regexp.Compile(escaped)
|
|
}
|
|
|
|
func (h *GlogHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
|
h.mu.RLock()
|
|
verbosity := h.verbosity
|
|
h.mu.RUnlock()
|
|
return level >= verbosity
|
|
}
|
|
|
|
func (h *GlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
h.mu.RLock()
|
|
verbosity := h.verbosity
|
|
vmodule := make(map[string]slog.Level, len(h.vmodule))
|
|
maps.Copy(vmodule, h.vmodule)
|
|
patternCache := make(map[string]*regexp.Regexp, len(h.patternCache))
|
|
maps.Copy(patternCache, h.patternCache)
|
|
h.mu.RUnlock()
|
|
|
|
return &GlogHandler{
|
|
origin: h.origin.WithAttrs(attrs),
|
|
verbosity: verbosity,
|
|
vmodule: vmodule,
|
|
patternCache: patternCache,
|
|
}
|
|
}
|
|
|
|
func (h *GlogHandler) WithGroup(name string) slog.Handler {
|
|
h.mu.RLock()
|
|
verbosity := h.verbosity
|
|
vmodule := make(map[string]slog.Level, len(h.vmodule))
|
|
maps.Copy(vmodule, h.vmodule)
|
|
patternCache := make(map[string]*regexp.Regexp, len(h.patternCache))
|
|
maps.Copy(patternCache, h.patternCache)
|
|
h.mu.RUnlock()
|
|
|
|
return &GlogHandler{
|
|
origin: h.origin.WithGroup(name),
|
|
verbosity: verbosity,
|
|
vmodule: vmodule,
|
|
patternCache: patternCache,
|
|
}
|
|
}
|
|
|
|
// TerminalHandler formats log records for human readability on a terminal.
|
|
type TerminalHandler struct {
|
|
mu sync.Mutex
|
|
wr io.Writer
|
|
lvl slog.Leveler
|
|
useColor bool
|
|
attrs []slog.Attr
|
|
fieldPadding map[string]int
|
|
buf []byte
|
|
Prefix func(r slog.Record) string
|
|
}
|
|
|
|
// NewTerminalHandler returns a handler which formats log records for human readability.
|
|
func NewTerminalHandler(wr io.Writer, useColor bool) *TerminalHandler {
|
|
return NewTerminalHandlerWithLevel(wr, levelMaxVerbosity, useColor)
|
|
}
|
|
|
|
// NewTerminalHandlerWithLevel returns a terminal handler with level filtering.
|
|
func NewTerminalHandlerWithLevel(wr io.Writer, lvl slog.Leveler, useColor bool) *TerminalHandler {
|
|
return &TerminalHandler{
|
|
wr: wr,
|
|
lvl: lvl,
|
|
useColor: useColor,
|
|
fieldPadding: make(map[string]int),
|
|
}
|
|
}
|
|
|
|
func (h *TerminalHandler) Handle(_ context.Context, r slog.Record) error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
buf := h.format(h.buf, r, h.useColor)
|
|
_, _ = h.wr.Write(buf)
|
|
h.buf = buf[:0]
|
|
return nil
|
|
}
|
|
|
|
func (h *TerminalHandler) Enabled(_ context.Context, level slog.Level) bool {
|
|
return level >= h.lvl.Level()
|
|
}
|
|
|
|
func (h *TerminalHandler) WithGroup(name string) slog.Handler {
|
|
// Create a new handler with the group prefix applied to attribute keys
|
|
return &TerminalHandler{
|
|
wr: h.wr,
|
|
lvl: h.lvl,
|
|
useColor: h.useColor,
|
|
attrs: h.attrs,
|
|
fieldPadding: make(map[string]int),
|
|
Prefix: func(r slog.Record) string {
|
|
prefix := ""
|
|
if h.Prefix != nil {
|
|
prefix = h.Prefix(r)
|
|
}
|
|
if name != "" {
|
|
prefix = prefix + name + "."
|
|
}
|
|
return prefix
|
|
},
|
|
}
|
|
}
|
|
|
|
func (h *TerminalHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
return &TerminalHandler{
|
|
wr: h.wr,
|
|
lvl: h.lvl,
|
|
useColor: h.useColor,
|
|
attrs: append(h.attrs, attrs...),
|
|
fieldPadding: make(map[string]int),
|
|
}
|
|
}
|
|
|
|
func (h *TerminalHandler) ResetFieldPadding() {
|
|
h.mu.Lock()
|
|
h.fieldPadding = make(map[string]int)
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
func (h *TerminalHandler) format(buf []byte, r slog.Record, usecolor bool) []byte {
|
|
msg := escapeMessage(r.Message)
|
|
var color = ""
|
|
if usecolor {
|
|
switch r.Level {
|
|
case slogLevelCrit:
|
|
color = "\x1b[35m"
|
|
case slog.LevelError:
|
|
color = "\x1b[31m"
|
|
case slog.LevelWarn:
|
|
color = "\x1b[33m"
|
|
case slog.LevelInfo:
|
|
color = "\x1b[32m"
|
|
case slog.LevelDebug:
|
|
color = "\x1b[36m"
|
|
case slogLevelTrace:
|
|
color = "\x1b[34m"
|
|
}
|
|
}
|
|
if buf == nil {
|
|
buf = make([]byte, 0, 30+termMsgJust)
|
|
}
|
|
b := bytes.NewBuffer(buf)
|
|
|
|
b.WriteString("[")
|
|
writeTimeTermFormat(b, r.Time)
|
|
b.WriteString("] ")
|
|
|
|
if color != "" {
|
|
b.WriteString(color)
|
|
b.WriteString(LevelAlignedString(r.Level))
|
|
b.WriteString("\x1b[0m")
|
|
} else {
|
|
b.WriteString(LevelAlignedString(r.Level))
|
|
}
|
|
|
|
b.WriteByte(' ')
|
|
|
|
if h.Prefix != nil {
|
|
b.WriteString(h.Prefix(r))
|
|
}
|
|
|
|
b.WriteString(msg)
|
|
|
|
length := len(msg)
|
|
if (r.NumAttrs()+len(h.attrs)) > 0 && length < termMsgJust {
|
|
b.Write(spaces[:termMsgJust-length])
|
|
}
|
|
h.formatAttributes(b, r, color)
|
|
|
|
return b.Bytes()
|
|
}
|
|
|
|
func (h *TerminalHandler) formatAttributes(buf *bytes.Buffer, r slog.Record, color string) {
|
|
var tmp = make([]byte, 40)
|
|
writeAttr := func(attr slog.Attr, _, last bool) {
|
|
buf.WriteByte(' ')
|
|
if color != "" {
|
|
buf.WriteString(color)
|
|
buf.Write(appendEscapeString(tmp[:0], attr.Key))
|
|
buf.WriteString("\x1b[0m=")
|
|
} else {
|
|
buf.Write(appendEscapeString(tmp[:0], attr.Key))
|
|
buf.WriteByte('=')
|
|
}
|
|
val := FormatSlogValue(attr.Value, tmp[:0])
|
|
padding := h.fieldPadding[attr.Key]
|
|
length := utf8.RuneCount(val)
|
|
if padding < length && length <= termCtxMaxPadding {
|
|
padding = length
|
|
h.fieldPadding[attr.Key] = padding
|
|
}
|
|
buf.Write(val)
|
|
if !last && padding > length {
|
|
buf.Write(spaces[:padding-length])
|
|
}
|
|
}
|
|
var n = 0
|
|
var nAttrs = len(h.attrs) + r.NumAttrs()
|
|
for _, attr := range h.attrs {
|
|
writeAttr(attr, n == 0, n == nAttrs-1)
|
|
n++
|
|
}
|
|
r.Attrs(func(attr slog.Attr) bool {
|
|
writeAttr(attr, n == 0, n == nAttrs-1)
|
|
n++
|
|
return true
|
|
})
|
|
buf.WriteByte('\n')
|
|
}
|
|
|
|
// FormatSlogValue formats a slog.Value for serialization to terminal.
|
|
func FormatSlogValue(v slog.Value, tmp []byte) (result []byte) {
|
|
var value any
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
if v := reflect.ValueOf(value); v.Kind() == reflect.Pointer && v.IsNil() {
|
|
result = []byte("<nil>")
|
|
} else {
|
|
panic(err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
switch v.Kind() {
|
|
case slog.KindString:
|
|
return appendEscapeString(tmp, v.String())
|
|
case slog.KindInt64:
|
|
return appendInt64(tmp, v.Int64())
|
|
case slog.KindUint64:
|
|
return appendUint64(tmp, v.Uint64(), false)
|
|
case slog.KindFloat64:
|
|
return strconv.AppendFloat(tmp, v.Float64(), floatFormat, 3, 64)
|
|
case slog.KindBool:
|
|
return strconv.AppendBool(tmp, v.Bool())
|
|
case slog.KindDuration:
|
|
value = v.Duration()
|
|
case slog.KindTime:
|
|
return v.Time().AppendFormat(tmp, timeFormat)
|
|
default:
|
|
value = v.Any()
|
|
}
|
|
if value == nil {
|
|
return []byte("<nil>")
|
|
}
|
|
switch v := value.(type) {
|
|
case *big.Int:
|
|
return appendBigInt(tmp, v)
|
|
case error:
|
|
return appendEscapeString(tmp, v.Error())
|
|
case fmt.Stringer:
|
|
return appendEscapeString(tmp, v.String())
|
|
}
|
|
internal := fmt.Appendf(tmp, "%+v", value)
|
|
return appendEscapeString(tmp, string(internal))
|
|
}
|
|
|
|
func appendInt64(dst []byte, n int64) []byte {
|
|
if n < 0 {
|
|
return appendUint64(dst, uint64(-n), true)
|
|
}
|
|
return appendUint64(dst, uint64(n), false)
|
|
}
|
|
|
|
func appendUint64(dst []byte, n uint64, neg bool) []byte {
|
|
if n < 100000 {
|
|
if neg {
|
|
return strconv.AppendInt(dst, -int64(n), 10)
|
|
}
|
|
return strconv.AppendInt(dst, int64(n), 10)
|
|
}
|
|
const maxLength = 26
|
|
var (
|
|
out = make([]byte, maxLength)
|
|
i = maxLength - 1
|
|
comma = 0
|
|
)
|
|
for ; n > 0; i-- {
|
|
if comma == 3 {
|
|
comma = 0
|
|
out[i] = ','
|
|
} else {
|
|
comma++
|
|
out[i] = '0' + byte(n%10)
|
|
n /= 10
|
|
}
|
|
}
|
|
if neg {
|
|
out[i] = '-'
|
|
i--
|
|
}
|
|
return append(dst, out[i+1:]...)
|
|
}
|
|
|
|
func appendBigInt(dst []byte, n *big.Int) []byte {
|
|
if n.IsUint64() {
|
|
return appendUint64(dst, n.Uint64(), false)
|
|
}
|
|
if n.IsInt64() {
|
|
return appendInt64(dst, n.Int64())
|
|
}
|
|
var (
|
|
text = n.String()
|
|
buf = make([]byte, len(text)+len(text)/3)
|
|
comma = 0
|
|
i = len(buf) - 1
|
|
)
|
|
for j := len(text) - 1; j >= 0; j, i = j-1, i-1 {
|
|
c := text[j]
|
|
switch {
|
|
case c == '-':
|
|
buf[i] = c
|
|
case comma == 3:
|
|
buf[i] = ','
|
|
i--
|
|
comma = 0
|
|
fallthrough
|
|
default:
|
|
buf[i] = c
|
|
comma++
|
|
}
|
|
}
|
|
return append(dst, buf[i+1:]...)
|
|
}
|
|
|
|
func appendEscapeString(dst []byte, s string) []byte {
|
|
needsQuoting := false
|
|
needsEscaping := false
|
|
for _, r := range s {
|
|
if r == ' ' || r == '=' {
|
|
needsQuoting = true
|
|
continue
|
|
}
|
|
if r <= '"' || r > '~' {
|
|
needsEscaping = true
|
|
break
|
|
}
|
|
}
|
|
if needsEscaping {
|
|
return strconv.AppendQuote(dst, s)
|
|
}
|
|
if needsQuoting {
|
|
dst = append(dst, '"')
|
|
dst = append(dst, []byte(s)...)
|
|
return append(dst, '"')
|
|
}
|
|
return append(dst, []byte(s)...)
|
|
}
|
|
|
|
func escapeMessage(s string) string {
|
|
needsQuoting := false
|
|
for _, r := range s {
|
|
if r == '\r' || r == '\n' || r == '\t' {
|
|
continue
|
|
}
|
|
if r < ' ' || r > '~' || r == '=' {
|
|
needsQuoting = true
|
|
break
|
|
}
|
|
}
|
|
if !needsQuoting {
|
|
return s
|
|
}
|
|
return strconv.Quote(s)
|
|
}
|
|
|
|
func writeTimeTermFormat(buf *bytes.Buffer, t time.Time) {
|
|
_, month, day := t.Date()
|
|
writePosIntWidth(buf, int(month), 2)
|
|
buf.WriteByte('-')
|
|
writePosIntWidth(buf, day, 2)
|
|
buf.WriteByte('|')
|
|
hour, min, sec := t.Clock()
|
|
writePosIntWidth(buf, hour, 2)
|
|
buf.WriteByte(':')
|
|
writePosIntWidth(buf, min, 2)
|
|
buf.WriteByte(':')
|
|
writePosIntWidth(buf, sec, 2)
|
|
ns := t.Nanosecond()
|
|
buf.WriteByte('.')
|
|
writePosIntWidth(buf, ns/1e6, 3)
|
|
}
|
|
|
|
func writePosIntWidth(b *bytes.Buffer, i, width int) {
|
|
if i < 0 {
|
|
panic("negative int")
|
|
}
|
|
var bb [20]byte
|
|
bp := len(bb) - 1
|
|
for i >= 10 || width > 1 {
|
|
width--
|
|
q := i / 10
|
|
bb[bp] = byte('0' + i - q*10)
|
|
bp--
|
|
i = q
|
|
}
|
|
bb[bp] = byte('0' + i)
|
|
b.Write(bb[bp:])
|
|
}
|
|
|
|
// JSONHandler returns a handler which prints records in JSON format.
|
|
func JSONHandler(wr io.Writer) slog.Handler {
|
|
return slog.NewJSONHandler(wr, &slog.HandlerOptions{
|
|
ReplaceAttr: builtinReplaceJSON,
|
|
})
|
|
}
|
|
|
|
// JSONHandlerWithLevel returns a JSON handler with level filtering.
|
|
func JSONHandlerWithLevel(wr io.Writer, level slog.Leveler) slog.Handler {
|
|
return slog.NewJSONHandler(wr, &slog.HandlerOptions{
|
|
ReplaceAttr: builtinReplaceJSON,
|
|
Level: level,
|
|
})
|
|
}
|
|
|
|
// LogfmtHandler returns a handler which prints records in logfmt format.
|
|
func LogfmtHandler(wr io.Writer) slog.Handler {
|
|
return slog.NewTextHandler(wr, &slog.HandlerOptions{
|
|
ReplaceAttr: builtinReplaceLogfmt,
|
|
})
|
|
}
|
|
|
|
// LogfmtHandlerWithLevel returns a logfmt handler with level filtering.
|
|
func LogfmtHandlerWithLevel(wr io.Writer, level slog.Leveler) slog.Handler {
|
|
return slog.NewTextHandler(wr, &slog.HandlerOptions{
|
|
ReplaceAttr: builtinReplaceLogfmt,
|
|
Level: level,
|
|
})
|
|
}
|
|
|
|
func builtinReplaceLogfmt(_ []string, attr slog.Attr) slog.Attr {
|
|
return builtinReplace(nil, attr, true)
|
|
}
|
|
|
|
func builtinReplaceJSON(_ []string, attr slog.Attr) slog.Attr {
|
|
return builtinReplace(nil, attr, false)
|
|
}
|
|
|
|
func builtinReplace(_ []string, attr slog.Attr, logfmt bool) slog.Attr {
|
|
switch attr.Key {
|
|
case slog.TimeKey:
|
|
if attr.Value.Kind() == slog.KindTime {
|
|
if logfmt {
|
|
return slog.String("timestamp", attr.Value.Time().Format(timeFormat))
|
|
}
|
|
return slog.Attr{Key: "timestamp", Value: attr.Value}
|
|
}
|
|
case slog.LevelKey:
|
|
if l, ok := attr.Value.Any().(slog.Level); ok {
|
|
return slog.Any("level", LevelString(l))
|
|
}
|
|
}
|
|
|
|
switch v := attr.Value.Any().(type) {
|
|
case time.Time:
|
|
if logfmt {
|
|
attr = slog.String(attr.Key, v.Format(timeFormat))
|
|
}
|
|
case *big.Int:
|
|
if v == nil {
|
|
attr.Value = slog.StringValue("<nil>")
|
|
} else {
|
|
attr.Value = slog.StringValue(v.String())
|
|
}
|
|
case fmt.Stringer:
|
|
if v == nil || (reflect.ValueOf(v).Kind() == reflect.Pointer && reflect.ValueOf(v).IsNil()) {
|
|
attr.Value = slog.StringValue("<nil>")
|
|
} else {
|
|
attr.Value = slog.StringValue(v.String())
|
|
}
|
|
}
|
|
return attr
|
|
}
|
|
|
|
// DiscardHandler returns a no-op handler.
|
|
func DiscardHandler() slog.Handler {
|
|
return &discardHandler{}
|
|
}
|
|
|
|
type discardHandler struct{}
|
|
|
|
func (h *discardHandler) Handle(_ context.Context, r slog.Record) error { return nil }
|
|
func (h *discardHandler) Enabled(_ context.Context, level slog.Level) bool { return false }
|
|
func (h *discardHandler) WithGroup(name string) slog.Handler { return h }
|
|
func (h *discardHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return h }
|
|
|
|
// TerminalStringer is an interface for custom terminal serialization.
|
|
type TerminalStringer interface {
|
|
TerminalString() string
|
|
}
|
|
|
|
// SetupTerminalLogger sets up a terminal logger with color support.
|
|
func SetupTerminalLogger(level slog.Level) SlogLogger {
|
|
useColor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb"
|
|
var output io.Writer = os.Stderr
|
|
if useColor {
|
|
output = colorable.NewColorableStderr()
|
|
}
|
|
handler := NewTerminalHandlerWithLevel(output, level, useColor)
|
|
return NewLogger(handler)
|
|
}
|