Files
routewatch/internal/server/server.go
T
clawbot b4dee48401
check / check (push) Waiting to run
Exit with status 1 when the web server cannot listen (closes #73)
`Server.Start` opened its port inside a goroutine and returned nil at once, so a port already in use was only logged and the daemon kept running with no web server. It now opens the listener before returning and returns that error; serving runs in the background on the open listener. `CLIEntry` turns an error from `Run` into an fx shutdown with exit status 1, after the normal stop sequence, so a container restart policy can act. A serving error after a successful listen is still only logged.

Model: opus-5-5
2026-10-14 18:26:04 +02:00

112 lines
2.6 KiB
Go

// Package server provides HTTP endpoints for status monitoring and statistics
package server
import (
"context"
"fmt"
"net"
"net/http"
"strconv"
"time"
"git.eeqj.de/sneak/routewatch/internal/config"
"git.eeqj.de/sneak/routewatch/internal/database"
"git.eeqj.de/sneak/routewatch/internal/logger"
"git.eeqj.de/sneak/routewatch/internal/streamer"
"github.com/go-chi/chi/v5"
)
// ASNFetcherStats contains WHOIS fetcher statistics.
type ASNFetcherStats struct {
SuccessesLastHour int
ErrorsLastHour int
CurrentInterval time.Duration
ConsecutiveFails int
}
// ASNFetcher is an interface for queuing ASN WHOIS lookups.
type ASNFetcher interface {
QueueImmediate(asn int)
GetStats() ASNFetcherStats
}
// Server provides HTTP endpoints for status monitoring
type Server struct {
router *chi.Mux
db database.Store
streamer *streamer.Streamer
logger *logger.Logger
port int
srv *http.Server
asnFetcher ASNFetcher
}
// New creates a new HTTP server
func New(db database.Store, streamer *streamer.Streamer, logger *logger.Logger, cfg *config.Config) *Server {
s := &Server{
db: db,
streamer: streamer,
logger: logger,
port: cfg.Port,
}
s.setupRoutes()
return s
}
// Start listens on the configured port and serves HTTP in the background.
// It returns an error if it cannot listen, for example because the port is
// already in use.
func (s *Server) Start() error {
const (
network = "tcp"
readHeaderTimeout = 40 * time.Second
readTimeout = 60 * time.Second
writeTimeout = 60 * time.Second
idleTimeout = 120 * time.Second
)
s.srv = &http.Server{
Addr: ":" + strconv.Itoa(s.port),
Handler: s.router,
ReadHeaderTimeout: readHeaderTimeout,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
}
s.logger.Info("Starting HTTP server", "port", s.port, "addr", s.srv.Addr)
listener, err := net.Listen(network, s.srv.Addr)
if err != nil {
return fmt.Errorf("starting HTTP server: %w", err)
}
s.logger.Info("HTTP server listening", "addr", listener.Addr().String())
go func() {
if err := s.srv.Serve(listener); err != nil && err != http.ErrServerClosed {
s.logger.Error("HTTP server error", "error", err)
}
}()
return nil
}
// Stop gracefully stops the HTTP server
func (s *Server) Stop(ctx context.Context) error {
if s.srv == nil {
return nil
}
s.logger.Info("Stopping HTTP server")
return s.srv.Shutdown(ctx)
}
// SetASNFetcher sets the ASN WHOIS fetcher for on-demand lookups.
func (s *Server) SetASNFetcher(fetcher ASNFetcher) {
s.asnFetcher = fetcher
}