A simple yet powerful Go logging package built on top of Zap that provides JSON-formatted logs with built-in gzip compression and advanced retrieval capabilities.
jsonlog-go simplifies structured logging for Go applications by combining the high-performance Zap logger with automatic JSON serialization, file compression, and intelligent log retrieval. Perfect for applications that need production-grade logging with minimal setup.
- Features
- Installation
- Quick Start
- Core Concepts
- Usage Examples
- API Reference
- Configuration
- Best Practices
- Testing
- 🚀 High-Performance Logging - Built on Uber's Zap logger for minimal overhead
- 📝 JSON Output - All logs are stored in structured JSON format for easy parsing and analysis
- 🗜️ Automatic Compression - Gzip compression reduces log file sizes by ~90% without data loss
- 📖 Log Retrieval - Read and parse compressed JSON logs programmatically
- 🔍 Smart Filtering - Filter logs by level, time range, or custom predicates
- 🎯 Simple API - Intuitive functions mirror standard logging patterns
- 📂 Configurable Storage - Specify custom paths for log storage at initialization
- 🖥️ Dual Output - Optional console logging alongside file logging
- 🔒 Thread-Safe - Safe for concurrent logging from multiple goroutines
go get github.com/gusdeyw/jsonlog-goThen import in your code:
import "github.com/gusdeyw/jsonlog-go"Here's a minimal example to get you logging in seconds:
package main
import (
"log"
"github.com/gusdeyw/jsonlog-go"
"go.uber.org/zap"
)
func main() {
// Initialize logger
config := jsonlog.Config{
LogPath: "./logs",
LogFileName: "app",
EnableConsoleOutput: true,
}
logger, err := jsonlog.NewLogger(config)
if err != nil {
log.Fatal(err)
}
defer logger.Close()
// Start logging
logger.Info("Application started", zap.String("version", "1.0.0"))
logger.Error("Something failed", zap.Error(err))
}Output files:
./logs/app.log- Contains newline-delimited JSON logs
Every logging session starts with NewLogger(). The Config struct defines where and how logs are saved:
config := jsonlog.Config{
LogPath: "./logs", // Directory to store logs (required)
LogFileName: "myapp", // File name prefix (optional, defaults to "app")
EnableConsoleOutput: true, // Also print to stdout (optional)
CompressOnClose: true, // Auto-compress on Close() (optional)
}
logger, err := jsonlog.NewLogger(config)
if err != nil {
log.Fatal(err)
}
defer logger.Close()Six standard logging levels are supported:
logger.Debug("Debug information", zap.String("key", "value"))
logger.Info("Informational message")
logger.Warn("Warning message")
logger.Error("Error occurred", zap.Error(err))
logger.Fatal("Fatal error - exits application")
logger.Panic("Panic - triggers panic recovery")Log custom data using Zap fields:
logger.Info("User action",
zap.String("user_id", "12345"),
zap.Int("attempt", 2),
zap.Float64("duration_ms", 145.5),
zap.Bool("success", true),
zap.Duration("elapsed", 2*time.Second),
zap.Error(err),
)Logs are stored as newline-delimited JSON (NDJSON):
{
"timestamp": "2025-01-15T10:30:45.123456Z",
"level": "info",
"caller": "main.go:25",
"message": "User action",
"user_id": "12345",
"attempt": 2,
"duration_ms": 145.5,
"success": true
}
{
"timestamp": "2025-01-15T10:30:46.234567Z",
"level": "error",
"caller": "main.go:30",
"message": "Database connection failed",
"error": "connection refused"
}Logs can be compressed with gzip to save storage space:
// Compress the current log file
if err := logger.CompressLogFile(); err != nil {
logger.Error("Compression failed", zap.Error(err))
}
// Creates: app.log.gzCompression Benefits:
- Reduces file size by ~90% for typical JSON logs
- Maintains full readability when decompressed
- No data loss or corruption
Read logs from compressed files back into memory:
logs, err := jsonlog.ReadCompressedLogs("./logs/app.log.gz")
if err != nil {
log.Fatal(err)
}
for i, log := range logs {
fmt.Printf("Log #%d: [%s] %s\n",
i+1,
log["level"],
log["message"],
)
}Each log entry is a map[string]interface{} containing all fields.
package main
import (
"github.com/gusdeyw/jsonlog-go"
"go.uber.org/zap"
)
func main() {
logger, _ := jsonlog.NewLogger(jsonlog.Config{
LogPath: "./logs",
LogFileName: "app",
EnableConsoleOutput: true,
})
defer logger.Close()
logger.Info("Server starting", zap.Int("port", 8080))
logger.Info("Accepting connections")
// ... handle requests ...
logger.Info("Server shutdown", zap.Int("requests_served", 1024))
}func logWithDynamicLevel(logger *jsonlog.Logger, level string, msg string) {
var logLevel jsonlog.LogLevel
switch level {
case "debug":
logLevel = jsonlog.DebugLevel
case "info":
logLevel = jsonlog.InfoLevel
case "warn":
logLevel = jsonlog.WarnLevel
case "error":
logLevel = jsonlog.ErrorLevel
default:
logLevel = jsonlog.InfoLevel
}
logger.LogWithLevel(logLevel, msg)
}func processPayment(logger *jsonlog.Logger, orderID string, amount float64) error {
logger.Info("Processing payment",
zap.String("order_id", orderID),
zap.Float64("amount", amount),
)
if err := chargeCard(amount); err != nil {
logger.Error("Payment processing failed",
zap.String("order_id", orderID),
zap.Float64("amount", amount),
zap.Error(err),
)
return err
}
logger.Info("Payment processed successfully",
zap.String("order_id", orderID),
zap.Float64("amount", amount),
)
return nil
}func analyzeErrors(filePath string) {
// Read all logs
logs, _ := jsonlog.ReadCompressedLogs(filePath)
// Filter for errors only
errorLogs, _ := jsonlog.ReadCompressedLogsFiltered(
filePath,
jsonlog.FilterByLevel("error"),
)
fmt.Printf("Total logs: %d\n", len(logs))
fmt.Printf("Error logs: %d\n", len(errorLogs))
for _, err := range errorLogs {
fmt.Printf(" - %s\n", err["message"])
}
}import "time"
func getLogs24Hours(filePath string) ([]map[string]interface{}, error) {
now := time.Now()
yesterday := now.Add(-24 * time.Hour)
return jsonlog.ReadCompressedLogsFiltered(
filePath,
jsonlog.FilterByTimeRange(yesterday, now),
)
}func getFailedRequests(filePath string) ([]map[string]interface{}, error) {
customFilter := func(log map[string]interface{}) bool {
// Include only error-level logs with "request" in message
if level, ok := log["level"].(string); ok && level == "error" {
if msg, ok := log["message"].(string); ok {
return strings.Contains(msg, "request")
}
}
return false
}
return jsonlog.ReadCompressedLogsFiltered(filePath, customFilter)
}String type for logging levels:
const (
DebugLevel LogLevel = "debug"
InfoLevel LogLevel = "info"
WarnLevel LogLevel = "warn"
ErrorLevel LogLevel = "error"
FatalLevel LogLevel = "fatal"
PanicLevel LogLevel = "panic"
)Logger configuration struct:
type Config struct {
LogPath string // Directory for logs (required)
LogFileName string // File name prefix (default: "app")
EnableConsoleOutput bool // Print to stdout
CompressOnClose bool // Auto-compress on Close()
}Main logging service:
type Logger struct {
// Contains filtered or unexported fields
}Filtering function type:
type FilterFunc func(log map[string]interface{}) boolCreates and initializes a new logger instance.
logger, err := jsonlog.NewLogger(jsonlog.Config{
LogPath: "./logs",
LogFileName: "app",
})
if err != nil {
log.Fatal(err)
}// Logging methods
func (l *Logger) Debug(message string, fields ...zap.Field)
func (l *Logger) Info(message string, fields ...zap.Field)
func (l *Logger) Warn(message string, fields ...zap.Field)
func (l *Logger) Error(message string, fields ...zap.Field)
func (l *Logger) Fatal(message string, fields ...zap.Field)
func (l *Logger) Panic(message string, fields ...zap.Field)
// Dynamic level logging
func (l *Logger) LogWithLevel(level LogLevel, message string, fields ...zap.Field)
// Lifecycle
func (l *Logger) Close() error
func (l *Logger) CompressLogFile() errorReads all logs from a compressed gzip file.
logs, err := jsonlog.ReadCompressedLogs("./logs/app.log.gz")
if err != nil {
log.Fatal(err)
}
// logs is []map[string]interface{}Reads logs applying a custom filter.
logs, err := jsonlog.ReadCompressedLogsFiltered(
"./logs/app.log.gz",
jsonlog.FilterByLevel("error"),
)Creates a filter matching a specific log level.
errorFilter := jsonlog.FilterByLevel("error")
logs, _ := jsonlog.ReadCompressedLogsFiltered("app.log.gz", errorFilter)Creates a filter for logs within a time range.
now := time.Now()
dayAgo := now.Add(-24 * time.Hour)
logs, _ := jsonlog.ReadCompressedLogsFiltered(
"app.log.gz",
jsonlog.FilterByTimeRange(dayAgo, now),
)config := jsonlog.Config{
LogPath: "./logs",
LogFileName: "myapp",
}
logger, _ := jsonlog.NewLogger(config)config := jsonlog.Config{
LogPath: "./logs", // Where to save logs
LogFileName: "myapp", // Log file name (without extension)
EnableConsoleOutput: true, // Also print to console
CompressOnClose: true, // Auto-compress when closing
}
logger, _ := jsonlog.NewLogger(config)- LogPath: Must be writable directory. Created if doesn't exist.
- LogFileName: Defaults to "app". Final file:
LogPath/LogFileName.log - EnableConsoleOutput: Useful for development, disable in production for better performance
- CompressOnClose: Not implemented yet; manual compression via
CompressLogFile()recommended
Ensures all buffers are flushed and resources cleaned up:
logger, err := jsonlog.NewLogger(config)
if err != nil {
log.Fatal(err)
}
defer logger.Close() // ← Always include thisStructured fields make logs queryable and analyzable:
// ❌ Bad
logger.Info("User login " + userID + " from " + ipAddr)
// ✅ Good
logger.Info("User login",
zap.String("user_id", userID),
zap.String("ip_address", ipAddr),
)Log relevant context when errors occur:
logger.Error("Database query failed",
zap.String("query", query),
zap.String("table", "users"),
zap.Error(err),
)Reduce storage by compressing old log files:
// Compress daily
logger.Close()
logger.CompressLogFile()
logger.NewLogger(config) // Start freshUse filtering to extract actionable insights:
// Get all errors from last hour
recentErrors, _ := jsonlog.ReadCompressedLogsFiltered(
"app.log.gz",
func(log map[string]interface{}) bool {
// Your custom logic
return true
},
)Maintain consistency for better log analysis:
// Always use same field names across your application
logger.Info("request", zap.String("request_id", rid))
logger.Info("response", zap.String("request_id", rid))go test ./...go test -cover ./...go test -run TestCompressLogFile ./...logger_test.go- Core functionality testsexample_test.go- Usage examples and demonstrations
- Log Writing: ~1,000 logs/ms (varies by field complexity)
- Compression: Typical 10:1 reduction for JSON logs
- Memory Usage: Minimal overhead, logs streamed when reading
- Thread Safety: Safe for concurrent writes from multiple goroutines
MIT License - See LICENSE file
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Submit a pull request
For issues or questions:
- Open an issue on GitHub
- Check existing documentation
- Review test examples
Built with ❤️ using Zap logger