Files
webhooker/internal/server/server.go
clawbot 4d5ebfd692
All checks were successful
check / check (push) Successful in 1m41s
refactor: use pinned golangci-lint Docker image for linting
Refactor Dockerfile to use a separate lint stage with a pinned
golangci-lint v2.11.3 Docker image instead of installing
golangci-lint via curl in the builder stage. This follows the
pattern used by sneak/pixa.

Changes:
- Dockerfile: separate lint stage using golangci/golangci-lint:v2.11.3
  (Debian-based, pinned by sha256) with COPY --from=lint dependency
- Bump Go from 1.24 to 1.26.1 (golang:1.26.1-bookworm, pinned)
- Bump golangci-lint from v1.64.8 to v2.11.3
- Migrate .golangci.yml from v1 to v2 format (same linters, format only)
- All Docker images pinned by sha256 digest
- Fix all lint issues from the v2 linter upgrade:
  - Add package comments to all packages
  - Add doc comments to all exported types, functions, and methods
  - Fix unchecked errors (errcheck)
  - Fix unused parameters (revive)
  - Fix gosec warnings (MaxBytesReader for form parsing)
  - Fix staticcheck suggestions (fmt.Fprintf instead of WriteString)
  - Rename DeliveryTask to Task to avoid stutter (delivery.Task)
  - Rename shadowed builtin 'max' parameter
- Update README.md version requirements
2026-03-17 05:46:03 -07:00

158 lines
3.7 KiB
Go

// Package server wires up HTTP routes and manages the application lifecycle.
package server
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"go.uber.org/fx"
"sneak.berlin/go/webhooker/internal/config"
"sneak.berlin/go/webhooker/internal/globals"
"sneak.berlin/go/webhooker/internal/handlers"
"sneak.berlin/go/webhooker/internal/logger"
"sneak.berlin/go/webhooker/internal/middleware"
"github.com/getsentry/sentry-go"
"github.com/go-chi/chi"
)
//nolint:revive // ServerParams is a standard fx naming convention.
type ServerParams struct {
fx.In
Logger *logger.Logger
Globals *globals.Globals
Config *config.Config
Middleware *middleware.Middleware
Handlers *handlers.Handlers
}
// Server is the main HTTP server that wires up routes and manages graceful shutdown.
type Server struct {
startupTime time.Time
exitCode int
sentryEnabled bool
log *slog.Logger
ctx context.Context
cancelFunc context.CancelFunc
httpServer *http.Server
router *chi.Mux
params ServerParams
mw *middleware.Middleware
h *handlers.Handlers
}
// New creates a Server that starts the HTTP listener on fx start and stops it gracefully.
func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
s := new(Server)
s.params = params
s.mw = params.Middleware
s.h = params.Handlers
s.log = params.Logger.Get()
lc.Append(fx.Hook{
OnStart: func(_ context.Context) error {
s.startupTime = time.Now()
go s.Run()
return nil
},
OnStop: func(_ context.Context) error {
s.cleanShutdown()
return nil
},
})
return s, nil
}
// Run configures Sentry and starts serving HTTP requests.
func (s *Server) Run() {
s.configure()
// logging before sentry, because sentry logs
s.enableSentry()
s.serve()
}
func (s *Server) enableSentry() {
s.sentryEnabled = false
if s.params.Config.SentryDSN == "" {
return
}
err := sentry.Init(sentry.ClientOptions{
Dsn: s.params.Config.SentryDSN,
Release: fmt.Sprintf("%s-%s", s.params.Globals.Appname, s.params.Globals.Version),
})
if err != nil {
s.log.Error("sentry init failure", "error", err)
// Don't use fatal since we still want the service to run
return
}
s.log.Info("sentry error reporting activated")
s.sentryEnabled = true
}
func (s *Server) serve() int {
s.ctx, s.cancelFunc = context.WithCancel(context.Background())
// signal watcher
go func() {
c := make(chan os.Signal, 1)
signal.Ignore(syscall.SIGPIPE)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
// block and wait for signal
sig := <-c
s.log.Info("signal received", "signal", sig.String())
if s.cancelFunc != nil {
// cancelling the main context will trigger a clean
// shutdown via the fx OnStop hook.
s.cancelFunc()
}
}()
go s.serveUntilShutdown()
<-s.ctx.Done()
// Shutdown is handled by the fx OnStop hook (cleanShutdown).
// Do not call cleanShutdown() here to avoid a double invocation.
return s.exitCode
}
func (s *Server) cleanupForExit() {
s.log.Info("cleaning up")
}
func (s *Server) cleanShutdown() {
// initiate clean shutdown
s.exitCode = 0
ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
if err := s.httpServer.Shutdown(ctxShutdown); err != nil {
s.log.Error("server clean shutdown failed", "error", err)
}
s.cleanupForExit()
if s.sentryEnabled {
sentry.Flush(2 * time.Second)
}
}
// MaintenanceMode returns whether the server is in maintenance mode.
func (s *Server) MaintenanceMode() bool {
return s.params.Config.MaintenanceMode
}
func (s *Server) configure() {
// identify ourselves in the logs
s.params.Logger.Identify()
}