logger

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 12, 2025 License: MIT Imports: 0 Imported by: 3

README

Logger

A minimal, flexible logging interface for Go libraries with multiple implementation backends. This package provides a common logging interface that can be used across all your Go libraries while allowing applications to choose their preferred logging implementation.

Design Philosophy

  • Interface-based: Libraries depend only on the logger.Logger interface
  • Implementation-agnostic: Applications choose slog, zerolog, or custom implementations
  • Minimal: Simple interface with just the essentials
  • Zero-dependency for the core interface
  • Performance-conscious: No unnecessary allocations or complexity

Installation

go get github.com/paularlott/logger

For specific implementations:

# For slog (standard library, no additional dependencies)
# Already available in Go 1.21+

# For zerolog
go get github.com/rs/zerolog

# For testing
# No additional dependencies needed

The Interface

type Logger interface {
    Trace(msg string, keysAndValues ...any)
    Debug(msg string, keysAndValues ...any)
    Info(msg string, keysAndValues ...any)
    Warn(msg string, keysAndValues ...any)
    Error(msg string, keysAndValues ...any)
    With(key string, value any) Logger
    WithError(err error) Logger
    WithGroup(group string) Logger
}

Available Implementations

1. Null Logger (No-op)

Perfect for tests or when logging is disabled:

import "github.com/paularlott/logger"

log := logger.NewNullLogger()
log.Info("this does nothing")
2. Slog Logger (Standard Library)

Uses Go's standard log/slog package with colored console output:

import logslog "github.com/paularlott/logger/slog"

log := logslog.New(logslog.Config{
    Level:  "info",      // trace, debug, info, warn, error
    Format: "console",   // console (colored) or json
    Writer: os.Stdout,   // any io.Writer
})

log.Info("server started", "port", 8080)
// Output: 15:04:05 INF server started port=8080
3. Zerolog Logger

Uses the popular zerolog package:

import logzerolog "github.com/paularlott/logger/zerolog"

log := logzerolog.New(logzerolog.Config{
    Level:  "debug",
    Format: "console",   // console or json
    Writer: os.Stdout,
})

log.Debug("processing request", "user_id", 123)
4. Mock Logger (Testing)

Captures log calls for assertions in tests:

import logtesting "github.com/paularlott/logger/testing"

func TestMyFunction(t *testing.T) {
    mock := logtesting.New()

    // Pass to your code
    myFunction(mock)

    // Assert logs
    if !mock.HasEntry("info", "operation complete") {
        t.Error("expected log entry not found")
    }

    if mock.CountEntries("error") > 0 {
        t.Error("unexpected errors:", mock.String())
    }
}

Usage Patterns

In Libraries

Libraries should accept the logger.Logger interface and provide a sensible default:

package mylib

import "github.com/paularlott/logger"

type Service struct {
    log logger.Logger
}

func NewService(log logger.Logger) *Service {
    if log == nil {
        log = logger.NewNullLogger() // Sensible default
    }
    return &Service{
        log: log.WithGroup("mylib"),
    }
}

func (s *Service) DoWork() error {
    s.log.Info("starting work")

    if err := s.process(); err != nil {
        s.log.WithError(err).Error("work failed")
        return err
    }

    s.log.Info("work completed")
    return nil
}
In Applications - Creating a Log Package

Applications should create their own log package that wraps the chosen implementation and provides package-level functions:

Step 1: Create internal/log/log.go in your application:

package log

import (
    "io"
    "os"

    "github.com/paularlott/logger"
    logslog "github.com/paularlott/logger/slog"
)

var defaultLogger logger.Logger

func init() {
    // Initialize with default configuration
    defaultLogger = logslog.New(logslog.Config{
        Level:  "info",
        Format: "console",
        Writer: os.Stdout,
    })
}

// Configure sets up the logger with the given settings
// Call this early in your main() function
func Configure(level, format string, writer io.Writer) {
    if writer == nil {
        writer = os.Stdout
    }

    defaultLogger = logslog.New(logslog.Config{
        Level:  level,
        Format: format,
        Writer: writer,
    })
}

// GetLogger returns the configured logger instance
// Use this when passing to libraries
func GetLogger() logger.Logger {
    return defaultLogger
}

