Logging Package
Structured logging with zerolog, supporting flexible output destinations (console, file, or both).
Features
- 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
Quick Start
Console Only (Default)
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")
}
File Only
import "github.com/jasoet/pkg/v2/logging"
func main() {
// All logs go to file (no console output)
if err := logging.InitializeWithFile("my-service", false,
logging.OutputFile,
&logging.FileConfig{
Path: "/var/log/myapp/app.log",
}); err != nil {
log.Fatal().Err(err).Msg("failed to initialize logging")
}
log.Info().Msg("This goes to file only")
}
Both Console and File
import "github.com/jasoet/pkg/v2/logging"
func main() {
// Logs appear in both console and file
if err := logging.InitializeWithFile("my-service", true,
logging.OutputConsole | logging.OutputFile, // Bitwise OR
&logging.FileConfig{
Path: "/var/log/myapp/app.log",
}); err != nil {
log.Fatal().Err(err).Msg("failed to initialize logging")
}
log.Info().Msg("Visible in console AND file")
}
API Reference
Initialize
func Initialize(serviceName string, debug bool) error
Sets up console-only logging. Returns an error if initialization fails.
Parameters:
serviceName: Service name added to all logs
debug: 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")
}
InitializeWithFile
func InitializeWithFile(serviceName string, debug bool, output OutputDestination, fileConfig *FileConfig) error
Sets up logging with flexible output destinations. Returns an error if the configuration
is invalid or the log file cannot be opened.
Parameters:
serviceName: Service name added to all logs
debug: If true, sets level to Debug; otherwise Info
output: 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
err := logging.InitializeWithFile("service", false,
logging.OutputFile,
&logging.FileConfig{Path: "app.log"})
// Both
err := logging.InitializeWithFile("service", true,
logging.OutputConsole | logging.OutputFile,
&logging.FileConfig{Path: "app.log"})
ContextLogger
func ContextLogger(ctx context.Context, component string) zerolog.Logger
Creates 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")
OutputDestination
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 outputs
FileConfig
type FileConfig struct {
Path string // Log file path (required)
}
Configuration for file-based logging. File rotation should be managed by OS tools (logrotate, etc.).
Console Output
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
File Output
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"}
Usage Patterns
Runnable Examples
See examples/logging/ for complete runnable examples:
console/ - Console-only logging
file/ - File-only logging
both/ - Dual console + file logging
environment/ - Environment-based configuration
Environment-Based Configuration
import (
"os"
"github.com/jasoet/pkg/v2/logging"
)
func main() {
env := os.Getenv("ENV")
var err error
if env == "production" {
// Production: file only, info level
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
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")
}
}
Component-Specific Logging
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")
}
Structured Logging
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,...}
Error Logging
if err != nil {
log.Error().
Err(err).
Str("operation", "database_query").
Msg("Database operation failed")
return err
}
File Rotation with logrotate
Since the package doesn't handle file rotation internally, use OS tools like logrotate:
logrotate Configuration
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
}
Testing logrotate
# Test configuration
logrotate -d /etc/logrotate.d/myapp
# Force rotation
logrotate -f /etc/logrotate.d/myapp
Log Levels
Use 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")
Best Practices
1. Initialize Once at Startup
func main() {
// Initialize logging first
if err := logging.InitializeWithFile("my-service", true,
logging.OutputConsole | logging.OutputFile,
&logging.FileConfig{Path: "app.log"}); err != nil {
log.Fatal().Err(err).Msg("failed to initialize logging")
}
// Then start your application
startServer()
}
2. Use Component Loggers
// 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")
}
3. Add Context to Logs
log.Info().
Str("request_id", requestID).
Str("user_id", userID).
Dur("latency", latency).
Msg("Request processed")
4. Don't Log Sensitive Data
// Bad
log.Info().Str("password", user.Password).Msg("User login")
// Good
log.Info().Str("user_id", user.ID).Msg("User login")
5. Use Structured Fields
// 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")
Migration from v1
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:
if err := logging.InitializeWithFile("my-service", true,
logging.OutputConsole | logging.OutputFile,
&logging.FileConfig{Path: "app.log"}); err != nil {
log.Fatal().Err(err).Msg("failed to initialize logging")
}
Testing
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")
err := logging.InitializeWithFile("test-service", true,
logging.OutputFile,
&logging.FileConfig{Path: logFile})
require.NoError(t, err)
// Run your test
MyFunction()
// Verify logs
content, _ := os.ReadFile(logFile)
assert.Contains(t, string(content), "expected log message")
}
OpenTelemetry Integration
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.
Troubleshooting
Logs not appearing in file
- Check file path exists and is writable
- Verify OutputFile flag is set
- Check FileConfig.Path is not empty
- Verify file permissions (should be 0644)
File grows indefinitely
- Set up logrotate (see above)
- Verify logrotate cron job is running
- Check logrotate configuration syntax
Cannot read log files
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
License
Part of github.com/jasoet/pkg/v2 - follows repository license.