check / check (push) Successful in 1m33s
DNSWATCHER_SENTRY_DSN was read but never used. This ports the Sentry integration from gohttpserver with sentry-go v0.49.0. The server's start hook calls sentry.Init when the DSN is set; a DSN Sentry cannot parse fails the hook, so startup stops with the parse error. sentryhttp, with Repanic, reports handler panics and passes them on to chi's Recoverer. Shutdown sends queued reports once the HTTP server has stopped. The client uses the older transport (DisableTelemetryBuffer): with the default one, Flush can return before sending a report made just before it. Client reports are off, so only panics are sent. The DSN is checked at server start, not in config, so the config test is unchanged. sentry-go raises several golang.org/x modules and moves go-spew and go-difflib to untagged commits. Model: opus-5-5
89 lines
2.5 KiB
Go
89 lines
2.5 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
sentryhttp "github.com/getsentry/sentry-go/http"
|
|
"github.com/go-chi/chi/v5"
|
|
chimw "github.com/go-chi/chi/v5/middleware"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
|
|
"sneak.berlin/go/dnswatcher/static"
|
|
)
|
|
|
|
// requestTimeout is the maximum duration for handling a request.
|
|
const requestTimeout = 60 * time.Second
|
|
|
|
// SetupRoutes configures all HTTP routes.
|
|
func (s *Server) SetupRoutes() {
|
|
s.router = chi.NewRouter()
|
|
|
|
// Global middleware
|
|
s.router.Use(chimw.Recoverer)
|
|
s.router.Use(chimw.RequestID)
|
|
s.router.Use(s.mw.SecurityHeaders())
|
|
s.router.Use(s.mw.Logging())
|
|
s.router.Use(chimw.Timeout(requestTimeout))
|
|
|
|
// Report panics in handlers to Sentry when DNSWATCHER_SENTRY_DSN is
|
|
// set. Repanic passes each panic on to chimw.Recoverer above, which
|
|
// still answers the request.
|
|
if s.sentryEnabled {
|
|
sentryHandler := sentryhttp.New(sentryhttp.Options{
|
|
Repanic: true,
|
|
})
|
|
s.router.Use(sentryHandler.Handle)
|
|
}
|
|
|
|
// Public, unauthenticated, read-only routes, the only ones
|
|
// REPO_POLICIES.md allows wildcard CORS on. CORS is middleware of
|
|
// this whole router, not of a Group, so that it also answers
|
|
// OPTIONS preflight requests, which no route here registers.
|
|
public := chi.NewRouter()
|
|
public.Use(s.mw.CORS())
|
|
|
|
// Dashboard (read-only web UI)
|
|
public.Get("/", s.handlers.HandleDashboard())
|
|
|
|
// Static assets (embedded CSS/JS)
|
|
public.Mount(
|
|
"/s",
|
|
http.StripPrefix(
|
|
"/s",
|
|
http.FileServer(http.FS(static.Static)),
|
|
),
|
|
)
|
|
|
|
// Health check (standard well-known path)
|
|
public.Get(
|
|
"/.well-known/healthcheck",
|
|
s.handlers.HandleHealthCheck(),
|
|
)
|
|
|
|
// Legacy health check (keep for backward compatibility)
|
|
public.Get("/health", s.handlers.HandleHealthCheck())
|
|
|
|
// API v1 routes
|
|
public.Route("/api/v1", func(r chi.Router) {
|
|
r.Get("/status", s.handlers.HandleStatus())
|
|
})
|
|
|
|
s.router.Mount("/", public)
|
|
|
|
// Metrics endpoint (optional, with basic auth) and no CORS: a
|
|
// Prometheus scraper is not a browser. It is mounted rather than
|
|
// added with Get so that every method on /metrics, OPTIONS
|
|
// included, ends here instead of falling through to the public
|
|
// router and its CORS. The rate limit comes before Basic Auth, so
|
|
// failed logins count against it and a request over the limit
|
|
// never reaches the password check.
|
|
if s.params.Config.MetricsUsername != "" {
|
|
metrics := chi.NewRouter()
|
|
metrics.Use(s.mw.MetricsRateLimit())
|
|
metrics.Use(s.mw.MetricsAuth())
|
|
metrics.Get("/", promhttp.Handler().ServeHTTP)
|
|
s.router.Mount("/metrics", metrics)
|
|
}
|
|
}
|