diff --git a/Makefile b/Makefile index c8d1a8f..e41946b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,11 @@ export DEBUG = routewatch +# Git revision for version embedding +GIT_REVISION := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown") +GIT_REVISION_SHORT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") +VERSION_PKG := git.eeqj.de/sneak/routewatch/internal/version +LDFLAGS := -X $(VERSION_PKG).GitRevision=$(GIT_REVISION) -X $(VERSION_PKG).GitRevisionShort=$(GIT_REVISION_SHORT) + .PHONY: test fmt lint build clean run asupdate all: test @@ -15,7 +21,7 @@ lint: golangci-lint run build: - CGO_ENABLED=1 go build -o bin/routewatch cmd/routewatch/main.go + CGO_ENABLED=1 go build -ldflags "$(LDFLAGS)" -o bin/routewatch cmd/routewatch/main.go clean: rm -rf bin/ diff --git a/internal/routewatch/prefixhandler.go b/internal/routewatch/prefixhandler.go index ebc5337..4223b20 100644 --- a/internal/routewatch/prefixhandler.go +++ b/internal/routewatch/prefixhandler.go @@ -113,6 +113,10 @@ func (h *PrefixHandler) HandleMessage(msg *ristypes.RISMessage) { timestamp: timestamp, path: msg.Path, }) + // Record announcement in metrics + if h.metrics != nil { + h.metrics.RecordAnnouncement() + } } } @@ -126,6 +130,10 @@ func (h *PrefixHandler) HandleMessage(msg *ristypes.RISMessage) { timestamp: timestamp, path: msg.Path, }) + // Record withdrawal in metrics + if h.metrics != nil { + h.metrics.RecordWithdrawal() + } } // Check if we need to flush diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 9ac2dea..59a9720 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -469,6 +469,25 @@ func (s *Server) handleStats() http.HandlerFunc { connectionDuration = time.Since(metrics.ConnectedSince).Truncate(time.Second).String() } + // Get announcement/withdrawal stats from metrics tracker + metricsTracker := s.streamer.GetMetricsTracker() + announcements := metricsTracker.GetAnnouncementCount() + withdrawals := metricsTracker.GetWithdrawalCount() + churnRate := metricsTracker.GetChurnRate() + bgpPeerCount := metricsTracker.GetBGPPeerCount() + + // Calculate last GC pause + const ( + nanosecondsPerMillisecond = 1e6 + gcPauseHistorySize = 256 // Size of runtime.MemStats.PauseNs circular buffer + ) + var lastPauseMs float64 + if memStats.NumGC > 0 { + // PauseNs is a circular buffer, get the most recent pause + lastPauseIdx := (memStats.NumGC + gcPauseHistorySize - 1) % gcPauseHistorySize + lastPauseMs = float64(memStats.PauseNs[lastPauseIdx]) / nanosecondsPerMillisecond + } + stats := StatsResponse{ Uptime: uptime, TotalMessages: metrics.TotalMessages, @@ -483,6 +502,19 @@ func (s *Server) handleStats() http.HandlerFunc { GoVersion: runtime.Version(), Goroutines: runtime.NumGoroutine(), MemoryUsage: humanize.Bytes(memStats.Alloc), + GC: GCStats{ + NumGC: memStats.NumGC, + TotalPauseMs: memStats.PauseTotalNs / uint64(nanosecondsPerMillisecond), + LastPauseMs: lastPauseMs, + HeapAllocBytes: memStats.HeapAlloc, + HeapSysBytes: memStats.HeapSys, + }, + Stream: StreamStats{ + Announcements: announcements, + Withdrawals: withdrawals, + RouteChurnPerSec: churnRate, + BGPPeerCount: bgpPeerCount, + }, ASNs: dbStats.ASNs, Prefixes: dbStats.Prefixes, IPv4Prefixes: dbStats.IPv4Prefixes, diff --git a/internal/streamer/streamer.go b/internal/streamer/streamer.go index 3c1f48b..4814707 100644 --- a/internal/streamer/streamer.go +++ b/internal/streamer/streamer.go @@ -114,6 +114,8 @@ type Streamer struct { metrics *metrics.Tracker totalDropped uint64 // Total dropped messages across all handlers random *rand.Rand // Random number generator for backpressure drops + bgpPeers map[string]bool // Track active BGP peers by peer IP + bgpPeersMu sync.RWMutex // Protects bgpPeers map } // New creates a new Streamer instance configured to connect to the RIS Live API. @@ -132,7 +134,8 @@ func New(logger *logger.Logger, metrics *metrics.Tracker) *Streamer { handlers: make([]*handlerInfo, 0), metrics: metrics, //nolint:gosec // Non-cryptographic randomness is fine for backpressure - random: rand.New(rand.NewSource(time.Now().UnixNano())), + random: rand.New(rand.NewSource(time.Now().UnixNano())), + bgpPeers: make(map[string]bool), } } @@ -608,18 +611,32 @@ func (s *Streamer) stream(ctx context.Context) error { // BGP keepalive messages - silently process continue case "OPEN": - // BGP open messages + // BGP open messages - track peer as active + s.bgpPeersMu.Lock() + s.bgpPeers[msg.Peer] = true + peerCount := len(s.bgpPeers) + s.bgpPeersMu.Unlock() + s.metrics.SetBGPPeerCount(peerCount) + s.logger.Info("BGP session opened", "peer", msg.Peer, "peer_asn", msg.PeerASN, + "total_peers", peerCount, ) continue case "NOTIFICATION": - // BGP notification messages (errors) + // BGP notification messages (session closed) + s.bgpPeersMu.Lock() + delete(s.bgpPeers, msg.Peer) + peerCount := len(s.bgpPeers) + s.bgpPeersMu.Unlock() + s.metrics.SetBGPPeerCount(peerCount) + s.logger.Warn("BGP notification", "peer", msg.Peer, "peer_asn", msg.PeerASN, + "total_peers", peerCount, ) continue diff --git a/internal/templates/status.html b/internal/templates/status.html index 5225c90..41b7f9c 100644 --- a/internal/templates/status.html +++ b/internal/templates/status.html @@ -72,6 +72,27 @@ border-radius: 4px; margin-top: 20px; } + .footer { + margin-top: 40px; + padding: 20px; + background: white; + border-radius: 8px; + box-shadow: 0 -2px 10px rgba(0,0,0,0.1); + text-align: center; + color: #666; + font-size: 14px; + } + .footer a { + color: #0066cc; + text-decoration: none; + } + .footer a:hover { + text-decoration: underline; + } + .footer .separator { + margin: 0 10px; + color: #ccc; + }
@@ -112,6 +133,10 @@ Reconnections - +