logze
July 8, 2025 ยท View on GitHub
A high-performance structured logging library for Go that combines the efficiency of zerolog with the simplicity of slog. Write clean, readable logging code that performs exceptionally well.
โจ Why Choose logze?
Simple, Clean Interface:
The following examples show the same logging operation in both zerolog and logze:
// Zerolog example:
log.Error().Err(err).Str("address", "127.0.0.1").Int("retry", n).Msg("cannot start server")
// Logze example:
logze.Err(err, "cannot start server", "address", "127.0.0.1", "retry", n)
Why this matters: Logze eliminates the need to remember multiple method names (.Str(), .Int(), .Msg()) and chain method calls. Instead, you use one simple method call with alternating key-value pairs, making the code more readable and less error-prone.
๐ฆ Installation
go get -u github.com/maxbolgarin/logze/v2
High Performance:
- ๐ 3x faster than standard
slog - โก Only 15% slower than raw
zerolog - ๐ฏ Zero allocations for most operations (thanks to
zerolog) - ๐ High-throughput with optional non-blocking I/O (thanks to
diode)
๐ Table of Contents
- Quick Start
- Installation
- Core Concepts
- Usage Examples
- Configuration
- Global Logger
- Pros and Cons
- Benchmarks
- Migration Guide
- Contributing
โก Quick Start
This example demonstrates the core features of logze in just a few lines:
package main
import (
"errors"
"github.com/maxbolgarin/logze/v2"
)
func main() {
// 1. Create a logger with JSON output
logger := logze.NewConsoleJSON()
// 2. Log with structured fields
logger.Info("Application started", "version", "1.0.0", "port", 8080)
// 3. Log errors efficiently
err := errors.New("database connection failed")
logger.Err(err, "Failed to connect", "host", "localhost", "retry", 3)
// 4. Use global logger for convenience
logze.Info("Processing request", "user_id", 12345, "action", "create_post")
}
What this does:
- Creates a JSON logger that outputs structured logs to stderr (ideal for production)
- Logs structured information with key-value pairs that are easy to parse and search
- Handles error logging efficiently with both the error object and contextual information
- Demonstrates global logging for cases where you don't want to pass logger instances around
Expected output:
{"level":"info","message":"Application started","version":"1.0.0","port":8080,"time":"2024-01-15T10:30:00Z"}
{"level":"error","message":"Failed to connect","error":"database connection failed","host":"localhost","retry":3,"time":"2024-01-15T10:30:01Z"}
{"level":"info","message":"Processing request","user_id":12345,"action":"create_post","time":"2024-01-15T10:30:02Z"}
๐ Usage Examples
Basic Logging
This example shows how to log at different levels with various data types:
logger := logze.New(logze.C().WithConsole().WithTrace())
// Different log levels
logger.Trace("Detailed execution trace", "function", "processData")
logger.Debug("Debug information", "variable", someVar)
logger.Info("General information", "statuses", []string{"healthy", "ready"})
logger.Warn("Warning message", "disk_usage", 85.5)
logger.Error("Error occurred", "component", "database")
Explanation:
- Trace: Most verbose level, typically used for detailed debugging (includes caller information)
- Debug: Detailed information for diagnosing problems
- Info: General operational messages about what the application is doing
- Warn: Potentially harmful situations that should be investigated
- Error: Error events that still allow the application to continue
Structured Fields
This example demonstrates different ways to add structured data to your logs:
// Prepare a new logger with additional fields
logger := logger.With("user_id", 123, "action", "login")
// Log with the prepared logger
logger.Info("User action")
// Complex data types
logger.Info("Request processed",
"duration", time.Since(start),
"headers", map[string]string{"Content-Type": "application/json"},
"response_size", 1024,
"success", true,
)
What each approach does:
-
Prepared logger with
.With(): Creates a new logger instance that automatically includes specified fields in every log message. This is perfect for request-scoped logging where you want to include request ID, user ID, etc. in all related logs. -
Complex data types: Shows how logze automatically handles different Go types (supports all types that
zerologsupports):time.Durationโ formatted as readable durationmap[string]stringโ serialized as JSON objectintโ numeric valueboolโ boolean value
Expected output:
{"level":"info","message":"User action","user_id":123,"action":"login","time":"2024-01-15T10:30:00Z"}
{"level":"info","message":"Request processed","duration":"150ms","headers":{"Content-Type":"application/json"},"response_size":1024,"success":true,"time":"2024-01-15T10:30:01Z"}
Error Logging
This example shows different patterns for logging errors:
// Basic error logging
err := errors.New("connection timeout")
logger.Err(err, "Database connection failed", "host", "db.example.com")
// Error with error object
// Returns: {"level":"error","message":"Database connection failed","error":"connection timeout","host":"db.example.com"}
logger.Error("Database connection failed", err, "host", "db.example.com")
// Error with stack trace and no error object
logger.WithStack().Error("Validation failed")
// Logging with stack trace
logger := logze.New(logze.C().WithConsoleJSON().WithStackTrace())
logger.Err(err, "Critical failure", "operation", "save_user")
Different error logging patterns:
-
logger.Err(err, message, fields...): Best practice for error logging. Automatically includes the error in the log entry and increments error counters if configured. -
logger.Error(message, error, fields...): Alternative syntax where you can mix the error with other fields. The error is automatically detected and properly formatted. -
logger.WithStack().Error(message): Captures and includes stack trace information, useful for debugging but has performance impact. -
Global stack trace configuration: When you configure the logger with
WithStackTrace(), all error logs from this logger instance automatically include stack traces.
Formatted Logging
This example demonstrates printf-style formatting combined with structured fields:
// Printf-style formatting with structured fields
// Returns: {"level":"info","message":"Processing 100 items in 10s","batch_id":"1234567890"}
logger.Infof("Processing %d items in %s", count, duration, "batch_id", batchID)
// All format verbs are supported
logger.Debugf("User %s (ID: %d) performed action: %v", username, userID, action,
"timestamp", time.Now(), "ip", clientIP)
How formatted logging works:
- Format string processing: The first part of the arguments is used for printf-style formatting
- Structured fields: interface{} remaining arguments after the format placeholders become structured key-value pairs
- Best of both worlds: You get readable formatted messages plus searchable structured data
Example breakdown:
"Processing %d items in %s"withcount=100, duration="10s"becomes"Processing 100 items in 10s""batch_id", batchIDbecomes a structured field in the JSON output- This gives you both human-readable messages and machine-parseable data
Conditional Logging
This example shows how to avoid expensive operations when logging is disabled:
// Log only when condition is true
logger.InfoIf(debugMode, "Debug mode enabled", "level", "verbose")
logger.ErrorIf(err != nil, "Operation failed", "error", err)
Sampling
This example demonstrates how to reduce log volume in high-throughput applications:
// Sample 10% of debug logs
logger := logze.New(logze.C().WithConsoleJSON().WithPercentageSampler(0.1, logze.LevelDebug))
// Allow only 100 debug logs per second
logger := logze.New(logze.C().WithConsoleJSON().WithMaxSampler(100, time.Second, logze.LevelDebug))
// Pass 100 logs per second and then allow 10% of all logs
logger := logze.New(logze.C().WithConsoleJSON().WithBurstSampler(0.1, 100, time.Second))
Sampling strategies explained:
-
Percentage Sampling: Randomly samples X% of logs at specified levels
- Use when you want a representative sample of all logs
- Good for getting a statistical overview without overwhelming log storage
-
Max Sampling: Allows up to X logs per time period, then drops the rest
- Use to prevent log flooding from busy code paths
- Guarantees you won't exceed storage/bandwidth limits
-
Burst Sampling: Allows X logs immediately, then switches to percentage sampling
- Use for bursty applications that need immediate logs but should throttle under sustained load
- Combines the benefits of both approaches
โ๏ธ Configuration
Output Configuration
These examples show different ways to configure where your logs are written:
// Console output (development)
logger := logze.New(logze.C().WithConsole()) // Colored output
logger := logze.New(logze.C().WithConsoleNoColor()) // Plain text
logger := logze.New(logze.C().WithConsoleJSON()) // JSON to stderr
// File output
config, closer, err := logze.C().WithFile("app.log", 0644)
if err != nil {
log.Fatal(err)
}
defer closer.Close()
logger := logze.New(config)
// Multiple outputs
var fileWriter io.Writer // your file writer
logger := logze.New(logze.C(fileWriter).WithConsole())
// Custom writer
logger := logze.New(logze.C(customWriter))
Output format explanations:
-
WithConsole(): Human-readable colored output, perfect for development- Example:
2:04PM INF User logged in user_id=123 action=login - Pros: Easy to read during development
- Cons: Slow performance, not machine-parseable
- Example:
-
WithConsoleJSON(): JSON output to stderr, ideal for production- Example:
{"level":"info","message":"User logged in","user_id":123,"time":"2024-01-15T14:04:00Z"} - Pros: Fast, machine-parseable, works with log aggregators
- Cons: Not human-readable
- Example:
-
File output: Writes logs to files with automatic rotation support
- Use case: When you need persistent logs or can't use stderr
- Remember: Always defer
closer.Close()to ensure logs are flushed
-
Multiple outputs: Send logs to multiple destinations simultaneously
- Use case: Log to both files and console, or send to multiple log aggregators
Level Configuration
These examples show how to control which log levels are output:
// Set minimum log level
logger := logze.New(logze.C().WithLevel(logze.LevelWarn)) // Only warn, error, fatal
// Level-specific configuration
logger := logze.New(logze.C().WithDebug()) // Debug and above
logger := logze.New(logze.C().WithInfo()) // Info and above (default)
logger := logze.New(logze.C().WithError()) // Error and above only
// Disable logging completely
logger := logze.New(logze.C().WithDisabled())
Advanced Features
This example shows advanced configuration options:
logger := logze.NewConfig().
WithLevel(logze.LevelInfo). // Set log level
WithAddCaller(). // Include caller info
WithStackTrace(). // Enable stack traces
WithSimpleErrorCounter(). // Count errors
WithToIgnore("health", "ping"). // Filter messages
WithTimeFieldFormat(time.RFC3339). // Custom time format
WithNoDiode(). // Disable buffering
New("service", "api", "version", "2.1.0") // Create logger based on config
logger.Info("health check") // Won't be printed
Advanced features explained:
-
WithAddCaller(): Adds file name and line number to logs- Use case: Debugging when you need to know exactly where logs come from
- Performance impact: Slight overhead due to runtime reflection
-
WithStackTrace(): Automatically includes stack traces in error logs- Use case: Debugging complex error conditions
- Performance impact: Significant overhead, use sparingly
-
WithSimpleErrorCounter(): Tracks how minterface{} errors have been logged- Use case: Monitoring and alerting based on error rates
- Access: Use
logger.GetErrorCounter()to get current count
-
WithToIgnore(): Filters out log messages containing specified strings- Use case: Reduce noise from health checks, monitoring pings, etc.
- Example: Logs containing "health" or "ping" won't be output
-
WithNoDiode(): Disables async buffering for synchronous logging- Use case: When you need guaranteed log delivery (e.g., before program exit)
- Trade-off: Better reliability but worse performance
-
New(): Allows to create a logger from config, fields"service", "api", "version", "2.1.0"are added to every log
๐ Global Logger
These examples show how to use the global logger for convenience:
// Initialize once at application start
logze.Init(logze.C().WithConsoleJSON().WithLevel("info"), "app", "myservice")
// Use interface{}where in your codebase
logze.Info("Server starting", "port", 8080)
logze.Err(err, "Failed to process request", "request_id", reqID)
// Create contextual loggers
requestLogger := logze.With("request_id", reqID, "user_id", userID)
requestLogger.Info("Processing request")
// Update global configuration
logze.Update(logze.C().WithLevel("debug")) // Enable debug logging
Global logger patterns:
-
logze.Init(): Sets up the global logger with configuration and default fields- Best practice: Call once during application startup
- Use case: When you don't want to pass logger instances everywhere
-
Direct global calls:
logze.Info(),logze.Err(), etc.- Advantage: Simple, no need to pass logger instances
- Disadvantage: Less flexible than instance loggers
-
logze.With(): Creates a new logger with additional fields- Use case: Request-scoped logging where you want consistent fields
- Pattern: Create once per request, use throughout request handling
-
logze.Update(): Changes global logger configuration at runtime- Use case: Enabling debug logging for troubleshooting without restart
- Warning: Affects all subsequent logging calls globally
๐ Migration Guide
From slog
The migration from slog is nearly seamless:
// Before (slog)
slog.Info("User created", "user_id", 123, "email", "user@example.com")
slog.Error("Database error", "error", err, "query", query)
// After (logze) - it's identical!
logze.Info("User created", "user_id", 123, "email", "user@example.com")
logze.Error("Database error", "error", err, "query", query) // Enhanced error handling
Migration benefits:
- No API changes: The basic logging calls are identical
- Enhanced error handling: Better error object processing and counting
- Performance improvement: 3x faster than slog with same interface
- Additional features: Caller info, stack traces, sampling, etc.
From zerolog
Logze simplifies zerolog usage:
// Before (zerolog)
log.Info().Str("user_id", "123").Str("email", "user@example.com").Msg("User created")
log.Error().Err(err).Str("query", query).Msg("Database error")
// After (logze) - much cleaner!
logze.Info("User created", "user_id", "123", "email", "user@example.com")
logze.Err(err, "Database error", "query", query)
Migration advantages:
- Simpler syntax: No method chaining or type-specific methods
- Same performance: Only 15% overhead compared to raw zerolog
- Fewer mistakes: No forgotten
.Msg()calls or wrong type methods - Better readability: Clear intent with single method calls
From Standard Library
Transform printf-style logging into structured logging:
// Before (standard log)
log.Printf("User %s created with ID %d", email, userID)
// After (logze) - structured and faster!
logze.Infof("User %s created with ID %d", email, userID)
// Or better yet:
logze.Info("User created", "email", email, "user_id", userID)
Why structured logging is better:
- Searchable: Query logs by specific field values
- Aggregatable: Calculate metrics from log data
- Consistent: Standardized format across different log sources
- Future-proof: Easy to parse and process programmatically
Migration strategy:
- Phase 1: Replace
log.Printfwithlogze.Infof(minimal changes) - Phase 2: Convert to structured logging with key-value pairs
- Phase 3: Add appropriate log levels and error handling
โ Pros and Cons
Pros
- ๐ High Performance: 3x faster than
slog, leveragingzerolog's efficient engine - ๐ Clean Interface: Simple, readable logging calls with structured fields
- ๐ง Flexible Configuration: Extensive configuration options for interface{} use case
- ๐ฏ Zero Allocations: Most operations don't allocate memory
- ๐ Easy Migration: Compatible interface with
slogpatterns
Cons
- ๐ Slight Overhead: ~15% slower than raw
zerologdue to field abstraction - ๐พ Message Loss Risk: Default diode buffering can drop messages under extreme load
- ๐จ Console Performance: Text/console output is significantly slower than JSON
๐ Benchmarks
Thoughts from benchmarks:
logzeis about 3 times faster thanslogand for 15% slower thanzerolog- Format methods like
logze.InfoforMsgfdon't add big overhead (only 30% slower and +1 alloc) - Stack trace is slow in all loggers, but
zerologis the fastest,logzeslight slower and makes more allocations (price for easy of use). - Console writer is slow in
logzeandzerologand should be used only in development (that the case whenslogwins - if you want to use text writer in production)
Here is result of go test -bench=. -benchmem -benchtime=2s:
goos: darwin
goarch: arm64
cpu: Apple M1 Pro
Logging Info and two fields:
BenchmarkZerologInfo-8 7689862 146.3 ns/op 0 B/op 0 allocs/op
BenchmarkLogzeInfo-8 6985168 184.1 ns/op 0 B/op 0 allocs/op
BenchmarkSLogInfo-8 2293354 523.7 ns/op 0 B/op 0 allocs/op
Logging Infof (formatted) and two fields:
BenchmarkZerologInfoFormat-8 11758495 207.0 ns/op 24 B/op 1 allocs/op
BenchmarkLogzeInfoFormat-8 10047972 239.8 ns/op 24 B/op 1 allocs/op
BenchmarkSLogInfoFormat-8 4026775 601.0 ns/op 24 B/op 1 allocs/op
Logging Error with error and two fields:
BenchmarkZerologError-8 7172144 166.6 ns/op 0 B/op 0 allocs/op
BenchmarkLogzeError-8 6193178 189.4 ns/op 0 B/op 0 allocs/op
BenchmarkSLogError-8 1930131 651.0 ns/op 0 B/op 0 allocs/op
Logging Error with error, stack trace and two fields:
BenchmarkZerologErrorWithStack-8 560763 2057 ns/op 824 B/op 7 allocs/op
BenchmarkLogzeErrorWithStack-8 485652 2406 ns/op 1307 B/op 10 allocs/op
BenchmarkSLogErrorWithStack-8 311107 3786 ns/op 1617 B/op 3 allocs/op
Logging Info and two fields using text handler for console / development:
BenchmarkZerologInfoConsole-8 682558 3306 ns/op 1922 B/op 51 allocs/op
BenchmarkLogzeInfoConsole-8 704247 3366 ns/op 1922 B/op 51 allocs/op
BenchmarkSLogInfoConsole-8 4058850 588.1 ns/op 0 B/op 0 allocs/op
Additional logze features
BenchmarkLogzeErr-8 6057480 206.1 ns/op 0 B/op 0 allocs/op
BenchmarkLogzeToIgnore5-8 4703277 248.3 ns/op 64 B/op 1 allocs/op
BenchmarkLogzeErrStack-8 438679 2644 ns/op 768 B/op 18 allocs/op
๐ค Contributing
We welcome contributions! Please feel free to:
- ๐ Report bugs by opening issues
- ๐ก Suggest features or improvements
- ๐ง Submit pull requests
- ๐ Improve documentation
- โก Add benchmarks or tests
๐ License
This project is licensed under the MIT License. See the LICENSE file for details.