logstuff

package module
v0.0.0-...-478bb54 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 6 Imported by: 0

README

tests/vet Go Reference

logstuff

A few small logging abstractions I use across my own Go projects:

  • a minimal swappable logging interface,
  • a slog wrapper with context-key propagation and trace level,
  • and a couple of optional capability interfaces (LevelEnabler, CtxKeyer).

LogSink

LogSink is a minimal logger interface. It only has two methods, Log and With, and uses exported LogLevel constants for the level argument. This keeps call sites light and behind interface, making it easy to modify logger setup without touching call sites.

type LogSink interface {
	Log(context.Context, LogLevel, string, ...any)
	With(...any) LogSink
}
Why LogSink at all?

Short answer: refactor pain insurance.

This started as a direct slog dependency, then got refactored into a custom logger, and the coupling between call sites and the concrete logger caused friction during that move.

LogSink exists so call sites depend on a two-method shape, not a specific implementation, so a future swap (by me, or by anyone forking/collaborating who already has their own logging setup) doesn't mean touching every call site again.

A few more things in this package, just convenience stuff:

  • SlogCtx
    • It's a slog wrapper that adds context keys (useful for request-id, traces or similar).
    • It also adds trace level.
  • slogSink
    • It's just a ready-made SlogCtx wrapper that satisfies the LogSink interface.
  • Logger
    • LogSink wrapper.
    • Provides all the convenience methods on top of a LogSink, Debug, Info, Error, etc.
    • It can be used with any LogSink but it doesn't satisfy LogSink interface itself.
  • discardSink
    • is just a ready-made implementation of a discard logger (a logger that doesn't do anything) that satisfies the LogSink interface.
  • LevelFromEnv
    • Package function for reading log level from env, returns directly a logstuff.LogLevel.

Building-block interfaces:

  • CtxKeyer
    • minimal interface to add ctx keys for the logger to track. WithCtxKeys method.
  • LevelEnabler
    • minimal interface to add finer control over what's logged. Enabled method.

Install

# Requires Go 1.26+ (uses the stdlib slog.MultiHandler)
go get github.com/mnzbono/logstuff

Usage example

// ...
type MyThingy struct {
    logger logstuff.LogSink // this is the interface that allows swapping
    // ...
}
// ...
func NewMyThingy() *MyThingy {
    logger := logstuff.NewSlogSink(logstuff.LevelInfo) // you can use the ready-made adapters or quickly roll your own.
    return &MyThingy{logger:logger}
}
// ...
func (m *MyThingy) Work() {
    m.logger.Log(ctx, logstuff.LevelInfo, "just logging around...")
    // ...
}
// ...
func TestMyThingy() {
    m := &MyThingy{logger: logstuff.NewDiscard() } // convenience discard logger while avoiding nil
    // ...
}

SlogCtx & slogSink

SlogCtx wraps slog and adds ability to track context keys.
If it isn't tracking any key it's basically slog, but without any methods, only Log and With.
It can use any io.Writer, text/json, or prebuilt handler (slog capabilities). If you don't provide any handler argument it builds a texthandler that outputs to os.Stdout. If you provide more than one handler it creates the new slog MultiHandler.

slogSink just wraps it so it can be used as a LogSink.

example:

// this example will create a slog.MultiHandler as a LogSink
logger := logstuff.NewSlogSink(
    logstuff.LevelDebug, 
    logstuff.WithJSONHandler(w), // you can write json to a buffer/file 
    logstuff.WithTextHandler(os.Stderr, logstuff.LevelError), // error only to stderr
    logstuff.WithHandler(myCustomdHandler) // or pass slog.Handler you already built.
)
// logger (slogSink) satisfies LogSink interface
logger.Log(ctx, logstuff.LevelInfo, "just logging around...")

context keys:

// good hygiene to set the keys in a single place
var ridKey logstuff.CtxKey = "request-id"

// we create a slogSink and set it to track the ctx key "request-id"
// we use interface assertion in this example, but you can compose an interface with CtxKeyer
baseLogger := logstuff.NewSlogSink(logstuff.LevelTrace)
ctxLogger := baseLogger.(logstuff.CtxKeyer).WithCtxKeys(
    ridKey,
    logstuff.CtxKey("user"), // variadic — pass any number of keys, deduped before saving
    )
// ...
func (h *Handler) HandleRequest(ctx context.Context) {
    ctx = context.WithValue(ctx, ridKey, newRequestId()) // request-id=123
    h.service.Work(ctx)
}
// if s.logger is set up to track ctx keys it captures them.
func (s *Service) Work(ctx context.Context) {
    s.logger.Log(ctx, logstuff.LevelDebug, "working...") // request-id=123
}

Wait, Logger doesn't satisfy LogSink?

No. LogSink is meant to be minimal, so you can quickly write a wrapper.
Logger is meant for when you want the convenience methods back. It wraps a LogSink (which only has Log method) and provides all the Debug, TraceCtx, ErrorCtx, etc, methods. easier to see some code:

// ...
type MyThingy struct {
    logger *logstuff.Logger // this will wrap the LogSink
    // ...
}
// ...
func NewMyThingy() *MyThingy {
    // imagine you build a logsink adapter to some kind of logger
    myAdapter := NewMyLoggerAdapter() 
    // you just wrap it with Logger and it gives you the convenience methods instead of just Log()
    logger := logstuff.NewLogger(myAdapter) 
    return &MyThingy{logger:logger}
}
// ...
func (m *MyThingy) Work() {
    m.logger.Debug("just debugging around...")
    m.logger.TraceContext(ctx, "just tracing around...")
    // etc ...
}

AI disclaimer: AI was used for code review, documentation review and english phrasing, and wrote a handful of tests. Design, implementation, all code, and most tests are human-made.

Documentation

Overview

Package logstuff provides a few minimal logging abstractions

  • LogSink, minimal log interface for logging swapability.
  • SlogCtx, slog wrapper that adds ctx key propagation and trace level.
  • slogSink, the LogSink adapter for SlogCtx.
  • Logger, adds logging cenvenience methods to a LogSink.
  • CtxKeyer, LevelEnbaler and more.

Index

Constants

This section is empty.

Variables

View Source
var EnvLogLevel = "LOGLEVEL"

EnvLogLevel defines the env var to use when setting log level, default "LOGLEVEL"

Functions

This section is empty.

Types

type CtxKey

type CtxKey string

CtxKey is the type of context keys that SlogCtx can track.

type CtxKeyer

type CtxKeyer interface {
	WithCtxKeys(...CtxKey) LogSink
}

CtxKeyer is a minimal interface that helps fork a logger and add ctx key capture to it.

type LevelEnabler

type LevelEnabler interface {
	Enabled(context.Context, LogLevel) bool
}

LevelEnabler is a minimal interface to gate logging behind a conditional.

type LogLevel

type LogLevel int
const (
	LevelTrace LogLevel = -8
	LevelDebug LogLevel = -4
	LevelInfo  LogLevel = 0
	LevelWarn  LogLevel = 4
	LevelError LogLevel = 8
)

func LevelFromEnv

func LevelFromEnv() LogLevel

LevelFromEnv reads log level form env. Reads exported var EnvLogLevel value, default "LOGLEVEL"

func (LogLevel) String

func (l LogLevel) String() string

type LogSink

type LogSink interface {
	Log(context.Context, LogLevel, string, ...any)
	With(...any) LogSink
}

LogSink is a minimal interface that helps logger swapping.

func NewDiscard

func NewDiscard() LogSink

NewDiscard returns discardSink, a logger that does nothing and satisfies LogSink interface.

func NewSlogSink

func NewSlogSink(l LogLevel, opts ...SlogHandler) LogSink

NewSlogSink returns a wrapper around SlogCtx that satisfies LogSink interface.

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

Logger is a LogSink wrapper that provides all expected convenience methods.

func NewLogger

func NewLogger(l LogSink) *Logger

NewLogger returns a *Logger

func (*Logger) Debug

func (l *Logger) Debug(msg string, args ...any)

func (*Logger) DebugContext

func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any)

