feat: implement Request ID middleware for log correlation#370
feat: implement Request ID middleware for log correlation#370aaronbrethorst merged 1 commit intoOneBusAway:mainfrom
Conversation
aaronbrethorst
left a comment
There was a problem hiding this comment.
Hey Adel, appreciate you tackling request tracing — having a correlation ID flowing through the system is genuinely important for production debugging. The middleware itself is clean and the tests cover the two core paths (generate vs. preserve). However, there are a few things we need to sort out before this can merge:
Critical
- A 22MB compiled binary was committed to the repo (
apiat the repo root): This is a Linux ELF executable that was accidentally included in the commit. Binary artifacts should never be checked into source control — they bloat the repo permanently (even after deletion, they remain in git history). Please:- Remove the file from the commit:
git rm api - Add
apito.gitignore(the existing entry/cmd/api/apionly covers builds in that subdirectory, not the repo root) - Force-push the cleaned branch so the binary doesn't persist in git history
- Remove the file from the commit:
Important
-
The request ID is never included in log output (
request_logging_middleware.go): The stated purpose of this feature is "log correlation," but the request logging middleware doesn't read the request ID from the context. Currently, the only visible effect is theX-Request-IDresponse header — no log entry anywhere includes the ID. For this feature to deliver on its promise, the request logging middleware needs to include the request ID:// In request_logging_middleware.go, inside the handler func: reqID, _ := r.Context().Value(restapi.RequestIDKey).(string) logging.LogHTTPRequest(logger, r.Method, r.URL.Path, wrapped.statusCode, float64(duration.Nanoseconds())/1e6, slog.String("request_id", reqID), // <-- add this slog.String("user_agent", r.Header.Get("User-Agent")), slog.String("component", "http_server"))
Without this, the feature is incomplete — we're generating IDs nobody can see.
-
No validation of incoming
X-Request-IDheader (request_id_middleware.go:16): The middleware blindly trusts whatever value a client sends. An attacker could pass a multi-megabyte string or one containing non-printable characters, which then gets stored in the context and potentially logged. Validate the input:reqID := r.Header.Get("X-Request-ID") if reqID == "" || len(reqID) > 128 || !isValidRequestID(reqID) { reqID = uuid.NewString() }
Where
isValidRequestIDchecks for alphanumeric characters, hyphens, underscores, dots, and colons (the typical character set for trace IDs). Add a test case for an oversized or malformedX-Request-IDbeing replaced. -
go mod tidynot run:github.com/google/uuidis now a direct import inrequest_id_middleware.go, butgo.modstill lists it as// indirect. Rungo mod tidyto fix the categorization. -
Wire the middleware inside
CreateServerinapp.go, not inmain.go(main.go:112-114): Every other middleware is composed insideCreateServer— that's where the middleware chain lives. Bolting this on afterwards inmain()bypasses the established pattern and makes the chain harder to reason about. Move it intoapp.goas the outermost layer (wrapping the request logging middleware):// In app.go CreateServer() handler := restapi.RequestIDMiddleware(requestLogMiddleware(secureHandler))
This also eliminates the unnecessary
if srv.Handler != nilguard, sinceCreateServeralways sets the handler.
Fit and Finish
- Add a test for the generated ID format (
request_id_middleware_test.go): The "should generate request ID if missing" test assertsNotEmptybut doesn't verify the ID is a valid UUID. Addassert.Regexp(t,^[0-9a-f-]{36}$, respID)to catch future regressions if the generation logic changes.
Recommended Action
- Remove the binary and add to
.gitignore(item 1) - Add request ID to log output (item 2)
- Validate incoming
X-Request-ID(item 3) - Run
go mod tidy(item 4) - Move middleware wiring to
app.go(item 5) - Squash into a single clean commit after changes
Thanks again, and I look forward to merging this change.
- Add RequestIDMiddleware to generate or propagate X-Request-ID. - Validate incoming IDs to prevent injection/abuse (max 128 chars, alphanumeric). - Update logging middleware to include request_id in logs. - Wire middleware in CreateServer (app.go) for correct chaining. - Update .gitignore to exclude build artifacts. - Run go mod tidy.
5c1b496 to
1d06b5a
Compare
|
@aaronbrethorst |
aaronbrethorst
left a comment
There was a problem hiding this comment.
Hey Adel, excellent work addressing all six items from the previous review — every single one is properly resolved. The binary is gone, validation is solid, the middleware is correctly wired inside CreateServer, request IDs flow through to log output, go.mod is tidy, and the UUID format assertion is in the tests. The commit is clean and well-organized.
I have a few non-blocking suggestions that would strengthen this feature, but none of them are required for merge:
Suggestions
-
Add a
GetRequestID(ctx)helper function (request_id_middleware.go): ThecontextKeytype is unexported, which means code in other packages (e.g.,logging,webui, or future handlers) can't easily read the request ID from context without importingrestapi. Adding a simple accessor follows the pattern already established bylogging.FromContext():func GetRequestID(ctx context.Context) string { if id, ok := ctx.Value(RequestIDKey).(string); ok { return id } return "" }
This isn't needed today since the only consumer (
request_logging_middleware.go) is in the same package, but it would make the feature more accessible as the codebase grows. -
Add an integration test for request ID appearing in log output (
request_id_middleware_test.goorrequest_logging_test.go): The whole purpose of this feature is log correlation, but no test currently verifies that composingRequestIDMiddlewarewithNewRequestLoggingMiddlewareactually produces"request_id":"..."in the log output. A quick integration test would guard against future regressions in context key wiring. -
Add a boundary test for exactly 128 characters (
request_id_middleware_test.go): The tests cover 129 characters (rejected) but not 128 characters (accepted). A boundary test would catch an off-by-one regression if someone changes>to>=.
There's nothing left to change — this PR is ready to merge. Clean middleware, thorough validation, proper test coverage, and all prior feedback addressed. Nice work.
Description
Addresses the High Severity feature gap regarding the lack of Request ID/Correlation ID tracking.
This PR introduces a middleware that ensures every request has a unique identifier, allowing for better log correlation and debugging across distributed components.
Changes:
internal/restapi/request_id_middleware.goto:X-Request-IDheader.cmd/api/main.goto wrap the API handler with this new middleware.internal/restapi/request_id_middleware_test.goto verify behavior for both existing and missing headers.Verification
@aaronbrethorst
closes : #369