Basic Usage

Learn the fundamentals of structured logging with handler types and output formats

This guide covers the essential operations for working with the logging package. Learn to choose handler types, set log levels, and produce structured log output.

Handler Types

The logging package supports three output formats, each optimized for different use cases.

JSON Handler (Production)

JSON format is ideal for production environments and log aggregation systems:

log := logging.MustNew(
    logging.WithJSONHandler(),
)

log.Info("user action", "user_id", "123", "action", "login")

Output:

{"time":"2024-01-15T10:30:45.123Z","level":"INFO","msg":"user action","user_id":"123","action":"login"}

Use cases:

  • Production environments.
  • Log aggregation systems like ELK, Splunk, Datadog.
  • Machine-parseable logs.
  • Cloud logging services.

Text Handler

Text format outputs key=value pairs, readable but still parseable:

log := logging.MustNew(
    logging.WithTextHandler(),
)

log.Info("request processed", "method", "GET", "path", "/api/users")

Output:

time=2024-01-15T10:30:45.123Z level=INFO msg="request processed" method=GET path=/api/users

Use cases:

  • Systems that prefer key=value format
  • Legacy log parsers
  • Environments where JSON is too verbose

Console Handler (Development)

Console format provides human-readable colored output for development:

log := logging.MustNew(
    logging.WithConsoleHandler(),
)

log.Info("server starting", "port", 8080)

Output (with colors):

10:30:45.123 INFO  server starting port=8080

Use cases:

  • Local development.
  • Debugging.
  • Terminal output.
  • Interactive troubleshooting.

Note: Console handler uses ANSI colors automatically. Colors are optimized for dark terminal themes.

Log Levels

Control log verbosity with log levels. Each level has a specific purpose.

Available Levels

// From most to least verbose:
logging.LevelDebug   // Detailed debugging information
logging.LevelInfo    // General informational messages
logging.LevelWarn    // Warning messages (not errors)
logging.LevelError   // Error messages

Setting Log Level

Configure the minimum log level during initialization:

log := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),  // Only Info, Warn, Error
)

log.Debug("this won't appear")  // Filtered out
log.Info("this will appear")    // Logged
log.Error("this will appear")   // Logged

Debug Level Shortcut

Enable debug logging with a convenience function:

log := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithDebugLevel(),  // Same as WithLevel(logging.LevelDebug)
)

Level Usage Guidelines

DEBUG - Detailed information for debugging

log.Debug("cache hit", "key", cacheKey, "ttl", ttl)

INFO - General informational messages

log.Info("server started", "port", 8080)

WARN - Warning but not an error

log.Warn("high memory usage", "used_mb", 8192, "total_mb", 16384)

ERROR - Errors that need attention

log.Error("database connection failed", "error", err, "retry_count", retries)

Structured Logging

The logging package uses structured logging with key-value pairs, not string concatenation.

Basic Structured Fields

log.Info("user logged in",
    "user_id", userID,
    "ip_address", ipAddress,
    "session_id", sessionID,
)

Output (JSON):

{
  "time": "2024-01-15T10:30:45.123Z",
  "level": "INFO",
  "msg": "user logged in",
  "user_id": "123",
  "ip_address": "192.168.1.1",
  "session_id": "abc-xyz"
}

Why Structured Logging?

BAD - String concatenation:

log.Info("User " + userID + " logged in from " + ipAddress)

GOOD - Structured fields:

log.Info("user logged in",
    "user_id", userID,
    "ip_address", ipAddress,
)

Benefits:

  • Machine-parseable
  • Searchable by field
  • Type-safe (numbers stay numbers)
  • Easier to aggregate and analyze

Type Support

The logger handles various types automatically:

log.Info("operation details",
    "name", "process_data",           // string
    "count", 1024,                     // int
    "enabled", true,                   // bool
    "duration", 250*time.Millisecond,  // duration
    "rate", 99.5,                      // float64
    "timestamp", time.Now(),           // time
    "error", err,                      // error
)

Complete Example

Putting it all together:

package main

import (
    "rivaas.dev/logging"
)

func main() {
    // Create logger for development
    log := logging.MustNew(
        logging.WithConsoleHandler(),
        logging.WithDebugLevel(),
    )

    // Log at different levels
    log.Debug("application starting", "version", "v1.0.0")
    log.Info("server listening", "port", 8080, "env", "development")
    log.Warn("high latency detected", "latency_ms", 250, "threshold_ms", 200)
    log.Error("database connection failed", "error", "connection timeout")
}

Common Patterns

Logging with Context

Add related fields that persist across multiple log calls:

// Create a logger with persistent fields
requestLog := log.With(
    "request_id", "req-123",
    "user_id", "user-456",
)

requestLog.Info("validation started")
requestLog.Info("validation completed")
// Both logs include request_id and user_id

Logging Errors

Always include the error:

if err := db.Connect(); err != nil {
    log.Error("database connection failed",
        "error", err,
        "host", dbHost,
        "port", dbPort,
        "retry_count", retries,
    )
}

Avoid Logging in Tight Loops

// BAD - logs thousands of times
for _, item := range items {
    log.Debug("processing", "item", item)
    process(item)
}

// GOOD - log once with summary
log.Info("processing batch", "count", len(items))
for _, item := range items {
    process(item)
}
log.Info("batch completed", "processed", len(items))

Next Steps

For complete API details, see the API Reference.