Skip to content

Logging

wiki edited this page Sep 4, 2026 · 1 revision

Logging

rex ships SlogLogger, backed by the standard library's log/slog. No external dependency.

import "github.com/kryovyx/rex/logger"

app := rex.New(rex.WithLogLevel(logger.LogLevelDebug))
// or
app.WithLogger(logger.NewSlogLoggerWithLevel(logger.LogLevelDebug))

The interface

type Logger interface {
	Trace(format string, args ...interface{})
	Debug(format string, args ...interface{})
	Info(format string, args ...interface{})
	Warn(format string, args ...interface{})
	Error(format string, args ...interface{})

	SetLogLevel(level LogLevel)
	WithField(key string, value interface{}) Logger
	WithFields(fields map[string]interface{}) Logger
	WithError(err error) Logger
}

Messages are printf-style. Structured context comes from the With* methods, which return a child logger:

log := app.Logger().WithFields(map[string]interface{}{
	"component": "checkout",
	"tenant":    tenantID,
})
log.WithError(err).Error("could not reserve stock for order %s", orderID)

Levels

LogLevelTrace  // most verbose
LogLevelDebug
LogLevelInfo   // default
LogLevelWarn
LogLevelError
LogLevelOff    // silent

logger.ParseLogLevel(s) maps "trace", "debug", "info", "warn", "error" (case-sensitive) to a level; anything unrecognised returns LogLevelInfo rather than erroring, so a typo in LOG_LEVEL is silently "info", not a crash:

app := rex.New(rex.WithLogLevel(logger.ParseLogLevel(os.Getenv("LOG_LEVEL"))))

If a typo should be a deployment failure, validate the string yourself before calling it.

Option ordering does not matter

// Both work.
rex.New(rex.WithLogLevel(logger.LogLevelDebug), rex.WithLogger(myLogger))
rex.New(rex.WithLogger(myLogger), rex.WithLogLevel(logger.LogLevelDebug))

WithLogLevel stores the level and applies it when a logger is set, so it works before one is assigned. (This was documented behaviour before it was implemented behaviour: the option previously only called SetLogLevel on whatever logger existed at the time, so WithLogLevel before WithLogger silently did nothing.)

Custom loggers

Implement the interface and pass it in:

type zapAdapter struct{ l *zap.SugaredLogger }

func (a *zapAdapter) Info(format string, args ...interface{}) { a.l.Infof(format, args...) }
// … Warn, Error, Debug, Trace, SetLogLevel …

func (a *zapAdapter) WithField(k string, v interface{}) rextension.Logger {
	return &zapAdapter{l: a.l.With(k, v)}
}
// … WithFields, WithError …

app.WithLogger(&zapAdapter{l: sugared})

Logger is declared in rextension and aliased by rex/logger, so the framework, your application and every extension share one type with no conversion.

The With* methods must return a new logger, not mutate the receiver — the framework and extensions both take field-scoped children and expect the parent to be unaffected.

What the framework logs

Level Examples
Info starting, running, stopping, extension initialisation, bound addresses
Warn unresolved request (404), body over the limit (413), a router serving without TLS because ListenSSL is off
Error hook failures, bind failures, a request reaching a router before its table was built
Debug incoming and handled requests with status, size and duration; hook progress

At Debug, every request produces two lines. That is fine for development and too much for production — a real access log belongs in middleware, where it can be formatted and sampled, or in a router.request.handled subscriber if approximate is acceptable.

Extensions

An extension logs through r.Logger(). By convention it takes a field-scoped child so its output can be filtered:

e.logger = r.Logger().WithField("extension", "myext")

Clone this wiki locally