- Implement comprehensive SQL query logging for queries over 10ms - Add logging wrapper methods for all database operations - Replace timing code in GetStats with simple info log messages - Add missing database indexes for better query performance: - idx_live_routes_lookup for common prefix/origin/peer lookups - idx_live_routes_withdraw for withdrawal updates - idx_prefixes_prefix for prefix lookups - idx_asn_peerings_lookup for peering relationship queries - Increase SQLite cache size to 512MB - Add performance-oriented SQLite pragmas - Extract HTML templates to separate files using go:embed - Add JSON response middleware with @meta field (like bgpview.io API) - Fix concurrent map write errors in HTTP handlers - Add request timeout handling with proper JSON error responses These changes significantly improve database query performance and provide visibility into slow queries for debugging purposes.
329 lines
8.7 KiB
Go
329 lines
8.7 KiB
Go
// 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 = 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"`
|
|
LiveRoutes int `json:"live_routes"`
|
|
}
|
|
|
|
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() {
|
|
s.logger.Debug("Starting database stats query")
|
|
dbStats, err := s.db.GetStats()
|
|
if err != nil {
|
|
s.logger.Debug("Database stats query failed", "error", err)
|
|
errChan <- err
|
|
|
|
return
|
|
}
|
|
s.logger.Debug("Database stats query completed")
|
|
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
|
|
|
|
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")
|
|
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 {
|
|
// 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, 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() {
|
|
s.logger.Debug("Starting database stats query")
|
|
dbStats, err := s.db.GetStats()
|
|
if err != nil {
|
|
s.logger.Debug("Database stats query failed", "error", err)
|
|
errChan <- err
|
|
|
|
return
|
|
}
|
|
s.logger.Debug("Database stats query completed")
|
|
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
|
|
|
|
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")
|
|
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)
|
|
}
|
|
}
|
|
}
|