l4g - Structured Logging Library
l4g (Log for Go) is a high-performance structured logging library, fully compatible with Go’s standard library log/slog package. It provides zero-allocation logging, buffer pool optimization, multiple output formats, and colored terminal output support. Minimum supported Go version: 1.21.0.
Installation
go get -u go-slim.dev/l4gQuick Start
package mainimport "go-slim.dev/l4g"func main() { // Use default logger l4g.Info("Service started", "port", 8080) l4g.Debug("Debug info", "user", "alice") l4g.Error("Error occurred", "error", err) // Create custom logger logger := l4g.New(l4g.Options{ Level: l4g.LevelDebug, Prefix: "myapp", }) logger.Info("Application started")}Core Concepts
Log Levels
l4g provides 7 log levels, from lowest to highest severity:
const ( LevelTrace // Most detailed trace information LevelDebug // Debug information LevelInfo // General information (default level) LevelWarn // Warning information LevelError // Error information LevelPanic // Serious error, will panic after logging LevelFatal // Fatal error, will exit program after logging)Log Format
Each log entry contains the following fields:
Time Level [Prefix] Message key1=value1 key2=value2Example output:
Jan 2 15:04:05 INFO [myapp] User logged in user=alice ip=192.168.1.100Jan 2 15:04:06 ERROR [myapp] Database error error="connection timeout" retry=3Basic Usage
Package-Level Functions (Using Default Logger)
import "go-slim.dev/l4g"func main() { // All levels of logging l4g.Trace("Trace info") l4g.Debug("Debug info") l4g.Info("General info") l4g.Warn("Warning info") l4g.Error("Error info") // With structured fields l4g.Info("User logged in", "user", "alice", "ip", "192.168.1.100") l4g.Error("Database error", "error", err, "retry", 3) // Formatted logs (Printf style) l4g.Infof("Server started on port %d", 8080) l4g.Errorf("Cannot connect to %s: %v", host, err) // JSON-style logs l4g.Infoj(map[string]any{ "event": "user_login", "user": "alice", "timestamp": time.Now(), })}Creating Custom Logger
import ( "os" "go-slim.dev/l4g")func main() { // Basic configuration logger := l4g.New(l4g.Options{ Output: os.Stdout, Level: l4g.LevelDebug, Prefix: "myapp", }) // Custom time format logger = l4g.New(l4g.Options{ TimeFormat: "2006-01-02 15:04:05", Level: l4g.LevelInfo, }) // Disable colored output logger = l4g.New(l4g.Options{ NoColor: true, }) // Custom level format logger = l4g.New(l4g.Options{ LevelFormat: func(lvl l4g.Level) string { return "[" + lvl.String() + "]" }, }) // Custom prefix format logger = l4g.New(l4g.Options{ PrefixFormat: func(prefix string) string { return "<" + prefix + ">" }, })}Logger Methods
logger := l4g.New(l4g.Options{Level: l4g.LevelDebug})// Basic log methodslogger.Trace("Trace", "detail", "...")logger.Debug("Debug", "var", value)logger.Info("Info", "status", "ok")logger.Warn("Warning", "threshold", 90)logger.Error("Error", "error", err)// Formatted methodslogger.Debugf("Variable value: %v", value)logger.Infof("Request processing time: %v", duration)logger.Errorf("Failure: %s", reason)// JSON-style methodslogger.Debugj(map[string]any{"query": sql, "duration": dur})logger.Infoj(map[string]any{"event": "request", "path": path})// Panic and Fatallogger.Panic("Serious error", "reason", reason) // Will panic after logginglogger.Fatal("Fatal error", "error", err) // Will call os.Exit(1) after loggingAdvanced Features
1. Structured Attributes
Use WithAttrs to create a logger with fixed attributes:
// Create logger with request IDrequestLogger := logger.WithAttrs("request_id", reqID, "user", userID)// All logs will include these attributesrequestLogger.Info("Processing started")requestLogger.Info("Processing completed", "duration", dur)// Output:// INFO Processing started request_id=abc123 user=alice// INFO Processing completed request_id=abc123 user=alice duration=150msChaining:
logger. WithAttrs("service", "api"). WithAttrs("version", "v1.0"). Info("Service started")2. Attribute Grouping
Use WithGroup to organize related attributes:
// Create logger with groupinghttpLogger := logger.WithGroup("http")httpLogger.Info("Request", "method", "GET", "path", "/api/users")// Output: INFO Request http.method=GET http.path=/api/users// Nested groupingserverLogger := logger.WithGroup("server").WithGroup("http")serverLogger.Info("Listening", "port", 8080)// Output: INFO Listening server.http.port=80803. Log Prefix
Use WithPrefix to add a prefix to logs:
// Create logger with prefixapiLogger := logger.WithPrefix("api")apiLogger.Info("Request processing completed")// Output: INFO [api] Request processing completed// Chained prefixesmoduleLogger := logger.WithPrefix("auth").WithPrefix("jwt")moduleLogger.Debug("Verifying token")// Output: DEBUG [jwtauth] Verifying token4. Channel Logger
Use Channel to create named logger instances. Multiple calls with the same name return the same instance:
// Use same logger in different packages// package databasedbLogger := l4g.Channel("database")dbLogger.Debug("Executing query", "sql", query)// package cachecacheLogger := l4g.Channel("cache")cacheLogger.Info("Cache hit", "key", key)// Same name returns same instancelogger1 := l4g.Channel("myapp")logger2 := l4g.Channel("myapp")// logger1 == logger2 (true)Custom Channel creation function:
// Set custom logger creation functionl4g.NewFunc = func(name string) *l4g.Logger { return l4g.New(l4g.Options{ Prefix: name, Level: l4g.LevelDebug, TimeFormat: "15:04:05", })}// Subsequently created Channel loggers all use this configurationlogger := l4g.Channel("myservice")5. Dynamic Log Level
Use LevelVar to implement runtime dynamic log level adjustment:
levelVar := l4g.NewLevelVar(l4g.LevelInfo)logger := l4g.New(l4g.Options{ Level: levelVar,})logger.Debug("Debug info") // Won't output (level lower than Info)logger.Info("General info") // Will output// Adjust level at runtimelevelVar.Set(l4g.LevelDebug)logger.Debug("Debug info") // Now will output6. Custom Output
Use OutputVar to implement dynamic output target switching:
import "os"outputVar := l4g.NewOutputVar(os.Stdout)logger := l4g.New(l4g.Options{ Output: outputVar,})logger.Info("Output to stdout")// Switch to filefile, _ := os.OpenFile("app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)outputVar.Set(file)logger.Info("Output to file")Using SetOutput method:
logger := l4g.New(l4g.Options{})logger.Info("Output to stderr") // Default// Switch outputfile, _ := os.Create("app.log")logger.SetOutput(file)logger.Info("Output to file")7. Attribute Replacement
Use ReplaceAttr to customize attribute output format:
logger := l4g.New(l4g.Options{ ReplaceAttr: func(groups []string, attr l4g.Attr) l4g.Attr { // Hide sensitive information if attr.Key == "password" { return l4g.String("password", "***") } // Custom time format if attr.Key == l4g.TimeKey { return l4g.String("time", time.Now().Format("15:04:05")) } // Custom level display if attr.Key == l4g.LevelKey { level := attr.Value.Any().(l4g.Level) return l4g.String("level", "[" + level.String() + "]") } return attr },})logger.Info("User login", "username", "alice", "password", "secret123")// Output: 15:04:05 [info] User login username=alice password=***8. Colored Output
l4g enables colored terminal output by default, using different colors for different levels:
- TRACE: Gray
- DEBUG: Bright cyan
- INFO: White
- WARN: Bright green
- ERROR: Bright yellow
- PANIC: Bright red
- FATAL: Bright red
Disable colors:
logger := l4g.New(l4g.Options{ NoColor: true,})Custom colored attributes:
import "go-slim.dev/l4g"// Use predefined error attribute (red)logger.Info("Operation failed", l4g.Err(err))// Custom colored attributeattr := l4g.ColorAttr(9, l4g.String("status", "critical"))logger.Warn("Warning", attr)// ANSI 256 color support// 0-7: Standard colors// 8-15: Bright colors// 16-231: 6×6×6 color cube// 232-255: 24-level grayscaleAttribute Types
l4g provides typed attribute constructor functions:
import ( "time" "go-slim.dev/l4g")logger.Info("Typed attributes", l4g.String("name", "alice"), // String l4g.Int("age", 30), // Integer l4g.Int64("count", 1000000), // int64 l4g.Uint("port", 8080), // Unsigned integer l4g.Uint64("size", 1024), // uint64 l4g.Float("score", 95.5), // Float l4g.Bool("active", true), // Boolean l4g.Time("timestamp", time.Now()), // Time l4g.Duration("elapsed", dur), // Duration l4g.Any("data", someValue), // Any type l4g.Err(err), // Error (red))// Grouped attributeslogger.Info("Server started", l4g.Group("server", l4g.String("host", "localhost"), l4g.Int("port", 8080), ), l4g.Group("database", l4g.String("driver", "postgres"), l4g.String("host", "db.example.com"), ),)// Output:// INFO Server started server.host=localhost server.port=8080 database.driver=postgres database.host=db.example.comCustom Handler
Implement your own Handler interface:
type Handler interface { Enabled(Level) bool Handle(Record) error WithAttrs(attrs []Attr) Handler WithGroup(name string) Handler WithPrefix(prefix string) Handler}// Example: JSON Handlertype JSONHandler struct { output io.Writer level l4g.Leveler}func (h *JSONHandler) Enabled(level l4g.Level) bool { return level >= h.level.Level()}func (h *JSONHandler) Handle(r l4g.Record) error { data := map[string]any{ "time": r.Time, "level": r.Level.String(), "msg": r.Message, } r.Attrs(func(attr l4g.Attr) bool { data[attr.Key] = attr.Value.Any() return true }) enc := json.NewEncoder(h.output) return enc.Encode(data)}func (h *JSONHandler) WithAttrs(attrs []l4g.Attr) l4g.Handler { // Implement attribute append logic return h}func (h *JSONHandler) WithGroup(name string) l4g.Handler { // Implement grouping logic return h}func (h *JSONHandler) WithPrefix(prefix string) l4g.Handler { // Implement prefix logic return h}// Use custom Handlerlogger := l4g.New(l4g.Options{ Handler: &JSONHandler{ output: os.Stdout, level: l4g.NewLevelVar(l4g.LevelInfo), },})Use Cases
1. Web Application Logging
import ( "go-slim.dev/l4g" "go-slim.dev/slim")func main() { // Create application logger logger := l4g.New(l4g.Options{ Prefix: "webapp", Level: l4g.LevelInfo, }) s := slim.New() // Request logging middleware s.Use(func(next slim.HandlerFunc) slim.HandlerFunc { return func(c slim.Context) error { start := time.Now() // Create independent logger for each request reqLogger := logger.WithAttrs( "request_id", c.Response().Header().Get("X-Request-ID"), "method", c.Request().Method, "path", c.Request().URL.Path, ) // Store logger in context c.Set("logger", reqLogger) err := next(c) // Log request completion reqLogger.Info("Request completed", "status", c.Response().Status, "duration", time.Since(start), ) return err } }) s.GET("/users/:id", func(c slim.Context) error { logger := c.Get("logger").(*l4g.Logger) logger.Debug("Getting user", "id", c.PathParam("id")) // ... return nil }) logger.Info("Server started", "port", 8080) s.Start(":8080")}2. Microservice Logging
// Service-level loggerserviceLogger := l4g.New(l4g.Options{ Prefix: "user-service",}).WithAttrs( "service", "user-service", "version", "v1.2.3", "instance", hostname,)// Module-level loggertype UserRepository struct { logger *l4g.Logger}func NewUserRepository() *UserRepository { return &UserRepository{ logger: serviceLogger.WithGroup("repository").WithPrefix("db"), }}func (r *UserRepository) FindByID(id int) (*User, error) { r.logger.Debug("Querying user", "user_id", id) user, err := r.queryUser(id) if err != nil { r.logger.Error("Query failed", "user_id", id, l4g.Err(err)) return nil, err } r.logger.Info("Query successful", "user_id", id) return user, nil}3. Background Task Logging
func runBackgroundJob(jobID string) { // Create independent logger for each task jobLogger := l4g.Channel("jobs").WithAttrs( "job_id", jobID, "worker", workerID, ) jobLogger.Info("Job started") defer func() { if r := recover(); r != nil { jobLogger.Panic("Job crashed", "panic", r) } }() // Execute task... for step := range steps { stepLogger := jobLogger.WithAttrs("step", step) stepLogger.Debug("Executing step") if err := executeStep(step); err != nil { stepLogger.Error("Step failed", l4g.Err(err)) return } stepLogger.Info("Step completed") } jobLogger.Info("Job completed")}4. Development/Production Environment Switching
func newLogger() *l4g.Logger { var opts l4g.Options if os.Getenv("ENV") == "production" { // Production: JSON format, Info level, output to file file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) opts = l4g.Options{ Output: file, Level: l4g.LevelInfo, NoColor: true, NewHandlerFunc: NewJSONHandler, // Assuming JSON Handler is implemented } } else { // Development: colored text, Debug level, output to console opts = l4g.Options{ Output: os.Stdout, Level: l4g.LevelDebug, NoColor: false, TimeFormat: "15:04:05", } } return l4g.New(opts)}5. Using in Tests
import ( "bytes" "testing" "go-slim.dev/l4g")func TestUserService(t *testing.T) { // Capture log output var buf bytes.Buffer logger := l4g.New(l4g.Options{ Output: &buf, Level: l4g.LevelDebug, }) service := NewUserService(logger) service.CreateUser("alice") // Verify log content logs := buf.String() if !strings.Contains(logs, "Creating user") { t.Error("Expected log missing") }}// Disable test loggingfunc TestQuiet(t *testing.T) { logger := l4g.New(l4g.Options{ Output: io.Discard, // Discard all logs }) // Test code...}Performance Optimization
Zero-Allocation Logging
When log level is disabled, l4g achieves zero memory allocation:
logger := l4g.New(l4g.Options{Level: l4g.LevelInfo})// Debug is disabled, this call won't allocate memorylogger.Debug("Debug info", "key", "value") // 0 allocs// Info is enabled, will allocate memory normallylogger.Info("General info", "key", "value") // has allocsCheck Level
Check level before building complex log messages:
// Not recommended: always builds expensive objectlogger.Debug("Detailed info", "data", buildExpensiveObject())// Recommended: check level firstif logger.Enabled(l4g.LevelDebug) { logger.Debug("Detailed info", "data", buildExpensiveObject())}Buffer Pool
l4g internally uses sync.Pool to reuse buffers, reducing GC pressure:
// Internal implementation (users don't need to worry)var bufferPool = sync.Pool{ New: func() any { return new(buffer) },}API Reference
Package-Level Functions
// Get and set default loggerfunc Default() *Loggerfunc SetDefault(l *Logger)// Get and set default outputfunc Output() io.Writerfunc SetOutput(w io.Writer)// Get and set default levelfunc GetLevel() Levelfunc SetLevel(level Level)// Log methods (all levels)func Trace(msg string, args ...any)func Debug(msg string, args ...any)func Info(msg string, args ...any)func Warn(msg string, args ...any)func Error(msg string, args ...any)func Panic(msg string, args ...any)func Fatal(msg string, args ...any)// Formatted log methodsfunc Tracef(format string, args ...any)func Debugf(format string, args ...any)func Infof(format string, args ...any)func Warnf(format string, args ...any)func Errorf(format string, args ...any)func Panicf(format string, args ...any)func Fatalf(format string, args ...any)// JSON-style log methodsfunc Tracej(j map[string]any)func Debugj(j map[string]any)func Infoj(j map[string]any)func Warnj(j map[string]any)func Errorj(j map[string]any)func Panicj(j map[string]any)func Fatalj(j map[string]any)// Create logger with attrs/group/prefixfunc WithAttrs(args ...any) *Loggerfunc WithPrefix(prefix string) *Loggerfunc WithGroup(name string) *Logger// Channel loggerfunc Channel(name string) *LoggerLogger Type
type Logger struct { ... }// Create loggerfunc New(opts Options) *Logger// Output controlfunc (l *Logger) Output() io.Writerfunc (l *Logger) SetOutput(w io.Writer)// Level controlfunc (l *Logger) Level() Levelfunc (l *Logger) SetLevel(lvl Level)func (l *Logger) Enabled(level Level) bool// Log methods (same as package-level functions)func (l *Logger) Trace(msg string, args ...any)func (l *Logger) Debug(msg string, args ...any)// ... other levels// Formatted methodsfunc (l *Logger) Tracef(format string, args ...any)func (l *Logger) Debugf(format string, args ...any)// ... other levels// JSON methodsfunc (l *Logger) Tracej(j map[string]any)func (l *Logger) Debugj(j map[string]any)// ... other levels// Create derived loggersfunc (l *Logger) WithAttrs(args ...any) *Loggerfunc (l *Logger) WithPrefix(prefix string) *Loggerfunc (l *Logger) WithGroup(name string) *Logger// Generic log methodsfunc (l *Logger) Log(level Leveler, msg string, args ...any)func (l *Logger) Logf(level Level, format string, args ...any)func (l *Logger) Logj(level Level, j map[string]any)Options Type
type Options struct { Prefix string // Log prefix Level Level // Minimum log level NewHandlerFunc func(HandlerOptions) Handler // Handler factory function Handler Handler // Custom Handler ReplaceAttr func([]string, Attr) Attr // Attribute replacement function TimeFormat string // Time format LevelFormat func(Level) string // Level formatting function PrefixFormat func(string) string // Prefix formatting function Output io.Writer // Output target NoColor bool // Disable colors}Level Type
type Level intfunc (l Level) Int() intfunc (l Level) Real() Levelfunc (l Level) String() stringfunc (l Level) Level() Levelfunc (l Level) MarshalJSON() ([]byte, error)func (l Level) UnmarshalJSON(data []byte) errorfunc (l Level) MarshalText() ([]byte, error)func (l Level) UnmarshalText(data []byte) errorLevelVar Type
type LevelVar struct { ... }func NewLevelVar(lvl Leveler) *LevelVarfunc (v *LevelVar) Level() Levelfunc (v *LevelVar) Set(l Level)func (v *LevelVar) Int() intfunc (v *LevelVar) String() stringAttribute Functions
func String[T ~string](key string, value T) Attrfunc Int[T ~int|~int8|~int16|~int32|~int64](key string, value T) Attrfunc Int64(key string, value int64) Attrfunc Uint[T ~uint|~uint8|~uint16|~uint32|~uint64](key string, value T) Attrfunc Float[T ~float32|~float64](key string, value T) Attrfunc Bool[T ~bool](key string, v T) Attrfunc Time(key string, v time.Time) Attrfunc Duration(key string, value time.Duration) Attrfunc Group(key string, args ...any) Attrfunc Any(key string, value any) Attrfunc Err(err error) Attrfunc ColorAttr(color uint8, attr Attr) AttrNotes
-
Panic and Fatal Behavior:
Panicmethod will callpanic()after loggingFatalmethod will callos.Exit(1)after logging- You can customize exit behavior by setting
l4g.OsExiter(mainly for testing)
-
Concurrency Safety:
Loggertype is concurrency-safe and can be used in multiple goroutinesHandlerimplementations must also be concurrency-safe
-
Level Checking:
- Using
Enabled()method can avoid building unnecessary log parameters - Disabled log levels return quickly, almost zero overhead
- Using
-
WithAttrs/WithGroup/WithPrefix:
- These methods return new Logger instances, don’t modify the original
- It’s safe to use derived loggers in multiple places
-
Attribute Parameter Format:
- Supports
key, value, key, value, ...format - Supports
Attrtype - Can mix both formats
- Supports
-
Time Format:
- Default uses
time.StampMilliformat - Can customize via
Options.TimeFormat
- Default uses
-
Channel Logger:
- Same name returns same instance
- Instances persist throughout program lifecycle
- Can customize creation logic via
l4g.NewFunc