Fix every finding surfaced by the canonical .golangci.yml with golangci-lint v2.12.2 (refs #61), behavior-preserving throughout: - err113: dynamic errors replaced with package-level sentinels and %w wrapping; direct comparisons converted to errors.Is - goprintffuncname: printf-style helpers renamed with an f suffix (ui.Writer message methods, cli.ReportErrorf, database.Fatalf, vaultik stdoutf) and all call sites updated - revive: stuttering type names renamed (blob.Handler, blob.WithReader, blob.ChunkPosition, storage.URL, storage.Info), doc comments added, unused parameters blanked, package comments added - contextcheck/noctx: ctx threaded through blob.Packer (AddChunk/Flush/FinalizeBlob/PackChunks) and scanner call sites; context-aware exec and sql variants used - funlen/cyclop/gocognit/nestif/dupl: oversized or duplicated functions split into focused helpers across production and test code - paralleltest/tparallel/thelper/usetesting/testpackage: tests parallelized where safe (global log.Initialize kept in the serial phase), helpers marked, t.TempDir adopted, external test packages where only exported API is used - gosec: integer conversions clamped or justified, header timeouts added, remaining findings suppressed with per-site justifications - mnd/goconst/lll/wsl_v5/nlreturn/noinlineerr/errcheck and other mechanical findings fixed directly Remove the deprecated log.LogOptions alias (callers migrated to log.Options). make check is green.
155 lines
3.5 KiB
Go
155 lines
3.5 KiB
Go
package log
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ANSI color codes
|
|
const (
|
|
colorReset = "\033[0m"
|
|
colorRed = "\033[31m"
|
|
colorYellow = "\033[33m"
|
|
colorBlue = "\033[34m"
|
|
colorGray = "\033[90m"
|
|
colorGreen = "\033[32m"
|
|
colorCyan = "\033[36m"
|
|
colorBold = "\033[1m"
|
|
)
|
|
|
|
// TTYHandler is a custom slog handler for TTY output with colors.
|
|
type TTYHandler struct {
|
|
opts slog.HandlerOptions
|
|
mu sync.Mutex
|
|
out io.Writer
|
|
}
|
|
|
|
// NewTTYHandler creates a new TTY handler with colored output.
|
|
func NewTTYHandler(out io.Writer, opts *slog.HandlerOptions) *TTYHandler {
|
|
if opts == nil {
|
|
opts = &slog.HandlerOptions{}
|
|
}
|
|
|
|
return &TTYHandler{
|
|
out: out,
|
|
opts: *opts,
|
|
}
|
|
}
|
|
|
|
// Enabled reports whether the handler handles records at the given level.
|
|
func (h *TTYHandler) Enabled(_ context.Context, level slog.Level) bool {
|
|
return level >= h.opts.Level.Level()
|
|
}
|
|
|
|
// Handle writes the log record to the output with color formatting.
|
|
func (h *TTYHandler) Handle(_ context.Context, r slog.Record) error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Format timestamp
|
|
timestamp := r.Time.Format("15:04:05")
|
|
|
|
// Level and color
|
|
level := r.Level.String()
|
|
|
|
var levelColor string
|
|
|
|
switch r.Level {
|
|
case slog.LevelDebug:
|
|
levelColor = colorGray
|
|
level = "DEBUG"
|
|
case slog.LevelInfo:
|
|
levelColor = colorGreen
|
|
level = "INFO "
|
|
case slog.LevelWarn:
|
|
levelColor = colorYellow
|
|
level = "WARN "
|
|
case slog.LevelError:
|
|
levelColor = colorRed
|
|
level = "ERROR"
|
|
default:
|
|
levelColor = colorReset
|
|
}
|
|
|
|
// Print main message
|
|
_, _ = fmt.Fprintf(h.out, "%s%s%s %s%s%s %s%s%s",
|
|
colorGray, timestamp, colorReset,
|
|
levelColor, level, colorReset,
|
|
colorBold, r.Message, colorReset)
|
|
|
|
// Print attributes
|
|
r.Attrs(func(a slog.Attr) bool {
|
|
value := a.Value.String()
|
|
// Special handling for certain attribute types
|
|
switch a.Value.Kind() {
|
|
case slog.KindDuration:
|
|
if d, ok := a.Value.Any().(time.Duration); ok {
|
|
value = formatDuration(d)
|
|
}
|
|
case slog.KindInt64:
|
|
if a.Key == "bytes" {
|
|
value = formatBytes(a.Value.Int64())
|
|
}
|
|
case slog.KindAny, slog.KindBool, slog.KindFloat64, slog.KindString,
|
|
slog.KindTime, slog.KindUint64, slog.KindGroup, slog.KindLogValuer:
|
|
// Plain string form above is already correct for these kinds.
|
|
default:
|
|
// Future kinds also use the plain string form.
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(h.out, " %s%s%s=%s%s%s",
|
|
colorCyan, a.Key, colorReset,
|
|
colorBlue, value, colorReset)
|
|
|
|
return true
|
|
})
|
|
|
|
_, _ = fmt.Fprintln(h.out)
|
|
|
|
return nil
|
|
}
|
|
|
|
// WithAttrs returns a new handler with the given attributes.
|
|
func (h *TTYHandler) WithAttrs(_ []slog.Attr) slog.Handler {
|
|
return h // Simplified for now
|
|
}
|
|
|
|
// WithGroup returns a new handler with the given group name.
|
|
func (h *TTYHandler) WithGroup(_ string) slog.Handler {
|
|
return h // Simplified for now
|
|
}
|
|
|
|
// formatDuration formats a duration in a human-readable way
|
|
func formatDuration(d time.Duration) string {
|
|
switch {
|
|
case d < time.Millisecond:
|
|
return fmt.Sprintf("%dµs", d.Microseconds())
|
|
case d < time.Second:
|
|
return fmt.Sprintf("%dms", d.Milliseconds())
|
|
case d < time.Minute:
|
|
return fmt.Sprintf("%.1fs", d.Seconds())
|
|
default:
|
|
return d.String()
|
|
}
|
|
}
|
|
|
|
// formatBytes formats bytes in a human-readable way
|
|
func formatBytes(b int64) string {
|
|
const unit = 1024
|
|
if b < unit {
|
|
return fmt.Sprintf("%d B", b)
|
|
}
|
|
|
|
div, exp := int64(unit), 0
|
|
for n := b / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
|
|
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
}
|