Slim

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

Example 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=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 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 logging

Advanced 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=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 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=8080

3. 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 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 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 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 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 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 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.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 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 allocs

Check 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) *Logger

Logger 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) error

LevelVar 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() string

Attribute 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) 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
Navigate Select esc Close search