When you are developing a web application, it’s important to understand what’s going on behind the scenes. Logging plays a critical role in providing insights into your web server’s activities and help you identify issues, monitor performance and ensure that the application runs well. In this post, I will explain how to enable logging in a Go web application with Zap.
We will understand the basics of setting up Zap with Go and how you can use it to write logs to the standard output as well as a special log file.
If you are new to working with web servers in Go, I recommend going through my post on setting up a Go HTTP Web Server.
1 – What is Zap?
When it comes to logging in Go, Zap is a pretty formidable choice.
And it’s not without reason.
Zap is a high-performance logging library that’s designed for Go applications. With it’s lightning-fast performance and rich features, Zap has become the primary choice for logging within the Go development community.
Here are some key reasons for this popularity:
- Performance – Zap is built with a focus on performance, making it exceptionally fast and lightweight. It basically ensures that logging doesn’t introduce undue overhead to your Go web servers.
- Structured Logging – Zap provides the ability to log data in a structured format. This makes it easy to parse and analyze log entries.
- Multiple Outputs – With Zap, you can also log output to various destinations such as files, standard output or even external services. In other words, you can easily tailor your logging strategy to specific needs.
- Active Development – Zap is actively maintained and developed. This means it’s constantly evolving along with the Go ecosystem and offers cutting-edge features.
2 – Setting up Zap Logging in a Go Project
Getting started with Zap is quite straightforward.
To begin with, you need to install Zap in your Go project. To do so, open the terminal and execute the following command to fetch the Zap package.
$ go get -u go.uber.org/zap
Basically, this command will download and install Zap so that you can use it in your project.
In the next step, you need to import the Zap package into your Go application. This is done with the below import statement.
import "go.uber.org/zap"
Following this, you need to configure Zap.
It provides basic configuration options through its zap.NewProduction()
and zap.NewDevelopment()
functions. Basically, these functions create pre-configured logger instances that are suitable for production and development environments.
Here’s an example on how to use them.
package main
import (
"go.uber.org/zap"
)
func main() {
logger, err := zap.NewDevelopment()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Info("Logging",
zap.String("username", "john_doe"),
)
}
If you run this program, you should see the below output in the console.
2023-10-03T08:41:21.190+0530 INFO logging-demo/main.go:16 Logging {"username": "john_doe"}
Basically, in the snippet, we create a development logger instance using zap.NewDevelopment()
. Then, we use the logger.Info()
method to write a structured log to the console.
3 – Logging to File with Go and Zap
Let’s now look at another case.
Instead of the standard output console, what if we wanted to write the logs to a file?
You can easily achieve this using Zap.
Check the below example:
package main
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
config := zap.NewProductionConfig()
config.OutputPaths = []string{"logs/app.log"}
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
logger, err := config.Build()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Info("Logging setup with Zap in Go")
logger.Info("User Info",
zap.String("username", "john_doe"),
zap.Int("user_id", 567),
zap.Bool("success", true),
)
}
Let’s understand what’s going on here:
- First, we configure a Zap logger with production-level settings. The log output is directed to a file named “app.log” and the log levels are formatted with capital letters for better readability.
- Next, we initialize the logger with the specified configuration. In case of any errors during the process, it will panic and indicate a critical issue.
- To ensure that any buffered log entries are flushed before the program exits, it defers the
logger.Sync()
call. This is a best practice to avoid losing log data - Lastly, the code uses the logger instance to log two messages. The first message just prints a basic info text. The second message provides additional structured data including a username, user ID and success boolean value.
If you run the program, you should be able to see the log entries in the “app.log” file. Just remember to create a folder named “logs” manually. The log file will be automatically created.
4 – Go Web Server Logging with Zap
Let’s now look at how you can create a middleware to perform logging in a Go web server with Zap.
Logging middleware is quite valuable for capturing information about incoming requests and outgoing responses.
Think of a middleware function as an intermediary that sits between the client’s HTTP request and the final handler function. These middleware functions can perform tasks like authentication, logging and request modification.
In this case, we’ll focus on creating a logging middleware to capture and log information about each HTTP request.
See the below code:
package main
import (
"net/http"
"go.uber.org/zap"
)
type ResponseWriter struct {
http.ResponseWriter
status int
}
func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
return &ResponseWriter{w, http.StatusOK}
}
func (rw *ResponseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create a logger instance
logger, _ := zap.NewProduction()
defer logger.Sync()
// Log the incoming request
logger.Info("Incoming request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
)
// Create a response writer that captures the response status code
rw := NewResponseWriter(w)
// Pass the request to the next handler
next.ServeHTTP(rw, r)
})
}
func main() {
// Initialize Zap logger (customize according to your configuration)
logger, _ := zap.NewProduction()
defer logger.Sync()
// Define a custom handler function for your route
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Your route handler logic here
// For example, you can log the incoming request
logger.Info("Incoming request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
)
// Write a response
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
// Log the response
logger.Info("Outgoing response",
zap.Int("status", http.StatusOK),
)
})
// Start the server
if err := http.ListenAndServe(":8080", nil); err != nil {
logger.Error("Server error", zap.Error(err))
}
}
Let’s go through the code:
- At the every top, we have the necessary import statements.
- The
ResponseWriter
struct defines a customResponseWriter
type that embeds anhttp.ResponseWriter
interface and includes an additional fieldstatus
. - Next, we have the
NewResponseWriter
function which is a constructor function for theResponseWriter
type. It takes anhttp.ResponseWriter
as an argument and returns a pointer to a newResponseWriter
instance with an initial status code of 200. - The
LoggingMiddleware
function takes anhttp.Handler
as an argument and returns a newhttp.Handler
that logs incoming requests and their responses. - Inside the middleware, we create a new Zap logger instance and use it to log details of the request.
- The
main
function is where the server setup and request handling occur. - It initializes a Zap logger instance and sets up an HTTP request handler using
http.HandleFunc
. - Within the handler, it logs the incoming request, sends an HTTP response and logs the response.
- Finally, the server listens on port 8080 and uses the custom middleware to log requests and responses.
5 – Best Practices Go Logging with Zap
Here are some best practices that you should try and follow while logging in Go with Zap.
Structured Logging
Structured logs are logs with consistent format. Basically, such logs make it easy to extract meaningful information.
Zap excels at writing logs with a structure, allowing you to log data as key-value pairs.
See the below example:
logger.Info("User login",
zap.String("username", "john_doe"),
zap.Int("user_id", 123),
zap.Bool("success", true),
)
Descriptive Log Messages
Ensure that your log messages are clear and informative.
Try and include specific details about the error such as its context and any relevant data. For example:
logger.Error("Failed to process request",
zap.String("endpoint", "/api/user"),
zap.Error(err),
)
Include Timestamps
Logging timestamps are crucial for tracking events.
Zap automatically adds timestamps to log entries so you don’t have to worry about manually including them.
Log Levels
Also, use appropriate log levels such as Debug, Info, Warn, Error and Fatal to categorize the severity of each log entry.
Debug messages help during development and debugging while Error and Fatal messages indicate issues that need attention.
Conclusion
In Go web development, effectively logging is not merely a helpful feature but an essential tool for ensuring the reliability and maintainability of your applications.
In this post, we focused on the specific aspects of logging in Go with Zap. We also looked at logging to the console as well as a dedicated file.
If you have any comments or queries about this post, please feel free to mention them in the comments section below.
0 Comments