Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mnah05-boiler-go-21-79.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Boiler-Go uses zerolog for fast, structured logging. The logger supports multiple output destinations including console and file logging.

Logger architecture

The logging system is implemented in pkg/logger/logger.go with three main components:

Global logger

Singleton instance for fallback logging

Output configuration

Flexible stdout and file output

Thread safety

Safe concurrent access with sync.Once

Output configuration

The logger supports three output modes configured via LOG_OUTPUT environment variable:
type OutputConfig struct {
    // FilePath is the path to the log file. If empty, no file logging.
    FilePath string
    // Stdout enables console output.
    Stdout bool
    // StdoutOnly disables file output and only logs to stdout.
    StdoutOnly bool
}

Output modes

Logs only to console with human-readable format:
LOG_OUTPUT=stdout
This is the default mode and recommended for development.

Creating loggers

Basic logger

Create a logger with default stdout configuration:
import "boiler-go/pkg/logger"

logg := logger.New()
logg.Info().Msg("application starting")

Configured logger

Both cmd/api/main.go and cmd/worker/main.go implement the newLogger() function:
func newLogger(cfg *config.Config, defaultFile string) zerolog.Logger {
    outputCfg := logger.OutputConfig{}
    
    switch cfg.LogOutput {
    case "stdout":
        outputCfg.Stdout = true
        outputCfg.StdoutOnly = true
    case "file":
        outputCfg.Stdout = false
        outputCfg.StdoutOnly = false
        outputCfg.FilePath = cfg.LogFile
        if outputCfg.FilePath == "" {
            outputCfg.FilePath = defaultFile
        }
    case "both":
        outputCfg.Stdout = true
        outputCfg.StdoutOnly = false
        outputCfg.FilePath = cfg.LogFile
        if outputCfg.FilePath == "" {
            outputCfg.FilePath = defaultFile
        }
    }
    
    return logger.NewWithOutput(outputCfg)
}

Usage in applications

// cmd/api/main.go:56
logg := newLogger(cfg, "logs/api.log")

Console output format

The console writer uses a human-readable format with timestamps:
zerolog.ConsoleWriter{
    Out:        os.Stdout,
    TimeFormat: "2006-01-02 15:04:05",
}
Example output:
2024-03-15 14:23:10 INF server starting port=8080
2024-03-15 14:23:10 INF database connected
2024-03-15 14:23:10 INF redis connected

Structured logging

Zerolog supports structured fields for better log analysis:
logg.Info().
    Str("user_id", "123").           // String field
    Int("count", 42).                // Integer field
    Dur("duration", elapsed).        // Duration field
    Err(err).                        // Error field
    Bool("success", true).           // Boolean field
    Time("created_at", time.Now()).  // Time field
    Msg("operation completed")

Real-world examples

// cmd/api/main.go:102-104
logg.Info().
    Str("port", cfg.AppPort).
    Msg("server starting")

Log levels

Zerolog supports multiple log levels (default level is Info):
logg.Debug().Msg("detailed debugging information")
logg.Info().Msg("informational message")
logg.Warn().Msg("warning message")
logg.Error().Err(err).Msg("error occurred")
logg.Fatal().Err(err).Msg("critical error, exiting") // Calls os.Exit(1)
Fatal() calls os.Exit(1) after logging. Use it only for unrecoverable errors during startup.

Fatal logging examples

// cmd/api/main.go:63
if err := db.Open(dbCtx, cfg); err != nil {
    logg.Fatal().Err(err).Msg("failed to initialize database")
}

File logging

When file output is enabled, the logger automatically:
  1. Creates the log directory if it doesn’t exist
  2. Opens the file in append mode
  3. Falls back to stdout if file operations fail
// pkg/logger/logger.go:49-56
dir := filepath.Dir(cfg.FilePath)
if dir != "" && dir != "." {
    if err := os.MkdirAll(dir, 0755); err != nil {
        fmt.Fprintf(os.Stderr, "failed to create log directory: %v\n", err)
        return zerolog.New(os.Stdout).With().Timestamp().Logger()
    }
}

file, err := os.OpenFile(cfg.FilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
The logger creates parent directories automatically. You don’t need to manually create logs/ directories.

Multi-writer output

When logging to both stdout and file, zerolog uses MultiLevelWriter:
// pkg/logger/logger.go:79
output = zerolog.MultiLevelWriter(writers...)
This ensures logs are written atomically to all destinations.

Global logger

The package provides a global logger for cases where dependency injection isn’t available:
import "boiler-go/pkg/logger"

func someFunction() {
    logg := logger.Global()
    logg.Info().Msg("using global logger")
}
Prefer passing loggers explicitly over using the global instance. This makes testing easier and dependencies clearer.
The global logger is initialized lazily:
// pkg/logger/logger.go:87-92
func Global() zerolog.Logger {
    globalOnce.Do(func() {
        global = New()
    })
    return global
}

Best practices

  1. Pass loggers as parameters: Avoid relying on the global logger
  2. Use structured fields: Include contextual information in every log
  3. Log at appropriate levels: Reserve Error for actual errors, use Info for normal operation
  4. Include correlation IDs: Add request IDs to track requests across services
  5. Don’t log sensitive data: Avoid logging passwords, tokens, or PII

Request logging example

func HandleRequest(w http.ResponseWriter, r *http.Request, logg zerolog.Logger) {
    requestID := generateRequestID()
    
    logg.Info().
        Str("request_id", requestID).
        Str("method", r.Method).
        Str("path", r.URL.Path).
        Str("remote_addr", r.RemoteAddr).
        Msg("request started")
    
    // Process request...
    
    logg.Info().
        Str("request_id", requestID).
        Int("status", 200).
        Msg("request completed")
}

Configuration reference

LOG_OUTPUT
string
default:"stdout"
Output destination: stdout, file, or both
LOG_FILE
string
File path when using file or both mode. Defaults:
  • API: logs/api.log
  • Worker: logs/worker.log