// Package-level convenience functions
func Trace(msg string, keysAndValues ...any) {
    defaultLogger.Trace(msg, keysAndValues...)
}

func Debug(msg string, keysAndValues ...any) {
    defaultLogger.Debug(msg, keysAndValues...)
}

func Info(msg string, keysAndValues ...any) {
    defaultLogger.Info(msg, keysAndValues...)
}

func Warn(msg string, keysAndValues ...any) {
    defaultLogger.Warn(msg, keysAndValues...)
}

func Error(msg string, keysAndValues ...any) {
    defaultLogger.Error(msg, keysAndValues...)
}

func With(key string, value any) logger.Logger {
    return defaultLogger.With(key, value)
}

func WithError(err error) logger.Logger {
    return defaultLogger.WithError(err)
}

func WithGroup(group string) logger.Logger {
    return defaultLogger.WithGroup(group)
}

Step 2: Use in your application:

package main

import (
    "flag"
    "os"

    "yourapp/internal/log"
    "yourapp/internal/service"
)

func main() {
    level := flag.String("log-level", "info", "Log level (trace|debug|info|warn|error)")
    format := flag.String("log-format", "console", "Log format (console|json)")
    flag.Parse()

    // Configure logging
    log.Configure(*level, *format, os.Stdout)

    log.Info("application starting", "version", "1.0.0")

    // Pass logger to libraries
    svc := service.New(log.GetLogger())

    if err := svc.Run(); err != nil {
        log.WithError(err).Error("service failed")
        os.Exit(1)
    }

    log.Info("application stopped")
}

Step 3: Use package-level functions throughout your application:

package handlers

import "yourapp/internal/log"

func HandleRequest(w http.ResponseWriter, r *http.Request) {
    log.Info("handling request", "method", r.Method, "path", r.URL.Path)

    // Use With for contextual logging
    reqLog := log.With("request_id", getRequestID(r))
    reqLog.Debug("processing")

    // Error handling
    if err := process(r); err != nil {
        reqLog.WithError(err).Error("processing failed")
        http.Error(w, "Internal error", 500)
        return
    }

    reqLog.Info("request completed")
}
Switching Implementations

To switch from slog to zerolog, just change your log package's Configure function:

import logzerolog "github.com/paularlott/logger/zerolog"

func Configure(level, format string, writer io.Writer) {
    if writer == nil {
        writer = os.Stdout
    }

    // Changed from logslog.New to logzerolog.New
    defaultLogger = logzerolog.New(logzerolog.Config{
        Level:  level,
        Format: format,
        Writer: writer,
    })
}

No other code needs to change!

Structured Logging

All implementations support structured key-value logging:

// Inline key-value pairs
log.Info("user logged in", "user_id", 123, "username", "john")

// With creates a child logger with persistent fields
userLog := log.With("user_id", 123).With("session", "abc")
userLog.Info("action performed")  // Includes user_id and session
userLog.Debug("another action")   // Includes user_id and session

// WithError for error context
if err := doSomething(); err != nil {
    log.WithError(err).Error("operation failed")
}

// WithGroup for component context
dbLog := log.WithGroup("database")
dbLog.Info("connected", "host", "localhost")
// Output: 15:04:05 INF database: connected host=localhost

Log Levels

  • Trace: Very detailed diagnostic information
  • Debug: Detailed information for debugging
  • Info: General informational messages
  • Warn: Warning messages for concerning but non-critical issues
  • Error: Error messages for failures

Output Formats

Console (Colored)

Provides human-readable colored output similar to zerolog:

15:04:05 INF server started port=8080 version=1.0
15:04:05 DBG connection opened conn_id=123
15:04:05 WRN cache miss key=user:456
15:04:05 ERR request failed error="connection timeout"
JSON

Machine-readable structured logs:

{"time":"2025-10-15T15:04:05Z","level":"info","msg":"server started","port":8080}
{"time":"2025-10-15T15:04:05Z","level":"error","msg":"request failed","error":"timeout"}

Best Practices

1. Accept the Interface in Libraries
// Good: Accepts interface
func NewService(log logger.Logger) *Service

