mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-06 14:22:15 +00:00
# Which Problems Are Solved - The `uri` label of `http.server.request_count` and `http.server.return_code_counter` holds the concrete request path, so every object ID served over a REST path becomes its own metric series. On our v4.16.2 instance `/debug/metrics` exposes 8,000 series, 4,467 of which (56%) belong to `http_server_return_code_counter_total` alone, spread over 4,390 distinct `uri` values such as `/v2/sessions/385063742120926058`. The set only grows with the number of objects the installation has served. - Paths matching no route are recorded verbatim, so unauthenticated scanner traffic (`/wp-json/`, `/login.php`, …) keeps adding series. - The grpc-gateway answers `405 Method Not Allowed` without going through the configured `errorHandler`, so those responses never reported a route pattern, not even before the regression. This is a regression, first shipped in v4.11.0; v4.10.1 is unaffected. #9286 introduced the mechanism, #9523 extended it to unknown paths, and #11435 removed it while reorganising the middleware packages: > Removed setting of URI to context in metric middleware. There were only setters and no getters. (Unused value) The getter was `*recorder.RequestURI` in `RegisterRequestCounter` / `RegisterRequestCodeCounter`, which the same PR replaced with `baseURI(r)`. Both ends went at once, so nothing failed. # How the Problems Are Solved - `metrics.WithRequestURIPattern` / `metrics.SetRequestURIPattern` are back, and `metrics.RequestURI` is now the single place deciding what to label with: the pattern a router reported, or the requested path if none did. `UnknownPath` moved to the metrics package so every surface shares one constant. - The HTTP metrics middleware prepares the context before passing the request on, which is the half that got dropped. - `setRequestURIPattern` in the grpc-gateway reports the pattern to metrics again, not only to tracing, and is now also called on the `405` branch. - Requests routed by chi (the OIDC endpoints) fall back to the pattern chi matched. That covers the RFC 7592 client configuration routes added in #12315 (`/oauth/v2/register/{client_id}`), which are templated on the client ID, and collapses unknown paths below the OIDC prefixes to `UNKNOWN_PATH` as well. Against the metrics dump from our instance, `http_server_return_code_counter_total` drops from 4,467 series to roughly 35, and the endpoint as a whole from 8,000 to about 3,570. # Additional Changes - Regression tests, which were missing and are the reason the revert went unnoticed: - `metrics`: resolution of the `uri` label, including that a pattern set on a derived context reaches the middleware. - `gateway`: every way the gateway can answer — success, error, unroutable path, wrong method. - `middleware`: both routing styles, i.e. a router reporting its own pattern and a chi routed request. - A doc comment on `SetRequestURIPattern` explaining that it is written by routers and read back by the middleware through the context, so it does not read as an unused setter again. # Additional Context - Closes #12556 - Restores #9286 and #9523 - Regressed in #11435 Co-authored-by: Cursor <[email protected]>
98 lines
3.3 KiB
Go
98 lines
3.3 KiB
Go
package metrics
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
)
|
|
|
|
const (
|
|
RequestCounter = "http.server.request_count"
|
|
RequestCountDescription = "Request counter"
|
|
TotalRequestCounter = "http.server.total_request_count"
|
|
TotalRequestDescription = "Total return code counter"
|
|
ReturnCodeCounter = "http.server.return_code_counter"
|
|
ReturnCodeCounterDescription = "Return code counter"
|
|
Method = "method"
|
|
URI = "uri"
|
|
ReturnCode = "return_code"
|
|
|
|
// UnknownPath is recorded as uri for requests that did not match any route.
|
|
// Recording the requested path instead would allow anyone to create an unbounded
|
|
// number of metric series by calling arbitrary paths.
|
|
UnknownPath = "UNKNOWN_PATH"
|
|
)
|
|
|
|
type MetricType int32
|
|
|
|
const (
|
|
MetricTypeTotalCount MetricType = iota
|
|
MetricTypeStatusCode
|
|
MetricTypeRequestCount
|
|
)
|
|
|
|
type StatusRecorder interface {
|
|
Status() int
|
|
}
|
|
|
|
type requestURIKey struct{}
|
|
|
|
// WithRequestURIPattern returns a context that carries the uri label of the HTTP metrics
|
|
// recorded for the current request. The metrics middleware must call this before passing
|
|
// the request on, so that the router serving it can substitute its route pattern through
|
|
// [SetRequestURIPattern].
|
|
func WithRequestURIPattern(ctx context.Context) context.Context {
|
|
var pattern string
|
|
return context.WithValue(ctx, requestURIKey{}, &pattern)
|
|
}
|
|
|
|
// SetRequestURIPattern records pattern as the uri label of the current request's HTTP
|
|
// metrics, e.g. "/v2/sessions/{session_id}" instead of "/v2/sessions/385063742120926058".
|
|
//
|
|
// Routers that serve templated paths must call this for every response they produce,
|
|
// including errors. Without it every resource ID gets its own metric series and the
|
|
// number of series only grows with the number of objects in the installation.
|
|
func SetRequestURIPattern(ctx context.Context, pattern string) {
|
|
uri, ok := ctx.Value(requestURIKey{}).(*string)
|
|
if !ok {
|
|
return
|
|
}
|
|
*uri = pattern
|
|
}
|
|
|
|
// RequestURI returns the uri label to record for r: the route pattern reported by the
|
|
// router through [SetRequestURIPattern], or the requested path if the router did not
|
|
// report one.
|
|
func RequestURI(r *http.Request) string {
|
|
if pattern, ok := r.Context().Value(requestURIKey{}).(*string); ok && *pattern != "" {
|
|
return *pattern
|
|
}
|
|
return strings.Split(r.RequestURI, "?")[0]
|
|
}
|
|
|
|
func RegisterRequestCounter(recorder StatusRecorder, r *http.Request) {
|
|
var labels = map[string]attribute.Value{
|
|
URI: attribute.StringValue(RequestURI(r)),
|
|
Method: attribute.StringValue(r.Method),
|
|
}
|
|
RegisterCounter(RequestCounter, RequestCountDescription)
|
|
AddCount(r.Context(), RequestCounter, 1, labels)
|
|
}
|
|
|
|
func RegisterTotalRequestCounter(r *http.Request) {
|
|
RegisterCounter(TotalRequestCounter, TotalRequestDescription)
|
|
AddCount(r.Context(), TotalRequestCounter, 1, nil)
|
|
}
|
|
|
|
func RegisterRequestCodeCounter(recorder StatusRecorder, r *http.Request) {
|
|
var labels = map[string]attribute.Value{
|
|
URI: attribute.StringValue(RequestURI(r)),
|
|
Method: attribute.StringValue(r.Method),
|
|
ReturnCode: attribute.IntValue(recorder.Status()),
|
|
}
|
|
RegisterCounter(ReturnCodeCounter, ReturnCodeCounterDescription)
|
|
AddCount(r.Context(), ReturnCodeCounter, 1, labels)
|
|
}
|