// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini package main import ( "context" "crypto/sha256" "encoding/hex" "errors" "io" "log/slog" ) // LevelTrace is the custom level below DEBUG const LevelTrace = slog.Level(-8) // Names of the supported levels, as printed and as accepted by ParseLogLevel var levelNames = map[slog.Level]string{ slog.LevelError: "ERROR", slog.LevelWarn: "WARN", slog.LevelInfo: "INFO", slog.LevelDebug: "DEBUG", LevelTrace: "TRACE", } // ParseLogLevel maps a level name (ASCII case-insensitive, exact match) to its slog level func ParseLogLevel(s string) (slog.Level, error) { for level, name := range levelNames { if asciiEqualFold(s, name) { return level, nil } } return 0, errors.New("unknown log level") } // LevelName renders a level using the five supported names, falling back to the slog default func LevelName(level slog.Level) string { if name, ok := levelNames[level]; ok { return name } return level.String() } // NewLogger builds the text logger writing to w with the given minimum level func NewLogger(w io.Writer, level slog.Level) *slog.Logger { return slog.New(slog.NewTextHandler(w, &slog.HandlerOptions{ Level: level, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { if len(groups) != 0 { return a } switch a.Key { case slog.LevelKey: // Render the level with the custom names (TRACE instead of DEBUG-4) if level, ok := a.Value.Any().(slog.Level); ok { a.Value = slog.StringValue(LevelName(level)) } case slog.TimeKey: // Always log timestamps in UTC if a.Value.Kind() == slog.KindTime { a.Value = slog.TimeValue(a.Value.Time().UTC()) } } return a }, })) } // trace emits a TRACE record func trace(ctx context.Context, log *slog.Logger, msg string, args ...any) { log.Log(ctx, LevelTrace, msg, args...) } // traceFunc emits a TRACE function-entry record and returns the matching function-exit emitter // Usage: defer traceFunc(ctx, log, "Name")() func traceFunc(ctx context.Context, log *slog.Logger, fn string) func() { trace(ctx, log, "function entry", "func", fn) return func() { trace(ctx, log, "function exit", "func", fn) } } // tokenID returns a stable, non-reversible identifier for a token, safe to log // It is the first 8 hex characters of the SHA-256 digest of the token func tokenID(token string) string { sum := sha256.Sum256([]byte(token)) return hex.EncodeToString(sum[:4]) } // asciiEqualFold compares two strings case-insensitively over ASCII letters only // Non-ASCII bytes must match exactly, so Unicode case folding never applies func asciiEqualFold(a, b string) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if asciiLower(a[i]) != asciiLower(b[i]) { return false } } return true } // asciiLower lowercases a single ASCII letter func asciiLower(c byte) byte { if c >= 'A' && c <= 'Z' { return c + ('a' - 'A') } return c }