Logrus: structured Go logging and a practical migration path
Use Logrus fields, JSON formatters, and synchronous hooks correctly; evaluate maintenance mode, rotation, and migration without breaking log queries.
Logrus provides structured logging for Go with a familiar standard-library-style API. It remains relevant to teams maintaining applications that already use it. The project describes itself as being in maintenance mode, focused on fixes and limited improvements rather than broad new feature development. For new applications, compare log/slog, Zap, and Zerolog against the required API and workload. Logrus project documentation
For an existing service, first make fields, output, and rotation dependable. A migration is valuable when it solves a measured performance issue or simplifies the logging interface; preserving working queries and alerts is part of that work.
Start with an isolated logger
This complete example writes JSON to stdout, uses an Info threshold, and adds structured fields and an error:
package main
import (
"errors"
"os"
log "github.com/sirupsen/logrus"
)
func main() {
logger := log.New()
logger.SetOutput(os.Stdout)
logger.SetFormatter(&log.JSONFormatter{})
logger.SetLevel(log.InfoLevel)
logger.Info("Service started")
logger.WithFields(log.Fields{
"user": "zhangsan",
"ip": "10.0.0.8",
}).Info("User signed in")
err := errors.New("payment gateway timed out")
logger.WithError(err).Error("Payment failed")
}
A representative sign-in record looks like this; the actual timestamp and JSON key order can differ:
{"level":"info","msg":"User signed in","user":"zhangsan","ip":"10.0.0.8","time":"2026-10-14T09:00:00+08:00"}
Package-level calls configure and use the global logger. A log.New() instance keeps application settings separate from libraries using the package default. Logrus defaults to stderr; this example explicitly selects stdout for a container-style collection path.
There are seven standard levels: Trace, Debug, Info, Warn, Error, Fatal, and Panic. Fatal logs and exits with status 1; Panic logs and panics. These are control-flow choices as well as severity choices. An ordinary recoverable error should normally use Error.
Select text or JSON formatting
Inside code with the logger instance above, a text formatter is useful for development:
logger.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
ForceColors: true,
})
Force colors only for an output that should contain terminal escape sequences. For collected production output, use JSON. Add time to the imports for this formatter:
logger.SetFormatter(&log.JSONFormatter{
TimestampFormat: time.RFC3339,
FieldMap: log.FieldMap{
log.FieldKeyTime: "time",
log.FieldKeyMsg: "message",
},
})
The field map changes the default msg key to message. That is a schema change for a parser or query that expects msg; update collection rules deliberately. WithError adds an error field, but it does not manufacture a call stack for an ordinary Go error.
Keep hooks out of the network critical path
Hooks run for selected levels and can add application-specific behavior. This example counts Error and Fatal events using a concurrency-safe counter. Add sync/atomic to the imports, and declare the type and methods outside main:
type ErrorCountHook struct {
Count atomic.Uint64
}
func (h *ErrorCountHook) Levels() []log.Level {
return []log.Level{log.ErrorLevel, log.FatalLevel}
}
func (h *ErrorCountHook) Fire(entry *log.Entry) error {
h.Count.Add(1)
return nil
}
Register it in main after creating the logger:
hook := &ErrorCountHook{}
logger.AddHook(hook)
logger.Error("Example error")
The counter is process-local and resets on restart; it is not a delivery acknowledgment or durable incident count. This particular level list excludes Panic. Include that level if the intended policy requires it.
Logrus invokes hooks synchronously. An HTTP notification inside Fire can delay the application's logging call and create another failure dependency. Central alert evaluation after collection is usually easier to operate than a notification request on every application error. If a hook queues work asynchronously, its queue limits, drops, and shutdown behavior become explicit responsibilities.
Measure performance on the actual logging path
Structured field maps, entry creation, formatting, and writes all cost CPU and allocations. Published library benchmarks can show differences, but their results depend on the operation, field count, versions, and output sink. A single “100 times faster” ratio is not a general property of every Logrus and Zap workload. The Zap benchmark section distinguishes several logging operations for this reason.
Profile the service before migrating for performance. Compare representative messages at the production log rate, including formatters, hooks, and the writer. A low-volume administrative service may gain little from a library replacement. A high-volume service with logging in its hot path may justify it. No cross-library performance benchmark was run for this guide.
Migrate without changing the operational schema accidentally
Continuing with Logrus can be reasonable when it meets the requirements and dependency updates are tracked. Maintenance mode does not mean every installed release is risk-free, nor does it promise a particular support response time.
For gradual migration, introduce an internal logging interface or use the standard slog API for new code, then adapt existing outputs. A slog.Handler backed by Logrus can let callers change first while the destination stays stable; verify the adapter's maintenance and behavior before adoption. Prioritize services according to measured logging cost and operational need.
Compare field names, field types, severity values, timestamps, error rendering, and call-site behavior during the transition. If a bridge forwards Logrus events to another logger, ensure the original output is disabled or intentionally retained. Otherwise each event may be written twice. Avoid a pair of adapters that forwards in both directions and recurses.
Collect and validate Logrus JSON
Write JSON to the selected file or container output and include a stable service identity and event timestamp. Configure the corresponding DataKit log collection path, then verify that a new sample reaches the workspace.
Parse the JSON with Pipeline, map level to status, and parse time as event time. Decide whether the message field is msg or message and keep that convention consistent. In the log explorer, filter by service and severity before creating a log detection rule with a window and threshold that reflect the failure's impact.
For trace-to-log navigation, add the current request's real trace_id through WithFields. The trace details guide describes related logs. In a multiple-index setup, configure the relevant index mapping and service, environment, and version attributes. A trace identifier alone cannot recover tracing data that was never collected or has expired.
Common questions
Has Logrus stopped being maintained?
The project calls its status maintenance mode. Its stated focus includes security, bug fixes, performance improvements, and limited interoperability work. Evaluate new projects against current alternatives; maintain existing dependencies according to the project's actual releases and your requirements.
What is the difference between WithField and WithFields?
The first adds one field; the second accepts a map. Both return an entry with context. Reuse an entry such as requestLog := logger.WithFields(...) for stable request fields instead of reconstructing the same context for every event. Do not share mutable request-specific field data across unrelated requests.
How do I rotate files?
Logrus does not rotate files itself. Use an appropriate rotating io.Writer, such as a compatible Lumberjack version, or an external strategy with a defined reopen procedure. copytruncate has a copy/truncate race that can lose records; it should not be described as lossless. Container stdout/stderr retention is the runtime or platform's responsibility.
Can a dependency using Logrus share an output with application code using slog?
Yes, through a suitable hook or handler adapter. Test severity mapping, context, errors, duplicate output, and shutdown. The objective is one deliberate collection path and a stable schema, not simply two APIs calling each other.
Related reading
See the English Zap guide, slog guide, and Zerolog guide. The Go logging library comparison is currently in Chinese.
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan