From f43c51d8b0a10090436e5c6e0496c4d2947d1897 Mon Sep 17 00:00:00 2001 From: brunognovaes Date: Tue, 19 May 2026 08:50:08 -0300 Subject: [PATCH 1/2] feat(middleware): skip /readyz by default and add WithExcludedRoutes --- middleware/logging.go | 34 ++++++++++++++- middleware/logging_test.go | 89 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/middleware/logging.go b/middleware/logging.go index b9e8179..cd65fac 100644 --- a/middleware/logging.go +++ b/middleware/logging.go @@ -52,9 +52,17 @@ type ResponseMetricsWrapper struct { Size int } +// defaultLogExcludedRoutes is the canonical set of probe paths that are +// suppressed from access logging by default. Readiness probes fire every +// few seconds per pod and would otherwise dominate the access log. +// Failures still surface through the per-route observability emitted by +// the handler itself (e.g. the "readyz_unhealthy" Warn entry on 503). +var defaultLogExcludedRoutes = []string{"/health", "/readyz"} + type logMiddleware struct { Logger obslog.Logger ObfuscationDisabled bool + ExcludedRoutes []string } // LogMiddlewareOption configures HTTP and gRPC logging middleware. @@ -76,10 +84,27 @@ func WithObfuscationDisabled(disabled bool) LogMiddlewareOption { } } +// WithExcludedRoutes suppresses access logs for any request whose path is +// prefixed by one of the supplied routes. Matches the prefix semantics used +// by TelemetryMiddleware.WithTelemetry so a single env-driven list can be +// threaded through both middlewares. Repeated calls append. +func WithExcludedRoutes(routes ...string) LogMiddlewareOption { + return func(l *logMiddleware) { + for _, r := range routes { + if r == "" { + continue + } + + l.ExcludedRoutes = append(l.ExcludedRoutes, r) + } + } +} + func buildOpts(opts ...LogMiddlewareOption) *logMiddleware { mid := &logMiddleware{ Logger: &obslog.GoLogger{}, ObfuscationDisabled: logObfuscationDisabled, + ExcludedRoutes: append([]string(nil), defaultLogExcludedRoutes...), } for _, opt := range opts { @@ -180,9 +205,15 @@ func (r *RequestInfo) FinishRequestInfo(rw *ResponseMetricsWrapper) { } // WithHTTPLogging logs Fiber HTTP access requests. +// +// By default the probe paths /health and /readyz, along with Swagger +// asset routes, are skipped. Use WithExcludedRoutes to suppress +// additional paths (e.g. /metrics) without losing the defaults. func WithHTTPLogging(opts ...LogMiddlewareOption) fiber.Handler { return func(c *fiber.Ctx) error { - if c.Path() == "/health" { + mid := buildOpts(opts...) + + if isRouteExcludedFromList(c, mid.ExcludedRoutes) { return c.Next() } @@ -192,7 +223,6 @@ func WithHTTPLogging(opts ...LogMiddlewareOption) fiber.Handler { setRequestHeaderID(c) - mid := buildOpts(opts...) info := NewRequestInfo(c, mid.ObfuscationDisabled) requestID := c.Get(headerID) diff --git a/middleware/logging_test.go b/middleware/logging_test.go index 7df247f..36a7f3d 100644 --- a/middleware/logging_test.go +++ b/middleware/logging_test.go @@ -140,6 +140,95 @@ func TestWithHTTPLoggingIgnoresTypedNilLogger(t *testing.T) { assert.Equal(t, http.StatusNoContent, resp.StatusCode) } +func TestWithHTTPLoggingSkipsDefaultProbePaths(t *testing.T) { + probePaths := []string{"/health", "/readyz"} + + for _, path := range probePaths { + t.Run(path, func(t *testing.T) { + logger := &captureLogger{} + app := fiber.New() + app.Use(WithHTTPLogging(WithCustomLogger(logger))) + app.Get(path, func(c *fiber.Ctx) error { + return c.SendStatus(http.StatusOK) + }) + + req := httptest.NewRequest(http.MethodGet, path, nil) + resp, err := app.Test(req) + require.NoError(t, err) + defer func() { require.NoError(t, resp.Body.Close()) }() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + messages, _ := logger.snapshot() + assert.Empty(t, messages, "no access log expected for default probe path %s", path) + }) + } +} + +func TestWithHTTPLoggingExcludedRoutesOptionSuppressesLogs(t *testing.T) { + logger := &captureLogger{} + app := fiber.New() + app.Use(WithHTTPLogging( + WithCustomLogger(logger), + WithExcludedRoutes("/metrics"), + )) + app.Get("/metrics/cpu", func(c *fiber.Ctx) error { + return c.SendString("ok") + }) + + req := httptest.NewRequest(http.MethodGet, "/metrics/cpu", nil) + resp, err := app.Test(req) + require.NoError(t, err) + defer func() { require.NoError(t, resp.Body.Close()) }() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + messages, _ := logger.snapshot() + assert.Empty(t, messages, "prefix-excluded path should not produce an access log") +} + +func TestWithHTTPLoggingExcludedRoutesOptionPreservesDefaults(t *testing.T) { + logger := &captureLogger{} + app := fiber.New() + app.Use(WithHTTPLogging( + WithCustomLogger(logger), + WithExcludedRoutes("/metrics"), + )) + app.Get("/readyz", func(c *fiber.Ctx) error { + return c.SendStatus(http.StatusOK) + }) + + req := httptest.NewRequest(http.MethodGet, "/readyz", nil) + resp, err := app.Test(req) + require.NoError(t, err) + defer func() { require.NoError(t, resp.Body.Close()) }() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + messages, _ := logger.snapshot() + assert.Empty(t, messages, "supplying custom excluded routes must not remove the default probe skip set") +} + +func TestWithHTTPLoggingExcludedRoutesIgnoresEmptyStrings(t *testing.T) { + logger := &captureLogger{} + app := fiber.New() + app.Use(WithHTTPLogging( + WithCustomLogger(logger), + WithExcludedRoutes("", "/skip"), + )) + app.Get("/ok", func(c *fiber.Ctx) error { + return c.SendString("ok") + }) + + req := httptest.NewRequest(http.MethodGet, "/ok", nil) + resp, err := app.Test(req) + require.NoError(t, err) + defer func() { require.NoError(t, resp.Body.Close()) }() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + messages, _ := logger.snapshot() + require.Len(t, messages, 1, "empty exclusion entries must not swallow every request") + assert.Contains(t, messages[0], "GET /ok") +} + func TestWithHTTPLoggingLogsErrorStatus(t *testing.T) { logger := &captureLogger{} app := fiber.New() From 5f3e0480fbd1a9c7c9c70c2bbb53fda8ba0f1769 Mon Sep 17 00:00:00 2001 From: brunognovaes Date: Tue, 19 May 2026 09:11:08 -0300 Subject: [PATCH 2/2] feat(middleware): skip probe and scrape paths by default; add WithExcludedRoutes tests --- middleware/helpers.go | 19 ++++++++--- middleware/helpers_test.go | 66 ++++++++++++++++++++++++++++++++++++++ middleware/logging.go | 20 ++++++------ middleware/logging_test.go | 8 ++--- 4 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 middleware/helpers_test.go diff --git a/middleware/helpers.go b/middleware/helpers.go index c356ba3..d0c46bf 100644 --- a/middleware/helpers.go +++ b/middleware/helpers.go @@ -18,12 +18,23 @@ var uuidPattern = regexp.MustCompile(`[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{ // internalServicePattern matches Lerian internal service user-agent strings. var internalServicePattern = regexp.MustCompile(`^[\w-]+/[\d.]+\s+LerianStudio$`) -// isRouteExcludedFromList reports whether the request path matches any excluded route prefix. -// This standalone function is used to evaluate route exclusions independently of whether -// the TelemetryMiddleware receiver is nil. +// isRouteExcludedFromList reports whether the request path matches any +// excluded route on a path-segment boundary. A route matches when the +// path equals it exactly or starts with "route + /", so "/health" excludes +// "/health" and "/health/check" but NOT "/healthz" or "/health-check". +// +// Trailing slashes on excluded entries are tolerated, and empty entries +// are ignored so they cannot act as accidental wildcards. func isRouteExcludedFromList(c *fiber.Ctx, excludedRoutes []string) bool { + path := c.Path() + for _, route := range excludedRoutes { - if strings.HasPrefix(c.Path(), route) { + route = strings.TrimRight(route, "/") + if route == "" { + continue + } + + if path == route || strings.HasPrefix(path, route+"/") { return true } } diff --git a/middleware/helpers_test.go b/middleware/helpers_test.go new file mode 100644 index 0000000..8968f34 --- /dev/null +++ b/middleware/helpers_test.go @@ -0,0 +1,66 @@ +//go:build unit + +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/stretchr/testify/assert" +) + +func TestIsRouteExcludedFromList(t *testing.T) { + cases := []struct { + name string + path string + excluded []string + want bool + }{ + // exact match + {name: "exact match", path: "/health", excluded: []string{"/health"}, want: true}, + + // subpath match (segment boundary) + {name: "subpath under excluded route", path: "/health/check", excluded: []string{"/health"}, want: true}, + {name: "deep subpath", path: "/api/v1/users/42", excluded: []string{"/api/v1"}, want: true}, + + // regression guards against the original raw-prefix bug + {name: "sibling with shared prefix (suffix letter)", path: "/healthz", excluded: []string{"/health"}, want: false}, + {name: "sibling with shared prefix (hyphenated)", path: "/health-check", excluded: []string{"/health"}, want: false}, + {name: "sibling with shared prefix (under /metrics)", path: "/metricsproxy", excluded: []string{"/metrics"}, want: false}, + {name: "minor version not a child", path: "/api/v1.0/users", excluded: []string{"/api/v1"}, want: false}, + + // trailing slash tolerance on the excluded entry + {name: "excluded route with trailing slash matches path without", path: "/metrics", excluded: []string{"/metrics/"}, want: true}, + {name: "excluded route with trailing slash matches subpath", path: "/metrics/cpu", excluded: []string{"/metrics/"}, want: true}, + + // empty / root entries are not wildcards + {name: "empty string entry is not a wildcard", path: "/anything", excluded: []string{""}, want: false}, + {name: "root slash entry is not a wildcard", path: "/anything", excluded: []string{"/"}, want: false}, + + // list semantics + {name: "no excluded routes", path: "/anywhere", excluded: nil, want: false}, + {name: "no match in list", path: "/v1/orders", excluded: []string{"/health", "/readyz"}, want: false}, + {name: "match second entry", path: "/readyz", excluded: []string{"/health", "/readyz"}, want: true}, + {name: "match wins over later non-matches", path: "/health/x", excluded: []string{"/health", "/no-match"}, want: true}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + app := fiber.New() + var got bool + app.Use(func(c *fiber.Ctx) error { + got = isRouteExcludedFromList(c, tc.excluded) + return c.SendStatus(http.StatusNoContent) + }) + + req := httptest.NewRequest(http.MethodGet, tc.path, nil) + resp, err := app.Test(req) + assert.NoError(t, err) + defer func() { _ = resp.Body.Close() }() + + assert.Equal(t, tc.want, got, "isRouteExcludedFromList(path=%q, excluded=%v)", tc.path, tc.excluded) + }) + } +} diff --git a/middleware/logging.go b/middleware/logging.go index cd65fac..6b3498c 100644 --- a/middleware/logging.go +++ b/middleware/logging.go @@ -52,12 +52,13 @@ type ResponseMetricsWrapper struct { Size int } -// defaultLogExcludedRoutes is the canonical set of probe paths that are -// suppressed from access logging by default. Readiness probes fire every -// few seconds per pod and would otherwise dominate the access log. -// Failures still surface through the per-route observability emitted by -// the handler itself (e.g. the "readyz_unhealthy" Warn entry on 503). -var defaultLogExcludedRoutes = []string{"/health", "/readyz"} +// defaultLogExcludedRoutes is the canonical set of probe and scrape paths +// that are suppressed from access logging by default. Readiness probes and +// Prometheus scrapes fire every few seconds per pod and would otherwise +// dominate the access log. Failures still surface through the per-route +// observability emitted by the handler itself (e.g. the "readyz_unhealthy" +// Warn entry on 503). +var defaultLogExcludedRoutes = []string{"/health", "/readyz", "/metrics"} type logMiddleware struct { Logger obslog.Logger @@ -206,9 +207,10 @@ func (r *RequestInfo) FinishRequestInfo(rw *ResponseMetricsWrapper) { // WithHTTPLogging logs Fiber HTTP access requests. // -// By default the probe paths /health and /readyz, along with Swagger -// asset routes, are skipped. Use WithExcludedRoutes to suppress -// additional paths (e.g. /metrics) without losing the defaults. +// By default the probe paths /health and /readyz, the Prometheus scrape +// path /metrics, and Swagger asset routes are skipped. Use +// WithExcludedRoutes to suppress additional paths without losing the +// defaults. func WithHTTPLogging(opts ...LogMiddlewareOption) fiber.Handler { return func(c *fiber.Ctx) error { mid := buildOpts(opts...) diff --git a/middleware/logging_test.go b/middleware/logging_test.go index 36a7f3d..68ceda4 100644 --- a/middleware/logging_test.go +++ b/middleware/logging_test.go @@ -141,7 +141,7 @@ func TestWithHTTPLoggingIgnoresTypedNilLogger(t *testing.T) { } func TestWithHTTPLoggingSkipsDefaultProbePaths(t *testing.T) { - probePaths := []string{"/health", "/readyz"} + probePaths := []string{"/health", "/readyz", "/metrics"} for _, path := range probePaths { t.Run(path, func(t *testing.T) { @@ -170,13 +170,13 @@ func TestWithHTTPLoggingExcludedRoutesOptionSuppressesLogs(t *testing.T) { app := fiber.New() app.Use(WithHTTPLogging( WithCustomLogger(logger), - WithExcludedRoutes("/metrics"), + WithExcludedRoutes("/internal"), )) - app.Get("/metrics/cpu", func(c *fiber.Ctx) error { + app.Get("/internal/diag", func(c *fiber.Ctx) error { return c.SendString("ok") }) - req := httptest.NewRequest(http.MethodGet, "/metrics/cpu", nil) + req := httptest.NewRequest(http.MethodGet, "/internal/diag", nil) resp, err := app.Test(req) require.NoError(t, err) defer func() { require.NoError(t, resp.Body.Close()) }()