Organize backend into product executable and shared Go modules

This commit is contained in:
Stephan D committed 2026-09-12 14:14:08 +02:00
1 parent 981e84a4b4
commit f0720ec66b
382 files changed
+1944 -6989

No files matched your search

+153
View File
@@ -0,0 +1,153 @@
package httptransport
import (
"net/http"
"os"
"path"
mr "git.sendico.io/tech/sxchnge/product/app/metrics"
"git.sendico.io/tech/sxchnge/product/auth/config"
httprouter "git.sendico.io/tech/sxchnge/product/auth/http"
"git.sendico.io/tech/sxchnge/product/auth/response"
authstorage "git.sendico.io/tech/sxchnge/product/auth/storage"
"git.sendico.io/tech/sxchnge/product/sessions/browser"
"git.sendico.io/tech/sxchnge/shared/database"
"git.sendico.io/tech/sxchnge/shared/errors"
api "git.sendico.io/tech/sxchnge/shared/http"
"git.sendico.io/tech/sxchnge/shared/http/response"
"git.sendico.io/tech/sxchnge/shared/logging"
"git.sendico.io/tech/sxchnge/shared/service"
"github.com/go-chi/chi/v5"
cm "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/metrics"
"go.uber.org/zap"
"moul.io/chizap"
)
type Middleware struct {
logger mlogger.Logger
router *chi.Mux
apiEndpoint string
metrics mr.Metrics
epdispatcher *httprouter.Dispatcher
}
func (mw *Middleware) Handler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.HandlerFunc) {
mw.epdispatcher.Handler(service, endpoint, method, handler)
}
func (mw *Middleware) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
mw.epdispatcher.AccountHandler(service, endpoint, method, handler)
}
func (mw *Middleware) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) {
mw.epdispatcher.PendingAccountHandler(service, endpoint, method, handler)
}
func (mw *Middleware) Finish() {
mw.metrics.Finish()
}
func (mw *Middleware) installMiddleware(config *middleware.Config, debug bool) {
mw.logger.Debug("Installing middleware stack...")
// Collect metrics for application traffic only. Scrapes and health probes are
// operational traffic and would otherwise distort request and in-flight SLIs.
mw.router.Use(metrics.Collector(metrics.CollectorOpts{
Host: false, // avoid high-cardinality "host" label
Proto: true, // include HTTP protocol label
Skip: func(r *http.Request) bool {
return isObservabilityRequest(mw.apiEndpoint, r.URL.Path)
},
}))
mw.router.Use(cm.RequestID)
if debug {
mw.router.Use(chizap.New(mw.logger.Named("http_trace"), &chizap.Opts{
WithReferer: true,
WithUserAgent: true,
}))
}
mw.router.Use(cors.Handler(cors.Options{
AllowedOrigins: config.CORS.AllowedOrigins,
AllowedMethods: config.CORS.AllowedMethods,
AllowedHeaders: config.CORS.AllowedHeaders,
ExposedHeaders: config.CORS.ExposedHeaders,
AllowCredentials: config.CORS.AllowCredentials,
MaxAge: config.CORS.MaxAge,
OptionsPassthrough: false,
Debug: debug,
}))
mw.router.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := browser.ValidateCSRF(r, config.CORS.AllowedOrigins); err != nil {
mw.logger.Info("Rejected browser request with invalid CSRF context",
zap.String("method", r.Method), zap.String("path", r.URL.Path))
response.Forbidden(mw.logger, mservice.Accounts, "csrf_rejected", "browser request failed CSRF validation").ServeHTTP(w, r)
return
}
next.ServeHTTP(w, r)
})
})
mw.router.Use(cm.Recoverer)
mw.router.Handle("/metrics", metrics.Handler())
mw.logger.Info("Middleware stack installation complete")
}
func isObservabilityRequest(apiEndpoint, requestPath string) bool {
healthPath := path.Join("/", apiEndpoint, "health")
metricsPath := path.Join("/", apiEndpoint, "metrics")
return requestPath == healthPath || requestPath == metricsPath || requestPath == "/metrics"
}
func CreateMiddleware(
logger mlogger.Logger,
db db.Factory,
apiRouter *chi.Mux,
config *middleware.Config,
authStorage *middleware.AuthStorageConfig,
debug bool,
) (*Middleware, error) {
if config == nil {
return nil, merrors.InvalidArgument("middleware configuration is required", "middleware")
}
if err := config.Token.Validate(); err != nil {
return nil, err
}
if err := config.CORS.Validate(); err != nil {
return nil, err
}
loginRateLimits, err := config.ResolveLoginRateLimits()
if err != nil {
logger.Error("Invalid login rate limit configuration", zap.Error(err))
return nil, err
}
p := &Middleware{
logger: logger.Named("middleware"),
router: apiRouter,
apiEndpoint: os.Getenv(config.EndPointEnv),
}
p.logger.Info("Set endpoint", zap.String("endpoint", p.apiEndpoint))
p.installMiddleware(config, debug)
if p.metrics, err = mr.NewMetricsRouter(p.logger, p.router, p.apiEndpoint); err != nil {
p.logger.Error("Failed to create metrics router", zap.Error(err), zap.String("api_endpoint", p.apiEndpoint))
return nil, err
}
stores, err := authstorage.OpenStores(p.logger, db, authStorage)
if err != nil {
return nil, err
}
p.epdispatcher = httprouter.NewDispatcher(
p.logger,
p.router,
stores.Accounts,
stores.Verification,
stores.RefreshTokens,
stores.LoginAttempts,
config,
loginRateLimits,
)
return p, nil
}
+155
View File
@@ -0,0 +1,155 @@
package httptransport
import (
"context"
"net/http"
"net/http/httptest"
"testing"
amr "git.sendico.io/tech/sxchnge/product/app/health"
"git.sendico.io/tech/sxchnge/product/app/health/status"
"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)
}
}