Structured logging with zerolog, supporting flexible output destinations (console, file, or both).
- Flexible Output: Console, file, or both simultaneously
- Structured Logging: JSON format for files, human-readable for console
- Multiple Log Levels: Debug, Info, Warn, Error
- Component Loggers: Create loggers for specific components
- Context Support: Pass context values to loggers
- Zero Dependencies: Only stdlib + zerolog
- OS-Managed Rotation: Use logrotate or similar tools for file rotation
import (
"github.com/jasoet/pkg/v2/logging"
"github.com/rs/zerolog/log"
)
func main() {
// Initialize with console output
if err := logging.Initialize("my-service", true); err != nil { // debug=true
log.Fatal().Err(err).Msg("failed to initialize logging")
}
// Use global logger
log.Info().Msg("Service started")
log.Debug().Str("config", "loaded").Msg("Configuration loaded")
}import "github.com/jasoet/pkg/v2/logging"
func main() {
// All logs go to file (no console output)
closer, err := logging.InitializeWithFile("my-service", false,
logging.OutputFile,
&logging.FileConfig{
Path: "/var/log/myapp/app.log",
})
if err != nil {
log.Fatal().Err(err).Msg("failed to initialize logging")
}
defer closer.Close()
log.Info().Msg("This goes to file only")
}import "github.com/jasoet/pkg/v2/logging"
func main() {
// Logs appear in both console and file
closer, err := logging.InitializeWithFile("my-service", true,
logging.OutputConsole | logging.OutputFile, // Bitwise OR
&logging.FileConfig{
Path: "/var/log/myapp/app.log",
})
if err != nil {
log.Fatal().Err(err).Msg("failed to initialize logging")
}
defer closer.Close()
log.Info().Msg("Visible in console AND file")
}func Initialize(serviceName string, debug bool) errorSets up console-only logging. Returns an error if initialization fails.
Parameters:
serviceName: Service name added to all logsdebug: If true, sets level to Debug; otherwise Info
Example:
if err := logging.Initialize("my-service", true); err != nil {
log.Fatal().Err(err).Msg("failed to initialize logging")
}func InitializeWithFile(serviceName string, debug bool, output OutputDestination, fileConfig *FileConfig) (io.Closer, error)Sets up logging with flexible output destinations. Returns an io.Closer (non-nil when file
output is enabled) that must be closed by the caller (typically via defer), and an error if
the configuration is invalid or the log file cannot be opened.
Parameters:
serviceName: Service name added to all logsdebug: If true, sets level to Debug; otherwise Infooutput: Output destination flags (OutputConsole, OutputFile, or both)fileConfig: File configuration (required if OutputFile specified)
Output Formats:
- Console: Human-readable, colored (via
zerolog.ConsoleWriter) - File: JSON format for parsing and log aggregation
Examples:
// Console only
_, err := logging.InitializeWithFile("service", true, logging.OutputConsole, nil)
// File only
closer, err := logging.InitializeWithFile("service", false,
logging.OutputFile,
&logging.FileConfig{Path: "app.log"})
if err != nil { log.Fatal(err) }
defer closer.Close()
// Both
closer, err := logging.InitializeWithFile("service", true,
logging.OutputConsole | logging.OutputFile,
&logging.FileConfig{Path: "app.log"})
if err != nil { log.Fatal(err) }
defer closer.Close()func ContextLogger(ctx context.Context, component string) zerolog.LoggerCreates a component-specific logger with context values.
Parameters:
ctx: Context (values will be added to logger)component: Component name
Returns: zerolog.Logger with component field
Example:
logger := logging.ContextLogger(ctx, "user-service")
logger.Info().Str("user_id", "123").Msg("User created")type OutputDestination int
const (
OutputConsole OutputDestination = 1 << 0 // Console (stderr)
OutputFile OutputDestination = 1 << 1 // File
)Bitwise flags for output destinations. Combine with | operator:
logging.OutputConsole | logging.OutputFile // Both outputstype FileConfig struct {
Path string // Log file path (required)
}Configuration for file-based logging. File rotation should be managed by OS tools (logrotate, etc.).
Human-readable with colors and timestamps:
2025-11-24T12:30:45+07:00 INF Service started service=my-service pid=12345
2025-11-24T12:30:46+07:00 DBG Configuration loaded config=loaded service=my-service pid=12345
Structured JSON for parsing:
{"level":"info","service":"my-service","pid":12345,"time":"2025-11-24T12:30:45+07:00","message":"Service started"}
{"level":"debug","service":"my-service","pid":12345,"config":"loaded","time":"2025-11-24T12:30:46+07:00","message":"Configuration loaded"}See examples/logging/ for complete runnable examples:
console/- Console-only loggingfile/- File-only loggingboth/- Dual console + file loggingenvironment/- Environment-based configuration
import (
"os"
"github.com/jasoet/pkg/v2/logging"
)
func main() {
env := os.Getenv("ENV")
var closer io.Closer
var err error
if env == "production" {
// Production: file only, info level
closer, err = logging.InitializeWithFile("my-service", false,
logging.OutputFile,
&logging.FileConfig{Path: "/var/log/myapp/app.log"})
} else if env == "staging" {
// Staging: both console and file, debug level
closer, err = logging.InitializeWithFile("my-service", true,
logging.OutputConsole | logging.OutputFile,
&logging.FileConfig{Path: "/var/log/myapp/app.log"})
} else {
// Development: console only, debug level
err = logging.Initialize("my-service", true)
}
if err != nil {
log.Fatal().Err(err).Msg("failed to initialize logging")
}
if closer != nil {
defer closer.Close()
}
}func ProcessOrder(ctx context.Context, orderID string) {
logger := logging.ContextLogger(ctx, "order-processor")
logger.Info().Str("order_id", orderID).Msg("Processing order")
// ... process order ...
logger.Info().
Str("order_id", orderID).
Str("status", "completed").
Msg("Order processed")
}log.Info().
Str("user_id", "123").
Int("age", 30).
Bool("premium", true).
Dur("response_time", 150*time.Millisecond).
Msg("User action completed")
// File output:
// {"level":"info","user_id":"123","age":30,"premium":true,"response_time":150,...}if err != nil {
log.Error().
Err(err).
Str("operation", "database_query").
Msg("Database operation failed")
return err
}Since the package doesn't handle file rotation internally, use OS tools like logrotate:
Create /etc/logrotate.d/myapp:
/var/log/myapp/*.log {
daily # Rotate daily
rotate 7 # Keep 7 days of logs
compress # Compress old logs
delaycompress # Compress after 2nd rotation
missingok # Don't error if log missing
notifempty # Don't rotate empty logs
create 0644 myapp myapp # Create new file with permissions
postrotate
# Send SIGHUP to app to reopen log files (if needed)
killall -SIGHUP myapp || true
endscript
}
# Test configuration
logrotate -d /etc/logrotate.d/myapp
# Force rotation
logrotate -f /etc/logrotate.d/myappUse appropriate log levels:
// Debug: Detailed information for debugging
log.Debug().Msg("Entering function ProcessUser")
// Info: General informational messages
log.Info().Msg("Service started successfully")
// Warn: Warning messages (not critical)
log.Warn().Msg("Cache miss, fetching from database")
// Error: Error conditions
log.Error().Err(err).Msg("Failed to connect to database")
// Fatal: Critical errors (exits with os.Exit(1))
log.Fatal().Msg("Unable to start server")
// Panic: Panic-level errors
log.Panic().Msg("Unrecoverable error")func main() {
// Initialize logging first
closer, err := logging.InitializeWithFile("my-service", true,
logging.OutputConsole | logging.OutputFile,
&logging.FileConfig{Path: "app.log"})
if err != nil {
log.Fatal().Err(err).Msg("failed to initialize logging")
}
defer closer.Close()
// Then start your application
startServer()
}// Create component-specific loggers
func NewUserService(ctx context.Context) *UserService {
return &UserService{
logger: logging.ContextLogger(ctx, "user-service"),
}
}
func (s *UserService) CreateUser(user User) {
s.logger.Info().Str("user_id", user.ID).Msg("Creating user")
}log.Info().
Str("request_id", requestID).
Str("user_id", userID).
Dur("latency", latency).
Msg("Request processed")// Bad
log.Info().Str("password", user.Password).Msg("User login")
// Good
log.Info().Str("user_id", user.ID).Msg("User login")// Good: Structured and parseable
log.Info().
Str("user_id", "123").
Int("order_count", 5).
Msg("User activity")
// Bad: Unstructured
log.Info().Msg("User 123 has 5 orders")Initialize and InitializeWithFile now return error instead of panicking.
Existing code that discards the return value will still compile, but you should
handle the error to avoid silent failures:
v1 code (still compiles but error is ignored):
logging.Initialize("my-service", true)Recommended v2 code:
if err := logging.Initialize("my-service", true); err != nil {
log.Fatal().Err(err).Msg("failed to initialize logging")
}To add file logging:
closer, err := logging.InitializeWithFile("my-service", true,
logging.OutputConsole | logging.OutputFile,
&logging.FileConfig{Path: "app.log"})
if err != nil {
log.Fatal().Err(err).Msg("failed to initialize logging")
}
defer closer.Close()When writing tests, you can redirect logs to a test file:
func TestMyFunction(t *testing.T) {
tempDir := t.TempDir()
logFile := filepath.Join(tempDir, "test.log")
closer, err := logging.InitializeWithFile("test-service", true,
logging.OutputFile,
&logging.FileConfig{Path: logFile})
require.NoError(t, err)
defer closer.Close()
// Run your test
MyFunction()
// Verify logs
content, _ := os.ReadFile(logFile)
assert.Contains(t, string(content), "expected log message")
}For OpenTelemetry-compatible logging with trace correlation, see the otel package:
import "github.com/jasoet/pkg/v2/otel"
// Create OTel LoggerProvider
loggerProvider, _ := otel.NewLoggerProviderWithOptions("my-service",
otel.WithLogLevel(logging.LogLevelInfo),
otel.WithConsoleOutput(true))
cfg := &otel.Config{
LoggerProvider: loggerProvider,
// ... other OTel config
}See otel/README.md for details.
- Check file path exists and is writable
- Verify OutputFile flag is set
- Check FileConfig.Path is not empty
- Verify file permissions (should be 0600)
- Set up logrotate (see above)
- Verify logrotate cron job is running
- Check logrotate configuration syntax
JSON logs can be pretty-printed:
# Pretty-print JSON logs
cat app.log | jq
# Filter by level
cat app.log | jq 'select(.level=="error")'
# Search for specific message
cat app.log | jq 'select(.message | contains("database"))'- Console output is slower (formatting overhead)
- File output is fast (direct JSON write)
- For production: use file only (
OutputFile) - For development: use console or both
Part of github.com/jasoet/pkg/v2 - follows repository license.