- Added new live_routes table with mask_length column for tracking CIDR prefix lengths - Updated PrefixHandler to maintain live routing table with additions and deletions - Added route expiration functionality (5 minute timeout) to in-memory routing table - Added prefix distribution stats showing count of prefixes by mask length - Added IPv4/IPv6 prefix distribution cards to status page - Updated database interface with UpsertLiveRoute, DeleteLiveRoute, and GetPrefixDistribution - Set all handler queue depths to 50000 for consistency - Doubled DBHandler batch size to 32000 for better throughput - Fixed withdrawal handling to delete routes when origin ASN is available
393 lines
13 KiB
Go
393 lines
13 KiB
Go
// Package server provides HTTP endpoints for status monitoring and statistics
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
|
"git.eeqj.de/sneak/routewatch/internal/routingtable"
|
|
"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
|
|
routingTable *routingtable.RoutingTable
|
|
streamer *streamer.Streamer
|
|
logger *logger.Logger
|
|
srv *http.Server
|
|
}
|
|
|
|
// New creates a new HTTP server
|
|
func New(db database.Store, rt *routingtable.RoutingTable, streamer *streamer.Streamer, logger *logger.Logger) *Server {
|
|
s := &Server{
|
|
db: db,
|
|
routingTable: rt,
|
|
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 = 2 * time.Second
|
|
r.Use(TimeoutMiddleware(requestTimeout))
|
|
r.Use(JSONResponseMiddleware)
|
|
|
|
// 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"`
|
|
DatabaseSizeBytes int64 `json:"database_size_bytes"`
|
|
LiveRoutes int `json:"live_routes"`
|
|
IPv4Routes int `json:"ipv4_routes"`
|
|
IPv6Routes int `json:"ipv6_routes"`
|
|
IPv4UpdatesPerSec float64 `json:"ipv4_updates_per_sec"`
|
|
IPv6UpdatesPerSec float64 `json:"ipv6_updates_per_sec"`
|
|
IPv4PrefixDistribution []database.PrefixDistribution `json:"ipv4_prefix_distribution"`
|
|
IPv6PrefixDistribution []database.PrefixDistribution `json:"ipv6_prefix_distribution"`
|
|
}
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Create a 1 second timeout context for this request
|
|
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
|
|
defer cancel()
|
|
|
|
metrics := s.streamer.GetMetrics()
|
|
|
|
// Get database stats with timeout
|
|
statsChan := make(chan database.Stats)
|
|
errChan := make(chan error)
|
|
|
|
go func() {
|
|
dbStats, err := s.db.GetStats()
|
|
if err != nil {
|
|
s.logger.Debug("Database stats query failed", "error", err)
|
|
errChan <- err
|
|
|
|
return
|
|
}
|
|
statsChan <- dbStats
|
|
}()
|
|
|
|
var dbStats database.Stats
|
|
select {
|
|
case <-ctx.Done():
|
|
s.logger.Error("Database stats timeout in status.json")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusRequestTimeout)
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"status": "error",
|
|
"error": map[string]interface{}{
|
|
"msg": "Database timeout",
|
|
"code": http.StatusRequestTimeout,
|
|
},
|
|
})
|
|
|
|
return
|
|
case err := <-errChan:
|
|
s.logger.Error("Failed to get database stats", "error", err)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"status": "error",
|
|
"error": map[string]interface{}{
|
|
"msg": err.Error(),
|
|
"code": http.StatusInternalServerError,
|
|
},
|
|
})
|
|
|
|
return
|
|
case dbStats = <-statsChan:
|
|
// Success
|
|
}
|
|
|
|
uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
|
|
if metrics.ConnectedSince.IsZero() {
|
|
uptime = "0s"
|
|
}
|
|
|
|
const bitsPerMegabit = 1000000.0
|
|
|
|
// Get detailed routing table stats
|
|
rtStats := s.routingTable.GetDetailedStats()
|
|
|
|
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,
|
|
DatabaseSizeBytes: dbStats.FileSizeBytes,
|
|
LiveRoutes: dbStats.LiveRoutes,
|
|
IPv4Routes: rtStats.IPv4Routes,
|
|
IPv6Routes: rtStats.IPv6Routes,
|
|
IPv4UpdatesPerSec: rtStats.IPv4UpdatesRate,
|
|
IPv6UpdatesPerSec: rtStats.IPv6UpdatesRate,
|
|
IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
|
|
IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
response := map[string]interface{}{
|
|
"status": "ok",
|
|
"data": stats,
|
|
}
|
|
if err := json.NewEncoder(w).Encode(response); 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 {
|
|
// HandlerStatsInfo represents handler statistics in the API response
|
|
type HandlerStatsInfo struct {
|
|
Name string `json:"name"`
|
|
QueueLength int `json:"queue_length"`
|
|
QueueCapacity int `json:"queue_capacity"`
|
|
ProcessedCount uint64 `json:"processed_count"`
|
|
DroppedCount uint64 `json:"dropped_count"`
|
|
AvgProcessTimeMs float64 `json:"avg_process_time_ms"`
|
|
MinProcessTimeMs float64 `json:"min_process_time_ms"`
|
|
MaxProcessTimeMs float64 `json:"max_process_time_ms"`
|
|
}
|
|
|
|
// 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"`
|
|
DatabaseSizeBytes int64 `json:"database_size_bytes"`
|
|
LiveRoutes int `json:"live_routes"`
|
|
IPv4Routes int `json:"ipv4_routes"`
|
|
IPv6Routes int `json:"ipv6_routes"`
|
|
IPv4UpdatesPerSec float64 `json:"ipv4_updates_per_sec"`
|
|
IPv6UpdatesPerSec float64 `json:"ipv6_updates_per_sec"`
|
|
HandlerStats []HandlerStatsInfo `json:"handler_stats"`
|
|
IPv4PrefixDistribution []database.PrefixDistribution `json:"ipv4_prefix_distribution"`
|
|
IPv6PrefixDistribution []database.PrefixDistribution `json:"ipv6_prefix_distribution"`
|
|
}
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Create a 1 second timeout context for this request
|
|
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
|
|
defer cancel()
|
|
|
|
// Check if context is already cancelled
|
|
select {
|
|
case <-ctx.Done():
|
|
http.Error(w, "Request timeout", http.StatusRequestTimeout)
|
|
|
|
return
|
|
default:
|
|
}
|
|
|
|
metrics := s.streamer.GetMetrics()
|
|
|
|
// Get database stats with timeout
|
|
statsChan := make(chan database.Stats)
|
|
errChan := make(chan error)
|
|
|
|
go func() {
|
|
dbStats, err := s.db.GetStats()
|
|
if err != nil {
|
|
s.logger.Debug("Database stats query failed", "error", err)
|
|
errChan <- err
|
|
|
|
return
|
|
}
|
|
statsChan <- dbStats
|
|
}()
|
|
|
|
var dbStats database.Stats
|
|
select {
|
|
case <-ctx.Done():
|
|
s.logger.Error("Database stats timeout")
|
|
http.Error(w, "Database timeout", http.StatusRequestTimeout)
|
|
|
|
return
|
|
case err := <-errChan:
|
|
s.logger.Error("Failed to get database stats", "error", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
case dbStats = <-statsChan:
|
|
// Success
|
|
}
|
|
|
|
uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
|
|
if metrics.ConnectedSince.IsZero() {
|
|
uptime = "0s"
|
|
}
|
|
|
|
const bitsPerMegabit = 1000000.0
|
|
|
|
// Get detailed routing table stats
|
|
rtStats := s.routingTable.GetDetailedStats()
|
|
|
|
// Get handler stats
|
|
handlerStats := s.streamer.GetHandlerStats()
|
|
handlerStatsInfo := make([]HandlerStatsInfo, 0, len(handlerStats))
|
|
const microsecondsPerMillisecond = 1000.0
|
|
for _, hs := range handlerStats {
|
|
handlerStatsInfo = append(handlerStatsInfo, HandlerStatsInfo{
|
|
Name: hs.Name,
|
|
QueueLength: hs.QueueLength,
|
|
QueueCapacity: hs.QueueCapacity,
|
|
ProcessedCount: hs.ProcessedCount,
|
|
DroppedCount: hs.DroppedCount,
|
|
AvgProcessTimeMs: float64(hs.AvgProcessTime.Microseconds()) / microsecondsPerMillisecond,
|
|
MinProcessTimeMs: float64(hs.MinProcessTime.Microseconds()) / microsecondsPerMillisecond,
|
|
MaxProcessTimeMs: float64(hs.MaxProcessTime.Microseconds()) / microsecondsPerMillisecond,
|
|
})
|
|
}
|
|
|
|
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,
|
|
DatabaseSizeBytes: dbStats.FileSizeBytes,
|
|
LiveRoutes: dbStats.LiveRoutes,
|
|
IPv4Routes: rtStats.IPv4Routes,
|
|
IPv6Routes: rtStats.IPv6Routes,
|
|
IPv4UpdatesPerSec: rtStats.IPv4UpdatesRate,
|
|
IPv6UpdatesPerSec: rtStats.IPv6UpdatesRate,
|
|
HandlerStats: handlerStatsInfo,
|
|
IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
|
|
IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
response := map[string]interface{}{
|
|
"status": "ok",
|
|
"data": stats,
|
|
}
|
|
if err := json.NewEncoder(w).Encode(response); 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)
|
|
}
|
|
}
|
|
}
|