

0 / 2 embers
0 / 3000 xp
click for more info
Complete a lesson to start your streak
click for more info
Difficulty: 7
click for more info
Not enough gems
Cost: 6 gems
1: Logging Context
incomplete
2: Build Information
incomplete
3: Instance Context
incomplete
4: Request Context
incomplete
5: User Context
incomplete
6: HTTP Error Responses
incomplete
7: Inter-Process Context
incomplete
Back
ctrl+,
Next
ctrl+.
This lesson's interactive features are locked, please to keep using them
"I wish the ring had never come to me."
– Frodo Baggins
A powerful quote. But what if he said that when he first saw Gandalf in the Shire instead of, "You're late!"?
That would be confusing. What ring is he talking about? Gandalf would be missing context.
For HTTP servers, much of the context to log should be related to the specific HTTP request being served. Some good ideas include:
200, 404, etc.)User-Agent headerContent-Type headerContent-Type headerDon't run out and log everything in every app, but add fields as they become useful.
Let's start with an easy one: response duration.
Record request start time, then subtract when the response finishes.
func requestLogger(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
logger.Info("Served request",
/* ... other fields ... */
slog.Duration("duration", time.Since(start)),
)
})
}
}
We can use a similar trick for request body size:
type spyReadCloser struct {
io.ReadCloser
bytesRead int
}
func (r *spyReadCloser) Read(p []byte) (int, error) {
n, err := r.ReadCloser.Read(p)
r.bytesRead += n
return n, err
}
Replace the request body with our spy wrapper:
func requestLogger(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
spyReader := &spyReadCloser{ReadCloser: r.Body}
r.Body = spyReader
next.ServeHTTP(w, r)
logger.Info("Served request",
/* ... other fields ... */
slog.Int("request_body_bytes", spyReader.bytesRead),
)
})
}
}
The default http.ResponseWriter provided by the standard library doesn't let us inspect the HTTP status sent, the number of bytes sent, or much else, really. However, because it's an interface, it gives us all the flexibility we need to implement our own version that does. Let's consider a simple example:
type spyResponseWriter struct {
http.ResponseWriter
bytesWritten int
statusCode int
}
func (w *spyResponseWriter) Write(p []byte) (int, error) {
if w.statusCode == 0 {
w.statusCode = http.StatusOK
}
n, err := w.ResponseWriter.Write(p)
w.bytesWritten += n
return n, err
}
func (w *spyResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
This wrapper delegates to an existing http.ResponseWriter while tracking bytes written and status code. We can use it to enrich response logging:
func requestLogger(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
spyWriter := &spyResponseWriter{ResponseWriter: w}
next.ServeHTTP(spyWriter, r)
logger.Info("Served request",
/* ... other fields ... */
slog.Int("response_status", spyWriter.statusCode),
slog.Int("response_body_bytes", spyWriter.bytesWritten),
)
})
}
}
Add request and response metadata to the "Served request" log entry.
Update your requestLogger middleware to include:
Rebuild your app with -ldflags, then run it with ENV and LINKO_LOG_FILE set:
go build \
-ldflags "-X boot.dev/linko/internal/build.GitSHA=$(git rev-parse HEAD) -X boot.dev/linko/internal/build.BuildTime=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
-o linko &&
LINKO_LOG_FILE=linko.access.log ENV=development ./linko
Run and submit the CLI tests from the root of the Linko repo.