devnw.dev/log

A structured logging library for Go built on top of the standard log/slog
package. It extends slog with additional log levels (Trace, DTrace,
Fatal, Sec, DPanic), text and JSON output formats, lumberjack-based
log file rotation, and a TestLogger helper that integrates with testing.T.
Installation
Requires Go 1.22 or later.
go get devnw.dev/log@latest
Quick Start
package main
import (
"context"
"devnw.dev/log"
)
func main() {
ctx := context.Background()
logger := log.New(ctx,
log.WithLevel(log.Info),
log.WithFormat(log.JSON),
)
logger.Info("application started", "port", 8080)
logger.Warn("low disk space", "free_gb", 2)
logger.Error("connection failed", "addr", "db:5432", "err", "connection refused")
}
Running the above produces structured JSON output on stderr:
{"time":"...","level":"INFO","msg":"application started","port":8080}
{"time":"...","level":"WARN","msg":"low disk space","free_gb":2}
{"time":"...","level":"ERROR+4","msg":"connection failed","addr":"db:5432","err":"connection refused"}
Usage
Constructors
| Function |
Description |
log.New(ctx, opts...) |
Create a new configured logger. |
log.Default() |
Wrap the current slog.Default() logger. |
log.TestLogger(ctx, t) |
Create a test logger that calls t.FailNow() on Error/Fatal. |
Log Levels
The package defines a superset of slog's built-in levels:
| Level |
Numeric value |
Description |
DPanic |
-20 |
Debug-mode panic: logs and then calls panic(msg). |
DTrace |
-15 |
Deep trace for very verbose diagnostics. |
Trace |
-6 |
Trace-level logging. |
Debug |
-4 |
Debug messages. |
Info |
0 |
Informational messages. |
Warn |
4 |
Warning messages. |
Error |
8 |
Error messages. Default minimum level. |
Crit |
10 |
Critical errors. |
Fatal |
15 |
Fatal errors. |
Sec |
20 |
Security events. |
Parse a level from a string (e.g., from a config file or environment variable):
level := log.ParseLevel(os.Getenv("LOG_LEVEL")) // returns DefaultLevel (Error) on unknown input
ParseLevel is case-insensitive and trims surrounding whitespace. It returns
DefaultLevel (Error) for any unrecognised input.
Options
Pass one or more Option values to log.New:
| Option |
Description |
WithLevel(level) |
Set the minimum log level. |
WithFormat(log.Text | log.JSON) |
Set the output format. Default: Text. |
WithGroup(name) |
Add a named group prefix to every log attribute. |
WithArgs(attrs...) |
Attach default slog.Attr values to every log entry. |
WithConfig(cfg) |
Pass a lumberjack Config for log file rotation. See note below. |
Note on WithConfig: The WithConfig option accepts a *log.Config
(which is lumberjack.Logger) and is intended to redirect log output to a
rotating file. The wiring between WithConfig and the output writer is not
yet complete in the current release (v0.1.0). To write to a file today,
construct a lumberjack.Logger directly and pass it as an io.Writer to a
custom handler. Full WithConfig support is planned for a future release.
See docs/configuration.md for a workaround.
Log File Rotation
Config is an alias for lumberjack.Logger. The fields map directly to
lumberjack v2:
cfg := &log.Config{
Filename: "/var/log/myapp/myapp.log",
MaxSize: 100, // megabytes before rotation
MaxBackups: 10,
MaxAge: 30, // days
LocalTime: true,
Compress: true,
}
See docs/configuration.md for the full field
reference and a workaround for file-based logging in the current release.
Using Groups and Default Attributes
logger := log.New(ctx,
log.WithLevel(log.Debug),
log.WithGroup("http"),
log.WithArgs(
slog.String("service", "api"),
slog.String("version", "1.2.3"),
),
)
logger.Info("request received", "method", "GET", "path", "/health")
// Output includes: service=api version=1.2.3 http.method=GET http.path=/health
Setting as the Global Default
logger := log.New(ctx, log.WithLevel(log.Debug))
logger.SetDefault() // replaces slog.Default() for the process lifetime
NOOP Logger
When logging must be silently discarded — useful in library code that accepts
an optional Logger:
var logger log.Logger = log.NOOP
logger.Info("this is silently discarded")
Test Logger
TestLogger creates a logger for use inside testing.T tests. It reads
LOG_LEVEL and LOG_FORMAT environment variables, and calls t.FailNow()
whenever Error or Fatal is called:
func TestMyFeature(t *testing.T) {
ctx := context.Background()
logger := log.TestLogger(ctx, t)
logger.Info("running test")
// logger.Error("...") would call t.FailNow()
}
Run tests with verbose logging:
go test -v ./...
# or with a specific level:
LOG_LEVEL=debug go test -v ./...
LOG_LEVEL=debug LOG_FORMAT=json go test -v ./...
Logger Interface
Any value that satisfies log.Logger can be used in place of a concrete logger:
type Logger interface {
SetDefault() Logger
DPanic(msg string, args ...any)
DTrace(msg string, args ...any)
Trace(msg string, args ...any)
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
Fatal(msg string, args ...any)
Log(ctx context.Context, level Level, msg string, args ...any)
}
Contributing
See CONTRIBUTING.md.
Security
To report a security vulnerability, email benji@devnw.com directly. Do not
open a public issue. See SECURITY.md for the full policy.
License
Copyright 2024-2026 Benji Vesterby. Licensed under the
Apache License, Version 2.0.