// Package server provides HTTP endpoints for status monitoring and statistics package server import ( "context" "encoding/json" "log/slog" "net/http" "os" "time" "git.eeqj.de/sneak/routewatch/internal/database" "git.eeqj.de/sneak/routewatch/internal/streamer" "git.eeqj.de/sneak/routewatch/internal/templates" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) // Server provides HTTP endpoints for status monitoring type Server struct { router *chi.Mux db database.Store streamer *streamer.Streamer logger *slog.Logger srv *http.Server } // New creates a new HTTP server func New(db database.Store, streamer *streamer.Streamer, logger *slog.Logger) *Server { s := &Server{ db: db, streamer: streamer, logger: logger, } s.setupRoutes() return s } // setupRoutes configures the HTTP routes func (s *Server) setupRoutes() { r := chi.NewRouter() // Middleware r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) const requestTimeout = 60 * time.Second r.Use(middleware.Timeout(requestTimeout)) // Routes r.Get("/", s.handleRoot()) r.Get("/status", s.handleStatusHTML()) r.Get("/status.json", s.handleStatusJSON()) // API routes r.Route("/api/v1", func(r chi.Router) { r.Get("/stats", s.handleStats()) }) s.router = r } // Start starts the HTTP server func (s *Server) Start() error { port := os.Getenv("PORT") if port == "" { port = "8080" } const readHeaderTimeout = 10 * time.Second s.srv = &http.Server{ Addr: ":" + port, Handler: s.router, ReadHeaderTimeout: readHeaderTimeout, } s.logger.Info("Starting HTTP server", "port", port) go func() { if err := s.srv.ListenAndServe(); 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) } // handleRoot returns a handler that redirects to /status func (s *Server) handleRoot() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/status", http.StatusSeeOther) } } // handleStatusJSON returns a handler that serves JSON statistics func (s *Server) handleStatusJSON() http.HandlerFunc { // Stats represents the statistics response type Stats struct { Uptime string `json:"uptime"` TotalMessages uint64 `json:"total_messages"` TotalBytes uint64 `json:"total_bytes"` MessagesPerSec float64 `json:"messages_per_sec"` MbitsPerSec float64 `json:"mbits_per_sec"` Connected bool `json:"connected"` ASNs int `json:"asns"` Prefixes int `json:"prefixes"` IPv4Prefixes int `json:"ipv4_prefixes"` IPv6Prefixes int `json:"ipv6_prefixes"` Peerings int `json:"peerings"` LiveRoutes int `json:"live_routes"` } return func(w http.ResponseWriter, _ *http.Request) { metrics := s.streamer.GetMetrics() // Get database stats dbStats, err := s.db.GetStats() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String() if metrics.ConnectedSince.IsZero() { uptime = "0s" } const bitsPerMegabit = 1000000.0 stats := Stats{ Uptime: uptime, TotalMessages: metrics.TotalMessages, TotalBytes: metrics.TotalBytes, MessagesPerSec: metrics.MessagesPerSec, MbitsPerSec: metrics.BitsPerSec / bitsPerMegabit, Connected: metrics.Connected, ASNs: dbStats.ASNs, Prefixes: dbStats.Prefixes, IPv4Prefixes: dbStats.IPv4Prefixes, IPv6Prefixes: dbStats.IPv6Prefixes, Peerings: dbStats.Peerings, LiveRoutes: dbStats.LiveRoutes, } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(stats); err != nil { s.logger.Error("Failed to encode stats", "error", err) } } } // handleStats returns a handler that serves API v1 statistics func (s *Server) handleStats() http.HandlerFunc { // StatsResponse represents the API statistics response type StatsResponse struct { Uptime string `json:"uptime"` TotalMessages uint64 `json:"total_messages"` TotalBytes uint64 `json:"total_bytes"` MessagesPerSec float64 `json:"messages_per_sec"` MbitsPerSec float64 `json:"mbits_per_sec"` Connected bool `json:"connected"` ASNs int `json:"asns"` Prefixes int `json:"prefixes"` IPv4Prefixes int `json:"ipv4_prefixes"` IPv6Prefixes int `json:"ipv6_prefixes"` Peerings int `json:"peerings"` LiveRoutes int `json:"live_routes"` } return func(w http.ResponseWriter, _ *http.Request) { metrics := s.streamer.GetMetrics() // Get database stats dbStats, err := s.db.GetStats() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String() if metrics.ConnectedSince.IsZero() { uptime = "0s" } const bitsPerMegabit = 1000000.0 stats := StatsResponse{ Uptime: uptime, TotalMessages: metrics.TotalMessages, TotalBytes: metrics.TotalBytes, MessagesPerSec: metrics.MessagesPerSec, MbitsPerSec: metrics.BitsPerSec / bitsPerMegabit, Connected: metrics.Connected, ASNs: dbStats.ASNs, Prefixes: dbStats.Prefixes, IPv4Prefixes: dbStats.IPv4Prefixes, IPv6Prefixes: dbStats.IPv6Prefixes, Peerings: dbStats.Peerings, LiveRoutes: dbStats.LiveRoutes, } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(stats); err != nil { s.logger.Error("Failed to encode stats", "error", err) } } } // handleStatusHTML returns a handler that serves the HTML status page func (s *Server) handleStatusHTML() http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") tmpl := templates.StatusTemplate() if err := tmpl.Execute(w, nil); err != nil { s.logger.Error("Failed to render template", "error", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } }