chi icon indicating copy to clipboard operation
chi copied to clipboard

[proposal]: centralized error logging middleware

Open ousloob opened this issue 1 year ago • 2 comments

Problem Statement: Currently, using go-chi/chi must manually log client errors (4xx) and server errors (5xx) in individual handlers or passing it into a context or helper functions. This leads to:

  • Repetitive code: Copy-pasting logging logic across handlers.
  • Inconsistent logging: Risk of mismatched log levels (e.g., logging client errors as error instead of warn).
  • Loss of context: Errors logged without request-specific data (e.g., trace ID, path, method).

A middleware solution would centralize error logging, enforce consistency, and simplify handler code.

Proposed Solution: Add a built-in error classification middleware to Chi that:

  • Automatically logs client errors (4xx) as warnings (slog.Warn).
  • Logs server errors (5xx) as errors (slog.Error).
  • Enriches logs with request context: (Trace ID, HTTP method, Path ,Status code, Error message)

How It Works: Extend Chi’s WrapResponseWriter to pass errors by adding a method called CaptureErr. Middleware: Post-request, classify errors based on status code and log them.

type ResponseWriter interface {
    http.ResponseWriter
    Status() int
    BytesWritten() int
    CaptureErr(err error) // <-- New method
    Tee(io.Writer)
    Unwrap() http.ResponseWriter
}

ousloob avatar Feb 20 '25 18:02 ousloob

Hi, how would this look like when used from within HTTP handlers? Can you provide an example, please?

Have you ever looked into https://github.com/go-chi/httplog?

VojtechVitek avatar Mar 09 '25 11:03 VojtechVitek

Glad you asked, inside an http handler:

err := someOperation()
if err != nil {
	// Capture the error for logging middleware should be in a helper function)
	if ww, ok := w.(middleware.WrapResponseWriter); ok {
		ww.CaptureErr(err)
	}
	w.WriteHeader(http.StatusInternalServerError)
	return
}

For the middlware we can add this in httplog or create our own middlware if we use another logger:

defer func() {
	status := ww.Status()

	switch {
	case status >= 500:
		log.ErrorContext(r.Context(), "server error",
			"err": ww.Err(),
                        ...
		)
	case status >= 400:
		log.WarnContext(r.Context(), "client error",
                        "err": ww.Err(),
			...
		)
	}
}()

next.ServeHTTP(ww, r)

Yes, I looked into httplog, the difference here is passing the error to the WrapResponseWriter so it could be used in a middleware, this will prevent us to log on each exit point inside a handler, I really try to find a very clean approach for that, what do you think?

ousloob avatar Mar 09 '25 14:03 ousloob

Hi,

  1. With github.com/go-chi/httplog/v3 package, you can now log the error as part of the request log:
import "github.com/go-chi/httplog/v3"

func SomeHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	if err := someOperation(); err != nil {
		httplog.SetError(ctx, err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
}
  1. If you're looking for a more centralized way of handling errors without http.Handler boilerplate, please have a look at https://github.com/webrpc/webrpc.

    Here's an example RPC method returning error. You can do logging/error inspection in one place across the codebase.

  2. We're unlikely to merge the CaptureErr(err error) method to the middleware.ResponseWriter.

Best, Vojtech

VojtechVitek avatar Jun 20 '25 12:06 VojtechVitek