From 44199b12c06d8f4feec452b7e31a82979d639f05 Mon Sep 17 00:00:00 2001 From: Bruno Carlin Date: Wed, 29 Jan 2025 23:21:16 +0100 Subject: [PATCH] feat: add a middleware to log requests --- doc.go | 2 ++ go.mod | 3 ++ log_request.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 doc.go create mode 100644 go.mod create mode 100644 log_request.go diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..2201495 --- /dev/null +++ b/doc.go @@ -0,0 +1,2 @@ +// Package mw provides a collection of middlewares for net/http. +package mw diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..346ed27 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module code.bcarlin.net/go/mw + +go 1.20 diff --git a/log_request.go b/log_request.go new file mode 100644 index 0000000..ad2359c --- /dev/null +++ b/log_request.go @@ -0,0 +1,83 @@ +package mw + +import ( + "fmt" + "log/slog" + "net/http" + "time" +) + +const ( + logRequest = "request" + logResponse = "response" + logMethod = "method" + logURI = "uri" + logStatus = "status" + logSize = "size" + logDuration = "duration" +) + +// LoggingMiddleware returns a middleware that logs the request and response, +// each in a loggroup. +// +// The request group contains the request method and URI. +// The response group contains the response status, size and duration. +func LoggingMiddleware(l *slog.Logger) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + + lwr := &loggingResponseWriter{ + ResponseWriter: w, + data: &responseData{status: http.StatusOK, size: 0}, + } + + next.ServeHTTP(lwr, r) + + l.InfoContext(r.Context(), "HTTP request answered", + slog.Group( + logRequest, + slog.String(logMethod, r.Method), + slog.String(logURI, r.RequestURI), + ), + slog.Group( + logResponse, + slog.Int(logStatus, lwr.data.status), + slog.Int(logSize, lwr.data.size), + slog.Duration(logDuration, time.Since(startTime)), + ), + ) + }) + } +} + +// Struct for holding response details. +type responseData struct { + status int + size int +} + +// loggingResponseWriter is a wrapper for http.ResponseWriter that records the +// size of data sent and the status code of the response. +type loggingResponseWriter struct { + http.ResponseWriter // compose original http.ResponseWriter + data *responseData +} + +// Write implements http.ResponseWriter. +func (r *loggingResponseWriter) Write(b []byte) (int, error) { + size, err := r.ResponseWriter.Write(b) + r.data.size += size + + if err != nil { + return size, fmt.Errorf("cannot forward data: %w", err) + } + + return size, nil +} + +// WriteHeader implements http.ResponseWriter. +func (r *loggingResponseWriter) WriteHeader(statusCode int) { + r.ResponseWriter.WriteHeader(statusCode) + r.data.status = statusCode +}