Files
Productier/apps/backend/internal/httpapi/metrics_test.go
T
Tomas Dvorak 3cb40adb23 first commit
2026-04-10 12:04:09 +02:00

158 lines
5.1 KiB
Go

package httpapi
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
)
func TestRequestMetricsObserveAndSnapshot(t *testing.T) {
t.Parallel()
metrics := newRequestMetrics()
metrics.observe(http.MethodGet, "/v1/health", http.StatusOK, 100*time.Millisecond)
metrics.observe(http.MethodGet, "/v1/health", http.StatusOK, 200*time.Millisecond)
metrics.observe(http.MethodPost, "/v1/tasks", http.StatusCreated, 40*time.Millisecond)
snapshot := metrics.snapshot()
if snapshot.RequestsTotal != 3 {
t.Fatalf("requestsTotal = %d, want 3", snapshot.RequestsTotal)
}
if snapshot.StatusClassTotals["2xx"] != 3 {
t.Fatalf("2xx total = %d, want 3", snapshot.StatusClassTotals["2xx"])
}
if len(snapshot.Routes) != 2 {
t.Fatalf("route bucket count = %d, want 2", len(snapshot.Routes))
}
if snapshot.UptimeSeconds < 0 {
t.Fatalf("uptimeSeconds = %d, want >= 0", snapshot.UptimeSeconds)
}
health := findRouteMetric(snapshot.Routes, http.MethodGet, "/v1/health", http.StatusOK)
if health == nil {
t.Fatal("missing route metric for GET /v1/health 200")
}
if health.Count != 2 {
t.Fatalf("health count = %d, want 2", health.Count)
}
if health.AvgLatencyMs != 150 {
t.Fatalf("health avgLatencyMs = %.2f, want 150", health.AvgLatencyMs)
}
if health.MaxLatencyMs != 200 {
t.Fatalf("health maxLatencyMs = %.2f, want 200", health.MaxLatencyMs)
}
if _, err := time.Parse(time.RFC3339Nano, health.LastSeenAt); err != nil {
t.Fatalf("health lastSeenAt parse error: %v", err)
}
if _, err := time.Parse(time.RFC3339Nano, snapshot.GeneratedAt); err != nil {
t.Fatalf("snapshot generatedAt parse error: %v", err)
}
}
func TestRequestMetricsMiddlewareSkipsMetricsEndpoint(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
metrics := newRequestMetrics()
router := gin.New()
router.Use(requestMetricsMiddleware(metrics))
router.GET("/v1/health", func(c *gin.Context) {
c.Status(http.StatusOK)
})
router.GET("/v1/metrics", func(c *gin.Context) {
c.Status(http.StatusOK)
})
router.GET("/v1/metrics/prometheus", func(c *gin.Context) {
c.Status(http.StatusOK)
})
healthRequest := httptest.NewRequest(http.MethodGet, "/v1/health", nil)
healthResponse := httptest.NewRecorder()
router.ServeHTTP(healthResponse, healthRequest)
if healthResponse.Code != http.StatusOK {
t.Fatalf("GET /v1/health status = %d, want 200", healthResponse.Code)
}
metricsRequest := httptest.NewRequest(http.MethodGet, "/v1/metrics", nil)
metricsResponse := httptest.NewRecorder()
router.ServeHTTP(metricsResponse, metricsRequest)
if metricsResponse.Code != http.StatusOK {
t.Fatalf("GET /v1/metrics status = %d, want 200", metricsResponse.Code)
}
prometheusRequest := httptest.NewRequest(http.MethodGet, "/v1/metrics/prometheus", nil)
prometheusResponse := httptest.NewRecorder()
router.ServeHTTP(prometheusResponse, prometheusRequest)
if prometheusResponse.Code != http.StatusOK {
t.Fatalf("GET /v1/metrics/prometheus status = %d, want 200", prometheusResponse.Code)
}
snapshot := metrics.snapshot()
if snapshot.RequestsTotal != 1 {
t.Fatalf("requestsTotal = %d, want 1", snapshot.RequestsTotal)
}
if findRouteMetric(snapshot.Routes, http.MethodGet, "/v1/metrics", http.StatusOK) != nil {
t.Fatal("metrics endpoint request should be excluded from tracking")
}
if findRouteMetric(snapshot.Routes, http.MethodGet, "/v1/metrics/prometheus", http.StatusOK) != nil {
t.Fatal("prometheus metrics endpoint request should be excluded from tracking")
}
}
func TestSnapshotPrometheus(t *testing.T) {
t.Parallel()
metrics := newRequestMetrics()
metrics.observe(http.MethodGet, "/v1/health", http.StatusOK, 50*time.Millisecond)
metrics.observe(http.MethodGet, "/v1/tasks", http.StatusNotFound, 25*time.Millisecond)
metrics.observe(http.MethodGet, `/v1/quoted"path`, http.StatusOK, 35*time.Millisecond)
output := metrics.snapshotPrometheus()
expectedFragments := []string{
"productier_http_uptime_seconds",
`productier_http_requests_total{status_class="2xx"} 2`,
`productier_http_requests_total{status_class="4xx"} 1`,
`productier_http_requests_route_total{method="GET",path="/v1/health",status="200"} 1`,
`productier_http_request_latency_avg_ms{method="GET",path="/v1/health",status="200"} 50.000`,
`productier_http_request_latency_max_ms{method="GET",path="/v1/tasks",status="404"} 25.000`,
`path="/v1/quoted\"path"`,
}
for _, fragment := range expectedFragments {
if !strings.Contains(output, fragment) {
t.Fatalf("expected prometheus output to contain %q\noutput:\n%s", fragment, output)
}
}
}
func TestItoa(t *testing.T) {
t.Parallel()
cases := map[int]string{
0: "0",
7: "7",
42: "42",
-10: "-10",
2048: "2048",
}
for input, want := range cases {
if got := itoa(input); got != want {
t.Fatalf("itoa(%d) = %q, want %q", input, got, want)
}
}
}
func findRouteMetric(routes []routeMetricSnapshot, method, path string, status int) *routeMetricSnapshot {
for _, route := range routes {
if route.Method == method && route.Path == path && route.Status == status {
result := route
return &result
}
}
return nil
}