Register prometheus Go and process collectors alongside the router metrics so /metrics exposes standard go_* and process_* series.
182 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|