Logging in Go
Logging is crucial for understanding application behavior, debugging issues, and monitoring production systems. Go provides a built-in log package and excellent third-party logging libraries. This guide covers logging fundamentals and best practices.
Built-in log Package
Basic Logging
package main
import "log"
func main() {
// Print logs to stdout
log.Print("This is a log message")
log.Println("This is another log message")
log.Printf("Formatted message: %d\n", 42)
// Fatal logs and exits
// log.Fatal("Fatal error")
// Panic logs and panics
// log.Panic("Panic error")
}
Log Output
package main
import (
"log"
"os"
)
func main() {
// Log to stdout (default)
log.Println("Log to stdout")
// Log to stderr
log.SetOutput(os.Stderr)
log.Println("Log to stderr")
// Log to file
f, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
panic(err)
}
defer f.Close()
log.SetOutput(f)
log.Println("Log to file")
}
Log Flags
package main
import (
"log"
)
func main() {
// Default flags
log.Println("Default flags")
// Set custom flags
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("With file and line number")
// Available flags
// log.Ldate - Date in local time zone
// log.Ltime - Time in local time zone
// log.Lmicroseconds - Microseconds
// log.Llongfile - Full file path and line number
// log.Lshortfile - Short file path and line number
// log.LUTC - UTC instead of local time
// log.Lmsgprefix - Move prefix to end
}
Custom Logger
package main
import (
"log"
"os"
)
func main() {
// Create custom logger
logger := log.New(os.Stdout, "INFO: ", log.LstdFlags)
logger.Println("Custom logger message")
// Create error logger
errorLogger := log.New(os.Stderr, "ERROR: ", log.LstdFlags|log.Lshortfile)
errorLogger.Println("Error message")
}
Structured Logging
JSON Logging
package main
import (
"encoding/json"
"fmt"
"time"
)
type LogEntry struct {
Timestamp string `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
Data map[string]interface{} `json:"data,omitempty"`
}
func logJSON(level, message string, data map[string]interface{}) {
entry := LogEntry{
Timestamp: time.Now().Format(time.RFC3339),
Level: level,
Message: message,
Data: data,
}
b, _ := json.Marshal(entry)
fmt.Println(string(b))
}
func main() {
logJSON("INFO", "User created", map[string]interface{}{
"user_id": 123,
"email": "[email protected]",
})
logJSON("ERROR", "Database error", map[string]interface{}{
"operation": "INSERT",
"table": "users",
"error": "connection refused",
})
}
Structured Logger Implementation
package main
import (
"encoding/json"
"fmt"
"time"
)
type Logger struct {
level string
}
type LogEntry struct {
Timestamp string `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
Data map[string]interface{} `json:"data,omitempty"`
}
func (l *Logger) log(level, message string, data map[string]interface{}) {
entry := LogEntry{
Timestamp: time.Now().Format(time.RFC3339),
Level: level,
Message: message,
Data: data,
}
b, _ := json.Marshal(entry)
fmt.Println(string(b))
}
func (l *Logger) Info(message string, data map[string]interface{}) {
l.log("INFO", message, data)
}
func (l *Logger) Error(message string, data map[string]interface{}) {
l.log("ERROR", message, data)
}
func (l *Logger) Debug(message string, data map[string]interface{}) {
l.log("DEBUG", message, data)
}
func main() {
logger := &Logger{}
logger.Info("User created", map[string]interface{}{
"user_id": 123,
"email": "[email protected]",
})
logger.Error("Database error", map[string]interface{}{
"operation": "INSERT",
"error": "connection refused",
})
}
Log Levels
Implementing Log Levels
package main
import (
"fmt"
"log"
"os"
)
type LogLevel int
const (
DEBUG LogLevel = iota
INFO
WARNING
ERROR
FATAL
)
var logLevelNames = map[LogLevel]string{
DEBUG: "DEBUG",
INFO: "INFO",
WARNING: "WARNING",
ERROR: "ERROR",
FATAL: "FATAL",
}
type Logger struct {
level LogLevel
logger *log.Logger
}
func NewLogger(level LogLevel) *Logger {
return &Logger{
level: level,
logger: log.New(os.Stdout, "", log.LstdFlags),
}
}
func (l *Logger) log(level LogLevel, message string) {
if level >= l.level {
l.logger.Printf("[%s] %s\n", logLevelNames[level], message)
}
}
func (l *Logger) Debug(message string) {
l.log(DEBUG, message)
}
func (l *Logger) Info(message string) {
l.log(INFO, message)
}
func (l *Logger) Warning(message string) {
l.log(WARNING, message)
}
func (l *Logger) Error(message string) {
l.log(ERROR, message)
}
func (l *Logger) Fatal(message string) {
l.log(FATAL, message)
os.Exit(1)
}
func main() {
logger := NewLogger(INFO)
logger.Debug("Debug message") // Not logged
logger.Info("Info message") // Logged
logger.Warning("Warning message") // Logged
logger.Error("Error message") // Logged
}
Third-Party Logging Libraries
Using logrus
# Install logrus
go get github.com/sirupsen/logrus
package main
import (
"github.com/sirupsen/logrus"
)
func main() {
log := logrus.New()
// Set log level
log.SetLevel(logrus.InfoLevel)
// Log with fields
log.WithFields(logrus.Fields{
"user_id": 123,
"email": "[email protected]",
}).Info("User created")
log.WithFields(logrus.Fields{
"operation": "INSERT",
"error": "connection refused",
}).Error("Database error")
}
Using zap
# Install zap
go get go.uber.org/zap
package main
import (
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("User created",
zap.Int("user_id", 123),
zap.String("email", "[email protected]"),
)
logger.Error("Database error",
zap.String("operation", "INSERT"),
zap.String("error", "connection refused"),
)
}
Logging Best Practices
Context-Aware Logging
package main
import (
"context"
"log"
)
type contextKey string
const requestIDKey contextKey = "request_id"
func logWithContext(ctx context.Context, message string) {
requestID := ctx.Value(requestIDKey)
log.Printf("[%v] %s\n", requestID, message)
}
func main() {
ctx := context.WithValue(context.Background(), requestIDKey, "req-123")
logWithContext(ctx, "Processing request")
}
Request Logging Middleware
package main
import (
"log"
"net/http"
"time"
)
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("Started %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
duration := time.Since(start)
log.Printf("Completed %s %s in %v", r.Method, r.URL.Path, duration)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello"))
})
http.ListenAndServe(":8080", loggingMiddleware(mux))
}
Practical Examples
Application Logger
package main
import (
"encoding/json"
"fmt"
"time"
)
type AppLogger struct {
minLevel string
}
type LogEntry struct {
Timestamp string `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
Data map[string]interface{} `json:"data,omitempty"`
}
func (l *AppLogger) log(level, message string, data map[string]interface{}) {
entry := LogEntry{
Timestamp: time.Now().Format(time.RFC3339),
Level: level,
Message: message,
Data: data,
}
b, _ := json.Marshal(entry)
fmt.Println(string(b))
}
func (l *AppLogger) Info(message string, data map[string]interface{}) {
l.log("INFO", message, data)
}
func (l *AppLogger) Error(message string, data map[string]interface{}) {
l.log("ERROR", message, data)
}
func (l *AppLogger) Debug(message string, data map[string]interface{}) {
l.log("DEBUG", message, data)
}
func main() {
logger := &AppLogger{}
logger.Info("Application started", nil)
logger.Info("User created", map[string]interface{}{
"user_id": 123,
"email": "[email protected]",
})
logger.Error("Database error", map[string]interface{}{
"operation": "INSERT",
"error": "connection refused",
})
}
Best Practices
โ Good Practices
- Use structured logging - JSON format for parsing
- Include context - Request IDs, user IDs, etc.
- Use appropriate log levels - DEBUG, INFO, WARNING, ERROR
- Log important events - User actions, errors, state changes
- Include timestamps - For correlation and debugging
- Use consistent format - Across all logs
- Don’t log sensitive data - Passwords, tokens, PII
- Rotate log files - Prevent disk space issues
โ Anti-Patterns
// โ Bad: Logging sensitive data
log.Printf("User password: %s", password)
// โ
Good: Don't log sensitive data
log.Printf("User authenticated: %s", username)
// โ Bad: No context
log.Println("Error occurred")
// โ
Good: Include context
log.Printf("Error in user creation: %v", err)
// โ Bad: Inconsistent format
log.Println("User created")
fmt.Println("Product updated")
// โ
Good: Consistent format
logger.Info("User created")
logger.Info("Product updated")
Summary
Effective logging is essential:
- Use the log package for basic logging
- Implement structured logging for production
- Use appropriate log levels
- Include context in logs
- Don’t log sensitive data
- Use consistent format
- Rotate log files
- Consider third-party libraries for advanced features
Master logging for better application observability.
Comments