All checks were successful
check / check (push) Successful in 3m1s
The metrics recorder labelled its `handler` dimension with the concrete request path, so every distinct /webhook/<uuid> minted a permanent label set that nothing ever evicted. Measured on this branch's parent: a scrape went from 106 series and 12 KB to 78,132 series and 10.7 MB after 3,000 unauthenticated POSTs to invented entrypoint UUIDs, and stayed there. It also published those UUIDs -- the receiver's only credential -- verbatim in the scrape. The label now comes from chi's route pattern. It cannot be supplied as go-http-metrics' handler id: the recorder is global middleware, so it is entered before chi has matched anything, and the library fixes the id up front. What the library does pass through unchanged is the request context, on every recorder call, and the duration and size observations happen after the wrapped handler returns -- the same point accessLogURL already reads the pattern from. So a recorder decorator rewrites the id there instead. std.Handler and its response-writer interceptor are untouched, so status and size capture are unchanged. Recording after the whole chain returns is what makes this hold for requests the route-level receiver limiter rejects, which were the majority of the leaked series: chi has matched the route before the limiter runs, so a 429 carries the pattern like any other response. A path matching no route carries the existing unmatchedRoute sentinel, the same fixed value the access log uses. http_requests_inflight cannot carry a pattern -- it is incremented before routing and decremented after, so a route-derived label would increment one series and decrement another and leave the gauge permanently wrong. It gets a fixed aggregate label instead. Verified against a live instance: 6,000 distinct receiver paths and 250 unmatched paths across two floods left the scrape at 206 series and 21 KB, flat between floods, with no UUID anywhere in the output and all 4,800 429s on the single pattern.
146 lines
4.1 KiB
Go
146 lines
4.1 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
httpmetrics "github.com/slok/go-http-metrics/metrics"
|
|
)
|
|
|
|
// MetricsMiddlewareForTest builds the metrics recording middleware
|
|
// against a caller-supplied recorder, so a test can gather from its
|
|
// own Prometheus registry rather than the process-wide default one
|
|
// that Middleware.Metrics uses.
|
|
func MetricsMiddlewareForTest(
|
|
rec httpmetrics.Recorder,
|
|
) func(http.Handler) http.Handler {
|
|
return metricsMiddleware(rec)
|
|
}
|
|
|
|
// UnmatchedRouteConst exposes the sentinel that stands in for a
|
|
// request matching no route pattern.
|
|
const UnmatchedRouteConst = unmatchedRoute
|
|
|
|
// InflightHandlerConst exposes the fixed handler label on the
|
|
// inflight gauge.
|
|
const InflightHandlerConst = inflightHandler
|
|
|
|
// NewLoggingResponseWriterForTest wraps newLoggingResponseWriter
|
|
// for use in external test packages.
|
|
func NewLoggingResponseWriterForTest(
|
|
w http.ResponseWriter,
|
|
) *loggingResponseWriter {
|
|
return newLoggingResponseWriter(w)
|
|
}
|
|
|
|
// LoggingResponseWriterStatusCode returns the status code
|
|
// captured by the loggingResponseWriter.
|
|
func LoggingResponseWriterStatusCode(
|
|
lrw *loggingResponseWriter,
|
|
) int {
|
|
return lrw.statusCode
|
|
}
|
|
|
|
// IPFromHostPort exposes ipFromHostPort for testing.
|
|
func IPFromHostPort(hp string) string {
|
|
return ipFromHostPort(hp)
|
|
}
|
|
|
|
// ClientKeyForTest exposes clientKey for testing.
|
|
func ClientKeyForTest(m *Middleware, r *http.Request) string {
|
|
return m.clientKey(r)
|
|
}
|
|
|
|
// IsClientTLS exposes isClientTLS for testing.
|
|
func IsClientTLS(r *http.Request) bool {
|
|
return isClientTLS(r)
|
|
}
|
|
|
|
// LoginRateLimitConst exposes the loginRateLimit constant: the
|
|
// number of FAILED login attempts one client may make against one
|
|
// submitted username per interval.
|
|
const LoginRateLimitConst = loginRateLimit
|
|
|
|
// LoginFailureMaxKeysConst exposes the cap on each of the login
|
|
// guard's key sets.
|
|
const LoginFailureMaxKeysConst = loginFailureMaxKeys
|
|
|
|
// PasswordVerifyConcurrencyConst exposes the bound on concurrent
|
|
// Argon2id verifications.
|
|
const PasswordVerifyConcurrencyConst = passwordVerifyConcurrency
|
|
|
|
// PasswordVerifyMaxWaitersConst exposes the bound on how many
|
|
// requests may queue for a verification slot.
|
|
const PasswordVerifyMaxWaitersConst = passwordVerifyMaxWaiters
|
|
|
|
// LoginGuard is the login failure counter and verification
|
|
// semaphore, exposed for direct testing.
|
|
type LoginGuard = loginGuard
|
|
|
|
// NewLoginGuardForTest builds a guard with test-sized parameters.
|
|
func NewLoginGuardForTest(
|
|
limit int,
|
|
interval time.Duration,
|
|
maxKeys, concurrency, maxWaiters int,
|
|
wait time.Duration,
|
|
) *LoginGuard {
|
|
return newLoginGuard(
|
|
limit, interval, maxKeys, concurrency, maxWaiters, wait,
|
|
)
|
|
}
|
|
|
|
// QueuedWaitersForTest reports how many requests are currently
|
|
// queued for a verification slot.
|
|
func (g *LoginGuard) QueuedWaitersForTest() int {
|
|
return len(g.queue)
|
|
}
|
|
|
|
// SetNowForTest replaces the guard's clock.
|
|
func (g *LoginGuard) SetNowForTest(now func() time.Time) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
|
|
g.now = now
|
|
}
|
|
|
|
// FailForTest exposes fail.
|
|
func (g *LoginGuard) FailForTest(clientKey, username string) bool {
|
|
return g.fail(clientKey, username)
|
|
}
|
|
|
|
// SucceedForTest exposes succeed.
|
|
func (g *LoginGuard) SucceedForTest(clientKey, username string) {
|
|
g.succeed(clientKey, username)
|
|
}
|
|
|
|
// AcquireForTest exposes acquire.
|
|
func (g *LoginGuard) AcquireForTest(
|
|
ctx context.Context,
|
|
) (func(), bool) {
|
|
return g.acquire(ctx)
|
|
}
|
|
|
|
// TrackedKeysForTest reports how many failure counters the guard
|
|
// holds, per-username and per-address respectively.
|
|
func (g *LoginGuard) TrackedKeysForTest() (int, int) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
|
|
return len(g.byUser), len(g.byAddr)
|
|
}
|
|
|
|
// PasswordChangeRateLimitConst exposes the
|
|
// passwordChangeRateLimit constant.
|
|
const PasswordChangeRateLimitConst = passwordChangeRateLimit
|
|
|
|
// ReceiverAggregateMultiplierConst exposes the
|
|
// receiverAggregateMultiplier constant.
|
|
const ReceiverAggregateMultiplierConst = receiverAggregateMultiplier
|
|
|
|
// ReceiverAggregateLimitForTest exposes receiverAggregateLimit for
|
|
// testing.
|
|
func ReceiverAggregateLimitForTest(perEntrypoint int) int {
|
|
return receiverAggregateLimit(perEntrypoint)
|
|
}
|