mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
99 lines
2.5 KiB
Go
99 lines
2.5 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
package debug
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"sync"
|
|
|
|
"quad4/reticulum-go/pkg/term"
|
|
)
|
|
|
|
// colorState is shared across WithAttrs/WithGroup clones.
|
|
type colorState struct {
|
|
mu sync.Mutex
|
|
buf bytes.Buffer
|
|
w io.Writer
|
|
file *os.File
|
|
}
|
|
|
|
// colorHandler wraps a text handler and injects ANSI around the level token.
|
|
// slog.TextHandler quotes strings that contain ESC, so ReplaceAttr cannot be used.
|
|
type colorHandler struct {
|
|
inner slog.Handler
|
|
state *colorState
|
|
}
|
|
|
|
func newColorHandler(w io.Writer, opts *slog.HandlerOptions) *colorHandler {
|
|
st := &colorState{w: w, file: os.Stderr}
|
|
return &colorHandler{
|
|
inner: slog.NewTextHandler(&st.buf, opts),
|
|
state: st,
|
|
}
|
|
}
|
|
|
|
func (h *colorHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
|
return h.inner.Enabled(ctx, level)
|
|
}
|
|
|
|
func (h *colorHandler) Handle(ctx context.Context, r slog.Record) error {
|
|
h.state.mu.Lock()
|
|
defer h.state.mu.Unlock()
|
|
h.state.buf.Reset()
|
|
if err := h.inner.Handle(ctx, r); err != nil {
|
|
return err
|
|
}
|
|
line := colorizeLevelBytes(h.state.buf.Bytes(), r.Level, h.state.file)
|
|
_, err := h.state.w.Write(line)
|
|
return err
|
|
}
|
|
|
|
func (h *colorHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
return &colorHandler{inner: h.inner.WithAttrs(attrs), state: h.state}
|
|
}
|
|
|
|
func (h *colorHandler) WithGroup(name string) slog.Handler {
|
|
return &colorHandler{inner: h.inner.WithGroup(name), state: h.state}
|
|
}
|
|
|
|
func colorizeLevelBytes(line []byte, level slog.Level, f *os.File) []byte {
|
|
plain := level.String()
|
|
token := append([]byte("level="), plain...)
|
|
idx := bytes.Index(line, token)
|
|
if idx < 0 {
|
|
return line
|
|
}
|
|
colored := colorLevelLabel(f, level, plain)
|
|
out := make([]byte, 0, len(line)+len(colored)-len(plain))
|
|
out = append(out, line[:idx+len("level=")]...)
|
|
out = append(out, colored...)
|
|
out = append(out, line[idx+len(token):]...)
|
|
return out
|
|
}
|
|
|
|
func colorLevelLabel(f *os.File, level slog.Level, label string) string {
|
|
switch {
|
|
case level >= slog.LevelError:
|
|
return term.Red(f, label)
|
|
case level >= slog.LevelWarn:
|
|
return term.Yellow(f, label)
|
|
case level >= slog.LevelInfo:
|
|
return term.Cyan(f, label)
|
|
default:
|
|
return term.Bold(f, label)
|
|
}
|
|
}
|
|
|
|
// useColorLogs reports whether text logs should use ANSI on stderr only.
|
|
// Disabled for JSON, file-only, or dual stderr+file writers so log files stay plain.
|
|
func useColorLogs() bool {
|
|
if jsonFormat || omitStderr || extraWriter != nil {
|
|
return false
|
|
}
|
|
return term.ColorEnabled(os.Stderr)
|
|
}
|