Skip to content

Commit 5e884d0

Browse files
committed
refactor(util): make request logger injectable
Allow LogRequestHandler to accept a writer or logger. Update tests to exercise LogRequestHandler directly.
1 parent 30dbeed commit 5e884d0

2 files changed

Lines changed: 65 additions & 66 deletions

File tree

src/util/log.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package util
22

33
import (
4+
"io"
45
"log/slog"
56
"net"
67
"net/http"
@@ -12,6 +13,8 @@ import (
1213

1314
type LogRequestHandlerOptions struct {
1415
Pretty bool
16+
Writer io.Writer
17+
Logger *slog.Logger
1518
}
1619

1720
// LogReqInfo describes info about HTTP request
@@ -48,11 +51,24 @@ func logHTTPReqInfo(l *slog.Logger, ri *HTTPReqInfo) {
4851
}
4952

5053
func LogRequestHandler(h http.Handler, opt *LogRequestHandlerOptions) http.Handler {
54+
if opt == nil {
55+
opt = &LogRequestHandlerOptions{}
56+
}
57+
5158
var logger *slog.Logger
52-
if opt.Pretty {
53-
logger = slog.New(slog.NewTextHandler(os.Stdout, nil))
59+
if opt.Logger != nil {
60+
logger = opt.Logger
5461
} else {
55-
logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))
62+
writer := opt.Writer
63+
if writer == nil {
64+
writer = os.Stdout
65+
}
66+
67+
if opt.Pretty {
68+
logger = slog.New(slog.NewTextHandler(writer, nil))
69+
} else {
70+
logger = slog.New(slog.NewJSONHandler(writer, nil))
71+
}
5672
}
5773

5874
fn := func(w http.ResponseWriter, r *http.Request) {

src/util/log_test.go

Lines changed: 46 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ package util
33
import (
44
"bytes"
55
"encoding/json"
6+
"io"
67
"log/slog"
78
"net"
89
"net/http"
910
"net/http/httptest"
11+
"os"
1012
"strings"
1113
"testing"
1214
"time"
13-
14-
"github.com/felixge/httpsnoop"
1515
)
1616

1717
func TestLogHTTPReqInfo(t *testing.T) {
@@ -60,7 +60,7 @@ func TestLogHTTPReqInfo(t *testing.T) {
6060
}
6161
}
6262

63-
// Check that duration field exists and is a number (slog duration format in nanoseconds)
63+
// Check that duration field exists and is a number (milliseconds)
6464
if duration, ok := logData["duration"]; !ok {
6565
t.Errorf("Expected log to contain 'duration' field, got: %s", logged)
6666
} else if _, ok := duration.(float64); !ok {
@@ -135,37 +135,15 @@ func TestLogRequestHandler(t *testing.T) {
135135
// Capture log output to a buffer instead of stdout
136136
var buf bytes.Buffer
137137

138-
// Create the logging handler with a custom logger that writes to our buffer
139138
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
140139
w.WriteHeader(http.StatusOK)
141140
w.Write([]byte("test response"))
142141
})
143142

144-
// Create a custom version of LogRequestHandler that uses our buffer for logging
145-
var logger *slog.Logger
146-
if tt.pretty {
147-
logger = slog.New(slog.NewTextHandler(&buf, nil))
148-
} else {
149-
logger = slog.New(slog.NewJSONHandler(&buf, nil))
150-
}
151-
152-
// Create a modified version of LogRequestHandler that uses our logger
153-
fn := func(w http.ResponseWriter, r *http.Request) {
154-
// runs handler and captures information about HTTP request
155-
mtr := httpsnoop.CaptureMetrics(dummyHandler, w, r)
156-
157-
logHTTPReqInfo(logger, &HTTPReqInfo{
158-
method: r.Method,
159-
path: r.URL.String(),
160-
code: mtr.Code,
161-
size: mtr.Written,
162-
duration: mtr.Duration,
163-
ipAddress: requestGetRemoteAddress(r),
164-
userAgent: r.Header.Get("User-Agent"),
165-
referer: r.Header.Get("Referer"),
166-
})
167-
}
168-
handler := http.HandlerFunc(fn)
143+
handler := LogRequestHandler(dummyHandler, &LogRequestHandlerOptions{
144+
Pretty: tt.pretty,
145+
Writer: &buf,
146+
})
169147

170148
// Create test request
171149
req := httptest.NewRequest(tt.method, tt.path, nil)
@@ -271,7 +249,6 @@ func TestLogRequestHandlerWithDifferentStatusCodes(t *testing.T) {
271249
t.Run(tt.name, func(t *testing.T) {
272250
// Capture log output
273251
var buf bytes.Buffer
274-
logger := slog.New(slog.NewJSONHandler(&buf, nil))
275252

276253
// Create a dummy handler that returns the specified status code
277254
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -281,22 +258,9 @@ func TestLogRequestHandlerWithDifferentStatusCodes(t *testing.T) {
281258
}
282259
})
283260

284-
// Create a custom handler that uses our logger
285-
fn := func(w http.ResponseWriter, r *http.Request) {
286-
mtr := httpsnoop.CaptureMetrics(dummyHandler, w, r)
287-
288-
logHTTPReqInfo(logger, &HTTPReqInfo{
289-
method: r.Method,
290-
path: r.URL.String(),
291-
code: mtr.Code,
292-
size: mtr.Written,
293-
duration: mtr.Duration,
294-
ipAddress: requestGetRemoteAddress(r),
295-
userAgent: r.Header.Get("User-Agent"),
296-
referer: r.Header.Get("Referer"),
297-
})
298-
}
299-
handler := http.HandlerFunc(fn)
261+
handler := LogRequestHandler(dummyHandler, &LogRequestHandlerOptions{
262+
Writer: &buf,
263+
})
300264

301265
// Create test request
302266
req := httptest.NewRequest("GET", "/test", nil)
@@ -338,29 +302,16 @@ func TestLogRequestHandlerWithDifferentStatusCodes(t *testing.T) {
338302
func TestLogRequestHandlerPrettyLogging(t *testing.T) {
339303
// Test that the pretty option works without errors
340304
var buf bytes.Buffer
341-
logger := slog.New(slog.NewTextHandler(&buf, nil))
342305

343306
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
344307
w.WriteHeader(http.StatusOK)
345308
w.Write([]byte("ok"))
346309
})
347310

348-
// Create a custom handler with pretty logging
349-
fn := func(w http.ResponseWriter, r *http.Request) {
350-
mtr := httpsnoop.CaptureMetrics(dummyHandler, w, r)
351-
352-
logHTTPReqInfo(logger, &HTTPReqInfo{
353-
method: r.Method,
354-
path: r.URL.String(),
355-
code: mtr.Code,
356-
size: mtr.Written,
357-
duration: mtr.Duration,
358-
ipAddress: requestGetRemoteAddress(r),
359-
userAgent: r.Header.Get("User-Agent"),
360-
referer: r.Header.Get("Referer"),
361-
})
362-
}
363-
handler := http.HandlerFunc(fn)
311+
handler := LogRequestHandler(dummyHandler, &LogRequestHandlerOptions{
312+
Pretty: true,
313+
Writer: &buf,
314+
})
364315

365316
req := httptest.NewRequest("GET", "/", nil)
366317
req.RemoteAddr = "127.0.0.1:8080"
@@ -390,3 +341,35 @@ func TestLogRequestHandlerPrettyLogging(t *testing.T) {
390341
t.Errorf("Expected log to contain message 'HTTP Request', got: %s", logged)
391342
}
392343
}
344+
345+
func TestLogRequestHandlerNilOptions(t *testing.T) {
346+
// Ensure nil options do not panic and still log to stdout.
347+
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
348+
w.WriteHeader(http.StatusOK)
349+
})
350+
351+
origStdout := os.Stdout
352+
r, w, err := os.Pipe()
353+
if err != nil {
354+
t.Fatalf("failed to create pipe: %v", err)
355+
}
356+
os.Stdout = w
357+
defer func() {
358+
os.Stdout = origStdout
359+
}()
360+
361+
handler := LogRequestHandler(dummyHandler, nil)
362+
req := httptest.NewRequest("GET", "/nil-options", nil)
363+
req.RemoteAddr = "127.0.0.1:8080"
364+
wr := httptest.NewRecorder()
365+
handler.ServeHTTP(wr, req)
366+
367+
w.Close()
368+
out, err := io.ReadAll(r)
369+
if err != nil {
370+
t.Fatalf("failed to read stdout: %v", err)
371+
}
372+
if len(out) == 0 {
373+
t.Errorf("expected log output, got empty string")
374+
}
375+
}

0 commit comments

Comments
 (0)