snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
61
oryx/prometheusx/exemplar.go
Normal file
61
oryx/prometheusx/exemplar.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package prometheusx
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// ExemplarFromContext returns exemplar labels (trace_id, span_id) for the
|
||||
// sampled trace span in ctx, or nil when there is none. The nil return makes
|
||||
// it directly usable with promhttp.WithExemplarFromContext.
|
||||
func ExemplarFromContext(ctx context.Context) prometheus.Labels {
|
||||
sc := trace.SpanContextFromContext(ctx)
|
||||
if !sc.IsValid() || !sc.IsSampled() {
|
||||
return nil
|
||||
}
|
||||
return prometheus.Labels{
|
||||
"trace_id": sc.TraceID().String(),
|
||||
"span_id": sc.SpanID().String(),
|
||||
}
|
||||
}
|
||||
|
||||
// AddWithExemplar adds v to counter. When ctx carries a sampled trace span,
|
||||
// the addition carries a trace exemplar so dashboards can link the counter
|
||||
// to the trace. Any panic from exemplar validation is swallowed: a lost
|
||||
// exemplar must never break the recording path, and client_golang applies
|
||||
// the addition before validating the exemplar, so the count is recorded
|
||||
// either way.
|
||||
func AddWithExemplar(ctx context.Context, counter prometheus.Counter, v float64) {
|
||||
exemplar := ExemplarFromContext(ctx)
|
||||
adder, ok := counter.(prometheus.ExemplarAdder)
|
||||
if exemplar == nil || !ok {
|
||||
counter.Add(v)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = recover()
|
||||
}()
|
||||
adder.AddWithExemplar(v, exemplar)
|
||||
}
|
||||
|
||||
// ObserveWithExemplar records v on obs. When ctx carries a sampled trace
|
||||
// span, the observation carries a trace exemplar on the histogram bucket it
|
||||
// falls into. The same panic-swallowing rationale as AddWithExemplar
|
||||
// applies.
|
||||
func ObserveWithExemplar(ctx context.Context, obs prometheus.Observer, v float64) {
|
||||
exemplar := ExemplarFromContext(ctx)
|
||||
observer, ok := obs.(prometheus.ExemplarObserver)
|
||||
if exemplar == nil || !ok {
|
||||
obs.Observe(v)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = recover()
|
||||
}()
|
||||
observer.ObserveWithExemplar(v, exemplar)
|
||||
}
|
||||
37
oryx/prometheusx/handler.go
Normal file
37
oryx/prometheusx/handler.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package prometheusx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
MetricsPrometheusPath = "/metrics/prometheus"
|
||||
)
|
||||
|
||||
type muxrouter interface {
|
||||
Handle(path string, handle http.Handler)
|
||||
}
|
||||
|
||||
// Handler returns the metrics handler. It mirrors promhttp.Handler() but
|
||||
// additionally serves the OpenMetrics format when the scraper negotiates it,
|
||||
// because that is the only exposition format that carries exemplars (trace
|
||||
// links on counters and histograms). It is safe to call multiple times.
|
||||
func Handler() http.Handler {
|
||||
return promhttp.InstrumentMetricHandler(
|
||||
prometheus.DefaultRegisterer,
|
||||
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{
|
||||
EnableOpenMetrics: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// SetMuxRoutes registers the prometheus handler.
|
||||
func SetMuxRoutes(mux muxrouter) {
|
||||
mux.Handle("GET "+MetricsPrometheusPath, Handler())
|
||||
}
|
||||
205
oryx/prometheusx/metrics.go
Normal file
205
oryx/prometheusx/metrics.go
Normal file
@@ -0,0 +1,205 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package prometheusx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
grpcPrometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/urfave/negroni"
|
||||
|
||||
"github.com/ory/x/httprouterx"
|
||||
)
|
||||
|
||||
type HTTPMetrics struct {
|
||||
responseTime *prometheus.HistogramVec
|
||||
totalRequests *prometheus.CounterVec
|
||||
duration *prometheus.HistogramVec
|
||||
responseSize *prometheus.HistogramVec
|
||||
requestSize *prometheus.HistogramVec
|
||||
handlerStatuses *prometheus.CounterVec
|
||||
}
|
||||
|
||||
const HTTPPrefix = "http"
|
||||
|
||||
func NewHTTPMetrics(app, metricsPrefix, version, hash, date string) *HTTPMetrics {
|
||||
labels := map[string]string{
|
||||
"app": app,
|
||||
"version": version,
|
||||
"hash": hash,
|
||||
"buildTime": date,
|
||||
}
|
||||
|
||||
if metricsPrefix != "" {
|
||||
metricsPrefix += "_"
|
||||
}
|
||||
|
||||
pm := &HTTPMetrics{
|
||||
responseTime: prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: metricsPrefix + "response_time_seconds",
|
||||
Help: "Description",
|
||||
ConstLabels: labels,
|
||||
}, []string{"endpoint"}),
|
||||
totalRequests: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: metricsPrefix + "requests_total",
|
||||
Help: "number of requests",
|
||||
ConstLabels: labels,
|
||||
}, []string{"code", "method", "endpoint"}),
|
||||
duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: metricsPrefix + "requests_duration_seconds",
|
||||
Help: "duration of a requests in seconds",
|
||||
ConstLabels: labels,
|
||||
}, []string{"code", "method", "endpoint"}),
|
||||
responseSize: prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: metricsPrefix + "response_size_bytes",
|
||||
Help: "size of the responses in bytes",
|
||||
ConstLabels: labels,
|
||||
}, []string{"code", "method"}),
|
||||
requestSize: prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: metricsPrefix + "requests_size_bytes",
|
||||
Help: "size of the requests in bytes",
|
||||
ConstLabels: labels,
|
||||
}, []string{"code", "method"}),
|
||||
handlerStatuses: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: metricsPrefix + "requests_statuses_total",
|
||||
Help: "count number of responses per status",
|
||||
ConstLabels: labels,
|
||||
}, []string{"method", "status_bucket"}),
|
||||
}
|
||||
|
||||
err := prometheus.Register(pm)
|
||||
if e := new(prometheus.AlreadyRegisteredError); errors.As(err, e) {
|
||||
return pm
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
grpcPrometheus.EnableHandlingTimeHistogram()
|
||||
|
||||
return pm
|
||||
}
|
||||
|
||||
// Describe implements prometheus Collector interface.
|
||||
func (h *HTTPMetrics) Describe(in chan<- *prometheus.Desc) {
|
||||
h.duration.Describe(in)
|
||||
h.totalRequests.Describe(in)
|
||||
h.requestSize.Describe(in)
|
||||
h.responseSize.Describe(in)
|
||||
h.handlerStatuses.Describe(in)
|
||||
h.responseTime.Describe(in)
|
||||
}
|
||||
|
||||
// Collect implements prometheus Collector interface.
|
||||
func (h *HTTPMetrics) Collect(in chan<- prometheus.Metric) {
|
||||
h.duration.Collect(in)
|
||||
h.totalRequests.Collect(in)
|
||||
h.requestSize.Collect(in)
|
||||
h.responseSize.Collect(in)
|
||||
h.handlerStatuses.Collect(in)
|
||||
h.responseTime.Collect(in)
|
||||
}
|
||||
|
||||
func (h *HTTPMetrics) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
rr := negroni.NewResponseWriter(rw)
|
||||
start := time.Now()
|
||||
|
||||
var pattern string
|
||||
next(rr, httprouterx.WithAfterMatchHook(r, func(r *http.Request) {
|
||||
pattern = r.Pattern
|
||||
}))
|
||||
|
||||
latency := time.Since(start)
|
||||
code := StatusCodeToString(rr.Status())
|
||||
method := sanitizeMethod(r.Method)
|
||||
endpoint := getLabelForPattern(pattern)
|
||||
|
||||
// Latency histograms and request counters carry trace exemplars so that
|
||||
// dashboards can link latency outliers and error spikes to traces. The
|
||||
// request's span is still open here: the tracing middleware wraps this
|
||||
// one.
|
||||
ctx := r.Context()
|
||||
h.responseSize.WithLabelValues(code, method).Observe(float64(rr.Size()))
|
||||
AddWithExemplar(ctx, h.totalRequests.WithLabelValues(code, method, endpoint), 1)
|
||||
ObserveWithExemplar(ctx, h.duration.WithLabelValues(code, method, endpoint), latency.Seconds())
|
||||
ObserveWithExemplar(ctx, h.responseTime.WithLabelValues(endpoint), latency.Seconds())
|
||||
h.requestSize.WithLabelValues(code, method).Observe(float64(computeApproximateRequestSize(r)))
|
||||
|
||||
statusBucket := "unknown"
|
||||
switch status := rr.Status(); {
|
||||
case status >= 200 && status <= 299:
|
||||
statusBucket = "2xx"
|
||||
case status >= 300 && status <= 399:
|
||||
statusBucket = "3xx"
|
||||
case status >= 400 && status <= 499:
|
||||
statusBucket = "4xx"
|
||||
case status >= 500 && status <= 599:
|
||||
statusBucket = "5xx"
|
||||
}
|
||||
|
||||
AddWithExemplar(ctx, h.handlerStatuses.WithLabelValues(r.Method, statusBucket), 1)
|
||||
}
|
||||
|
||||
var (
|
||||
paramPlaceHolderRE = regexp.MustCompile(`\{[a-zA-Z0-9_-]+}`)
|
||||
// patternLabelCache is used to cache the generated label for a given pattern to avoid recomputing it on every request.
|
||||
// We use a sync.Map over a ristretto cache as the number of unique patterns is expected to be low and the cache will not grow indefinitely.
|
||||
//
|
||||
// > The sync.Map type is optimized for two common use cases: (1) when the entry for a given
|
||||
// > key is only ever written once but read many times, as in caches that only grow,
|
||||
// > or (2) when multiple goroutines read, write, and overwrite entries for disjoint
|
||||
// > sets of keys. In these two cases, use of a Map may significantly reduce lock
|
||||
// > contention compared to a Go map paired with a separate [Mutex] or [RWMutex].
|
||||
// https://pkg.go.dev/sync#Map
|
||||
patternLabelCache = sync.Map{}
|
||||
)
|
||||
|
||||
func getLabelForPattern(pattern string) string {
|
||||
if label, ok := patternLabelCache.Load(pattern); ok {
|
||||
return label.(string)
|
||||
}
|
||||
|
||||
cleanedPattern := pattern
|
||||
// remove the method if it is included
|
||||
if _, pattern, ok := strings.Cut(cleanedPattern, " "); ok {
|
||||
cleanedPattern = pattern
|
||||
}
|
||||
// trim space, just to be sure
|
||||
cleanedPattern = strings.TrimSpace(cleanedPattern)
|
||||
|
||||
label := paramPlaceHolderRE.ReplaceAllString(strings.TrimSuffix(cleanedPattern, "/{$}"), "{param}")
|
||||
// Cache the generated label for the pattern to avoid recomputing it on every request.
|
||||
patternLabelCache.Store(pattern, label)
|
||||
return label
|
||||
}
|
||||
|
||||
// computeApproximateRequestSize is copied from the promhttp package as it is not exported
|
||||
func computeApproximateRequestSize(r *http.Request) int {
|
||||
s := 0
|
||||
if r.URL != nil {
|
||||
s += len(r.URL.String())
|
||||
}
|
||||
|
||||
s += len(r.Method)
|
||||
s += len(r.Proto)
|
||||
for name, values := range r.Header {
|
||||
s += len(name)
|
||||
for _, value := range values {
|
||||
s += len(value)
|
||||
}
|
||||
}
|
||||
s += len(r.Host)
|
||||
|
||||
// N.B. r.Form and r.MultipartForm are assumed to be included in r.URL.
|
||||
|
||||
if r.ContentLength != -1 {
|
||||
s += int(r.ContentLength)
|
||||
}
|
||||
return s
|
||||
}
|
||||
31
oryx/prometheusx/sanitize_method.go
Normal file
31
oryx/prometheusx/sanitize_method.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package prometheusx
|
||||
|
||||
func sanitizeMethod(m string) string {
|
||||
switch m {
|
||||
case "GET", "get":
|
||||
return "get"
|
||||
case "PUT", "put":
|
||||
return "put"
|
||||
case "HEAD", "head":
|
||||
return "head"
|
||||
case "POST", "post":
|
||||
return "post"
|
||||
case "DELETE", "delete":
|
||||
return "delete"
|
||||
case "CONNECT", "connect":
|
||||
return "connect"
|
||||
case "OPTIONS", "options":
|
||||
return "options"
|
||||
case "NOTIFY", "notify":
|
||||
return "notify"
|
||||
case "TRACE", "trace":
|
||||
return "trace"
|
||||
case "PATCH", "patch":
|
||||
return "patch"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
32
oryx/prometheusx/status_code_strings.go
Normal file
32
oryx/prometheusx/status_code_strings.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package prometheusx
|
||||
|
||||
import "strconv"
|
||||
|
||||
var toString = [600]string{
|
||||
100: "100", 101: "101", 102: "102", 103: "103",
|
||||
|
||||
200: "200", 201: "201", 202: "202", 203: "203", 204: "204", 205: "205", 206: "206", 207: "207", 208: "208",
|
||||
226: "226",
|
||||
|
||||
300: "300", 301: "301", 302: "302", 303: "303", 304: "304", 305: "305", 307: "307", 308: "308",
|
||||
400: "400", 401: "401", 402: "402", 403: "403", 404: "404", 405: "405", 406: "406", 407: "407", 408: "408", 409: "409", 410: "410", 411: "411", 412: "412", 413: "413", 414: "414", 415: "415", 416: "416", 417: "417", 418: "418", 421: "421", 422: "422", 423: "423", 424: "424", 425: "425", 426: "426", 428: "428", 429: "429",
|
||||
431: "431",
|
||||
451: "451",
|
||||
499: "499",
|
||||
|
||||
500: "500", 501: "501", 502: "502", 503: "503", 504: "504", 505: "505", 506: "506", 507: "507", 508: "508", 510: "510", 511: "511",
|
||||
}
|
||||
|
||||
// StatusCodeToString converts an HTTP status code to its string representation.
|
||||
// It is highly optimized for the well-known HTTP status codes.
|
||||
func StatusCodeToString(code int) string {
|
||||
if code >= 0 && code < len(toString) {
|
||||
if s := toString[code]; s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return strconv.Itoa(code)
|
||||
}
|
||||
Reference in New Issue
Block a user