// Bad: Depends on specific implementation
func NewService(log *slog.Logger) *Service
2. Provide Sensible Defaults
func NewService(log logger.Logger) *Service {
    if log == nil {
        log = logger.NewNullLogger()
    }
    // ...
}
3. Use Groups for Components
func NewDatabase(log logger.Logger) *Database {
    return &Database{
        log: log.WithGroup("database"),
    }
}
4. Don't Log and Return Errors
// Bad: Logs AND returns
if err != nil {
    log.Error("failed", "error", err)
    return err
}

// Good: Let caller decide
if err != nil {
    return fmt.Errorf("operation failed: %w", err)
}

// Good: Log when handling
if err := doWork(); err != nil {
    log.WithError(err).Error("work failed")
    // Handle the error
}
5. Use Appropriate Log Levels
log.Trace("entering function", "args", args)           // Very detailed
log.Debug("cache miss", "key", key)                    // Debugging info
log.Info("server started", "port", port)               // Important events
log.Warn("deprecated feature used", "feature", name)   // Warnings
log.Error("request failed", "error", err)              // Errors

Testing

Use the mock logger to verify logging behavior:

func TestService(t *testing.T) {
    mock := logtesting.New()
    svc := NewService(mock)

    err := svc.ProcessData("invalid")

    // Verify error was logged
    if !mock.HasEntry("error", "invalid data") {
        t.Error("expected error log")
    }

    // Check attributes
    lastEntry := mock.LastEntry()
    if lastEntry.Attrs["data"] != "invalid" {
        t.Error("expected data attribute")
    }

    // Print all logs for debugging
    t.Log(mock.String())
}

Migration from Other Loggers

From logrus:
// Before
log.WithFields(log.Fields{"user": 123}).Info("logged in")

// After
log.With("user", 123).Info("logged in")
From zap:
// Before
log.Info("logged in", zap.Int("user", 123))

// After
log.Info("logged in", "user", 123)
From standard log:
// Before
log.Printf("user %d logged in", userID)

// After
log.Info("user logged in", "user_id", userID)

Examples

Complete working examples are available in the example/ directory:

Both examples demonstrate:

  • Creating an application-level log package with convenience functions
  • Configuring the logger with command-line flags
  • Different log levels and output formats (console with colors, JSON)
  • Contextual logging with With() and WithGroup()
  • Integration with services that accept the logger interface

See example/README.md for details on running the examples and comparing outputs.

License

See LICENSE.txt

Contributing

This is a minimal interface by design. New methods should only be added if they're essential for all logging use cases.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Logger

type Logger interface {
	Trace(msg string, keysAndValues ...any)
	Debug(msg string, keysAndValues ...any)
	Info(msg string, keysAndValues ...any)
	Warn(msg string, keysAndValues ...any)
	Error(msg string, keysAndValues ...any)
	Fatal(msg string, keysAndValues ...any) // Logs and exits with status 1
	With(key string, value any) Logger
	WithError(err error) Logger
	WithGroup(group string) Logger
}

Logger is the minimal interface all paularlott/* libraries accept

func NewNullLogger

func NewNullLogger() Logger

type NullLogger

type NullLogger struct{}

NullLogger is a no-op logger implementation

func (NullLogger) Debug

func (NullLogger) Debug(msg string, keysAndValues ...any)

func (NullLogger) Error

func (NullLogger) Error(msg string, keysAndValues ...any)

func (NullLogger) Fatal added in v0.2.0

func (NullLogger) Fatal(msg string, keysAndValues ...any)

func (NullLogger) Info

func (NullLogger) Info(msg string, keysAndValues ...any)

func (NullLogger) Trace

func (NullLogger) Trace(msg string, keysAndValues ...any)

func (NullLogger) Warn

func (NullLogger) Warn(msg string, keysAndValues ...any)

func (NullLogger) With

func (n NullLogger) With(key string, value any) Logger

func (NullLogger) WithError

func (n NullLogger) WithError(err error) Logger

func (NullLogger) WithGroup

func (n NullLogger) WithGroup(group string) Logger

Directories

Path Synopsis
example
slog command
zerolog command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL