package httptransport import ( "context" "net/http" "net/http/httptest" "testing" amr "git.sendico.io/tech/sxchnge/bff/pkg/api/routers" "git.sendico.io/tech/sxchnge/bff/pkg/api/routers/health" "github.com/go-chi/chi/v5" chimetrics "github.com/go-chi/metrics" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" "moul.io/chizap" ) func TestHTTPCollectorDoesNotGatherHealthOrMetricsRequests(t *testing.T) { router := chi.NewRouter() router.Use(chimetrics.Collector(chimetrics.CollectorOpts{ Proto: true, Skip: func(r *http.Request) bool { return isObservabilityRequest("/api/v1", r.URL.Path) }, })) router.Get("/api/v1/health", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) router.Handle("/metrics", chimetrics.Handler()) router.Handle("/api/v1/metrics", chimetrics.Handler()) router.Get("/api/v1/accounts", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) beforeHealth := gatheredHTTPRequestCount(t, "GET /api/v1/health") beforeMetrics := gatheredHTTPRequestCount(t, "GET /metrics") beforeAPIMetrics := gatheredHTTPRequestCount(t, "GET /api/v1/metrics") beforeApplication := gatheredHTTPRequestCount(t, "GET /api/v1/accounts") for _, requestPath := range []string{"/api/v1/health", "/metrics", "/api/v1/metrics", "/api/v1/accounts"} { request := httptest.NewRequestWithContext(context.Background(), http.MethodGet, requestPath, nil) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("GET %s returned %d", requestPath, response.Code) } } if got := gatheredHTTPRequestCount(t, "GET /api/v1/health") - beforeHealth; got != 0 { t.Fatalf("health probe changed http_requests_total by %v", got) } if got := gatheredHTTPRequestCount(t, "GET /metrics") - beforeMetrics; got != 0 { t.Fatalf("metrics scrape changed http_requests_total by %v", got) } if got := gatheredHTTPRequestCount(t, "GET /api/v1/metrics") - beforeAPIMetrics; got != 0 { t.Fatalf("API metrics scrape changed http_requests_total by %v", got) } if got := gatheredHTTPRequestCount(t, "GET /api/v1/accounts") - beforeApplication; got != 1 { t.Fatalf("application request changed http_requests_total by %v, want 1", got) } } func gatheredHTTPRequestCount(t *testing.T, endpoint string) float64 { t.Helper() families, err := prometheus.DefaultGatherer.Gather() if err != nil { t.Fatalf("gather metrics: %v", err) } for _, family := range families { if family.GetName() != "http_requests_total" { continue } var total float64 for _, metric := range family.GetMetric() { for _, label := range metric.GetLabel() { if label.GetName() == "endpoint" && label.GetValue() == endpoint { total += metric.GetCounter().GetValue() } } } return total } return 0 } func TestObservabilityRequestsAreExcludedFromHTTPMetrics(t *testing.T) { tests := []struct { name string apiEndpoint string requestPath string want bool }{ {name: "health under API endpoint", apiEndpoint: "/api/v1", requestPath: "/api/v1/health", want: true}, {name: "metrics at root", apiEndpoint: "/api/v1", requestPath: "/metrics", want: true}, {name: "metrics under API endpoint", apiEndpoint: "/api/v1", requestPath: "/api/v1/metrics", want: true}, {name: "trailing endpoint slash", apiEndpoint: "/api/v1/", requestPath: "/api/v1/health", want: true}, {name: "application endpoint", apiEndpoint: "/api/v1", requestPath: "/api/v1/accounts", want: false}, {name: "health suffix is not a probe", apiEndpoint: "/api/v1", requestPath: "/api/v1/health/history", want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := isObservabilityRequest(tt.apiEndpoint, tt.requestPath); got != tt.want { t.Fatalf("isObservabilityRequest(%q, %q) = %t, want %t", tt.apiEndpoint, tt.requestPath, got, tt.want) } }) } } func TestHealthMountedOutsideTraceMiddleware(t *testing.T) { core, observed := observer.New(zapcore.InfoLevel) logger := zap.New(core) rootRouter := chi.NewRouter() apiRouter := chi.NewRouter() apiRouter.Use(chizap.New(logger.Named("middleware").Named("http_trace"), &chizap.Opts{})) apiRouter.Get("/api/v1/accounts", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) healthRouter, err := amr.NewHealthRouter(logger.Named("middleware"), rootRouter, "/api/v1") if err != nil { t.Fatalf("failed to create health router: %v", err) } healthRouter.SetStatus(health.SSRunning) rootRouter.Mount("/", apiRouter) healthReq := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/api/v1/health", nil) healthRes := httptest.NewRecorder() rootRouter.ServeHTTP(healthRes, healthReq) if healthRes.Code != http.StatusOK { t.Fatalf("expected health status %d, got %d", http.StatusOK, healthRes.Code) } if got := observed.FilterMessage("Served").Len(); got != 0 { t.Fatalf("expected no trace logs for /health, got %d", got) } apiReq := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/api/v1/accounts", nil) apiRes := httptest.NewRecorder() rootRouter.ServeHTTP(apiRes, apiReq) if apiRes.Code != http.StatusOK { t.Fatalf("expected api status %d, got %d", http.StatusOK, apiRes.Code) } entries := observed.FilterMessage("Served").All() if len(entries) != 1 { t.Fatalf("expected one trace log for non-health request, got %d", len(entries)) } if got := entries[0].ContextMap()["path"]; got != "/api/v1/accounts" { t.Fatalf("expected logged path %q, got %#v", "/api/v1/accounts", got) } }