mattermost-push-proxy-router/metrics_test.go
Felipe M. 8789af2997
feat: include Go runtime and process metrics on /metrics
Register prometheus Go and process collectors alongside the router
metrics so /metrics exposes standard go_* and process_* series.
2026-07-27 08:39:11 +02:00

182 lines
4.8 KiB
Go

package main
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
func counterValue(t *testing.T, reg *prometheus.Registry, name string, labels map[string]string) float64 {
t.Helper()
metrics, err := reg.Gather()
if err != nil {
t.Fatalf("Gather() error: %v", err)
}
for _, mf := range metrics {
if mf.GetName() != name {
continue
}
for _, m := range mf.GetMetric() {
if labelsMatch(m, labels) {
return m.GetCounter().GetValue()
}
}
}
return 0
}
func labelsMatch(m *dto.Metric, want map[string]string) bool {
if len(m.GetLabel()) != len(want) {
return false
}
for _, label := range m.GetLabel() {
if want[label.GetName()] != label.GetValue() {
return false
}
}
return true
}
func TestHandleRouteRecordsRequestMetrics(t *testing.T) {
reg := prometheus.NewRegistry()
metrics := NewMetrics(reg)
custom := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":"OK"}`))
}))
defer custom.Close()
cfg := Config{
Routes: []Route{{Prefixes: []string{"apple_bubbles"}, URL: custom.URL}},
RequestTimeout: time.Duration(defaultRequestTimeoutSec) * time.Second,
}
proxy := NewProxy(cfg, metrics)
req := httptest.NewRequest(http.MethodPost, "/api/v1/send_push", strings.NewReader(`{"platform":"apple_bubbles","device_id":"tok","server_id":"s1"}`))
rec := httptest.NewRecorder()
handleRoute(rec, req, proxy, cfg, "/api/v1/send_push")
if got := counterValue(t, reg, metricRequestsTotalName, map[string]string{
"path": "send_push",
"platform": "apple_bubbles",
}); got != 1 {
t.Fatalf("router_requests_total = %v, want 1", got)
}
if got := counterValue(t, reg, metricUpstreamRequestsTotalName, map[string]string{
"platform": "apple_bubbles",
"outcome": "success",
}); got != 1 {
t.Fatalf("router_upstream_requests_total = %v, want 1", got)
}
}
func TestHandleRouteRecordsUnknownPlatformMetric(t *testing.T) {
reg := prometheus.NewRegistry()
metrics := NewMetrics(reg)
cfg := Config{
Routes: []Route{{Prefixes: []string{"apple_bubbles"}, URL: "http://localhost:1"}},
RequestTimeout: time.Duration(defaultRequestTimeoutSec) * time.Second,
}
proxy := NewProxy(cfg, metrics)
req := httptest.NewRequest(http.MethodPost, "/api/v1/send_push", strings.NewReader(`{"platform":"unknown_app","device_id":"tok","server_id":"s1"}`))
rec := httptest.NewRecorder()
handleRoute(rec, req, proxy, cfg, "/api/v1/send_push")
if got := counterValue(t, reg, metricRoutingErrorsTotalName, map[string]string{
"reason": "unknown_platform",
}); got != 1 {
t.Fatalf("router_routing_errors_total = %v, want 1", got)
}
}
func TestMetricsEndpoint(t *testing.T) {
reg := prometheus.NewRegistry()
metrics := NewMetrics(reg)
metrics.incRequest("send_push", "apple_bubbles")
metrics.incRoutingError("unknown_platform")
metrics.observeUpstream("apple_bubbles", "success", time.Millisecond)
mux := http.NewServeMux()
mux.Handle("GET /metrics", metrics.Handler())
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d", rec.Code)
}
body, err := io.ReadAll(rec.Body)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(body), metricRequestsTotalName) {
t.Fatalf("metrics body missing %q", metricRequestsTotalName)
}
families, err := reg.Gather()
if err != nil {
t.Fatalf("Gather() error: %v", err)
}
got := make(map[string]struct{}, len(families))
for _, family := range families {
got[family.GetName()] = struct{}{}
}
for _, name := range []string{
metricRequestsTotalName,
metricRoutingErrorsTotalName,
metricUpstreamRequestsTotalName,
metricUpstreamDurationName,
"go_goroutines",
"process_cpu_seconds_total",
} {
if _, ok := got[name]; !ok {
t.Fatalf("registry missing metric %q", name)
}
}
}
func TestUpstreamOutcome(t *testing.T) {
tests := []struct {
name string
statusCode int
body string
want string
}{
{"ok response", http.StatusOK, `{"status":"OK"}`, "success"},
{"fail response", http.StatusOK, `{"status":"FAIL","error":"nope"}`, "fail"},
{"http error", http.StatusBadGateway, `{}`, "fail"},
{"unparseable success", http.StatusOK, `not-json`, "success"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := upstreamOutcome(tt.statusCode, []byte(tt.body)); got != tt.want {
t.Fatalf("upstreamOutcome() = %q, want %q", got, tt.want)
}
})
}
}
func TestRoutePathLabel(t *testing.T) {
if got := routePathLabel("/api/v1/ack"); got != "ack" {
t.Fatalf("routePathLabel() = %q, want ack", got)
}
}