.
This commit is contained in:
754
vendor/github.com/lmittmann/tint/handler.go
generated
vendored
Normal file
754
vendor/github.com/lmittmann/tint/handler.go
generated
vendored
Normal file
@@ -0,0 +1,754 @@
|
||||
/*
|
||||
Package tint implements a zero-dependency [slog.Handler] that writes tinted
|
||||
(colorized) logs. The output format is inspired by the [zerolog.ConsoleWriter]
|
||||
and [slog.TextHandler].
|
||||
|
||||
The output format can be customized using [Options], which is a drop-in
|
||||
replacement for [slog.HandlerOptions].
|
||||
|
||||
# Customize Attributes
|
||||
|
||||
Options.ReplaceAttr can be used to alter or drop attributes. If set, it is
|
||||
called on each non-group attribute before it is logged.
|
||||
See [slog.HandlerOptions] for details.
|
||||
|
||||
Create a new logger with a custom TRACE level:
|
||||
|
||||
const LevelTrace = slog.LevelDebug - 4
|
||||
|
||||
w := os.Stderr
|
||||
logger := slog.New(tint.NewHandler(w, &tint.Options{
|
||||
Level: LevelTrace,
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Key == slog.LevelKey && len(groups) == 0 {
|
||||
level, ok := a.Value.Any().(slog.Level)
|
||||
if ok && level <= LevelTrace {
|
||||
return tint.Attr(13, slog.String(a.Key, "TRC"))
|
||||
}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}))
|
||||
|
||||
Create a new logger that doesn't write the time:
|
||||
|
||||
w := os.Stderr
|
||||
logger := slog.New(
|
||||
tint.NewHandler(w, &tint.Options{
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Key == slog.TimeKey && len(groups) == 0 {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
Create a new logger that writes all errors in red:
|
||||
|
||||
w := os.Stderr
|
||||
logger := slog.New(
|
||||
tint.NewHandler(w, &tint.Options{
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Value.Kind() == slog.KindAny {
|
||||
if _, ok := a.Value.Any().(error); ok {
|
||||
return tint.Attr(9, a)
|
||||
}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
# Automatically Enable Colors
|
||||
|
||||
Colors are enabled by default. Use the Options.NoColor field to disable
|
||||
color output. To automatically enable colors based on terminal capabilities, use
|
||||
e.g., the [go-isatty] package:
|
||||
|
||||
w := os.Stderr
|
||||
logger := slog.New(
|
||||
tint.NewHandler(w, &tint.Options{
|
||||
NoColor: !isatty.IsTerminal(w.Fd()),
|
||||
}),
|
||||
)
|
||||
|
||||
# Windows Support
|
||||
|
||||
Color support on Windows can be added by using e.g., the [go-colorable] package:
|
||||
|
||||
w := os.Stderr
|
||||
logger := slog.New(
|
||||
tint.NewHandler(colorable.NewColorable(w), nil),
|
||||
)
|
||||
|
||||
[zerolog.ConsoleWriter]: https://pkg.go.dev/github.com/rs/zerolog#ConsoleWriter
|
||||
[go-isatty]: https://pkg.go.dev/github.com/mattn/go-isatty
|
||||
[go-colorable]: https://pkg.go.dev/github.com/mattn/go-colorable
|
||||
*/
|
||||
package tint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
// ANSI modes
|
||||
ansiEsc = '\u001b'
|
||||
ansiReset = "\u001b[0m"
|
||||
ansiFaint = "\u001b[2m"
|
||||
ansiResetFaint = "\u001b[22m"
|
||||
ansiBrightRed = "\u001b[91m"
|
||||
ansiBrightGreen = "\u001b[92m"
|
||||
ansiBrightYellow = "\u001b[93m"
|
||||
|
||||
errKey = "err"
|
||||
|
||||
defaultLevel = slog.LevelInfo
|
||||
defaultTimeFormat = time.StampMilli
|
||||
)
|
||||
|
||||
// Options for a slog.Handler that writes tinted logs. A zero Options consists
|
||||
// entirely of default values.
|
||||
//
|
||||
// Options can be used as a drop-in replacement for [slog.HandlerOptions].
|
||||
type Options struct {
|
||||
// Enable source code location (Default: false)
|
||||
AddSource bool
|
||||
|
||||
// Minimum level to log (Default: slog.LevelInfo)
|
||||
Level slog.Leveler
|
||||
|
||||
// ReplaceAttr is called to rewrite each non-group attribute before it is logged.
|
||||
// See https://pkg.go.dev/log/slog#HandlerOptions for details.
|
||||
ReplaceAttr func(groups []string, attr slog.Attr) slog.Attr
|
||||
|
||||
// Time format (Default: time.StampMilli)
|
||||
TimeFormat string
|
||||
|
||||
// Disable color (Default: false)
|
||||
NoColor bool
|
||||
}
|
||||
|
||||
func (o *Options) setDefaults() {
|
||||
if o.Level == nil {
|
||||
o.Level = defaultLevel
|
||||
}
|
||||
if o.TimeFormat == "" {
|
||||
o.TimeFormat = defaultTimeFormat
|
||||
}
|
||||
}
|
||||
|
||||
// NewHandler creates a [slog.Handler] that writes tinted logs to Writer w,
|
||||
// using the default options. If opts is nil, the default options are used.
|
||||
func NewHandler(w io.Writer, opts *Options) slog.Handler {
|
||||
if opts == nil {
|
||||
opts = &Options{}
|
||||
}
|
||||
opts.setDefaults()
|
||||
|
||||
return &handler{
|
||||
mu: &sync.Mutex{},
|
||||
w: w,
|
||||
opts: *opts,
|
||||
}
|
||||
}
|
||||
|
||||
// handler implements a [slog.Handler].
|
||||
type handler struct {
|
||||
attrsPrefix string
|
||||
groupPrefix string
|
||||
groups []string
|
||||
|
||||
mu *sync.Mutex
|
||||
w io.Writer
|
||||
|
||||
opts Options
|
||||
}
|
||||
|
||||
func (h *handler) clone() *handler {
|
||||
return &handler{
|
||||
attrsPrefix: h.attrsPrefix,
|
||||
groupPrefix: h.groupPrefix,
|
||||
groups: h.groups,
|
||||
mu: h.mu, // mutex shared among all clones of this handler
|
||||
w: h.w,
|
||||
opts: h.opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return level >= h.opts.Level.Level()
|
||||
}
|
||||
|
||||
func (h *handler) Handle(_ context.Context, r slog.Record) error {
|
||||
// get a buffer from the sync pool
|
||||
buf := newBuffer()
|
||||
defer buf.Free()
|
||||
|
||||
rep := h.opts.ReplaceAttr
|
||||
|
||||
// write time
|
||||
if !r.Time.IsZero() {
|
||||
val := r.Time.Round(0) // strip monotonic to match Attr behavior
|
||||
if rep == nil {
|
||||
h.appendTintTime(buf, r.Time, -1)
|
||||
buf.WriteByte(' ')
|
||||
} else if a := rep(nil /* groups */, slog.Time(slog.TimeKey, val)); a.Key != "" {
|
||||
val, color := h.resolve(a.Value)
|
||||
if val.Kind() == slog.KindTime {
|
||||
h.appendTintTime(buf, val.Time(), color)
|
||||
} else {
|
||||
h.appendTintValue(buf, val, false, color, true)
|
||||
}
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
|
||||
// write level
|
||||
if rep == nil {
|
||||
h.appendTintLevel(buf, r.Level, -1)
|
||||
buf.WriteByte(' ')
|
||||
} else if a := rep(nil /* groups */, slog.Any(slog.LevelKey, r.Level)); a.Key != "" {
|
||||
val, color := h.resolve(a.Value)
|
||||
if val.Kind() == slog.KindAny {
|
||||
if lvlVal, ok := val.Any().(slog.Level); ok {
|
||||
h.appendTintLevel(buf, lvlVal, color)
|
||||
} else {
|
||||
h.appendTintValue(buf, val, false, color, false)
|
||||
}
|
||||
} else {
|
||||
h.appendTintValue(buf, val, false, color, false)
|
||||
}
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
// write source
|
||||
if h.opts.AddSource {
|
||||
fs := runtime.CallersFrames([]uintptr{r.PC})
|
||||
f, _ := fs.Next()
|
||||
if f.File != "" {
|
||||
src := &slog.Source{
|
||||
Function: f.Function,
|
||||
File: f.File,
|
||||
Line: f.Line,
|
||||
}
|
||||
|
||||
if rep == nil {
|
||||
if h.opts.NoColor {
|
||||
appendSource(buf, src)
|
||||
} else {
|
||||
buf.WriteString(ansiFaint)
|
||||
appendSource(buf, src)
|
||||
buf.WriteString(ansiReset)
|
||||
}
|
||||
buf.WriteByte(' ')
|
||||
} else if a := rep(nil /* groups */, slog.Any(slog.SourceKey, src)); a.Key != "" {
|
||||
val, color := h.resolve(a.Value)
|
||||
h.appendTintValue(buf, val, false, color, true)
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write message
|
||||
if rep == nil {
|
||||
buf.WriteString(r.Message)
|
||||
buf.WriteByte(' ')
|
||||
} else if a := rep(nil /* groups */, slog.String(slog.MessageKey, r.Message)); a.Key != "" {
|
||||
val, color := h.resolve(a.Value)
|
||||
h.appendTintValue(buf, val, false, color, false)
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
// write handler attributes
|
||||
if len(h.attrsPrefix) > 0 {
|
||||
buf.WriteString(h.attrsPrefix)
|
||||
}
|
||||
|
||||
// write attributes
|
||||
r.Attrs(func(attr slog.Attr) bool {
|
||||
h.appendAttr(buf, attr, h.groupPrefix, h.groups)
|
||||
return true
|
||||
})
|
||||
|
||||
if len(*buf) == 0 {
|
||||
buf.WriteByte('\n')
|
||||
} else {
|
||||
(*buf)[len(*buf)-1] = '\n' // replace last space with newline
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
_, err := h.w.Write(*buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
if len(attrs) == 0 {
|
||||
return h
|
||||
}
|
||||
h2 := h.clone()
|
||||
|
||||
buf := newBuffer()
|
||||
defer buf.Free()
|
||||
|
||||
// write attributes to buffer
|
||||
for _, attr := range attrs {
|
||||
h.appendAttr(buf, attr, h.groupPrefix, h.groups)
|
||||
}
|
||||
h2.attrsPrefix = h.attrsPrefix + string(*buf)
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *handler) WithGroup(name string) slog.Handler {
|
||||
if name == "" {
|
||||
return h
|
||||
}
|
||||
h2 := h.clone()
|
||||
h2.groupPrefix += name + "."
|
||||
h2.groups = append(h2.groups, name)
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *handler) appendTintTime(buf *buffer, t time.Time, color int16) {
|
||||
if h.opts.NoColor {
|
||||
*buf = t.AppendFormat(*buf, h.opts.TimeFormat)
|
||||
} else {
|
||||
if color >= 0 {
|
||||
appendAnsi(buf, uint8(color), true)
|
||||
} else {
|
||||
buf.WriteString(ansiFaint)
|
||||
}
|
||||
*buf = t.AppendFormat(*buf, h.opts.TimeFormat)
|
||||
buf.WriteString(ansiReset)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) appendTintLevel(buf *buffer, level slog.Level, color int16) {
|
||||
str := func(base string, val slog.Level) []byte {
|
||||
if val == 0 {
|
||||
return []byte(base)
|
||||
} else if val > 0 {
|
||||
return strconv.AppendInt(append([]byte(base), '+'), int64(val), 10)
|
||||
}
|
||||
return strconv.AppendInt([]byte(base), int64(val), 10)
|
||||
}
|
||||
|
||||
if !h.opts.NoColor {
|
||||
if color >= 0 {
|
||||
appendAnsi(buf, uint8(color), false)
|
||||
} else {
|
||||
switch {
|
||||
case level < slog.LevelInfo:
|
||||
case level < slog.LevelWarn:
|
||||
buf.WriteString(ansiBrightGreen)
|
||||
case level < slog.LevelError:
|
||||
buf.WriteString(ansiBrightYellow)
|
||||
default:
|
||||
buf.WriteString(ansiBrightRed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case level < slog.LevelInfo:
|
||||
buf.Write(str("DBG", level-slog.LevelDebug))
|
||||
case level < slog.LevelWarn:
|
||||
buf.Write(str("INF", level-slog.LevelInfo))
|
||||
case level < slog.LevelError:
|
||||
buf.Write(str("WRN", level-slog.LevelWarn))
|
||||
default:
|
||||
buf.Write(str("ERR", level-slog.LevelError))
|
||||
}
|
||||
|
||||
if !h.opts.NoColor && level >= slog.LevelInfo {
|
||||
buf.WriteString(ansiReset)
|
||||
}
|
||||
}
|
||||
|
||||
func appendSource(buf *buffer, src *slog.Source) {
|
||||
dir, file := filepath.Split(src.File)
|
||||
|
||||
buf.WriteString(filepath.Join(filepath.Base(dir), file))
|
||||
buf.WriteByte(':')
|
||||
*buf = strconv.AppendInt(*buf, int64(src.Line), 10)
|
||||
}
|
||||
|
||||
func (h *handler) resolve(val slog.Value) (resolvedVal slog.Value, color int16) {
|
||||
if !h.opts.NoColor && val.Kind() == slog.KindLogValuer {
|
||||
if tintVal, ok := val.Any().(tintValue); ok {
|
||||
return tintVal.Value.Resolve(), int16(tintVal.Color)
|
||||
}
|
||||
}
|
||||
return val.Resolve(), -1
|
||||
}
|
||||
|
||||
func (h *handler) appendAttr(buf *buffer, attr slog.Attr, groupsPrefix string, groups []string) {
|
||||
var color int16 // -1 if no color
|
||||
attr.Value, color = h.resolve(attr.Value)
|
||||
if rep := h.opts.ReplaceAttr; rep != nil && attr.Value.Kind() != slog.KindGroup {
|
||||
attr = rep(groups, attr)
|
||||
var colorRep int16
|
||||
attr.Value, colorRep = h.resolve(attr.Value)
|
||||
if colorRep >= 0 {
|
||||
color = colorRep
|
||||
}
|
||||
}
|
||||
|
||||
if attr.Equal(slog.Attr{}) {
|
||||
return
|
||||
}
|
||||
|
||||
if attr.Value.Kind() == slog.KindGroup {
|
||||
if attr.Key != "" {
|
||||
groupsPrefix += attr.Key + "."
|
||||
groups = append(groups, attr.Key)
|
||||
}
|
||||
for _, groupAttr := range attr.Value.Group() {
|
||||
h.appendAttr(buf, groupAttr, groupsPrefix, groups)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if h.opts.NoColor {
|
||||
h.appendKey(buf, attr.Key, groupsPrefix)
|
||||
h.appendValue(buf, attr.Value, true)
|
||||
} else {
|
||||
if color >= 0 {
|
||||
appendAnsi(buf, uint8(color), true)
|
||||
h.appendKey(buf, attr.Key, groupsPrefix)
|
||||
buf.WriteString(ansiResetFaint)
|
||||
h.appendValue(buf, attr.Value, true)
|
||||
buf.WriteString(ansiReset)
|
||||
} else {
|
||||
buf.WriteString(ansiFaint)
|
||||
h.appendKey(buf, attr.Key, groupsPrefix)
|
||||
buf.WriteString(ansiReset)
|
||||
h.appendValue(buf, attr.Value, true)
|
||||
}
|
||||
}
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
func (h *handler) appendKey(buf *buffer, key, groups string) {
|
||||
appendString(buf, groups+key, true, !h.opts.NoColor)
|
||||
buf.WriteByte('=')
|
||||
}
|
||||
|
||||
func (h *handler) appendValue(buf *buffer, v slog.Value, quote bool) {
|
||||
switch v.Kind() {
|
||||
case slog.KindString:
|
||||
appendString(buf, v.String(), quote, !h.opts.NoColor)
|
||||
case slog.KindInt64:
|
||||
*buf = strconv.AppendInt(*buf, v.Int64(), 10)
|
||||
case slog.KindUint64:
|
||||
*buf = strconv.AppendUint(*buf, v.Uint64(), 10)
|
||||
case slog.KindFloat64:
|
||||
*buf = strconv.AppendFloat(*buf, v.Float64(), 'g', -1, 64)
|
||||
case slog.KindBool:
|
||||
*buf = strconv.AppendBool(*buf, v.Bool())
|
||||
case slog.KindDuration:
|
||||
appendString(buf, v.Duration().String(), quote, !h.opts.NoColor)
|
||||
case slog.KindTime:
|
||||
*buf = appendRFC3339Millis(*buf, v.Time())
|
||||
case slog.KindAny:
|
||||
defer func() {
|
||||
// Copied from log/slog/handler.go.
|
||||
if r := recover(); r != nil {
|
||||
// If it panics with a nil pointer, the most likely cases are
|
||||
// an encoding.TextMarshaler or error fails to guard against nil,
|
||||
// in which case "<nil>" seems to be the feasible choice.
|
||||
//
|
||||
// Adapted from the code in fmt/print.go.
|
||||
if v := reflect.ValueOf(v.Any()); v.Kind() == reflect.Pointer && v.IsNil() {
|
||||
buf.WriteString("<nil>")
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise just print the original panic message.
|
||||
appendString(buf, fmt.Sprintf("!PANIC: %v", r), true, !h.opts.NoColor)
|
||||
}
|
||||
}()
|
||||
|
||||
switch cv := v.Any().(type) {
|
||||
case encoding.TextMarshaler:
|
||||
data, err := cv.MarshalText()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
appendString(buf, string(data), quote, !h.opts.NoColor)
|
||||
case *slog.Source:
|
||||
appendSource(buf, cv)
|
||||
default:
|
||||
appendString(buf, fmt.Sprintf("%+v", cv), quote, !h.opts.NoColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) appendTintValue(buf *buffer, val slog.Value, quote bool, color int16, faint bool) {
|
||||
if h.opts.NoColor {
|
||||
h.appendValue(buf, val, quote)
|
||||
} else {
|
||||
if color >= 0 {
|
||||
appendAnsi(buf, uint8(color), faint)
|
||||
} else if faint {
|
||||
buf.WriteString(ansiFaint)
|
||||
}
|
||||
h.appendValue(buf, val, quote)
|
||||
if color >= 0 || faint {
|
||||
buf.WriteString(ansiReset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from log/slog/handler.go.
|
||||
func appendRFC3339Millis(b []byte, t time.Time) []byte {
|
||||
// Format according to time.RFC3339Nano since it is highly optimized,
|
||||
// but truncate it to use millisecond resolution.
|
||||
// Unfortunately, that format trims trailing 0s, so add 1/10 millisecond
|
||||
// to guarantee that there are exactly 4 digits after the period.
|
||||
const prefixLen = len("2006-01-02T15:04:05.000")
|
||||
n := len(b)
|
||||
t = t.Truncate(time.Millisecond).Add(time.Millisecond / 10)
|
||||
b = t.AppendFormat(b, time.RFC3339Nano)
|
||||
b = append(b[:n+prefixLen], b[n+prefixLen+1:]...) // drop the 4th digit
|
||||
return b
|
||||
}
|
||||
|
||||
func appendAnsi(buf *buffer, color uint8, faint bool) {
|
||||
buf.WriteString("\u001b[")
|
||||
if faint {
|
||||
buf.WriteString("2;")
|
||||
}
|
||||
if color < 8 {
|
||||
*buf = strconv.AppendUint(*buf, uint64(color)+30, 10)
|
||||
} else if color < 16 {
|
||||
*buf = strconv.AppendUint(*buf, uint64(color)+82, 10)
|
||||
} else {
|
||||
buf.WriteString("38;5;")
|
||||
*buf = strconv.AppendUint(*buf, uint64(color), 10)
|
||||
}
|
||||
buf.WriteByte('m')
|
||||
}
|
||||
|
||||
func appendString(buf *buffer, s string, quote, color bool) {
|
||||
if quote && !color {
|
||||
// trim ANSI escape sequences
|
||||
var inEscape bool
|
||||
s = cut(s, func(r rune) bool {
|
||||
if r == ansiEsc {
|
||||
inEscape = true
|
||||
} else if inEscape && unicode.IsLetter(r) {
|
||||
inEscape = false
|
||||
return true
|
||||
}
|
||||
|
||||
return inEscape
|
||||
})
|
||||
}
|
||||
|
||||
quote = quote && needsQuoting(s)
|
||||
switch {
|
||||
case color && quote:
|
||||
s = strconv.Quote(s)
|
||||
s = strings.ReplaceAll(s, `\x1b`, string(ansiEsc))
|
||||
buf.WriteString(s)
|
||||
case !color && quote:
|
||||
*buf = strconv.AppendQuote(*buf, s)
|
||||
default:
|
||||
buf.WriteString(s)
|
||||
}
|
||||
}
|
||||
|
||||
func cut(s string, f func(r rune) bool) string {
|
||||
var res []rune
|
||||
for i := 0; i < len(s); {
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
if r == utf8.RuneError {
|
||||
break
|
||||
}
|
||||
if !f(r) {
|
||||
res = append(res, r)
|
||||
}
|
||||
i += size
|
||||
}
|
||||
return string(res)
|
||||
}
|
||||
|
||||
// Copied from log/slog/text_handler.go.
|
||||
func needsQuoting(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return true
|
||||
}
|
||||
for i := 0; i < len(s); {
|
||||
b := s[i]
|
||||
if b < utf8.RuneSelf {
|
||||
// Quote anything except a backslash that would need quoting in a
|
||||
// JSON string, as well as space and '='
|
||||
if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) {
|
||||
return true
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) {
|
||||
return true
|
||||
}
|
||||
i += size
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Copied from log/slog/json_handler.go.
|
||||
//
|
||||
// safeSet is extended by the ANSI escape code "\u001b".
|
||||
var safeSet = [utf8.RuneSelf]bool{
|
||||
' ': true,
|
||||
'!': true,
|
||||
'"': false,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': true,
|
||||
'\'': true,
|
||||
'(': true,
|
||||
')': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
',': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'/': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
':': true,
|
||||
';': true,
|
||||
'<': true,
|
||||
'=': true,
|
||||
'>': true,
|
||||
'?': true,
|
||||
'@': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'V': true,
|
||||
'W': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'[': true,
|
||||
'\\': false,
|
||||
']': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'{': true,
|
||||
'|': true,
|
||||
'}': true,
|
||||
'~': true,
|
||||
'\u007f': true,
|
||||
'\u001b': true,
|
||||
}
|
||||
|
||||
type tintValue struct {
|
||||
slog.Value
|
||||
Color uint8
|
||||
}
|
||||
|
||||
// LogValue implements the [slog.LogValuer] interface.
|
||||
func (v tintValue) LogValue() slog.Value {
|
||||
return v.Value
|
||||
}
|
||||
|
||||
// Err returns a tinted (colorized) [slog.Attr] that will be written in red color
|
||||
// by the [tint.Handler]. When used with any other [slog.Handler], it behaves as
|
||||
//
|
||||
// slog.Any("err", err)
|
||||
func Err(err error) slog.Attr {
|
||||
return Attr(9, slog.Any(errKey, err))
|
||||
}
|
||||
|
||||
// Attr returns a tinted (colorized) [slog.Attr] that will be written in the
|
||||
// specified color by the [tint.Handler]. When used with any other [slog.Handler], it behaves as a
|
||||
// plain [slog.Attr].
|
||||
//
|
||||
// Use the uint8 color value to specify the color of the attribute:
|
||||
//
|
||||
// - 0-7: standard ANSI colors
|
||||
// - 8-15: high intensity ANSI colors
|
||||
// - 16-231: 216 colors (6×6×6 cube)
|
||||
// - 232-255: grayscale from dark to light in 24 steps
|
||||
//
|
||||
// See https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
|
||||
func Attr(color uint8, attr slog.Attr) slog.Attr {
|
||||
attr.Value = slog.AnyValue(tintValue{attr.Value, color})
|
||||
return attr
|
||||
}
|
||||
Reference in New Issue
Block a user