func (*Logger) Enabled

func (l *Logger) Enabled(ctx context.Context, lvl LogLevel) bool

func (*Logger) Error

func (l *Logger) Error(msg string, args ...any)

func (*Logger) ErrorContext

func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any)

func (*Logger) Info

func (l *Logger) Info(msg string, args ...any)

func (*Logger) InfoContext

func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any)

func (*Logger) Log

func (l *Logger) Log(ctx context.Context, lvl LogLevel, msg string, args ...any)

func (*Logger) Trace

func (l *Logger) Trace(msg string, args ...any)

func (*Logger) TraceContext

func (l *Logger) TraceContext(ctx context.Context, msg string, args ...any)

func (*Logger) Warn

func (l *Logger) Warn(msg string, args ...any)

func (*Logger) WarnContext

func (l *Logger) WarnContext(ctx context.Context, msg string, args ...any)

func (*Logger) With

func (l *Logger) With(args ...any) *Logger

type SlogCtx

type SlogCtx struct {
	// contains filtered or unexported fields
}

SlogCtx is a wrapper around slog.Logger that adds support for context keys.

func NewSlogCtx

func NewSlogCtx(l LogLevel, opts ...SlogHandler) *SlogCtx

NewSlogCtx returns a *SlogCtx. variadic SlogHandler allows to pass 0, 1 or more build options.

  • If no SlogHandler is passed, the default TEXT slog.Handler that outputs to stdout will be used.
  • If more than one is passed, then a slog.Multihandler will be created.

func (*SlogCtx) Enabled

func (l *SlogCtx) Enabled(ctx context.Context, lvl LogLevel) bool

func (*SlogCtx) Log

func (l *SlogCtx) Log(ctx context.Context, lvl LogLevel, msg string, args ...any)

func (*SlogCtx) With

func (l *SlogCtx) With(args ...any) *SlogCtx

func (*SlogCtx) WithCtxKeys

func (l *SlogCtx) WithCtxKeys(args ...CtxKey) *SlogCtx

type SlogHandler

type SlogHandler func(*slog.HandlerOptions) slog.Handler

SlogHandler builds a slog.Handler from the given options.

func WithHandler

func WithHandler(h slog.Handler) SlogHandler

WithHandler allows to passing directly a pre-built slog.Handler. NOTE: Handlers passed in WithHandler won't do ReplaceAttr (trace level).

func WithJSONHandler

func WithJSONHandler(w io.Writer, logLevel ...LogLevel) SlogHandler

WithJSONHandler creates a JSON slog.Handler. If logLevel is passed it overwrites the base logLevel of constructor.

func WithTextHandler

func WithTextHandler(w io.Writer, logLevel ...LogLevel) SlogHandler

WithTextHandler creates a TEXT slog.Handler. If logLevel is passed it overwrites the base logLevel of constructor.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL