Structured Logging in Go with Logrus
Introduction
In the world of Go development, logging is often one of the first infrastructure concerns developers face. While the standard library’s log package provides basic functionality, it lacks the flexibility and structure needed for modern, production-grade applications. Enter Logrus—a powerful, structured logging library for Go that has garnered 25,712 stars on GitHub and become the de facto standard for many Go projects.
Logrus is designed to solve a fundamental problem: traditional logging outputs unstructured text that’s difficult to parse, filter, and analyze at scale. In microservices architectures, distributed systems, and cloud-native applications, logs are often the primary source of truth for debugging, monitoring, and compliance. Structured logging addresses this by outputting logs in key-value pairs (typically JSON), making them machine-readable and easily integrable with log aggregation tools like ELK stack, Splunk, or Datadog.
Why should you care? Consider these real-world scenarios:
- Debugging production issues: When your application spans dozens of services, searching through unstructured logs for a specific user ID or request ID is like finding a needle in a haystack.
- Compliance and auditing: Many industries require detailed audit trails with consistent, queryable log formats.
- Monitoring and alerting: Structured logs enable precise metric extraction and automated alerting based on specific fields.
- Development productivity: Developers can filter logs by context (environment, service, version) without regex gymnastics.
Logrus provides a drop-in replacement for the standard log package while adding hooks for output destinations, custom formatters, and log levels—all without sacrificing performance. Whether you’re building a CLI tool, HTTP API, or data processing pipeline, Logrus scales from development to production with minimal configuration changes.
Key Features
Logrus distinguishes itself through several powerful features that address both simplicity and advanced use cases:
1. Structured Logging with Fields
Unlike the standard library’s formatted strings, Logrus uses WithField and WithFields to attach structured key-value pairs to log entries. This creates consistent, queryable logs:
1logrus.WithField("user_id", 12345).WithField("action", "login").Info("User authenticated")
2// Output: {"level":"info","msg":"User authenticated","user_id":12345,"action":"login"}
This approach eliminates ad-hoc formatting and ensures every log entry contains contextual metadata.
2. Multiple Output Hooks
Logrus supports hooks—plugins that send logs to external destinations. Built-in hooks include:
logrus/hooks/filefor file rotationlogrus/hooks/syslogfor syslog integrationlogrus/hooks/kafkafor streaming to Kafka
You can chain multiple hooks simultaneously, sending logs to both files and monitoring systems concurrently.
3. Custom Formatters
The default JSON formatter can be replaced with TextFormatter for human-readable output during development. You can also implement custom formatters for proprietary formats or specific log aggregation requirements:
1logrus.SetFormatter(&logrus.TextFormatter{
2 FullTimestamp: true,
3 TimestampFormat: "2006-01-02 15:04:05",
4})
4. Log Level Granularity
Logrus implements all standard log levels (Trace, Debug, Info, Warn, Error, Fatal, Panic) plus Exit (like Fatal but with os.Exit(1)). Levels can be set globally or per-logger instance, enabling verbose debugging in staging while keeping production logs concise.
5. Contextual Logging
While not built-in, Logrus works seamlessly with Go’s context.Context through helper extensions. You can propagate request-scoped values (trace IDs, user sessions) across service boundaries:
1func handler(w http.ResponseWriter, r *http.Request) {
2 logger := logrus.WithContext(r.Context())
3 logger.Info("Processing request")
4}
6. Thread-Safe by Default
All logger methods are safe for concurrent use, crucial for high-throughput applications. The global logger (logrus.StandardLogger()) uses a mutex internally, while custom logger instances can be configured with different concurrency strategies.
7. Environment-Based Configuration
Logrus integrates naturally with 12-factor app principles. You can configure log level, format, and output via environment variables:
1export LOG_LEVEL=debug
2export LOG_FORMAT=json
This enables zero-downtime configuration changes in containerized environments.
8. Extensible Entry Mechanism
Every log call returns a logrus.Entry—a snapshot of the logger at that moment. Entries can be modified, reused, or passed between functions, enabling complex logging patterns without repeated field duplication.
Compared to the standard library, Logrus eliminates the “stringly-typed” logging problem, provides production-ready features out-of-the-box, and maintains backward compatibility through logrus.StandardLogger().Out = os.Stderr to mimic log behavior.
Installation and Setup
Logrus requires Go 1.13 or newer and has no external dependencies beyond the standard library. Installation is straightforward:
1go get github.com/Sirupsen/logrus/v1
Note the /v1 suffix—Logrus uses Go modules with major versioning. Import it in your code:
1import "github.com/Sirupsen/logrus/v1"
For most projects, this single command suffices. However, if you need specific hooks (like file rotation or syslog), install them separately:
1go get github.com/Sirupsen/logrus/v1/hooks/file
2go get github.com/Sirupsen/logrus/v1/hooks/syslog
Verification: Create a simple main.go:
1package main
2
3import (
4 "github.com/Sirupsen/logrus/v1"
5)
6
7func main() {
8 logrus.Info("Logrus is working!")
9}
Run it:
1go run main.go
2# Output: {"level":"info","msg":"Logrus is working!","time":"2024-01-15T10:30:00-05:00"}
If you see JSON output, installation succeeded. No additional configuration is required—Logrus works with sensible defaults.
Basic Usage
Let’s start with the simplest possible example, then break it down:
1package main
2
3import (
4 "github.com/Sirupsen/logrus/v1"
5)
6
7func main() {
8 // 1. Set log level (optional, defaults to Info)
9 logrus.SetLevel(logrus.DebugLevel)
10
11 // 2. Log at different levels
12 logrus.Trace("This is a trace message")
13 logrus.Debug("Debugging details")
14 logrus.Info("Normal operation message")
15 logrus.Warn("Warning about something")
16 logrus.Error("Error occurred")
17
18 // 3. Structured logging with fields
19 logrus.WithFields(logrus.Fields{
20 "user_id": 42,
21 "session": "abc123",
22 }).Info("User logged in")
23
24 // 4. Single field shortcut
25 logrus.WithField("request_id", "req-789").Warn("Slow database query")
26
27 // 5. Formatting output (optional)
28 logrus.SetFormatter(&logrus.JSONFormatter{
29 TimestampFormat: "2006-01-02 15:04:05",
30 })
31 logrus.Info("Now using custom JSON format")
32}
Expected output (timestamps will vary):
1{"level":"trace","msg":"This is a trace message","time":"2024-01-15T10:30:00-05:00"}
2{"level":"debug","msg":"Debugging details","time":"2024-01-15T10:30:00-05:00"}
3{"level":"info","msg":"Normal operation message","time":"2024-01-15T10:30:00-05:00"}
4{"level":"warning","msg":"Warning about something","time":"2024-01-15T10:30:00-05:00"}
5{"level":"error","msg":"Error occurred","time":"2024-01-15T10:30:00-05:00"}
6{"level":"info","msg":"User logged in","user_id":42,"session":"abc123","time":"2024-01-15T10:30:00-05:00"}
7{"level":"warning","msg":"Slow database query","request_id":"req-789","time":"2024-01-15T10:30:00-05:00"}
8{"level":"info","msg":"Now using custom JSON format","time":"2024-01-15T10:30:00-05:00"}
Explanation:
- Line 7: Sets the minimum log level to
Debug. Lower levels (Trace) are filtered out by default. - Lines 10-14: Demonstrates all log levels.
FatalandPanicwould exit the program. - Line 17:
WithFieldsattaches multiple key-value pairs to the log entry. These fields appear as top-level JSON keys. - Line 22:
WithFieldis a convenience for single fields. - Lines 25-27: Configures a custom JSON formatter with a specific timestamp format. Without this, Logrus uses RFC3339 by default.
The global logger (logrus package-level functions) is convenient but limits flexibility. For larger applications, create custom loggers:
1logger := logrus.New()
2logger.Out = os.Stdout // Write to stdout instead of stderr
3logger.SetLevel(logrus.WarnLevel)
4logger.WithField("component", "auth").Info("Service started")
Real-World Examples
Example 1: HTTP Middleware with Structured Logging
This example demonstrates a production-ready HTTP server with request-scoped logging, middleware, and error handling. Each request gets a unique request_id propagated through all logs.
1package main
2
3import (
4 "context"
5 "encoding/json"
6 "log"
7 "net/http"
8 "time"
9
10 "github.com/Sirupsen/logrus/v1"
11)
12
13// requestIDKey is the context key for request ID
14type contextKey string
15
16const requestIDKey contextKey = "request_id"
17
18// LoggerMiddleware injects a logger with request_id into the context
19func LoggerMiddleware(next http.Handler) http.Handler {
20 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21 // Generate or extract request ID
22 requestID := r.Header.Get("X-Request-ID")
23 if requestID == "" {
24 requestID = generateRequestID()
25 }
26
27 // Create logger with request-scoped fields
28 logger := logrus.New()
29 logger.SetLevel(logrus.InfoLevel)
30 logger.SetFormatter(&logrus.JSONFormatter{
31 TimestampFormat: time.RFC3339,
32 })
33 logger = logger.WithFields(logrus.Fields{
34 "request_id": requestID,
35 "remote_addr": r.RemoteAddr,
36 "method": r.Method,
37 "path": r.URL.Path,
38 })
39
40 // Inject logger into context
41 ctx := context.WithValue(r.Context(), requestIDKey, logger)
42 r = r.WithContext(ctx)
43
44 // Log request start
45 logger.Info("Request started")
46
47 // Capture response status and duration
48 start := time.Now()
49 rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
50 next.ServeHTTP(rw, r)
51
52 // Log request completion
53 duration := time.Since(start)
54 logger = logger.WithField("duration_ms", duration.Milliseconds())
55 logger = logger.WithField("status_code", rw.statusCode)
56 if rw.statusCode >= 400 {
57 logger.Warn("Request completed with error")
58 } else {
59 logger.Info("Request completed")
60 }
61 })
62}
63
64// responseWriter wraps http.ResponseWriter to capture status code
65type responseWriter struct {
66 http.ResponseWriter
67 statusCode int
68}
69
70func (rw *responseWriter) WriteHeader(code int) {
71 rw.statusCode = code
72 rw.ResponseWriter.WriteHeader(code)
73}
74
75// GetLoggerFromContext retrieves the logger from context
76func GetLoggerFromContext(ctx context.Context) *logrus.Logger {
77 if logger, ok := ctx.Value(requestIDKey).(*logrus.Logger); ok {
78 return logger
79 }
80 // Fallback to global logger
81 return logrus.StandardLogger()
82}
83
84// generateRequestID creates a unique ID (in production, use UUID or similar)
85func generateRequestID() string {
86 return "req-" + time.Now().Format("20060102150405") + "-" + randomString(6)
87}
88
89func randomString(n int) string {
90 const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
91 b := make([]byte, n)
92 for i := range b {
93 b[i] = letters[time.Now().UnixNano()%int64(len(letters))]
94 }
95 return string(b)
96}
97
98// Handler demonstrates using context logger
99func helloHandler(w http.ResponseWriter, r *http.Request) {
100 logger := GetLoggerFromContext(r.Context())
101
102 name := r.URL.Query().Get("name")
103 if name == "" {
104 logger.Error("Missing 'name' query parameter")
105 http.Error(w, "Missing name parameter", http.StatusBadRequest)
106 return
107 }
108
109 logger.WithField("name", name).Info("Serving hello request")
110 json.NewEncoder(w).Encode(map[string]string{
111 "message": "Hello, " + name,
112 })
113}
114
115// Simulated database function with logging
116func queryDatabase(userID string) error {
117 logger := logrus.WithField("component", "database")
118 logger.WithField("user_id", userID).Debug("Executing database query")
119
120 // Simulate database operation
121 time.Sleep(50 * time.Millisecond)
122
123 if userID == "error" {
124 logger.WithField("user_id", userID).Error("Database query failed")
125 return &queryError{userID: userID}
126 }
127
128 logger.WithField("user_id", userID).Debug("Query successful")
129 return nil
130}
131
132type queryError struct {
133 userID string
134}
135
136func (e *queryError) Error() string {
137 return "query failed for user: " + e.userID
138}
139
140// UserHandler demonstrates error handling with logging
141func userHandler(w http.ResponseWriter, r *http.Request) {
142 logger := GetLoggerFromContext(r.Context())
143 userID
144
145---
146
147*Photo by [Quentin Caron](https://unsplash.com/@quentincaron) on [Unsplash](https://unsplash.com)*