darvaza.org/slog
provides a backend-agnostic interface for structured logging
in Go. It defines a simple, standardised API that libraries can use without
forcing a specific logging implementation on their users.
- Backend-agnostic: Define logging interfaces without forcing implementation choices.
- Structured logging: Support for typed fields with string keys.
- Method chaining: Fluent API for composing log entries.
- Six log levels: Debug, Info, Warn, Error, Fatal, and Panic.
- Context integration: Store and retrieve loggers from context values.
- Standard library compatible: Adapters for Go's standard
log
package. - Multiple handlers: Pre-built integrations with popular logging libraries.
- Immutable logger instances: Each modification creates a new logger, enabling safe concurrent use and proper branching behaviour.
go get darvaza.org/slog
package main
import (
"darvaza.org/slog"
"darvaza.org/slog/handlers/discard"
)
func main() {
// Create a logger (using discard handler for example)
logger := discard.New()
// Log with different levels
logger.Info().Print("Application started")
// Add fields
logger.Debug().
WithField("user", "john").
WithField("action", "login").
Print("User logged in")
// Use Printf-style formatting
logger.Warn().
WithField("retry_count", 3).
Printf("Connection failed, will retry")
}
The slog.Logger
interface provides a fluent API where most methods return a
Logger
for method chaining. A log entry is composed by:
- Setting the log level
- Optionally adding fields and call stack information
- Emitting the entry with a Print method
Disabled log entries incur minimal overhead as string formatting and field collection are skipped.
The library supports six log levels with clear semantics:
- Debug: Detailed information for developers.
- Info: General informational messages.
- Warn: Warning messages for potentially harmful situations.
- Error: Error conditions that allow continued operation.
- Fatal: Critical errors that terminate the program (like
log.Fatal()
). - Panic: Errors that trigger a recoverable panic (like
log.Panic()
).
Create log entries using named methods (Debug()
, Info()
, etc.) or
WithLevel(level)
.
A log entry is "enabled" if the handler will actually emit it. Operating on disabled loggers is safe and efficient - string formatting and field collection are skipped.
Use WithEnabled()
to check if a level is enabled:
if log, ok := logger.Debug().WithEnabled(); ok {
// Expensive debug logging
log.WithField("details", expensiveOperation()).Print("Debug info")
} else if log, ok := logger.Info().WithEnabled(); ok {
// Simpler info logging
log.Print("Operation completed")
}
Note: Fatal and Panic levels always execute regardless of enabled state.
Fields are key/value pairs for structured logging:
- Keys must be non-empty strings
- Values can be any type
- Fields are attached using
WithField(key, value)
- Multiple fields can be attached by chaining calls
import "time"
start := time.Now()
// ... perform some work ...
logger.Info().
WithField("user_id", 123).
WithField("duration", time.Since(start)).
Print("Request processed")
Each logger instance is immutable. When you call methods like WithField()
or
WithLevel()
, you get a new logger instance that inherits from the parent:
// Create a base logger with common fields
baseLogger := logger.WithField("service", "api")
// Branch off for different request handlers
userLogger := baseLogger.WithField("handler", "user")
adminLogger := baseLogger.WithField("handler", "admin")
// Each logger maintains its own field chain
userLogger.Info().Print("Processing user request")
// Output includes: service=api, handler=user
adminLogger.Info().Print("Processing admin request")
// Output includes: service=api, handler=admin
// Original logger is unchanged
baseLogger.Info().Print("Base logger message") // only has service=api
This design ensures:
- Thread-safe concurrent use without locks
- No unintended field pollution between different code paths
- Clear ownership and lifecycle of logger configurations
Attach stack traces to log entries using WithStack(skip)
:
logger.Error().
WithStack(0). // 0 = current function
WithField("error", err).
Print("Operation failed")
The skip
parameter specifies how many stack frames to skip (0 = current
function).
Three print methods match the fmt
package conventions:
Print(v ...any)
: Likefmt.Print
Println(v ...any)
: Likefmt.Println
Printf(format string, v ...any)
: Likefmt.Printf
These methods emit the log entry with all attached fields.
Integrate with Go's standard log
package:
import (
"log"
"net/http"
"darvaza.org/slog"
)
// Assuming you have a slog logger instance
// var logger slog.Logger
// Create a standard logger that writes to slog
stdLogger := slog.NewStdLogger(logger, "[HTTP]", log.LstdFlags)
// Use with libraries expecting *log.Logger
server := &http.Server{
ErrorLog: stdLogger,
}
For custom parsing, use NewLogWriter()
with a handler function.
┌────────────────────────────────────────────────────────────────────┐
│ External Dependencies │
├─────────────────────────────┬──────────────────────────────────────┤
│ darvaza.org/core │ Go Standard Library │
└─────────────┬───────────────┴───────────┬──────────────────────────┘
│ │
▼ ▼
┌────────────────────────────────────────────────────────────────────┐
│ slog Core │
├──────────────────────┬─────────────────────┬───────────────────────┤
│ Logger Interface │ Context Integration │ Std Library Adapter │
├──────────────────────┴─────────────────────┴───────────────────────┤
│ internal.Loglet (field chain management) │
└──────────┬─────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────┐
│ Handlers │
├─────────┬─────────┬─────────┬─────────┬─────────┬────────┬─────────┤
│ logr │ logrus │ zap │ zerolog │ cblog │ filter │ discard │
└─────────┴─────────┴─────────┴─────────┴─────────┴────────┴─────────┘
All handlers use the internal.Loglet
type for consistent field chain
management and immutable logger behaviour.
Handlers fall into two categories based on their integration capabilities:
These handlers allow conversion in both directions - you can use the external logging library as a slog backend, OR use slog as a backend for the external library:
- logr:
Full bidirectional adapter for go-logr/logr interface.
logr.Logger
→slog.Logger
(use logr as slog backend)slog.Logger
→logr.Logger
(use slog as logr backend)
logrus
: Bidirectional adapter for Sirupsen/logrus.logrus.Logger
→slog.Logger
(use logrus as slog backend)slog.Logger
→logrus.Logger
(use slog as logrus backend)
zap
: Bidirectional adapter between Uber's zap and slog. Use zap as a slog backend or create zap loggers backed by any slog implementation.
These handlers only allow using the external logging library as a backend for slog. They wrap existing loggers but don't provide the reverse conversion:
zerolog
: Wraps rs/zerolog as a slog backend.
These handlers provide additional functionality without external dependencies:
-
cblog
: Channel-based handler for custom log processing. -
filter
: Middleware to filter and transform log entries. -
mock
: Mock logger implementation that records messages for testing and verification. Provides a fully functional slog.Logger that captures all log entries with their levels, messages, and fields for programmatic inspection.import ( "testing" "darvaza.org/slog/handlers/mock" ) func TestMyCode(t *testing.T) { logger := mock.NewLogger() // Use logger in your code myFunction(logger) // Verify what was logged messages := logger.GetMessages() if len(messages) != 1 { t.Fatalf("expected 1 message, got %d", len(messages)) } msg := messages[0] if msg.Level != slog.Info || msg.Message != "expected message" { t.Errorf("unexpected log entry: %v", msg) } }
-
discard
: No-op handler for testing and optional logging.
Bidirectional adapters are valuable when:
- Integration with libraries that expect a specific logger interface is required.
- Gradual migration between logging systems is in progress.
- A common interface is desired across different application components while maintaining compatibility with existing code.
Unidirectional adapters are simpler and suitable when:
- An existing logger serves as the slog backend without reverse integration.
- New applications can adopt slog as the primary logging interface.
- Libraries expecting the backend's specific interface are not a concern.
The package provides comprehensive test utilities for handler implementations in
internal/testing
. These utilities help ensure consistent testing patterns and
reduce code duplication across handlers.
See internal/testing/README.md for detailed documentation on using the test utilities, including:
- Test logger for recording and verifying log messages
- Assertion helpers for message verification
- Compliance test suite for interface conformance
- Concurrency testing utilities
See AGENT.md for development guidelines and LICENCE.txt for licensing information.