Short and single-letter names in application code become full words (tests keep theirs). Wrapped calls, signatures and composite literals put one item per line, and one-line function bodies are expanded. Exported names and groups of constants get doc comments, and comments refer to the renamed identifiers.
153 lines
4.1 KiB
Go
153 lines
4.1 KiB
Go
package logging
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/lmittmann/tint"
|
|
)
|
|
|
|
// ---------- Standard log handling (terminal or file based) ----------
|
|
|
|
// Logger state shared by the terminal and file loggers.
|
|
var (
|
|
// logLevel is the minimum level logged, read by whichever handler is
|
|
// installed, so changing it takes effect immediately.
|
|
logLevel slog.LevelVar
|
|
// fileLogger is the log file opened by LogToFile, or nil when logging
|
|
// to the terminal.
|
|
fileLogger *os.File
|
|
// terminal is where LogToTerminal and PrintToTerminal write.
|
|
terminal = os.Stdout
|
|
// isTerminal reports whether file is an interactive terminal rather than
|
|
// a file or pipe. Tests replace it.
|
|
isTerminal = func(file *os.File) bool {
|
|
info, err := file.Stat()
|
|
return err == nil && info.Mode()&os.ModeCharDevice != 0
|
|
}
|
|
)
|
|
|
|
// SetLevel sets the logging level without logging the change.
|
|
func SetLevel(level slog.Level) {
|
|
logLevel.Set(level)
|
|
}
|
|
|
|
// EnableDebug lowers the logging level to debug, if it isn't already, and
|
|
// logs that it did.
|
|
func EnableDebug() {
|
|
if logLevel.Level() > slog.LevelDebug {
|
|
logLevel.Set(slog.LevelDebug)
|
|
slog.Debug("debug logging enabled")
|
|
}
|
|
}
|
|
|
|
// LogToFile sends all logging to logfile as JSON, replacing the default slog
|
|
// logger. The file is created if needed and appended to; any log file opened
|
|
// by an earlier call is closed.
|
|
func LogToFile(logfile string) error {
|
|
closeErr := Shutdown()
|
|
|
|
// Open the log file
|
|
var err error
|
|
fileLogger, err = os.OpenFile(
|
|
filepath.Clean(logfile), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600,
|
|
)
|
|
if err != nil {
|
|
return errors.Join(err, closeErr)
|
|
}
|
|
|
|
opts := &slog.HandlerOptions{
|
|
Level: &logLevel,
|
|
}
|
|
handler := slog.NewJSONHandler(fileLogger, opts)
|
|
logger := slog.New(handler)
|
|
slog.SetDefault(logger)
|
|
|
|
return closeErr
|
|
}
|
|
|
|
// Shutdown closes the log file opened by LogToFile, if any.
|
|
func Shutdown() error {
|
|
var err error = nil
|
|
if fileLogger != nil {
|
|
err = (*fileLogger).Close()
|
|
fileLogger = nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// LogToTerminal sends all logging to stdout as coloured text, replacing the
|
|
// default slog logger. When stdout is an interactive terminal, loggers
|
|
// derived from it report so to WritesToTerminal.
|
|
func LogToTerminal() {
|
|
opts := &tint.Options{
|
|
Level: &logLevel,
|
|
TimeFormat: "*",
|
|
}
|
|
handler := tint.NewTextHandler(terminal, opts)
|
|
if isTerminal(terminal) {
|
|
handler = terminalHandler{
|
|
Handler: handler,
|
|
}
|
|
}
|
|
|
|
slog.SetDefault(slog.New(handler))
|
|
}
|
|
|
|
// WritesToTerminal reports whether handler writes to an interactive
|
|
// terminal, where multi-line text (such as a trace) reads best printed as
|
|
// it is with PrintToTerminal, rather than escaped into a field.
|
|
func WritesToTerminal(handler slog.Handler) bool {
|
|
_, ok := handler.(terminalHandler)
|
|
return ok
|
|
}
|
|
|
|
// PrintToTerminal writes text to the terminal as it is, on lines of its own.
|
|
// It is for detail that a handler would escape into one line; check
|
|
// WritesToTerminal first.
|
|
func PrintToTerminal(text string) {
|
|
fmt.Fprintln(terminal, text) //nolint:errcheck // nothing to do if the terminal has gone
|
|
}
|
|
|
|
// ---------- terminalHandler ----------
|
|
|
|
// terminalHandler marks the handler LogToTerminal installs on an interactive
|
|
// terminal. WithAttrs and WithGroup keep the mark, so a logger derived with
|
|
// With still reports to WritesToTerminal.
|
|
type terminalHandler struct {
|
|
slog.Handler
|
|
}
|
|
|
|
// WithAttrs implements slog.Handler, keeping the terminal mark.
|
|
func (h terminalHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
return terminalHandler{
|
|
Handler: h.Handler.WithAttrs(attrs),
|
|
}
|
|
}
|
|
|
|
// WithGroup implements slog.Handler, keeping the terminal mark.
|
|
func (h terminalHandler) WithGroup(name string) slog.Handler {
|
|
return terminalHandler{
|
|
Handler: h.Handler.WithGroup(name),
|
|
}
|
|
}
|
|
|
|
// IsJSONHandler reports whether handler writes JSON, as the file logger does.
|
|
func IsJSONHandler(handler slog.Handler) bool {
|
|
_, ok := handler.(*slog.JSONHandler)
|
|
return ok
|
|
}
|
|
|
|
// IsFileLogger reports whether logging currently goes to a file.
|
|
func IsFileLogger() bool {
|
|
return fileLogger != nil
|
|
}
|
|
|
|
// IsDebugEnabled reports whether debug messages are logged.
|
|
func IsDebugEnabled() bool {
|
|
return logLevel.Level() <= slog.LevelDebug
|
|
}
|