Skip to content

Instantly share code, notes, and snippets.

@mikeschinkel
Last active March 30, 2025 06:10
Show Gist options
  • Save mikeschinkel/3b1fb9ac4bbc200c95d0fc31692959ce to your computer and use it in GitHub Desktop.
Save mikeschinkel/3b1fb9ac4bbc200c95d0fc31692959ce to your computer and use it in GitHub Desktop.
Go logging package that leverages slog.Default() for creating package-level loggers (Proof-of-concept)

This solution is built on top of Go's log/slog package and leverages slog.Default(). To use it a developer should declare a package variable logger in every package that is part of their Go project that needs to perform logging (e.g. a package containing only type, var and/or const declarations would usually not need a logger.go file.)

Add a logger.go file to every packages and then add this line to the file, updating it with the package's name:

var logger = logging.NewPackageLogger("<package_name>")    

This uses a small logging package you can find below which has a Logger type simply to embed slog.Log. This allows adding extra methods beyond that provided by slog.Logger and to preprocess calls to *slog.Logger if we later find that to be needed.

Now, I do not love this approach because it introduces a package-scoped variable into every package, but it is the most apparently workable solution I have tried to date, this being about my 5th iteration of trying to find a workable solution for logging.

BTW, sure would be nice if the Go team would add a way for the compiler to capture the name of the current package using a well-known constant like PACKAGE_NAME or similar, but I digress.

At the time of this posting (2025-03) this code has not yet been run in production so it may turn out to be unworkable. If you are reading this and have reason to believe it is unworkable please do help me and anyone else who might read this by elaborating in depth as any issues you can see with this approach.

package main
import (
"github.com/myorg/myrepo/logging"
)
const (
ErrorLogArg = logging.ErrorLogArg
)
var logger = logging.NewCLILogger("mycli"). // Replace with your own CLI's name
func main() {
logger := logger.WithMethod("main") // Replace with your own daemon's name
logger.Info("Starting CLI")
// Remained of CLI code goes here
}
package main
import (
"github.com/myorg/myrepo/logging"
)
const (
ErrorLogArg = logging.ErrorLogArg
)
var logger = logging.NewDaemonLogger("mydaemon"). // Replace with your own daemon's name
func main() {
logger := logger.WithMethod("main") // Replace with your own daemon's name
logger.Info("Starting daemon")
// Remained of daemon code goes here
}
package dbutil
import (
"github.com/myorg/myrepo/logging"
)
const ErrorLogArg = logging.ErrorLogArg
var logger = logging.NewPackageLogger("dbutil")
package logging
import (
"errors"
"fmt"
"log/slog"
"os"
"strings"
)
const (
GoCLILogArg = "go_cli"
GoDaemonLogArg = "go_daemon"
GoPackageLogArg = "go_package"
ErrorLogArg = "error"
MethodNameLogArg = "method_name"
HTTPBodyLogArg = "http_body"
)
func init() {
SetLogHandler(slog.NewTextHandler(os.Stdout, nil))
}
func SetLogHandler(lh slog.Handler) {
l := NewLogger(slog.New(lh))
slog.SetDefault(l.Logger)
}
type Logger struct {
*slog.Logger
}
func NewLogger(logger *slog.Logger) *Logger {
return &Logger{Logger: logger}
}
func NewPackageLogger(name string) *Logger {
return NewLogger(slog.Default().With(GoPackageLogArg, name))
}
func NewCLILogger(name string) *Logger {
return NewLogger(slog.Default().With(GoCLILogArg, name))
}
func NewDaemonLogger(name string) *Logger {
return NewLogger(slog.Default().With(GoDaemonLogArg, name))
}
func (l *Logger) WithMethod(name string) *Logger {
return l.With(MethodNameLogArg, name)
}
func (l *Logger) With(args ...any) *Logger {
return NewLogger(l.Logger.With(args...))
}
func (l *Logger) FatalFunc(msg func() (string, []any)) {
l.Logger.Error(msg())
os.Exit(1)
}
func (l *Logger) ErrorFunc(msg func() (string, []any)) {
l.Logger.Error(msg())
}
func (l *Logger) WarnFunc(msg func() (string, []any)) {
l.Logger.Warn(msg())
}
func (l *Logger) InfoFunc(msg func() (string, []any)) {
l.Logger.Info(msg())
}
func (l *Logger) Fatal(msg string, args ...any) {
l.Logger.Error(msg, args...)
os.Exit(1)
}
var ErrAlsoLogged = errors.New("(this error was also logged)")
// ErrorErr both logs an error and also returns an error. This should RARELY ever
// be used — ideally either log error OR return, not both — but there are
// unfortunately times when this is needed.
func (l *Logger) ErrorErr(msg string, args ...any) (err error) {
l.Logger.Error(msg, args...)
sb := strings.Builder{}
sb.WriteString(msg)
switch {
case len(args)%2 != 0:
// The args passed are not a multiple of two; in other words, they are
// mismatched, e.g. ("the message", "foo", 1, "bar")
sb.WriteString(" (mismatched log args passed): ")
for i, arg := range args {
sb.WriteString(fmt.Sprintf("%v", arg))
if i < len(args)-1 {
sb.WriteString(", ")
}
}
default:
numArgs := len(args) / 2
for i := 0; i < numArgs; i += 2 {
sb.WriteString(fmt.Sprintf("%s=%v", args[i], args[i+1]))
}
}
// Add a sentinel error in case someone needs to branch on these errors
return errors.Join(
errors.New(sb.String()),
ErrAlsoLogged,
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment