When developing enterprise software or any other software for that matter, it is always good practice to have a structured logging framework. Logrus is a structured logger for Go and is compatible with the standard log package. The standard logging package is limited in some ways and that led to development of other logging packages like Logrus, oklog and zerolog. Logging is an important aspect in modern programming and are used to :
- Log Errors or Warning messages that developers need to know about.
- Log performance metrics , memory consumption etc
- Log Debug messages to help developers troubleshoot their software.
Logging is a way of letting our software communicate to us what is going on. For that reason , not everything should be logged as that will be a waste of time and resources. Developers need to only log messages that are meaningful. These logs messages can be sent to the stdout, or files like log.txt file.
To follow along with this tutorial , please ensure you have basic Go skills and you have Go language installed on your computer.
Installing logrus
Logrus is not a standard package that comes with Go. It has to be added into your package manually . To get started , navigate to your working directory and use the below commands to create a module and install Logrus package.
$ go get github.com/sirupsen/logrus
go get: added github.com/sirupsen/logrus v1.9.0
go get: added golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8go get github.com/sirupsen.logrus
Now you can directly start using logrus in your package file:
package main
import (
"github.com/sirupsen/logrus"
)
func main() {
// Your Code
}
Getting started with Logrus
Now that we have successfully installed the logrus package, let us now learn how we can use logrus in the initial stages.Logrus has different methods just like the fmt package used to log messages to the terminal or any other output. These methods are Println()
, Printf()
, Fatal()
, Fatalf()
and many more.
Example
package main
package main
import (
"os"
"github.com/sirupsen/logrus"
)
func main() {
logrus.Println("Hello, world!")
}
Explanation
In the above example, we are importing the logrus package and assigning it the alias name log. In the main function we use the log alias name to access the Println()
methods that come with the logrus package.
Output
$ go run main.go
INFO[0000] Hello, world!
Choosing different log levels in logrus
Logrus offers different log levels e.g panic, fatal, error, warning, info, debug and trace. A log level is a way of determining the severity/priority of any type of event. These levels are ordered from the lowest to the highest as shown below.
- Trace
- Debug
- Info
- Warn
- Error
- Fatal
- Panic
You can set the log level using:
logrus.SetLevel(logrus.TraceLevel)
logrus.SetLevel(logrus.DebugLevel)
logrus.SetLevel(logrus.InfoLevel)
logrus.SetLevel(logrus.WarnLevel)
logrus.SetLevel(logrus.ErrorLevel)
logrus.SetLevel(logrus.FatalLevel)
Here we have set the log level as TraceLevel
so we will log everything. For Example:
package main
import (
"github.com/sirupsen/logrus"
)
func main() {
logrus.SetLevel(logrus.TraceLevel)
logrus.Traceln("Trace Level")
logrus.Debugln("Debug Level")
logrus.Infoln("Info Level")
logrus.Warningln("Warning Level")
logrus.Errorln("Error Level")
logrus.Fatalln("Fatal Level")
logrus.Panicln("Panic Level")
}
Since we have set the log level as TraceLevel
so all levels of logs are getting logged. Output:
$ go run main.go
TRAC[0000] Trace Level
DEBU[0000] Debug Level
INFO[0000] Info Level
WARN[0000] Warning Level
ERRO[0000] Error Level
FATA[0000] Fatal Level
exit status 1
Now let us set the log level to DebugLevel
:
package main
import (
"github.com/sirupsen/logrus"
)
func main() {
logrus.SetLevel(logrus.DebugLevel)
logrus.Traceln("Trace Level")
logrus.Debugln("Debug Level")
logrus.Infoln("Info Level")
logrus.Warningln("Warning Level")
logrus.Errorln("Error Level")
logrus.Fatalln("Fatal Level")
logrus.Panicln("Panic Level")
}
As expected, trace level messages are not shown anymore:
$ go run main.go
DEBU[0000] Debug Level
INFO[0000] Info Level
WARN[0000] Warning Level
ERRO[0000] Error Level
FATA[0000] Fatal Level
exit status 1
logrus.SetOutput to stdOut, stderr and/or log file
logrus supports different options to print and log messages. We can
- Only print messages on console
- Only log messages to log file
- Print on console and log messages to log file
Print messages on console (stdout and stderr)
Use SetOutput
to os.Stdout
or os.Stderr
to print messages on console:
package main
import (
"os"
log "github.com/sirupsen/logrus"
)
func main() {
// Output to stdout instead of the default stderr
log.SetOutput(os.Stdout)
// Only log the debug severity or above
log.SetLevel(log.DebugLevel)
log.Info("Info message")
log.Warn("Warn message")
log.Error("Error message")
log.Fatal("Fatal message")
}
Log messages in log file
To log messages into log file use SetOutput
with the filename using logrus.SetOutput(file)
:
package main
import (
"fmt"
"os"
log "github.com/sirupsen/logrus"
)
func main() {
logFile := "log.txt"
f, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
fmt.Println("Failed to create logfile" + logFile)
panic(err)
}
defer f.Close()
// Output to stdout instead of the default stderr
log.SetOutput(f)
// Only log the debug severity or above
log.SetLevel(log.DebugLevel)
log.Info("Info message")
log.Warn("Warn message")
log.Error("Error message")
log.Fatal("Fatal message")
}
Print on console and also in log file
package main
import (
"fmt"
"io"
"os"
"github.com/sirupsen/logrus"
easy "github.com/t-tomalak/logrus-easy-formatter"
)
func main() {
f, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
fmt.Println("Failed to create logfile" + "log.txt")
panic(err)
}
defer f.Close()
log := &logrus.Logger{
// Log into f file handler and on os.Stdout
Out: io.MultiWriter(f, os.Stdout),
Level: logrus.DebugLevel,
Formatter: &easy.Formatter{
TimestampFormat: "2006-01-02 15:04:05",
LogFormat: "[%lvl%]: %time% - %msg%\n",
},
}
log.Trace("Trace message")
log.Info("Info message")
log.Warn("Warn message")
log.Error("Error message")
log.Fatal("Fatal message")
}
Output:
# go run main.go
[INFO]: 2022-08-25 11:27:06 - Info message
[WARNING]: 2022-08-25 11:27:06 - Warn message
[ERROR]: 2022-08-25 11:27:06 - Error message
[FATAL]: 2022-08-25 11:27:06 - Fatal message
exit status 1
Content of log.txt
file
[INFO]: 2022-08-25 11:27:06 - Info message
[WARNING]: 2022-08-25 11:27:06 - Warn message
[ERROR]: 2022-08-25 11:27:06 - Error message
[FATAL]: 2022-08-25 11:27:06 - Fatal message
Alternatively you can just set multi writer using SetOutput
:
multi := io.MultiWriter(logFile, os.Stdout)
logrus.SetOutput(multi)
logrus show line number
To print the line number of the entries in the code which is printing or logging the messages we can use SetReportCaller
:
package main
import (
"os"
log "github.com/sirupsen/logrus"
)
func main() {
// Output to stdout instead of the default stderr
log.SetOutput(os.Stdout)
// Only log the debug severity or above
log.SetLevel(log.DebugLevel)
// logrus show line number
log.SetReportCaller(true)
log.Info("Info message")
log.Warn("Warn message")
log.Error("Error message")
log.Fatal("Fatal message")
}
Output:
$ go run main.go INFO[0000]/opt/deepak/scripts/goexamples/global-vars/main.go:18 main.main() Info message WARN[0000]/opt/deepak/scripts/goexamples/global-vars/main.go:19 main.main() Warn message ERRO[0000]/opt/deepak/scripts/goexamples/global-vars/main.go:20 main.main() Error message FATA[0000]/opt/deepak/scripts/goexamples/global-vars/main.go:21 main.main() Fatal message exit status 1
Using hooks in logrus
In Logrus, hooks are essentially interfaces that can be added to the logger to execute custom logic or side effects when log entries are made. A hook in Logrus only has to satisfy the Hook
interface, which involves implementing two methods: Levels()
and Fire()
.
Levels()
: Defines for which log levels this hook will be triggered.Fire()
: Contains the logic that gets executed when a log of the specified levels is made.
Define the Hook:
First, let's design a hook that writes ErrorLevel
log entries to a file named error.log
.
package main
import (
"fmt"
"os"
"github.com/sirupsen/logrus"
)
type FileHook struct {
File *os.File
}
func (hook *FileHook) Levels() []logrus.Level {
return []logrus.Level{logrus.ErrorLevel}
}
func (hook *FileHook) Fire(entry *logrus.Entry) error {
msg := fmt.Sprintf("%s: %s\n", entry.Time.Format("2006-01-02 15:04:05"), entry.Message)
_, err := hook.File.WriteString(msg)
return err
}
Using the Hook in Your Application:
Now, we can integrate this hook with our Logrus logger:
func main() {
logger := logrus.New()
// Open a file for appending logs
file, err := os.OpenFile("error.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
logger.Fatalf("Failed to open log file: %v", err)
}
defer file.Close()
hook := &FileHook{File: file}
logger.AddHook(hook)
// This error will be written to the error.log file!
logger.Error("This is a critical error!")
}
Explanation:
- The
FileHook
type is defined to satisfy thelogrus.Hook
interface. - The
Levels
method specifies that it only cares about error logs. - The
Fire
method writes the error log entry to a local file with a timestamp. - In the main function, we instantiate the logger, create/open the
error.log
file, add our custom file hook, and then make an error log.
# go run main.go ERRO[0000] This is a critical error! # cat error.log 2023-08-20 14:33:43: This is a critical error!
Format logrus messages
Logrus has two main built-in formatters namely logrus.TextFormatter
and logrus.JSONFormatter
. They are used to format the logs coming from the logger using an object formatter. By default the the formatter in logrus will set logrus.TextFormatter
.The logrus.TextFormatter
can be used to log events in colors if the stdout is a tty. To be able to switch between these two formatters, logrus offers a method called <code>logger.SetFormatter()
, that takes the format type as the input. In the following examples we will use <code>logrus.TextFormatter
as the object formatter.
logrus.TextFormatter
logrus.TextFormatter
provides logs in the form of key-value pairs. It is designed for console output where developers can view logs in real-time or when logs are directed to a plain-text file. The format ensures that the logs are easily discernible and structured enough for visual understanding.
The key features include:
- Timestamps: By default, each log entry starts with a timestamp indicating when the log was generated.
- Log Levels: Indicates the severity of the log, like
info
,warn
,error
, etc. - Log Message: The main message of the log.
- Fields: Any additional fields are displayed in key-value pairs, offering context or more data regarding the log.
- Color-Coding (Optional): When outputting to a terminal,
TextFormatter
can use colors to differentiate log levels, making it easier to spot warnings, errors, or other log levels. - Configuration Options: Provides several customization features such as:
- Disabling timestamps.
- Choosing custom timestamp formats.
- Enabling/Disabling color coding.
When initializing Logrus without setting any formatter, it uses TextFormatter
as default:
import (
"github.com/sirupsen/logrus"
)
func main() {
logrus.WithField("example", "textFormatter").Info("Hello from Logrus!")
}
The log output might look something like this:
time="2021-08-20T14:23:12Z" level=info msg="Hello from Logrus!" example=textFormatter
Customizing Field Delimiters
You can customize the delimiters used between different components of the log entry, such as timestamp, level, message, and fields.
logrus.SetFormatter(&logrus.TextFormatter{
DisableTimestamp: false,
TimestampFormat: "2006-01-02 15:04:05",
DisableColors: false,
QuoteEmptyFields: true,
ForceFormatting: false,
DisableLevelTruncation: true,
PadLevelText: true,
FullTimestamp: false,
// Customizing delimiters
FieldMap: logrus.FieldMap{
logrus.FieldKeyTime: "@timestamp",
logrus.FieldKeyLevel: "severity",
logrus.FieldKeyMsg: "message",
logrus.FieldKeyFunc: "caller",
},
})
logrus.JSONFormatter
logrus.JSONFormatter
outputs log entries in a structured JSON format. JSON (JavaScript Object Notation) is a lightweight data-interchange format that is both human-readable and machine-parsable. This formatter is ideal when you need to aggregate logs, analyze them using log analysis tools, or when you need to integrate logs into other systems.
The key features include:
- Structured Data: Logs are structured in key-value pairs, making them machine-friendly and easily searchable.
- Compact Format: JSON format is concise and efficient, which is beneficial when dealing with large volumes of logs.
- Interoperability: JSON is a widely accepted standard for data exchange, ensuring compatibility with various tools and systems.
- Human-Readable (to an extent): While JSON is machine-readable, some log management tools offer pretty-printing to make it more human-readable when needed.
To use the logrus.JSONFormatter
, you simply set it as the formatter for your Logrus instance:
import (
"github.com/sirupsen/logrus"
)
func main() {
logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.Info("This is a JSON-formatted log")
}
The log output might look something like this:
{"time":"2021-08-20T14:23:12Z","level":"info","msg":"This is a JSON-formatted log"}
The logrus.JSONFormatter
provides a few customization options to tailor the JSON-formatted log outputs according to your preferences and requirements. Here are the different possible customizations:
package main
import (
"github.com/sirupsen/logrus"
)
func main() {
// Create a JSON formatter instance with the valid customizations
jsonFormatter := &logrus.JSONFormatter{
PrettyPrint: true,
TimestampFormat: "2006-01-02 15:04:05",
}
// Set the JSON formatter instance as the formatter for the logger
logrus.SetFormatter(jsonFormatter)
// Now, log something
logrus.WithFields(logrus.Fields{
"user": "john_doe",
"action": "login",
}).Info("User logged in")
}
Custom Formatter
Logrus is a flexible and efficient logging library in Go, and one of its powerful features is its ability to have custom formatters. A formatter in logrus is responsible for converting log entries into byte slices, which are then written to the desired output.
Let us create a custom formatter with a formatted message that contains
- Timestamp
- Log level (e.g., INFO, ERROR)
- Session ID
- Script name (filename)
- Line number
- The actual log message
package main
import (
"fmt"
"path/filepath"
"runtime"
"strings"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
type CustomTextFormatter struct {
SessionID string
}
func (f *CustomTextFormatter) Format(entry *logrus.Entry) ([]byte, error) {
// Get the file and line number where the log was called
_, filename, line, _ := runtime.Caller(7)
// Get the script name from the full file path
scriptName := filepath.Base(filename)
// Format the log message
message := fmt.Sprintf("[%s] [%s] [%s] [%s:%d] %s\n",
entry.Time.Format("2006-01-02 15:04:05"), // Date-time
entry.Level.String(), // Log level
f.SessionID, // Unique session ID
scriptName, // Script name
line, // Line number
entry.Message, // Log message
)
return []byte(message), nil
}
// Generate a unique session ID
func generateSessionID() string {
randomUUID := uuid.New()
return strings.Replace(randomUUID.String(), "-", "", -1)
}
func main() {
// Generate a new unique session ID
sessionID := generateSessionID()
// Create a new instance of the custom formatter
customFormatter := &CustomTextFormatter{SessionID: sessionID}
// Set the custom formatter as the formatter for the logger
logrus.SetFormatter(customFormatter)
// Now, log something
logrus.Info("This is a custom-formatted log")
}
Output:
[2023-08-20 14:22:21] [info] [449499175f8a44fcb7ee6b5b28c60a4d] [main.go:54] This is a custom-formatted log
Summary
Structured logging determines how reliable an application is. Go comes with a standard logger which is limited in terms of functionality. Logrus makes logging easy and fun and can be used to build metrics, log latency and many more. It also tries to make logging possible in a structured fashion.
References
https://pkg.go.dev/github.com/sirupsen/logrus#JSONFormatter
logrus set output format