Refactor server package: split handlers and routes into separate files
- Move all handler functions to handlers.go - Move setupRoutes to routes.go - Clean up server.go to only contain core server logic - Add missing GetASDetails and GetPrefixDetails to mockStore for tests - Fix linter errors (magic numbers, unused parameters, blank lines)
This commit is contained in:
parent
2fc24bb937
commit
27ae80ea2e
@ -743,3 +743,101 @@ func CalculateIPv4Range(cidr string) (start, end uint32, err error) {
|
|||||||
|
|
||||||
return start, end, nil
|
return start, end, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetASDetails returns detailed information about an AS including prefixes
|
||||||
|
func (d *Database) GetASDetails(asn int) (*ASN, []LiveRoute, error) {
|
||||||
|
// Get AS information
|
||||||
|
var asnInfo ASN
|
||||||
|
var idStr string
|
||||||
|
var handle, description sql.NullString
|
||||||
|
err := d.db.QueryRow(
|
||||||
|
"SELECT id, number, handle, description, first_seen, last_seen FROM asns WHERE number = ?",
|
||||||
|
asn,
|
||||||
|
).Scan(&idStr, &asnInfo.Number, &handle, &description, &asnInfo.FirstSeen, &asnInfo.LastSeen)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil, fmt.Errorf("%w: AS%d", ErrNoRoute, asn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, fmt.Errorf("failed to query AS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
asnInfo.ID, _ = uuid.Parse(idStr)
|
||||||
|
asnInfo.Handle = handle.String
|
||||||
|
asnInfo.Description = description.String
|
||||||
|
|
||||||
|
// Get prefixes announced by this AS
|
||||||
|
query := `
|
||||||
|
SELECT DISTINCT prefix, mask_length, ip_version, last_updated
|
||||||
|
FROM live_routes
|
||||||
|
WHERE origin_asn = ?
|
||||||
|
ORDER BY ip_version, mask_length, prefix
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := d.db.Query(query, asn)
|
||||||
|
if err != nil {
|
||||||
|
return &asnInfo, nil, fmt.Errorf("failed to query prefixes: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var prefixes []LiveRoute
|
||||||
|
for rows.Next() {
|
||||||
|
var route LiveRoute
|
||||||
|
err := rows.Scan(&route.Prefix, &route.MaskLength, &route.IPVersion, &route.LastUpdated)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
route.OriginASN = asn
|
||||||
|
prefixes = append(prefixes, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &asnInfo, prefixes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefixDetails returns detailed information about a prefix
|
||||||
|
func (d *Database) GetPrefixDetails(prefix string) ([]LiveRoute, error) {
|
||||||
|
query := `
|
||||||
|
SELECT lr.origin_asn, lr.peer_ip, lr.as_path, lr.next_hop, lr.last_updated,
|
||||||
|
a.handle, a.description
|
||||||
|
FROM live_routes lr
|
||||||
|
LEFT JOIN asns a ON a.number = lr.origin_asn
|
||||||
|
WHERE lr.prefix = ?
|
||||||
|
ORDER BY lr.origin_asn, lr.peer_ip
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := d.db.Query(query, prefix)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query prefix details: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var routes []LiveRoute
|
||||||
|
for rows.Next() {
|
||||||
|
var route LiveRoute
|
||||||
|
var pathJSON string
|
||||||
|
var handle, description sql.NullString
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&route.OriginASN, &route.PeerIP, &pathJSON, &route.NextHop,
|
||||||
|
&route.LastUpdated, &handle, &description,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode AS path
|
||||||
|
if err := json.Unmarshal([]byte(pathJSON), &route.ASPath); err != nil {
|
||||||
|
route.ASPath = []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
route.Prefix = prefix
|
||||||
|
routes = append(routes, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(routes) == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrNoRoute, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes, nil
|
||||||
|
}
|
||||||
|
@ -47,6 +47,10 @@ type Store interface {
|
|||||||
// IP lookup operations
|
// IP lookup operations
|
||||||
GetASInfoForIP(ip string) (*ASInfo, error)
|
GetASInfoForIP(ip string) (*ASInfo, error)
|
||||||
|
|
||||||
|
// AS and prefix detail operations
|
||||||
|
GetASDetails(asn int) (*ASN, []LiveRoute, error)
|
||||||
|
GetPrefixDetails(prefix string) ([]LiveRoute, error)
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
@ -201,6 +201,26 @@ func (m *mockStore) GetASInfoForIP(ip string) (*database.ASInfo, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetASDetails mock implementation
|
||||||
|
func (m *mockStore) GetASDetails(asn int) (*database.ASN, []database.LiveRoute, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
// Check if ASN exists
|
||||||
|
if asnInfo, exists := m.ASNs[asn]; exists {
|
||||||
|
// Return empty prefixes for now
|
||||||
|
return asnInfo, []database.LiveRoute{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, database.ErrNoRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefixDetails mock implementation
|
||||||
|
func (m *mockStore) GetPrefixDetails(prefix string) ([]database.LiveRoute, error) {
|
||||||
|
// Return empty routes for now
|
||||||
|
return []database.LiveRoute{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestRouteWatchLiveFeed(t *testing.T) {
|
func TestRouteWatchLiveFeed(t *testing.T) {
|
||||||
|
|
||||||
// Create mock database
|
// Create mock database
|
||||||
|
473
internal/server/handlers.go
Normal file
473
internal/server/handlers.go
Normal file
@ -0,0 +1,473 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||||
|
"git.eeqj.de/sneak/routewatch/internal/templates"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONError writes a standardized JSON error response
|
||||||
|
func writeJSONError(w http.ResponseWriter, statusCode int, message string) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "error",
|
||||||
|
"error": map[string]interface{}{
|
||||||
|
"msg": message,
|
||||||
|
"code": statusCode,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONSuccess writes a standardized JSON success response
|
||||||
|
func writeJSONSuccess(w http.ResponseWriter, data interface{}) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "ok",
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
GoVersion string `json:"go_version"`
|
||||||
|
Goroutines int `json:"goroutines"`
|
||||||
|
MemoryUsage string `json:"memory_usage"`
|
||||||
|
ASNs int `json:"asns"`
|
||||||
|
Prefixes int `json:"prefixes"`
|
||||||
|
IPv4Prefixes int `json:"ipv4_prefixes"`
|
||||||
|
IPv6Prefixes int `json:"ipv6_prefixes"`
|
||||||
|
Peerings int `json:"peerings"`
|
||||||
|
Peers int `json:"peers"`
|
||||||
|
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")
|
||||||
|
writeJSONError(w, http.StatusRequestTimeout, "Database timeout")
|
||||||
|
|
||||||
|
return
|
||||||
|
case err := <-errChan:
|
||||||
|
s.logger.Error("Failed to get database stats", "error", err)
|
||||||
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
|
||||||
|
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 route counts from database
|
||||||
|
ipv4Routes, ipv6Routes, err := s.db.GetLiveRouteCounts()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to get live route counts", "error", err)
|
||||||
|
// Continue with zero counts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get route update metrics
|
||||||
|
routeMetrics := s.streamer.GetMetricsTracker().GetRouteMetrics()
|
||||||
|
|
||||||
|
// Get memory stats
|
||||||
|
var memStats runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&memStats)
|
||||||
|
|
||||||
|
stats := Stats{
|
||||||
|
Uptime: uptime,
|
||||||
|
TotalMessages: metrics.TotalMessages,
|
||||||
|
TotalBytes: metrics.TotalBytes,
|
||||||
|
MessagesPerSec: metrics.MessagesPerSec,
|
||||||
|
MbitsPerSec: metrics.BitsPerSec / bitsPerMegabit,
|
||||||
|
Connected: metrics.Connected,
|
||||||
|
GoVersion: runtime.Version(),
|
||||||
|
Goroutines: runtime.NumGoroutine(),
|
||||||
|
MemoryUsage: humanize.Bytes(memStats.Alloc),
|
||||||
|
ASNs: dbStats.ASNs,
|
||||||
|
Prefixes: dbStats.Prefixes,
|
||||||
|
IPv4Prefixes: dbStats.IPv4Prefixes,
|
||||||
|
IPv6Prefixes: dbStats.IPv6Prefixes,
|
||||||
|
Peerings: dbStats.Peerings,
|
||||||
|
Peers: dbStats.Peers,
|
||||||
|
DatabaseSizeBytes: dbStats.FileSizeBytes,
|
||||||
|
LiveRoutes: dbStats.LiveRoutes,
|
||||||
|
IPv4Routes: ipv4Routes,
|
||||||
|
IPv6Routes: ipv6Routes,
|
||||||
|
IPv4UpdatesPerSec: routeMetrics.IPv4UpdatesPerSec,
|
||||||
|
IPv6UpdatesPerSec: routeMetrics.IPv6UpdatesPerSec,
|
||||||
|
IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
|
||||||
|
IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeJSONSuccess(w, 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 {
|
||||||
|
// 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"`
|
||||||
|
GoVersion string `json:"go_version"`
|
||||||
|
Goroutines int `json:"goroutines"`
|
||||||
|
MemoryUsage string `json:"memory_usage"`
|
||||||
|
ASNs int `json:"asns"`
|
||||||
|
Prefixes int `json:"prefixes"`
|
||||||
|
IPv4Prefixes int `json:"ipv4_prefixes"`
|
||||||
|
IPv6Prefixes int `json:"ipv6_prefixes"`
|
||||||
|
Peerings int `json:"peerings"`
|
||||||
|
Peers int `json:"peers"`
|
||||||
|
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 route counts from database
|
||||||
|
ipv4Routes, ipv6Routes, err := s.db.GetLiveRouteCounts()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to get live route counts", "error", err)
|
||||||
|
// Continue with zero counts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get route update metrics
|
||||||
|
routeMetrics := s.streamer.GetMetricsTracker().GetRouteMetrics()
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get memory stats
|
||||||
|
var memStats runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&memStats)
|
||||||
|
|
||||||
|
stats := StatsResponse{
|
||||||
|
Uptime: uptime,
|
||||||
|
TotalMessages: metrics.TotalMessages,
|
||||||
|
TotalBytes: metrics.TotalBytes,
|
||||||
|
MessagesPerSec: metrics.MessagesPerSec,
|
||||||
|
MbitsPerSec: metrics.BitsPerSec / bitsPerMegabit,
|
||||||
|
Connected: metrics.Connected,
|
||||||
|
GoVersion: runtime.Version(),
|
||||||
|
Goroutines: runtime.NumGoroutine(),
|
||||||
|
MemoryUsage: humanize.Bytes(memStats.Alloc),
|
||||||
|
ASNs: dbStats.ASNs,
|
||||||
|
Prefixes: dbStats.Prefixes,
|
||||||
|
IPv4Prefixes: dbStats.IPv4Prefixes,
|
||||||
|
IPv6Prefixes: dbStats.IPv6Prefixes,
|
||||||
|
Peerings: dbStats.Peerings,
|
||||||
|
Peers: dbStats.Peers,
|
||||||
|
DatabaseSizeBytes: dbStats.FileSizeBytes,
|
||||||
|
LiveRoutes: dbStats.LiveRoutes,
|
||||||
|
IPv4Routes: ipv4Routes,
|
||||||
|
IPv6Routes: ipv6Routes,
|
||||||
|
IPv4UpdatesPerSec: routeMetrics.IPv4UpdatesPerSec,
|
||||||
|
IPv6UpdatesPerSec: routeMetrics.IPv6UpdatesPerSec,
|
||||||
|
HandlerStats: handlerStatsInfo,
|
||||||
|
IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
|
||||||
|
IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeJSONSuccess(w, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleIPLookup returns a handler that looks up AS information for an IP address
|
||||||
|
func (s *Server) handleIPLookup() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ip := chi.URLParam(r, "ip")
|
||||||
|
if ip == "" {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "IP parameter is required")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up AS information for the IP
|
||||||
|
asInfo, err := s.db.GetASInfoForIP(ip)
|
||||||
|
if err != nil {
|
||||||
|
// Check if it's an invalid IP error
|
||||||
|
if errors.Is(err, database.ErrInvalidIP) {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, err.Error())
|
||||||
|
} else {
|
||||||
|
// All other errors (including ErrNoRoute) are 404
|
||||||
|
writeJSONError(w, http.StatusNotFound, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return successful response
|
||||||
|
if err := writeJSONSuccess(w, asInfo); err != nil {
|
||||||
|
s.logger.Error("Failed to encode AS info", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleASDetailJSON returns AS details as JSON
|
||||||
|
func (s *Server) handleASDetailJSON() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
asnStr := chi.URLParam(r, "asn")
|
||||||
|
asn, err := strconv.Atoi(asnStr)
|
||||||
|
if err != nil {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "Invalid ASN")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
asInfo, prefixes, err := s.db.GetASDetails(asn)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, database.ErrNoRoute) {
|
||||||
|
writeJSONError(w, http.StatusNotFound, err.Error())
|
||||||
|
} else {
|
||||||
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group prefixes by IP version
|
||||||
|
const ipVersionV4 = 4
|
||||||
|
var ipv4Prefixes, ipv6Prefixes []database.LiveRoute
|
||||||
|
for _, p := range prefixes {
|
||||||
|
if p.IPVersion == ipVersionV4 {
|
||||||
|
ipv4Prefixes = append(ipv4Prefixes, p)
|
||||||
|
} else {
|
||||||
|
ipv6Prefixes = append(ipv6Prefixes, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"asn": asInfo,
|
||||||
|
"ipv4_prefixes": ipv4Prefixes,
|
||||||
|
"ipv6_prefixes": ipv6Prefixes,
|
||||||
|
"total_count": len(prefixes),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeJSONSuccess(w, response); err != nil {
|
||||||
|
s.logger.Error("Failed to encode AS details", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePrefixDetailJSON returns prefix details as JSON
|
||||||
|
func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
prefix := chi.URLParam(r, "prefix")
|
||||||
|
if prefix == "" {
|
||||||
|
writeJSONError(w, http.StatusBadRequest, "Prefix parameter is required")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err := s.db.GetPrefixDetails(prefix)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, database.ErrNoRoute) {
|
||||||
|
writeJSONError(w, http.StatusNotFound, err.Error())
|
||||||
|
} else {
|
||||||
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by origin AS
|
||||||
|
originMap := make(map[int][]database.LiveRoute)
|
||||||
|
for _, route := range routes {
|
||||||
|
originMap[route.OriginASN] = append(originMap[route.OriginASN], route)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"prefix": prefix,
|
||||||
|
"routes": routes,
|
||||||
|
"origins": originMap,
|
||||||
|
"peer_count": len(routes),
|
||||||
|
"origin_count": len(originMap),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeJSONSuccess(w, response); err != nil {
|
||||||
|
s.logger.Error("Failed to encode prefix details", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleASDetail returns a handler that serves the AS detail HTML page
|
||||||
|
func (s *Server) handleASDetail() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
// TODO: Implement AS detail HTML page
|
||||||
|
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePrefixDetail returns a handler that serves the prefix detail HTML page
|
||||||
|
func (s *Server) handlePrefixDetail() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
// TODO: Implement prefix detail HTML page
|
||||||
|
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
}
|
41
internal/server/routes.go
Normal file
41
internal/server/routes.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
|
||||||
|
// AS and prefix detail pages
|
||||||
|
r.Get("/as/{asn}", s.handleASDetail())
|
||||||
|
r.Get("/prefix/{prefix}", s.handlePrefixDetail())
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
r.Route("/api/v1", func(r chi.Router) {
|
||||||
|
r.Get("/stats", s.handleStats())
|
||||||
|
r.Get("/ip/{ip}", s.handleIPLookup())
|
||||||
|
r.Get("/as/{asn}", s.handleASDetailJSON())
|
||||||
|
r.Get("/prefix/{prefix}", s.handlePrefixDetailJSON())
|
||||||
|
})
|
||||||
|
|
||||||
|
s.router = r
|
||||||
|
}
|
@ -3,20 +3,14 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||||
"git.eeqj.de/sneak/routewatch/internal/logger"
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
||||||
"git.eeqj.de/sneak/routewatch/internal/streamer"
|
"git.eeqj.de/sneak/routewatch/internal/streamer"
|
||||||
"git.eeqj.de/sneak/routewatch/internal/templates"
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server provides HTTP endpoints for status monitoring
|
// Server provides HTTP endpoints for status monitoring
|
||||||
@ -41,33 +35,6 @@ func New(db database.Store, streamer *streamer.Streamer, logger *logger.Logger)
|
|||||||
return s
|
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())
|
|
||||||
r.Get("/ip/{ip}", s.handleIPLookup())
|
|
||||||
})
|
|
||||||
|
|
||||||
s.router = r
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the HTTP server
|
// Start starts the HTTP server
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
@ -103,357 +70,3 @@ func (s *Server) Stop(ctx context.Context) error {
|
|||||||
|
|
||||||
return s.srv.Shutdown(ctx)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeJSONError writes a standardized JSON error response
|
|
||||||
func writeJSONError(w http.ResponseWriter, statusCode int, message string) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(statusCode)
|
|
||||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
||||||
"status": "error",
|
|
||||||
"error": map[string]interface{}{
|
|
||||||
"msg": message,
|
|
||||||
"code": statusCode,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeJSONSuccess writes a standardized JSON success response
|
|
||||||
func writeJSONSuccess(w http.ResponseWriter, data interface{}) error {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
return json.NewEncoder(w).Encode(map[string]interface{}{
|
|
||||||
"status": "ok",
|
|
||||||
"data": data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
GoVersion string `json:"go_version"`
|
|
||||||
Goroutines int `json:"goroutines"`
|
|
||||||
MemoryUsage string `json:"memory_usage"`
|
|
||||||
ASNs int `json:"asns"`
|
|
||||||
Prefixes int `json:"prefixes"`
|
|
||||||
IPv4Prefixes int `json:"ipv4_prefixes"`
|
|
||||||
IPv6Prefixes int `json:"ipv6_prefixes"`
|
|
||||||
Peerings int `json:"peerings"`
|
|
||||||
Peers int `json:"peers"`
|
|
||||||
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")
|
|
||||||
writeJSONError(w, http.StatusRequestTimeout, "Database timeout")
|
|
||||||
|
|
||||||
return
|
|
||||||
case err := <-errChan:
|
|
||||||
s.logger.Error("Failed to get database stats", "error", err)
|
|
||||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
||||||
|
|
||||||
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 route counts from database
|
|
||||||
ipv4Routes, ipv6Routes, err := s.db.GetLiveRouteCounts()
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Warn("Failed to get live route counts", "error", err)
|
|
||||||
// Continue with zero counts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get route update metrics
|
|
||||||
routeMetrics := s.streamer.GetMetricsTracker().GetRouteMetrics()
|
|
||||||
|
|
||||||
// Get memory stats
|
|
||||||
var memStats runtime.MemStats
|
|
||||||
runtime.ReadMemStats(&memStats)
|
|
||||||
|
|
||||||
stats := Stats{
|
|
||||||
Uptime: uptime,
|
|
||||||
TotalMessages: metrics.TotalMessages,
|
|
||||||
TotalBytes: metrics.TotalBytes,
|
|
||||||
MessagesPerSec: metrics.MessagesPerSec,
|
|
||||||
MbitsPerSec: metrics.BitsPerSec / bitsPerMegabit,
|
|
||||||
Connected: metrics.Connected,
|
|
||||||
GoVersion: runtime.Version(),
|
|
||||||
Goroutines: runtime.NumGoroutine(),
|
|
||||||
MemoryUsage: humanize.Bytes(memStats.Alloc),
|
|
||||||
ASNs: dbStats.ASNs,
|
|
||||||
Prefixes: dbStats.Prefixes,
|
|
||||||
IPv4Prefixes: dbStats.IPv4Prefixes,
|
|
||||||
IPv6Prefixes: dbStats.IPv6Prefixes,
|
|
||||||
Peerings: dbStats.Peerings,
|
|
||||||
Peers: dbStats.Peers,
|
|
||||||
DatabaseSizeBytes: dbStats.FileSizeBytes,
|
|
||||||
LiveRoutes: dbStats.LiveRoutes,
|
|
||||||
IPv4Routes: ipv4Routes,
|
|
||||||
IPv6Routes: ipv6Routes,
|
|
||||||
IPv4UpdatesPerSec: routeMetrics.IPv4UpdatesPerSec,
|
|
||||||
IPv6UpdatesPerSec: routeMetrics.IPv6UpdatesPerSec,
|
|
||||||
IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
|
|
||||||
IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writeJSONSuccess(w, 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 {
|
|
||||||
// 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"`
|
|
||||||
GoVersion string `json:"go_version"`
|
|
||||||
Goroutines int `json:"goroutines"`
|
|
||||||
MemoryUsage string `json:"memory_usage"`
|
|
||||||
ASNs int `json:"asns"`
|
|
||||||
Prefixes int `json:"prefixes"`
|
|
||||||
IPv4Prefixes int `json:"ipv4_prefixes"`
|
|
||||||
IPv6Prefixes int `json:"ipv6_prefixes"`
|
|
||||||
Peerings int `json:"peerings"`
|
|
||||||
Peers int `json:"peers"`
|
|
||||||
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 route counts from database
|
|
||||||
ipv4Routes, ipv6Routes, err := s.db.GetLiveRouteCounts()
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Warn("Failed to get live route counts", "error", err)
|
|
||||||
// Continue with zero counts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get route update metrics
|
|
||||||
routeMetrics := s.streamer.GetMetricsTracker().GetRouteMetrics()
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get memory stats
|
|
||||||
var memStats runtime.MemStats
|
|
||||||
runtime.ReadMemStats(&memStats)
|
|
||||||
|
|
||||||
stats := StatsResponse{
|
|
||||||
Uptime: uptime,
|
|
||||||
TotalMessages: metrics.TotalMessages,
|
|
||||||
TotalBytes: metrics.TotalBytes,
|
|
||||||
MessagesPerSec: metrics.MessagesPerSec,
|
|
||||||
MbitsPerSec: metrics.BitsPerSec / bitsPerMegabit,
|
|
||||||
Connected: metrics.Connected,
|
|
||||||
GoVersion: runtime.Version(),
|
|
||||||
Goroutines: runtime.NumGoroutine(),
|
|
||||||
MemoryUsage: humanize.Bytes(memStats.Alloc),
|
|
||||||
ASNs: dbStats.ASNs,
|
|
||||||
Prefixes: dbStats.Prefixes,
|
|
||||||
IPv4Prefixes: dbStats.IPv4Prefixes,
|
|
||||||
IPv6Prefixes: dbStats.IPv6Prefixes,
|
|
||||||
Peerings: dbStats.Peerings,
|
|
||||||
Peers: dbStats.Peers,
|
|
||||||
DatabaseSizeBytes: dbStats.FileSizeBytes,
|
|
||||||
LiveRoutes: dbStats.LiveRoutes,
|
|
||||||
IPv4Routes: ipv4Routes,
|
|
||||||
IPv6Routes: ipv6Routes,
|
|
||||||
IPv4UpdatesPerSec: routeMetrics.IPv4UpdatesPerSec,
|
|
||||||
IPv6UpdatesPerSec: routeMetrics.IPv6UpdatesPerSec,
|
|
||||||
HandlerStats: handlerStatsInfo,
|
|
||||||
IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
|
|
||||||
IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writeJSONSuccess(w, 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleIPLookup returns a handler that looks up AS information for an IP address
|
|
||||||
func (s *Server) handleIPLookup() http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ip := chi.URLParam(r, "ip")
|
|
||||||
if ip == "" {
|
|
||||||
writeJSONError(w, http.StatusBadRequest, "IP parameter is required")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look up AS information for the IP
|
|
||||||
asInfo, err := s.db.GetASInfoForIP(ip)
|
|
||||||
if err != nil {
|
|
||||||
// Check if it's an invalid IP error
|
|
||||||
if errors.Is(err, database.ErrInvalidIP) {
|
|
||||||
writeJSONError(w, http.StatusBadRequest, err.Error())
|
|
||||||
} else {
|
|
||||||
// All other errors (including ErrNoRoute) are 404
|
|
||||||
writeJSONError(w, http.StatusNotFound, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return successful response
|
|
||||||
if err := writeJSONSuccess(w, asInfo); err != nil {
|
|
||||||
s.logger.Error("Failed to encode AS info", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user