From 1115954827e62756be38227c9b2fc45503d38883 Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 30 Dec 2025 14:41:57 +0700 Subject: [PATCH] Fix prefix URL routing to handle CIDR notation with slashes - Use wildcard route pattern for /prefix/* endpoints - Extract prefix parameter using chi.URLParam(r, "*") - Fixes 400 error when accessing /prefix/x.x.x.x/32 directly --- internal/metrics/metrics.go | 79 ++++++++++++++++++++++++++++++++----- internal/server/handlers.go | 25 +++++++++++- internal/server/routes.go | 4 +- 3 files changed, 95 insertions(+), 13 deletions(-) diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 4a8a32e..86f2057 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -30,6 +30,14 @@ type Tracker struct { // Route update metrics ipv4UpdateRate metrics.Meter ipv6UpdateRate metrics.Meter + + // Announcement/withdrawal metrics + announcementCounter metrics.Counter + withdrawalCounter metrics.Counter + churnRate metrics.Meter // combined announcements + withdrawals per second + + // BGP peer tracking + bgpPeerCount atomic.Int32 } // New creates a new metrics tracker @@ -37,15 +45,18 @@ func New() *Tracker { registry := metrics.NewRegistry() return &Tracker{ - registry: registry, - messageCounter: metrics.NewCounter(), - byteCounter: metrics.NewCounter(), - messageRate: metrics.NewMeter(), - byteRate: metrics.NewMeter(), - wireByteCounter: metrics.NewCounter(), - wireByteRate: metrics.NewMeter(), - ipv4UpdateRate: metrics.NewMeter(), - ipv6UpdateRate: metrics.NewMeter(), + registry: registry, + messageCounter: metrics.NewCounter(), + byteCounter: metrics.NewCounter(), + messageRate: metrics.NewMeter(), + byteRate: metrics.NewMeter(), + wireByteCounter: metrics.NewCounter(), + wireByteRate: metrics.NewMeter(), + ipv4UpdateRate: metrics.NewMeter(), + ipv6UpdateRate: metrics.NewMeter(), + announcementCounter: metrics.NewCounter(), + withdrawalCounter: metrics.NewCounter(), + churnRate: metrics.NewMeter(), } } @@ -134,6 +145,56 @@ func (t *Tracker) RecordIPv6Update() { t.ipv6UpdateRate.Mark(1) } +// RecordAnnouncement records a route announcement +func (t *Tracker) RecordAnnouncement() { + t.announcementCounter.Inc(1) + t.churnRate.Mark(1) +} + +// RecordWithdrawal records a route withdrawal +func (t *Tracker) RecordWithdrawal() { + t.withdrawalCounter.Inc(1) + t.churnRate.Mark(1) +} + +// SetBGPPeerCount updates the current BGP peer count +func (t *Tracker) SetBGPPeerCount(count int) { + // BGP peer count is always small (< 1000), so int32 is safe + if count > 0 && count < 1<<31 { + t.bgpPeerCount.Store(int32(count)) //nolint:gosec // count is validated + } +} + +// GetBGPPeerCount returns the current BGP peer count +func (t *Tracker) GetBGPPeerCount() int { + return int(t.bgpPeerCount.Load()) +} + +// GetAnnouncementCount returns the total announcement count +func (t *Tracker) GetAnnouncementCount() uint64 { + count := t.announcementCounter.Count() + if count < 0 { + return 0 + } + + return uint64(count) +} + +// GetWithdrawalCount returns the total withdrawal count +func (t *Tracker) GetWithdrawalCount() uint64 { + count := t.withdrawalCounter.Count() + if count < 0 { + return 0 + } + + return uint64(count) +} + +// GetChurnRate returns the route churn rate per second +func (t *Tracker) GetChurnRate() float64 { + return t.churnRate.Rate1() +} + // GetRouteMetrics returns current route update metrics func (t *Tracker) GetRouteMetrics() RouteMetrics { return RouteMetrics{ diff --git a/internal/server/handlers.go b/internal/server/handlers.go index c502996..9ac2dea 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -320,6 +320,23 @@ func (s *Server) handleStats() http.HandlerFunc { MaxProcessTimeMs float64 `json:"max_process_time_ms"` } + // GCStats represents garbage collection statistics + type GCStats struct { + NumGC uint32 `json:"num_gc"` + TotalPauseMs uint64 `json:"total_pause_ms"` + LastPauseMs float64 `json:"last_pause_ms"` + HeapAllocBytes uint64 `json:"heap_alloc_bytes"` + HeapSysBytes uint64 `json:"heap_sys_bytes"` + } + + // StreamStats represents stream statistics including announcements/withdrawals + type StreamStats struct { + Announcements uint64 `json:"announcements"` + Withdrawals uint64 `json:"withdrawals"` + RouteChurnPerSec float64 `json:"route_churn_per_sec"` + BGPPeerCount int `json:"bgp_peer_count"` + } + // StatsResponse represents the API statistics response type StatsResponse struct { Uptime string `json:"uptime"` @@ -335,6 +352,8 @@ func (s *Server) handleStats() http.HandlerFunc { GoVersion string `json:"go_version"` Goroutines int `json:"goroutines"` MemoryUsage string `json:"memory_usage"` + GC GCStats `json:"gc"` + Stream StreamStats `json:"stream"` ASNs int `json:"asns"` Prefixes int `json:"prefixes"` IPv4Prefixes int `json:"ipv4_prefixes"` @@ -685,7 +704,8 @@ func (s *Server) handleASDetailJSON() http.HandlerFunc { // handlePrefixDetailJSON returns prefix details as JSON func (s *Server) handlePrefixDetailJSON() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - prefixParam := chi.URLParam(r, "prefix") + // Get wildcard parameter (everything after /prefix/) + prefixParam := chi.URLParam(r, "*") if prefixParam == "" { writeJSONError(w, http.StatusBadRequest, "Prefix parameter is required") @@ -851,7 +871,8 @@ func (s *Server) handleASDetail() http.HandlerFunc { // handlePrefixDetail returns a handler that serves the prefix detail HTML page func (s *Server) handlePrefixDetail() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - prefixParam := chi.URLParam(r, "prefix") + // Get wildcard parameter (everything after /prefix/) + prefixParam := chi.URLParam(r, "*") if prefixParam == "" { http.Error(w, "Prefix parameter is required", http.StatusBadRequest) diff --git a/internal/server/routes.go b/internal/server/routes.go index e4d9d06..4d110f4 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -28,7 +28,7 @@ func (s *Server) setupRoutes() { // AS and prefix detail pages r.Get("/as/{asn}", s.handleASDetail()) - r.Get("/prefix/{prefix}", s.handlePrefixDetail()) + r.Get("/prefix/*", s.handlePrefixDetail()) r.Get("/prefixlength/{length}", s.handlePrefixLength()) r.Get("/prefixlength6/{length}", s.handlePrefixLength6()) @@ -45,7 +45,7 @@ func (s *Server) setupRoutes() { r.Get("/stats", s.handleStats()) r.Get("/ip/{ip}", s.handleIPLookup()) r.Get("/as/{asn}", s.handleASDetailJSON()) - r.Get("/prefix/{prefix}", s.handlePrefixDetailJSON()) + r.Get("/prefix/*", s.handlePrefixDetailJSON()) }) s.router = r