Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions middleware/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
66 changes: 66 additions & 0 deletions middleware/helpers_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
36 changes: 34 additions & 2 deletions middleware/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,18 @@ type ResponseMetricsWrapper struct {
Size int
}

// 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
ObfuscationDisabled bool
ExcludedRoutes []string
}

// LogMiddlewareOption configures HTTP and gRPC logging middleware.
Expand All @@ -76,10 +85,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 {
Expand Down Expand Up @@ -180,9 +206,16 @@ func (r *RequestInfo) FinishRequestInfo(rw *ResponseMetricsWrapper) {
}

// WithHTTPLogging logs Fiber HTTP access requests.
//
// 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 {
if c.Path() == "/health" {
mid := buildOpts(opts...)

if isRouteExcludedFromList(c, mid.ExcludedRoutes) {
return c.Next()
}
Comment thread
brunognovaes marked this conversation as resolved.

Expand All @@ -192,7 +225,6 @@ func WithHTTPLogging(opts ...LogMiddlewareOption) fiber.Handler {

setRequestHeaderID(c)

mid := buildOpts(opts...)
info := NewRequestInfo(c, mid.ObfuscationDisabled)

requestID := c.Get(headerID)
Expand Down
89 changes: 89 additions & 0 deletions middleware/logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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", "/metrics"}

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("/internal"),
))
app.Get("/internal/diag", func(c *fiber.Ctx) error {
return c.SendString("ok")
})

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()) }()
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()
Expand Down
Loading