Ctrl K
ComunidadGuías…

coderd/httpmw/prometheus.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package httpmw

import (
	"context"
	"net/http"
	"strconv"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"

	"github.com/coder/coder/v2/coderd/httpapi"
	"github.com/coder/coder/v2/coderd/tracing"
)

// WSMetrics groups all WebSocket-related Prometheus metrics so they
// can be created once and shared between the HTTP middleware and the
// WSWatcher probe recorder.
type WSMetrics struct {
	Concurrent *prometheus.GaugeVec
	Durations  *prometheus.HistogramVec
	Probes     *prometheus.CounterVec
}

// NewWSMetrics registers and returns WebSocket metrics. The returned
// struct is safe to pass to both Prometheus() and
// WSMetrics.RecordProbe.
func NewWSMetrics(reg prometheus.Registerer) *WSMetrics {
	factory := promauto.With(reg)
	return &WSMetrics{
		Concurrent: factory.NewGaugeVec(prometheus.GaugeOpts{
			Namespace: "coderd",
			Subsystem: "api",
			Name:      "concurrent_websockets",
			Help:      "The total number of concurrent API websockets.",
		}, []string{"path"}),
		Durations: factory.NewHistogramVec(prometheus.HistogramOpts{
			Namespace: "coderd",
			Subsystem: "api",
			Name:      "websocket_durations_seconds",
			Help:      "Websocket duration distribution of requests in seconds.",
			Buckets: []float64{
				0.001, // 1ms
				1,
				60,           // 1 minute
				60 * 60,      // 1 hour
				60 * 60 * 15, // 15 hours
				60 * 60 * 30, // 30 hours
			},
		}, []string{"path"}),
		Probes: factory.NewCounterVec(prometheus.CounterOpts{
			Namespace: "coderd",
			Subsystem: "api",
			Name:      "websocket_probes_total",
			Help: "WebSocket liveness probe outcomes by route. " +
				"Compare rate(...{result=\"ok\"}[1m]) against " +
				"coderd_api_concurrent_websockets to detect " +
				"unresponsive WebSocket connections.",
		}, []string{"path", "result"}),
	}
}

// RecordProbe records a single liveness probe outcome. It extracts
// the HTTP route from ctx via ExtractHTTPRoute.
func (m *WSMetrics) RecordProbe(ctx context.Context, r httpapi.ProbeResult) {
	m.Probes.WithLabelValues(ExtractHTTPRoute(ctx), string(r)).Inc()
}

func Prometheus(register prometheus.Registerer, ws *WSMetrics) func(http.Handler) http.Handler {
	if ws == nil {
		panic("developer error: WSMetrics is nil")
	}
	factory := promauto.With(register)
	requestsProcessed := factory.NewCounterVec(prometheus.CounterOpts{
		Namespace: "coderd",
		Subsystem: "api",
		Name:      "requests_processed_total",
		Help:      "The total number of processed API requests",
	}, []string{"code", "method", "path"})
	requestsConcurrent := factory.NewGaugeVec(prometheus.GaugeOpts{
		Namespace: "coderd",
		Subsystem: "api",
		Name:      "concurrent_requests",
		Help:      "The number of concurrent API requests.",
	}, []string{"method", "path"})
	requestsDist := factory.NewHistogramVec(prometheus.HistogramOpts{
		Namespace: "coderd",
		Subsystem: "api",
		Name:      "request_latencies_seconds",
		Help:      "Latency distribution of requests in seconds.",
		Buckets:   []float64{0.001, 0.005, 0.010, 0.025, 0.050, 0.100, 0.500, 1, 5, 10, 30},
	}, []string{"method", "path"})
	// Series here exist only for routes that have actually rejected a body,
	// which is what makes this readable at a glance where filtering
	// requests_processed_total by code is not.
	requestsTooLarge := factory.NewCounterVec(prometheus.CounterOpts{
		Namespace: "coderd",
		Subsystem: "api",
		Name:      "requests_too_large_total",
		Help: "The total number of API requests answered 413, by the reason " +
			"they were rejected. A sustained rate of reason=\"request_body\" " +
			"on one route is more often a limit set too tight for a " +
			"legitimate payload than an attempt to exhaust memory. " +
			"reason=\"other\" counts 413s raised for reasons unrelated to " +
			"the size of the request body, such as agent log storage " +
			"overflow or an archive that is too large once expanded.",
	}, []string{"method", "path", "reason"})

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			var (
				start  = time.Now()
				method = r.Method
			)

			sw, ok := w.(*tracing.StatusWriter)
			if !ok {
				panic("dev error: http.ResponseWriter is not *tracing.StatusWriter")
			}

			var (
				dist     *prometheus.HistogramVec
				distOpts []string
				path     = ExtractHTTPRoute(r.Context())
			)

			// We want to count WebSockets separately.
			if httpapi.IsWebsocketUpgrade(r) {
				ws.Concurrent.WithLabelValues(path).Inc()
				defer ws.Concurrent.WithLabelValues(path).Dec()

				dist = ws.Durations
			} else {
				requestsConcurrent.WithLabelValues(method, path).Inc()
				defer requestsConcurrent.WithLabelValues(method, path).Dec()

				dist = requestsDist
				distOpts = []string{method}
			}

			// Counting by status rather than at the point of rejection reaches
			// handlers that bound their bodies themselves to keep their own
			// error shapes, such as aibridge. It also reaches the 413s that have
			// nothing to do with body size, such as agent log storage overflow,
			// so the tracker separates the two.
			bodyLimit := &httpapi.RequestBodyLimitTracker{}
			r = r.WithContext(httpapi.WithRequestBodyLimitTracker(r.Context(), bodyLimit))

			next.ServeHTTP(w, r)

			distOpts = append(distOpts, path)
			statusStr := strconv.Itoa(sw.Status)

			requestsProcessed.WithLabelValues(statusStr, method, path).Inc()
			dist.WithLabelValues(distOpts...).Observe(time.Since(start).Seconds())

			if sw.Status == http.StatusRequestEntityTooLarge {
				reason := "other"
				if bodyLimit.Exceeded() {
					reason = "request_body"
				}
				requestsTooLarge.WithLabelValues(method, path, reason).Inc()
			}
		})
	}
}