snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (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:
upstream-mirror
2026-09-16 01:08:34 +05:00
commit 9456a4e922
2560 changed files with 120911 additions and 0 deletions

View 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)
}

View 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
View 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
}

View 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"
}
}

View 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)
}