Skip to content

Instantly share code, notes, and snippets.

@kriansa
Created June 16, 2025 13:06
Show Gist options
  • Save kriansa/5db352099abebe119b91f380fb0ce617 to your computer and use it in GitHub Desktop.
Save kriansa/5db352099abebe119b91f380fb0ce617 to your computer and use it in GitHub Desktop.
Simple slog wrapper with good DevX
package log
import (
"context"
"log/slog"
"os"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
"github.com/mattn/go-isatty"
)
var logger *slog.Logger
type LogFormat string
const (
LogFormatHuman LogFormat = "human"
LogFormatLogfmt LogFormat = "logfmt"
LogFormatAuto LogFormat = "auto"
)
func Setup(verbose bool, format LogFormat) {
var handler slog.Handler
if format == LogFormatHuman {
handler = getHumanHandler(verbose)
} else if format == LogFormatLogfmt {
handler = getLogfmtHandler(verbose)
} else {
if format != LogFormatAuto {
Warn("Invalid log format specified, using auto format", "format", format)
}
if isatty.IsTerminal(os.Stdout.Fd()) {
handler = getHumanHandler(verbose)
} else {
handler = getLogfmtHandler(verbose)
}
}
logger = slog.New(handler)
}
func getLogfmtHandler(verbose bool) slog.Handler {
var logLevel slog.Level
if verbose {
logLevel = slog.LevelDebug
} else {
logLevel = slog.LevelInfo
}
return slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})
}
func getHumanHandler(verbose bool) slog.Handler {
styles := log.DefaultStyles()
// Set the logfmt tags to be displayed colored, highlighting their output
styles.Key = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("25"))
styles.Value = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("29"))
// Style the different log levels
styles.Levels[log.ErrorLevel] = lipgloss.NewStyle().
SetString("Error:").Bold(true).Foreground(lipgloss.Color("1"))
styles.Levels[log.WarnLevel] = lipgloss.NewStyle().
SetString("Warning:").Bold(true).Foreground(lipgloss.Color("3"))
styles.Levels[log.DebugLevel] = lipgloss.NewStyle().SetString("+").Foreground(lipgloss.Color("8"))
// Because most log entries are at Info level, we don't want to display a prefix
delete(styles.Levels, log.InfoLevel)
var logLevel log.Level
if verbose {
logLevel = log.DebugLevel
} else {
logLevel = log.InfoLevel
}
handler := log.NewWithOptions(os.Stdout, log.Options{
Level: logLevel,
ReportTimestamp: true,
TimeFormat: time.TimeOnly,
})
handler.SetStyles(styles)
return handler
}
// Wraps the slog.Logger methods to provide a more convenient API.
// Log emits a log record with the current time and the given level and message.
// The Record's Attrs consist of the Logger's attributes followed by
// the Attrs specified by args.
//
// The attribute arguments are processed as follows:
// - If an argument is an Attr, it is used as is.
// - If an argument is a string and this is not the last argument,
// the following argument is treated as the value and the two are combined
// into an Attr.
// - Otherwise, the argument is treated as a value with key "!BADKEY".
func Log(ctx context.Context, level slog.Level, msg string, args ...any) {
logger.Log(ctx, level, msg, args...)
}
// LogAttrs is a more efficient version of [Logger.Log] that accepts only Attrs.
func LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) {
logger.LogAttrs(ctx, level, msg, attrs...)
}
// Debug logs at [LevelDebug].
func Debug(msg string, args ...any) {
logger.Debug(msg, args...)
}
// DebugContext logs at [LevelDebug] with the given context.
func DebugContext(ctx context.Context, msg string, args ...any) {
logger.DebugContext(ctx, msg, args...)
}
// Info logs at [LevelInfo].
func Info(msg string, args ...any) {
logger.Info(msg, args...)
}
// InfoContext logs at [LevelInfo] with the given context.
func InfoContext(ctx context.Context, msg string, args ...any) {
logger.InfoContext(ctx, msg, args...)
}
// Warn logs at [LevelWarn].
func Warn(msg string, args ...any) {
logger.Warn(msg, args...)
}
// WarnContext logs at [LevelWarn] with the given context.
func WarnContext(ctx context.Context, msg string, args ...any) {
logger.WarnContext(ctx, msg, args...)
}
// Error logs at [LevelError].
func Error(msg string, args ...any) {
logger.Error(msg, args...)
}
// ErrorContext logs at [LevelError] with the given context.
func ErrorContext(ctx context.Context, msg string, args ...any) {
logger.ErrorContext(ctx, msg, args...)
}
// With returns a Logger that includes the given attributes
// in each output operation. Arguments are converted to
// attributes as if by [Logger.Log].
func With(args ...any) *slog.Logger {
return logger.With(args...)
}
// WithGroup returns a Logger that starts a group, if name is non-empty.
// The keys of all attributes added to the Logger will be qualified by the given
// name. (How that qualification happens depends on the [Handler.WithGroup]
// method of the Logger's Handler.)
//
// If name is empty, WithGroup returns the receiver.
func WithGroup(name string) *slog.Logger {
return logger.WithGroup(name)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment