Namespace-based debug logging with zero overhead when disabled, following the pattern of the debug npm package.
A simple, debug-style logging framework for Go that follows the pattern matching syntax of the debug npm package.
- Namespace-based logging: Each logger has a namespace (e.g.,
workflow:compiler,cli:audit) - Pattern matching: Enable/disable loggers using wildcards and exclusions via the
DEBUGenvironment variable - Printf interface: Standard printf-style formatting
- Time diff display: Shows time elapsed since last log call (like debug npm package)
- Automatic color coding: Each namespace gets a unique color, determined by
DEBUG_COLORSand rendered by lipgloss - Zero overhead: Logger enabled state is computed once at construction time
- Thread-safe: Safe for concurrent use
| Type | Kind | Description |
|---|---|---|
Logger |
struct | Namespace-based debug logger; enabled state and namespace label are computed once at construction time |
SlogHandler |
struct | Implements slog.Handler by delegating to a Logger; allows libraries that expect a standard slog.Logger to use the gh-aw logger |
The Logger type provides namespace-based debug logging with pattern matching, printf interface, and time-diff display.
| Method / Function | Signature | Description |
|---|---|---|
New |
func(namespace string) *Logger |
Creates a new logger for the given namespace |
(*Logger).Printf |
func(format string, args ...any) |
Formatted output (always adds newline) |
(*Logger).Print |
func(args ...any) |
Simple concatenation (always adds newline) |
(*Logger).Enabled |
func() bool |
Returns true if the logger matches the active DEBUG pattern |
NewSlogHandler |
func(logger *Logger) *SlogHandler |
Creates a slog.Handler wrapping the given Logger |
NewSlogLoggerWithHandler |
func(logger *Logger) *slog.Logger |
Creates a slog.Logger backed by the given Logger |
Behavioral contracts:
NewMUST compute the enabled state exactly once at construction time from theDEBUGenvironment variable (orACTIONS_RUNNER_DEBUG=true/RUNNER_DEBUG=1as a fallback); subsequent changes to those variables MUST NOT affect already-constructedLoggerinstances.Logger.PrintfandLogger.PrintMUST be no-ops (return immediately before any string formatting or I/O) whenEnabled()returnsfalse.Logger.PrintfandLogger.PrintMUST write all output toos.Stderrand MUST append a trailing newline to every message.Logger.PrintfandLogger.PrintMUST include a+<duration>suffix showing elapsed time since the previous call on the same instance.Logger.PrintfandLogger.PrintMUST acquire an internal mutex before updating thelastLogtimestamp; concurrent callers MUST receive independent, accurate time-diff values without data races.SlogHandler.EnabledMUST returnfalsewhen the underlyingLogger.Enabled()isfalse, preventing attribute collection overhead for disabled loggers.SlogHandler.WithAttrsandSlogHandler.WithGroupMUST return the receiver unchanged; attributes and groups are not accumulated across calls.- Color styling MUST be disabled when
DEBUG_COLORS=0; when enabled, actual color rendering is delegated to lipgloss which adapts to the output terminal's capabilities.
package main
import "github.com/github/gh-aw/pkg/logger"
var log = logger.New("myapp:feature")
func main() {
log.Printf("Starting application with config: %s", config)
log.Print("Multiple", " ", "arguments")
}Output shows namespace, message, and time diff:
myapp:feature Starting application with config: production +0ns
myapp:feature Multiple arguments +125ms
Check if a logger is enabled before performing expensive operations:
if log.Enabled() {
// Do expensive work only if logging is enabled
result := expensiveOperation()
log.Printf("Result: %v", result)
}Like the debug npm package, each log shows the time elapsed since the last log call:
log.Printf("Starting task")
// ... do some work ...
log.Printf("Task completed") // Shows +2.5s (or +500ms, +100µs, etc.)Control which loggers are enabled using the DEBUG environment variable with patterns. When ACTIONS_RUNNER_DEBUG=true or RUNNER_DEBUG=1 is set and DEBUG is not explicitly set, all loggers are enabled automatically — equivalent to DEBUG=*.
# Enable all loggers
DEBUG=*
# Enable all loggers in the 'workflow' namespace
DEBUG=workflow:*
# Enable specific loggers
DEBUG=workflow:compiler,cli:audit
# Enable all except specific loggers
DEBUG=*,-workflow:compiler
# Enable namespace but exclude specific patterns
DEBUG=workflow:*,-workflow:compiler:cache
# Multiple patterns with exclusions
DEBUG=workflow:*,cli:*,-workflow:testColor styling is enabled when DEBUG_COLORS is not set to 0. The lipgloss library handles actual color rendering and adapts to the output terminal's capabilities; colors are typically suppressed when piping output to a non-TTY stream.
Each namespace gets a consistent color based on a FNV-1a hash of its name. This makes it easy to visually distinguish between different loggers.
# Disable colors
DEBUG_COLORS=0 DEBUG=* gh aw compile workflow.md
# Colors are automatically disabled when piping output
DEBUG=* gh aw compile workflow.md 2>&1 | tee output.log*- Matches all loggersnamespace:*- Matches all loggers with the given prefix*:suffix- Matches all loggers with the given suffixprefix:*:suffix- Matches loggers with both prefix and suffix-pattern- Excludes loggers matching the pattern (takes precedence)pattern1,pattern2- Multiple patterns separated by commas
The enabled state is computed once at logger construction time based on the DEBUG environment variable. This means:
- Zero overhead for disabled loggers (simple boolean check)
DEBUGchanges after the process starts won't affect existing loggers
Each logger tracks the time of its last log call to display elapsed time, similar to the debug npm package. This helps identify performance bottlenecks and understand timing relationships between log messages.
All log output goes to stderr to avoid interfering with stdout data (JSON, command output, etc.).
The logger provides a familiar printf-style interface that Go developers expect:
Printf(format, args...)- Formatted output (always adds newline)Print(args...)- Simple concatenation (always adds newline)
// In pkg/workflow/compiler.go
var log = logger.New("workflow:compiler")
// In pkg/cli/audit.go
var log = logger.New("cli:audit")
// In pkg/parser/frontmatter.go
var log = logger.New("parser:frontmatter")Enable with:
DEBUG=workflow:* go run main.go # Only workflow package
DEBUG=cli:*,parser:* go run main.go # CLI and parser packages
DEBUG=* go run main.go # Everythingvar compileLog = logger.New("compile")
var parseLog = logger.New("parse")
var validateLog = logger.New("validate")The package includes a bridge to Go's standard log/slog library for libraries that expect a slog.Logger instead of the custom Logger type.
SlogHandler implements slog.Handler by delegating to an existing Logger. It respects the logger's enabled state, formats attributes as key=value pairs, and prefixes each message with compact terminal-friendly glyphs (· for debug/info, ⚠ for warning, ✗ for error).
Creates a new slog.Handler wrapping the provided Logger.
import "github.com/github/gh-aw/pkg/logger"
var log = logger.New("myapp:feature")
handler := logger.NewSlogHandler(log)
slogLogger := slog.New(handler)
slogLogger.Info("using slog interface", "key", "value")Convenience constructor that creates both the SlogHandler and the slog.Logger in one call.
var log = logger.New("myapp:feature")
slogLogger := logger.NewSlogLoggerWithHandler(log)
slogLogger.Warn("something unusual happened", "count", 42)- Enabled check:
SlogHandler.Enabledreturnsfalsewhen the underlyingLoggeris disabled (i.e. the namespace does not match theDEBUGpattern). This prevents expensive attribute collection for disabled loggers. - Attribute formatting: All record attributes are appended as
key=valuepairs after the message. - Groups and persistent attributes:
WithAttrsandWithGroupreturn the handler unchanged — attributes are not persisted across calls. This keeps the adapter lightweight. - Output destination: All output goes to
stderrvia the underlyingLogger.
Logger instances are safe for concurrent use from multiple goroutines. The Printf and Print methods acquire a sync.Mutex before updating the lastLog timestamp, so concurrent callers receive accurate time-diff values without data races. The Enabled method is read-only and requires no lock.
SlogHandler instances are safe for concurrent use when the underlying Logger is safe for concurrent use.
Internal:
github.com/github/gh-aw/pkg/colorwriter— color-profile-aware stderr writer selectiongithub.com/github/gh-aw/pkg/timeutil— time-diff formatting for log outputgithub.com/github/gh-aw/pkg/styles— shared terminal style constants and color helpers
- The
DEBUGenvironment variable is read once when the package is initialized - Thread-safe using
sync.Mutexfor time tracking - Simple pattern matching without regex (prefix, suffix, and middle wildcards only)
- Exclusion patterns (prefixed with
-) take precedence over inclusion patterns - Time diff formatted like debug npm package (ns, µs, ms, s, m, h)
- Colors assigned using FNV-1a hash for consistent namespace-to-color mapping
- Color palette chosen for readability on both light and dark terminals
- Uses ANSI 256-color codes for better compatibility
This appendix is generated from the current non-test Go source files in this package and records any exported top-level symbols that are not already described above.
| Category | Count |
|---|---|
| Types | 2 |
| Constants | 0 |
| Variables | 0 |
| Functions and methods | 10 |
| Additional symbols documented in this appendix | 1 |
This specification is automatically maintained by the spec-extractor workflow.
