Logging in Go
Logging is essential for debugging, monitoring, and maintaining Go applications. Go’s standard library provides basic logging, and there are several popular third-party libraries for more advanced features.
Standard Library Logging
Go’s log package provides basic logging functionality:
package main
import (
"log"
"os"
)
func main() {
// Basic logging
log.Println("This is a log message")
// Formatted logging
name := "Alice"
age := 30
log.Printf("User %s is %d years old", name, age)
// Fatal logging (exits program)
// log.Fatal("This is a fatal error")
// Panic logging (panics)
// log.Panic("This is a panic")
// Custom logger
logger := log.New(os.Stdout, "MYAPP: ", log.LstdFlags)
logger.Println("Custom prefix message")
// Log to file
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal(err)
}
defer file.Close()
fileLogger := log.New(file, "LOG: ", log.LstdFlags|log.Lshortfile)
fileLogger.Println("This goes to file")
}Log Levels
Implementing different log levels:
package main
import (
"log"
"os"
)
type Logger struct {
info *log.Logger
warn *log.Logger
error *log.Logger
}
func NewLogger() *Logger {
flags := log.LstdFlags | log.Lshortfile
return &Logger{
info: log.New(os.Stdout, "INFO: ", flags),
warn: log.New(os.Stdout, "WARN: ", flags),
error: log.New(os.Stderr, "ERROR: ", flags),
}
}
func (l *Logger) Info(msg string) {
l.info.Println(msg)
}
func (l *Logger) Warn(msg string) {
l.warn.Println(msg)
}
func (l *Logger) Error(msg string) {
l.error.Println(msg)
}
func main() {
logger := NewLogger()
logger.Info("Application started")
logger.Warn("This is a warning")
logger.Error("Something went wrong")
}Structured Logging with Logrus
Logrus is a popular structured logging library:
package main
import (
"os"
"time"
"github.com/sirupsen/logrus"
)
func main() {
// Create logger
logger := logrus.New()
// Set log level
logger.SetLevel(logrus.DebugLevel)
// Set format
logger.SetFormatter(&logrus.JSONFormatter{})
// Log to file
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
logger.Fatal(err)
}
logger.SetOutput(file)
// Basic logging
logger.Info("Application started")
logger.Warn("This is a warning")
logger.Error("This is an error")
// Structured logging with fields
logger.WithFields(logrus.Fields{
"user_id": 123,
"action": "login",
"ip": "192.168.1.1",
}).Info("User login successful")
// Different log levels
logger.Debug("Debug message")
logger.Info("Info message")
logger.Warn("Warning message")
logger.Error("Error message")
// With error
_, err = os.Open("nonexistent.txt")
logger.WithError(err).Error("Failed to open file")
// With timing
start := time.Now()
time.Sleep(100 * time.Millisecond)
logger.WithField("duration", time.Since(start)).Info("Operation completed")
}High-Performance Logging with Zap
Zap is designed for high-performance logging:
package main
import (
"time"
"go.uber.org/zap"
)
func main() {
// Create logger
logger, err := zap.NewProduction()
if err != nil {
panic(err)
}
defer logger.Sync() // Flush any buffered log entries
// Basic logging
logger.Info("Application started")
logger.Error("Something went wrong")
// Structured logging
logger.Info("User login",
zap.String("username", "john_doe"),
zap.Int("user_id", 123),
zap.String("ip", "192.168.1.1"),
)
// With error
err = someFunction()
if err != nil {
logger.Error("Operation failed",
zap.Error(err),
zap.String("operation", "database_query"),
)
}
// Development logger (more readable)
devLogger, _ := zap.NewDevelopment()
devLogger.Info("Development mode logging",
zap.String("key", "value"),
zap.Int("number", 42),
)
// Custom logger
config := zap.Config{
Level: zap.NewAtomicLevelAt(zap.InfoLevel),
Development: false,
Sampling: &zap.SamplingConfig{
Initial: 100,
Thereafter: 100,
},
Encoding: "json",
EncoderConfig: zap.NewProductionEncoderConfig(),
OutputPaths: []string{"stdout", "app.log"},
ErrorOutputPaths: []string{"stderr"},
}
customLogger, err := config.Build()
if err != nil {
panic(err)
}
customLogger.Info("Custom logger message")
}Contextual Logging
Adding context to logs:
package main
import (
"context"
"net/http"
"github.com/sirupsen/logrus"
)
type contextKey string
const loggerKey contextKey = "logger"
func withLogger(ctx context.Context, logger *logrus.Entry) context.Context {
return context.WithValue(ctx, loggerKey, logger)
}
func getLogger(ctx context.Context) *logrus.Entry {
if logger, ok := ctx.Value(loggerKey).(*logrus.Entry); ok {
return logger
}
// Fallback to default logger
return logrus.NewEntry(logrus.StandardLogger())
}
// Middleware for HTTP logging
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Create request-specific logger
requestLogger := logrus.WithFields(logrus.Fields{
"method": r.Method,
"url": r.URL.Path,
"ip": r.RemoteAddr,
})
ctx := withLogger(r.Context(), requestLogger)
r = r.WithContext(ctx)
// Wrap response writer to capture status code
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rw, r.WithContext(ctx))
// Log request completion
requestLogger.WithFields(logrus.Fields{
"status": rw.statusCode,
"duration": time.Since(start),
}).Info("Request completed")
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func handler(w http.ResponseWriter, r *http.Request) {
logger := getLogger(r.Context())
logger.Info("Processing request")
// Simulate some work
time.Sleep(50 * time.Millisecond)
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
loggedMux := loggingMiddleware(mux)
logrus.Info("Server starting on :8080")
http.ListenAndServe(":8080", loggedMux)
}Log Rotation
Handling log file rotation:
package main
import (
"gopkg.in/natefinch/lumberjack.v2"
"github.com/sirupsen/logrus"
)
func setupLogging() {
// Create rotating logger
lumberjackLogger := &lumberjack.Logger{
Filename: "app.log",
MaxSize: 10, // megabytes
MaxBackups: 3,
MaxAge: 28, // days
Compress: true,
}
// Use lumberjack with logrus
logrus.SetOutput(lumberjackLogger)
logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.SetLevel(logrus.InfoLevel)
}
func main() {
setupLogging()
for i := 0; i < 1000; i++ {
logrus.WithField("iteration", i).Info("Log message")
}
}Centralized Logging
Sending logs to external systems:
package main
import (
"bytes"
"encoding/json"
"net/http"
"github.com/sirupsen/logrus"
)
type HTTPHook struct {
URL string
}
func (h *HTTPHook) Fire(entry *logrus.Entry) error {
// Convert log entry to JSON
data, err := entry.String()
if err != nil {
return err
}
// Send to HTTP endpoint
resp, err := http.Post(h.URL, "application/json", bytes.NewBufferString(data))
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func (h *HTTPHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}
}
func main() {
logger := logrus.New()
logger.SetFormatter(&logrus.JSONFormatter{})
// Add HTTP hook for errors
logger.AddHook(&HTTPHook{
URL: "https://api.logservice.com/logs",
})
logger.Info("This is an info message") // Won't be sent to HTTP
logger.Error("This is an error message") // Will be sent to HTTP
}Best Practices
- Use structured logging: Include relevant context in logs
- Choose appropriate log levels: Don’t log everything at info level
- Log errors with context: Include stack traces and relevant data
- Use consistent formatting: JSON for production, text for development
- Rotate log files: Prevent disk space issues
- Consider performance: Use efficient logging libraries for high-throughput apps
- Include timestamps: Always know when events occurred
- Log to multiple outputs: Console for development, files for production
- Handle logging errors: Don’t let logging failures crash your app
- Monitor logs: Set up alerts for error patterns
Choosing a Logging Library
- Standard library: Simple applications, no external dependencies
- Logrus: Popular, feature-rich, good for most applications
- Zap: High performance, structured logging, complex configurations
- ZeroLog: JSON-only, very fast, minimal allocations
Effective logging is crucial for maintaining and debugging Go applications. Choose the right logging approach based on your application’s needs and performance requirements.
For more on error handling, check our error handling tutorial. If you’re building web applications, see the web servers tutorial.