Tiny namespaced debug logger for Go. Inspired by the npm
debug package.
- Color-coded namespace, auto-derived from FNV hash of the name
- Per-call millisecond diff since previous log
- Production-safe: ANSI color auto-disabled when output is not a TTY, or
forced off with
NO_COLOR=1— no escape sequences in logs piped to files - JSON output with
DEBUG_FORMAT=jsonfor Datadog / log shippers - Printf-style
Logf/Errorfwith syntax-highlighted JSON for struct / map / slice arguments - Namespace filter via
GO_DEBUG=app:*,-app:audit(npm-debug style) - Safe for concurrent use
go get github.com/SlavaBatig/godebug
package main
import debug "github.com/SlavaBatig/godebug"
func main() {
log := debug.New("app:auth")
log.Debug("session lookup starting")
log.Info("server starting")
log.Infof("user %s logged in from %s", "alice", "192.168.1.1")
log.Warn("rate limit close to threshold")
type session struct {
UserID int `json:"user_id"`
Token string `json:"token"`
}
log.Infof("session=%v", session{UserID: 42, Token: "abc"})
log.Errorf("auth failed: %v", map[string]any{"reason": "expired"})
// log.Fatal("db dead") // would write + os.Exit(1)
}| Var | Effect |
|---|---|
NO_COLOR |
Any non-empty value disables ANSI color codes |
DEBUG_FORMAT |
json switches output to newline-delimited JSON |
GO_DEBUG |
Namespace filter (npm-debug style). Unset = log everything. |
Color is also automatically disabled when stdout / stderr is not a terminal (pipe, file redirect, captured CI output), so production logs stay clean.
Filter loggers by namespace at process start. Patterns are comma- or
whitespace-separated. * is a wildcard. A - prefix turns a pattern into a
skip.
GO_DEBUG='app:*' # only app:* (including app:auth:retry)
GO_DEBUG='app:auth,db:*' # exact app:auth plus everything under db
GO_DEBUG='*,-server:*' # everything except server:*
GO_DEBUG='-app:audit' # everything except app:audit
GO_DEBUG= # (unset / empty) log everything
Filtered-out loggers short-circuit on the first byte of every call:
~1 ns/op with zero allocations, so it's safe to leave verbose debug.Logf
calls compiled into production code and toggle them via env. Use
logger.Enabled() to guard expensive argument assembly:
if log.Enabled() {
log.Logf("snapshot=%v", buildExpensiveSnapshot())
}The filter can also be set explicitly via Options.Filter, which overrides
GO_DEBUG (useful for tests and library configuration).
DEBUG_FORMAT=json ./your-app
Each line is a single JSON object — exactly one newline per entry, all
control characters and newlines inside values escaped by encoding/json.
Safe for Datadog, Sumo, Splunk, Vector, Fluent Bit, and any other
newline-delimited JSON consumer.
{
"timestamp": "2026-05-20T12:00:00.123456789Z",
"namespace": "app:auth",
"level": "info",
"message": "user alice logged in",
"diff_ms": 12,
"params": ["alice"]
}params is populated for Logf / Errorf calls so structured fields are
preserved for indexing.
If you point a JSON-mode logger at a terminal, output is syntax-highlighted
(keys cyan, strings green, numbers yellow, bools/null magenta, punctuation
dim). The same TTY heuristic that disables colored text mode disables this
when output is piped or redirected, so your-app | tee app.log still emits
plain JSON suitable for ingestion. NO_COLOR=1 forces it off either way.
type Logger struct { /* ... */ }
func New(namespace string) *Logger
func NewWith(namespace string, opts Options) *Logger
// Plain.
func (l *Logger) Debug(messages ...string)
func (l *Logger) Info(messages ...string)
func (l *Logger) Warn(messages ...string)
func (l *Logger) Error(messages ...string)
func (l *Logger) Fatal(messages ...string) // writes then os.Exit(1)
// Printf-style.
func (l *Logger) Debugf(format string, args ...any)
func (l *Logger) Infof(format string, args ...any)
func (l *Logger) Warnf(format string, args ...any)
func (l *Logger) Errorf(format string, args ...any)
func (l *Logger) Fatalf(format string, args ...any) // writes then os.Exit(1)
func (l *Logger) Enabled() bool| Level | Stream | Text suffix | Notes |
|---|---|---|---|
| debug | stdout | :debug |
|
| info | stdout | (none) | Default — namespace shown clean |
| warn | stderr | :warn |
|
| error | stderr | :error |
|
| fatal | stderr | :fatal |
Always calls os.Exit(1) after writing |
Fatal / Fatalf exit unconditionally — even when the namespace is
filtered out via GO_DEBUG, the exit still happens. The filter controls
verbosity, not whether the process dies.
Options lets callers override the writers, format, color, filter, and
clock — see debug.go for details.
go test -race ./...
go test -bench=. -benchmem ./...