Skip to main content

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/l4g

Quick Start

package main

import "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=value2

Example output:

Jan  2 15:04:05 INFO [myapp] User logged in user=alice ip=192.168.1.100
Jan 2 15:04:06 ERROR [myapp] Database error error="connection timeout" retry=3

Basic 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 methods
logger.Trace("Trace", "detail", "...")
logger.Debug("Debug", "var", value)
logger.Info("Info", "status", "ok")
logger.Warn("Warning", "threshold", 90)
logger.Error("Error", "error", err)

// Formatted methods
logger.Debugf("Variable value: %v", value)
logger.Infof("Request processing time: %v", duration)
logger.Errorf("Failure: %s", reason)

// JSON-style methods
logger.Debugj(map[string]any{"query": sql, "duration": dur})
logger.Infoj(map[string]any{"event": "request", "path": path})

// Panic and Fatal
logger.Panic("Serious error", "reason", reason) // Will panic after logging
logger.Fatal("Fatal error", "error", err) // Will call os.Exit(1) after logging

Advanced Features

1. Structured Attributes

Use WithAttrs to create a logger with fixed attributes:

// Create logger with request ID
requestLogger := logger.WithAttrs("request_id", reqID, "user", userID)

// All logs will include these attributes
requestLogger.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=150ms

Chaining:

logger.
WithAttrs("service", "api").
WithAttrs("version", "v1.0").
Info("Service started")

2. Attribute Grouping

Use WithGroup to organize related attributes:

// Create logger with grouping
httpLogger := logger.WithGroup("http")
httpLogger.Info("Request", "method", "GET", "path", "/api/users")

// Output: INFO Request http.method=GET http.path=/api/users

// Nested grouping
serverLogger := logger.WithGroup("server").WithGroup("http")
serverLogger.Info("Listening", "port", 8080)

// Output: INFO Listening server.http.port=8080

3. Log Prefix

Use WithPrefix to add a prefix to logs:

// Create logger with prefix
apiLogger := logger.WithPrefix("api")
apiLogger.Info("Request processing completed")

// Output: INFO [api] Request processing completed

// Chained prefixes
moduleLogger := logger.WithPrefix("auth").WithPrefix("jwt")
moduleLogger.Debug("Verifying token")

// Output: DEBUG [jwtauth] Verifying token

4. 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 database
dbLogger := l4g.Channel("database")
dbLogger.Debug("Executing query", "sql", query)

// package cache
cacheLogger := l4g.Channel("cache")
cacheLogger.Info("Cache hit", "key", key)

// Same name returns same instance
logger1 := l4g.Channel("myapp")
logger2 := l4g.Channel("myapp")
// logger1 == logger2 (true)

Custom Channel creation function:

// Set custom logger creation function
l4g.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 configuration
logger := 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 runtime
levelVar.Set(l4g.LevelDebug)

logger.Debug("Debug info") // Now will output

6. 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 file
file, _ := 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 output
file, _ := 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 attribute
attr := 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 grayscale

Attribute 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 attributes
logger.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.com

Custom 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 Handler
type 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 Handler
logger := 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 logger
serviceLogger := l4g.New(l4g.Options{
Prefix: "user-service",
}).WithAttrs(
"service", "user-service",
"version", "v1.2.3",
"instance", hostname,
)

// Module-level logger
type 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 logging
func 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 memory
logger.Debug("Debug info", "key", "value") // 0 allocs

// Info is enabled, will allocate memory normally
logger.Info("General info", "key", "value") // has allocs

Check Level

Check level before building complex log messages:

// Not recommended: always builds expensive object
logger.Debug("Detailed info", "data", buildExpensiveObject())

// Recommended: check level first
if 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 logger
func Default() *Logger
func SetDefault(l *Logger)

// Get and set default output
func Output() io.Writer
func SetOutput(w io.Writer)

// Get and set default level
func GetLevel() Level
func 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 methods
func 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 methods
func 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/prefix
func WithAttrs(args ...any) *Logger
func WithPrefix(prefix string) *Logger
func WithGroup(name string) *Logger

// Channel logger
func Channel(name string) *Logger

Logger Type

type Logger struct { ... }

// Create logger
func New(opts Options) *Logger

// Output control
func (l *Logger) Output() io.Writer
func (l *Logger) SetOutput(w io.Writer)

// Level control
func (l *Logger) Level() Level
func (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 methods
func (l *Logger) Tracef(format string, args ...any)
func (l *Logger) Debugf(format string, args ...any)
// ... other levels

// JSON methods
func (l *Logger) Tracej(j map[string]any)
func (l *Logger) Debugj(j map[string]any)
// ... other levels

// Create derived loggers
func (l *Logger) WithAttrs(args ...any) *Logger
func (l *Logger) WithPrefix(prefix string) *Logger
func (l *Logger) WithGroup(name string) *Logger

// Generic log methods
func (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 int

func (l Level) Int() int
func (l Level) Real() Level
func (l Level) String() string
func (l Level) Level() Level
func (l Level) MarshalJSON() ([]byte, error)
func (l Level) UnmarshalJSON(data []byte) error
func (l Level) MarshalText() ([]byte, error)
func (l Level) UnmarshalText(data []byte) error

LevelVar Type

type LevelVar struct { ... }

func NewLevelVar(lvl Leveler) *LevelVar
func (v *LevelVar) Level() Level
func (v *LevelVar) Set(l Level)
func (v *LevelVar) Int() int
func (v *LevelVar) String() string

Attribute Functions

func String[T ~string](key string, value T) Attr
func Int[T ~int|~int8|~int16|~int32|~int64](key string, value T) Attr
func Int64(key string, value int64) Attr
func Uint[T ~uint|~uint8|~uint16|~uint32|~uint64](key string, value T) Attr
func Float[T ~float32|~float64](key string, value T) Attr
func Bool[T ~bool](key string, v T) Attr
func Time(key string, v time.Time) Attr
func Duration(key string, value time.Duration) Attr
func Group(key string, args ...any) Attr
func Any(key string, value any) Attr
func Err(err error) Attr
func ColorAttr(color uint8, attr Attr) Attr

Notes

  1. Panic and Fatal Behavior:

    • Panic method will call panic() after logging
    • Fatal method will call os.Exit(1) after logging
    • You can customize exit behavior by setting l4g.OsExiter (mainly for testing)
  2. Concurrency Safety:

    • Logger type is concurrency-safe and can be used in multiple goroutines
    • Handler implementations must also be concurrency-safe
  3. Level Checking:

    • Using Enabled() method can avoid building unnecessary log parameters
    • Disabled log levels return quickly, almost zero overhead
  4. WithAttrs/WithGroup/WithPrefix:

    • These methods return new Logger instances, don't modify the original
    • It's safe to use derived loggers in multiple places
  5. Attribute Parameter Format:

    • Supports key, value, key, value, ... format
    • Supports Attr type
    • Can mix both formats
  6. Time Format:

    • Default uses time.StampMilli format
    • Can customize via Options.TimeFormat
  7. Channel Logger:

    • Same name returns same instance
    • Instances persist throughout program lifecycle
    • Can customize creation logic via l4g.NewFunc