check / check (push) Waiting to run
With SENTRY_DSN set, the server initialises sentry-go with the release netwatch-server-<version>, adds the sentryhttp middleware with Repanic as the last router-wide middleware, after the timeout, and flushes Sentry for 2 seconds on shutdown. A DSN Sentry refuses stops the start with an error naming SENTRY_DSN. With it empty, nothing is set up. The metrics middleware stays on the matched routes only, so it runs inside the Sentry middleware rather than before it. Model: opus-5-5
143 lines
3.4 KiB
Go
143 lines
3.4 KiB
Go
// Package server provides the HTTP server lifecycle,
|
|
// including startup, routing, and graceful shutdown. The
|
|
// process lifetime is owned by fx: shutdown is requested
|
|
// through fx.Shutdowner so every component's OnStop hook runs
|
|
// in dependency order.
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"sneak.berlin/go/netwatch/internal/config"
|
|
"sneak.berlin/go/netwatch/internal/globals"
|
|
"sneak.berlin/go/netwatch/internal/handlers"
|
|
"sneak.berlin/go/netwatch/internal/logger"
|
|
"sneak.berlin/go/netwatch/internal/middleware"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
"github.com/go-chi/chi/v5"
|
|
"go.uber.org/fx"
|
|
)
|
|
|
|
// sentryFlushTimeout is how long shutdown waits for Sentry to send
|
|
// what it still holds.
|
|
const sentryFlushTimeout = 2 * time.Second
|
|
|
|
// Params defines the dependencies for Server.
|
|
type Params struct {
|
|
fx.In
|
|
|
|
Config *config.Config
|
|
Globals *globals.Globals
|
|
Handlers *handlers.Handlers
|
|
Logger *logger.Logger
|
|
Middleware *middleware.Middleware
|
|
Shutdowner fx.Shutdowner
|
|
}
|
|
|
|
// Server is the top-level HTTP server orchestrator.
|
|
type Server struct {
|
|
h *handlers.Handlers
|
|
httpServer *http.Server
|
|
log *slog.Logger
|
|
mw *middleware.Middleware
|
|
params Params
|
|
router *chi.Mux
|
|
shutdowner fx.Shutdowner
|
|
}
|
|
|
|
// New creates a Server and registers lifecycle hooks for
|
|
// starting and stopping it.
|
|
func New(
|
|
lc fx.Lifecycle,
|
|
params Params,
|
|
) (*Server, error) {
|
|
s := new(Server)
|
|
s.params = params
|
|
s.mw = params.Middleware
|
|
s.h = params.Handlers
|
|
s.log = params.Logger.Get()
|
|
s.shutdowner = params.Shutdowner
|
|
|
|
err := s.enableSentry()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lc.Append(fx.Hook{
|
|
OnStart: func(_ context.Context) error {
|
|
// Build the router and http.Server synchronously
|
|
// here, before spawning the serving goroutine, so
|
|
// httpServer is fully constructed by the time OnStop
|
|
// (or an early signal) can read it. fx guarantees
|
|
// OnStart returns before OnStop runs, so no
|
|
// synchronization or nil check is needed at shutdown.
|
|
s.SetupRoutes()
|
|
s.httpServer = s.newHTTPServer()
|
|
|
|
go s.listenAndServe()
|
|
|
|
return nil
|
|
},
|
|
OnStop: func(ctx context.Context) error {
|
|
return s.shutdown(ctx)
|
|
},
|
|
})
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// ServeHTTP delegates to the chi router.
|
|
func (s *Server) ServeHTTP(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
) {
|
|
s.router.ServeHTTP(w, r)
|
|
}
|
|
|
|
// enableSentry sets Sentry up when SENTRY_DSN is set, so that
|
|
// SetupRoutes can report panics to it. With SENTRY_DSN empty it does
|
|
// nothing. A DSN Sentry refuses stops the start.
|
|
func (s *Server) enableSentry() error {
|
|
if s.params.Config.SentryDSN == "" {
|
|
return nil
|
|
}
|
|
|
|
err := sentry.Init(sentry.ClientOptions{
|
|
Dsn: s.params.Config.SentryDSN,
|
|
Release: s.params.Globals.Appname + "-" + s.params.Globals.Version,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("SENTRY_DSN: %w", err)
|
|
}
|
|
|
|
s.log.Info("sentry error reporting activated")
|
|
|
|
return nil
|
|
}
|
|
|
|
// shutdown gracefully stops the HTTP server within the
|
|
// deadline of the context fx provides for OnStop, then gives
|
|
// Sentry, if set up, time to send what it still holds.
|
|
func (s *Server) shutdown(ctx context.Context) error {
|
|
err := s.httpServer.Shutdown(ctx)
|
|
|
|
if s.params.Config.SentryDSN != "" {
|
|
sentry.Flush(sentryFlushTimeout)
|
|
}
|
|
|
|
if err != nil {
|
|
s.log.Error("server clean shutdown failed", "error", err)
|
|
|
|
return err
|
|
}
|
|
|
|
s.log.Info("server stopped")
|
|
|
|
return nil
|
|
}
|