Compare commits
2 Commits
9043cf9bc0
...
c116b035bd
| Author | SHA1 | Date | |
|---|---|---|---|
| c116b035bd | |||
| 1115954827 |
8
Makefile
8
Makefile
@ -1,5 +1,11 @@
|
|||||||
export DEBUG = routewatch
|
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
|
.PHONY: test fmt lint build clean run asupdate
|
||||||
|
|
||||||
all: test
|
all: test
|
||||||
@ -15,7 +21,7 @@ lint:
|
|||||||
golangci-lint run
|
golangci-lint run
|
||||||
|
|
||||||
build:
|
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:
|
clean:
|
||||||
rm -rf bin/
|
rm -rf bin/
|
||||||
|
|||||||
@ -30,6 +30,14 @@ type Tracker struct {
|
|||||||
// Route update metrics
|
// Route update metrics
|
||||||
ipv4UpdateRate metrics.Meter
|
ipv4UpdateRate metrics.Meter
|
||||||
ipv6UpdateRate 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
|
// New creates a new metrics tracker
|
||||||
@ -46,6 +54,9 @@ func New() *Tracker {
|
|||||||
wireByteRate: metrics.NewMeter(),
|
wireByteRate: metrics.NewMeter(),
|
||||||
ipv4UpdateRate: metrics.NewMeter(),
|
ipv4UpdateRate: metrics.NewMeter(),
|
||||||
ipv6UpdateRate: 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)
|
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
|
// GetRouteMetrics returns current route update metrics
|
||||||
func (t *Tracker) GetRouteMetrics() RouteMetrics {
|
func (t *Tracker) GetRouteMetrics() RouteMetrics {
|
||||||
return RouteMetrics{
|
return RouteMetrics{
|
||||||
|
|||||||
@ -113,6 +113,10 @@ func (h *PrefixHandler) HandleMessage(msg *ristypes.RISMessage) {
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
path: msg.Path,
|
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,
|
timestamp: timestamp,
|
||||||
path: msg.Path,
|
path: msg.Path,
|
||||||
})
|
})
|
||||||
|
// Record withdrawal in metrics
|
||||||
|
if h.metrics != nil {
|
||||||
|
h.metrics.RecordWithdrawal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we need to flush
|
// Check if we need to flush
|
||||||
|
|||||||
@ -320,6 +320,23 @@ func (s *Server) handleStats() http.HandlerFunc {
|
|||||||
MaxProcessTimeMs float64 `json:"max_process_time_ms"`
|
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
|
// StatsResponse represents the API statistics response
|
||||||
type StatsResponse struct {
|
type StatsResponse struct {
|
||||||
Uptime string `json:"uptime"`
|
Uptime string `json:"uptime"`
|
||||||
@ -335,6 +352,8 @@ func (s *Server) handleStats() http.HandlerFunc {
|
|||||||
GoVersion string `json:"go_version"`
|
GoVersion string `json:"go_version"`
|
||||||
Goroutines int `json:"goroutines"`
|
Goroutines int `json:"goroutines"`
|
||||||
MemoryUsage string `json:"memory_usage"`
|
MemoryUsage string `json:"memory_usage"`
|
||||||
|
GC GCStats `json:"gc"`
|
||||||
|
Stream StreamStats `json:"stream"`
|
||||||
ASNs int `json:"asns"`
|
ASNs int `json:"asns"`
|
||||||
Prefixes int `json:"prefixes"`
|
Prefixes int `json:"prefixes"`
|
||||||
IPv4Prefixes int `json:"ipv4_prefixes"`
|
IPv4Prefixes int `json:"ipv4_prefixes"`
|
||||||
@ -450,6 +469,25 @@ func (s *Server) handleStats() http.HandlerFunc {
|
|||||||
connectionDuration = time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
|
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{
|
stats := StatsResponse{
|
||||||
Uptime: uptime,
|
Uptime: uptime,
|
||||||
TotalMessages: metrics.TotalMessages,
|
TotalMessages: metrics.TotalMessages,
|
||||||
@ -464,6 +502,19 @@ func (s *Server) handleStats() http.HandlerFunc {
|
|||||||
GoVersion: runtime.Version(),
|
GoVersion: runtime.Version(),
|
||||||
Goroutines: runtime.NumGoroutine(),
|
Goroutines: runtime.NumGoroutine(),
|
||||||
MemoryUsage: humanize.Bytes(memStats.Alloc),
|
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,
|
ASNs: dbStats.ASNs,
|
||||||
Prefixes: dbStats.Prefixes,
|
Prefixes: dbStats.Prefixes,
|
||||||
IPv4Prefixes: dbStats.IPv4Prefixes,
|
IPv4Prefixes: dbStats.IPv4Prefixes,
|
||||||
@ -685,7 +736,8 @@ func (s *Server) handleASDetailJSON() http.HandlerFunc {
|
|||||||
// handlePrefixDetailJSON returns prefix details as JSON
|
// handlePrefixDetailJSON returns prefix details as JSON
|
||||||
func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
|
func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
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 == "" {
|
if prefixParam == "" {
|
||||||
writeJSONError(w, http.StatusBadRequest, "Prefix parameter is required")
|
writeJSONError(w, http.StatusBadRequest, "Prefix parameter is required")
|
||||||
|
|
||||||
@ -851,7 +903,8 @@ func (s *Server) handleASDetail() http.HandlerFunc {
|
|||||||
// handlePrefixDetail returns a handler that serves the prefix detail HTML page
|
// handlePrefixDetail returns a handler that serves the prefix detail HTML page
|
||||||
func (s *Server) handlePrefixDetail() http.HandlerFunc {
|
func (s *Server) handlePrefixDetail() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
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 == "" {
|
if prefixParam == "" {
|
||||||
http.Error(w, "Prefix parameter is required", http.StatusBadRequest)
|
http.Error(w, "Prefix parameter is required", http.StatusBadRequest)
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ func (s *Server) setupRoutes() {
|
|||||||
|
|
||||||
// AS and prefix detail pages
|
// AS and prefix detail pages
|
||||||
r.Get("/as/{asn}", s.handleASDetail())
|
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("/prefixlength/{length}", s.handlePrefixLength())
|
||||||
r.Get("/prefixlength6/{length}", s.handlePrefixLength6())
|
r.Get("/prefixlength6/{length}", s.handlePrefixLength6())
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ func (s *Server) setupRoutes() {
|
|||||||
r.Get("/stats", s.handleStats())
|
r.Get("/stats", s.handleStats())
|
||||||
r.Get("/ip/{ip}", s.handleIPLookup())
|
r.Get("/ip/{ip}", s.handleIPLookup())
|
||||||
r.Get("/as/{asn}", s.handleASDetailJSON())
|
r.Get("/as/{asn}", s.handleASDetailJSON())
|
||||||
r.Get("/prefix/{prefix}", s.handlePrefixDetailJSON())
|
r.Get("/prefix/*", s.handlePrefixDetailJSON())
|
||||||
})
|
})
|
||||||
|
|
||||||
s.router = r
|
s.router = r
|
||||||
|
|||||||
@ -114,6 +114,8 @@ type Streamer struct {
|
|||||||
metrics *metrics.Tracker
|
metrics *metrics.Tracker
|
||||||
totalDropped uint64 // Total dropped messages across all handlers
|
totalDropped uint64 // Total dropped messages across all handlers
|
||||||
random *rand.Rand // Random number generator for backpressure drops
|
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.
|
// New creates a new Streamer instance configured to connect to the RIS Live API.
|
||||||
@ -133,6 +135,7 @@ func New(logger *logger.Logger, metrics *metrics.Tracker) *Streamer {
|
|||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
//nolint:gosec // Non-cryptographic randomness is fine for backpressure
|
//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
|
// BGP keepalive messages - silently process
|
||||||
continue
|
continue
|
||||||
case "OPEN":
|
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",
|
s.logger.Info("BGP session opened",
|
||||||
"peer", msg.Peer,
|
"peer", msg.Peer,
|
||||||
"peer_asn", msg.PeerASN,
|
"peer_asn", msg.PeerASN,
|
||||||
|
"total_peers", peerCount,
|
||||||
)
|
)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
case "NOTIFICATION":
|
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",
|
s.logger.Warn("BGP notification",
|
||||||
"peer", msg.Peer,
|
"peer", msg.Peer,
|
||||||
"peer_asn", msg.PeerASN,
|
"peer_asn", msg.PeerASN,
|
||||||
|
"total_peers", peerCount,
|
||||||
)
|
)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -72,6 +72,27 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-top: 20px;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -112,6 +133,10 @@
|
|||||||
<span class="metric-label">Reconnections</span>
|
<span class="metric-label">Reconnections</span>
|
||||||
<span class="metric-value" id="reconnect_count">-</span>
|
<span class="metric-value" id="reconnect_count">-</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">BGP Peers</span>
|
||||||
|
<span class="metric-value" id="bgp_peer_count">-</span>
|
||||||
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<span class="metric-label">Total Messages</span>
|
<span class="metric-label">Total Messages</span>
|
||||||
<span class="metric-value" id="total_messages">-</span>
|
<span class="metric-value" id="total_messages">-</span>
|
||||||
@ -120,6 +145,18 @@
|
|||||||
<span class="metric-label">Messages/sec</span>
|
<span class="metric-label">Messages/sec</span>
|
||||||
<span class="metric-value" id="messages_per_sec">-</span>
|
<span class="metric-value" id="messages_per_sec">-</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">Announcements</span>
|
||||||
|
<span class="metric-value" id="announcements">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">Withdrawals</span>
|
||||||
|
<span class="metric-value" id="withdrawals">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">Route Churn/sec</span>
|
||||||
|
<span class="metric-value" id="route_churn_per_sec">-</span>
|
||||||
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<span class="metric-label">Total Data</span>
|
<span class="metric-label">Total Data</span>
|
||||||
<span class="metric-value" id="total_wire_bytes">-</span>
|
<span class="metric-value" id="total_wire_bytes">-</span>
|
||||||
@ -130,6 +167,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="status-card">
|
||||||
|
<h2>GC Statistics</h2>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">GC Runs</span>
|
||||||
|
<span class="metric-value" id="gc_num">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">Total Pause</span>
|
||||||
|
<span class="metric-value" id="gc_total_pause">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">Last Pause</span>
|
||||||
|
<span class="metric-value" id="gc_last_pause">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">Heap Alloc</span>
|
||||||
|
<span class="metric-value" id="gc_heap_alloc">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">Heap Sys</span>
|
||||||
|
<span class="metric-value" id="gc_heap_sys">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="status-card">
|
<div class="status-card">
|
||||||
<h2>Database Statistics</h2>
|
<h2>Database Statistics</h2>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
@ -344,10 +405,19 @@
|
|||||||
document.getElementById('memory_usage').textContent = '-';
|
document.getElementById('memory_usage').textContent = '-';
|
||||||
document.getElementById('connection_duration').textContent = '-';
|
document.getElementById('connection_duration').textContent = '-';
|
||||||
document.getElementById('reconnect_count').textContent = '-';
|
document.getElementById('reconnect_count').textContent = '-';
|
||||||
|
document.getElementById('bgp_peer_count').textContent = '-';
|
||||||
document.getElementById('total_messages').textContent = '-';
|
document.getElementById('total_messages').textContent = '-';
|
||||||
document.getElementById('messages_per_sec').textContent = '-';
|
document.getElementById('messages_per_sec').textContent = '-';
|
||||||
|
document.getElementById('announcements').textContent = '-';
|
||||||
|
document.getElementById('withdrawals').textContent = '-';
|
||||||
|
document.getElementById('route_churn_per_sec').textContent = '-';
|
||||||
document.getElementById('total_wire_bytes').textContent = '-';
|
document.getElementById('total_wire_bytes').textContent = '-';
|
||||||
document.getElementById('wire_mbits_per_sec').textContent = '-';
|
document.getElementById('wire_mbits_per_sec').textContent = '-';
|
||||||
|
document.getElementById('gc_num').textContent = '-';
|
||||||
|
document.getElementById('gc_total_pause').textContent = '-';
|
||||||
|
document.getElementById('gc_last_pause').textContent = '-';
|
||||||
|
document.getElementById('gc_heap_alloc').textContent = '-';
|
||||||
|
document.getElementById('gc_heap_sys').textContent = '-';
|
||||||
document.getElementById('asns').textContent = '-';
|
document.getElementById('asns').textContent = '-';
|
||||||
document.getElementById('prefixes').textContent = '-';
|
document.getElementById('prefixes').textContent = '-';
|
||||||
document.getElementById('ipv4_prefixes').textContent = '-';
|
document.getElementById('ipv4_prefixes').textContent = '-';
|
||||||
@ -421,6 +491,23 @@
|
|||||||
document.getElementById('ipv4_updates_per_sec').textContent = data.ipv4_updates_per_sec.toFixed(1);
|
document.getElementById('ipv4_updates_per_sec').textContent = data.ipv4_updates_per_sec.toFixed(1);
|
||||||
document.getElementById('ipv6_updates_per_sec').textContent = data.ipv6_updates_per_sec.toFixed(1);
|
document.getElementById('ipv6_updates_per_sec').textContent = data.ipv6_updates_per_sec.toFixed(1);
|
||||||
|
|
||||||
|
// Update stream stats
|
||||||
|
if (data.stream) {
|
||||||
|
document.getElementById('bgp_peer_count').textContent = formatNumber(data.stream.bgp_peer_count);
|
||||||
|
document.getElementById('announcements').textContent = formatNumber(data.stream.announcements);
|
||||||
|
document.getElementById('withdrawals').textContent = formatNumber(data.stream.withdrawals);
|
||||||
|
document.getElementById('route_churn_per_sec').textContent = data.stream.route_churn_per_sec.toFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update GC stats
|
||||||
|
if (data.gc) {
|
||||||
|
document.getElementById('gc_num').textContent = formatNumber(data.gc.num_gc);
|
||||||
|
document.getElementById('gc_total_pause').textContent = data.gc.total_pause_ms + ' ms';
|
||||||
|
document.getElementById('gc_last_pause').textContent = data.gc.last_pause_ms.toFixed(3) + ' ms';
|
||||||
|
document.getElementById('gc_heap_alloc').textContent = formatBytes(data.gc.heap_alloc_bytes);
|
||||||
|
document.getElementById('gc_heap_sys').textContent = formatBytes(data.gc.heap_sys_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
// Update WHOIS stats
|
// Update WHOIS stats
|
||||||
if (data.whois_stats) {
|
if (data.whois_stats) {
|
||||||
document.getElementById('whois_fresh').textContent = formatNumber(data.whois_stats.fresh_asns);
|
document.getElementById('whois_fresh').textContent = formatNumber(data.whois_stats.fresh_asns);
|
||||||
@ -455,5 +542,13 @@
|
|||||||
updateStatus();
|
updateStatus();
|
||||||
setInterval(updateStatus, 2000);
|
setInterval(updateStatus, 2000);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<span><a href="{{appRepoURL}}">{{appName}}</a> by <a href="{{appAuthorURL}}">{{appAuthor}}</a></span>
|
||||||
|
<span class="separator">|</span>
|
||||||
|
<span>{{appLicense}}</span>
|
||||||
|
<span class="separator">|</span>
|
||||||
|
<span><a href="{{appGitCommitURL}}">{{appGitRevision}}</a></span>
|
||||||
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -7,6 +7,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/routewatch/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed status.html
|
//go:embed status.html
|
||||||
@ -88,10 +90,17 @@ func initTemplates() {
|
|||||||
funcs := template.FuncMap{
|
funcs := template.FuncMap{
|
||||||
"timeSince": timeSince,
|
"timeSince": timeSince,
|
||||||
"urlEncode": url.QueryEscape,
|
"urlEncode": url.QueryEscape,
|
||||||
|
"appName": func() string { return version.Name },
|
||||||
|
"appAuthor": func() string { return version.Author },
|
||||||
|
"appAuthorURL": func() string { return version.AuthorURL },
|
||||||
|
"appLicense": func() string { return version.License },
|
||||||
|
"appRepoURL": func() string { return version.RepoURL },
|
||||||
|
"appGitRevision": func() string { return version.GitRevisionShort },
|
||||||
|
"appGitCommitURL": func() string { return version.CommitURL() },
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse status template
|
// Parse status template
|
||||||
defaultTemplates.Status, err = template.New("status").Parse(statusHTML)
|
defaultTemplates.Status, err = template.New("status").Funcs(funcs).Parse(statusHTML)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("failed to parse status template: " + err.Error())
|
panic("failed to parse status template: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
34
internal/version/version.go
Normal file
34
internal/version/version.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Package version provides build version information
|
||||||
|
package version
|
||||||
|
|
||||||
|
// Build-time variables set via ldflags
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // These must be variables to allow ldflags injection at build time
|
||||||
|
var (
|
||||||
|
// GitRevision is the git commit hash
|
||||||
|
GitRevision = "unknown"
|
||||||
|
// GitRevisionShort is the short git commit hash (7 chars)
|
||||||
|
GitRevisionShort = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Name is the program name
|
||||||
|
Name = "routewatch"
|
||||||
|
// Author is the program author
|
||||||
|
Author = "@sneak"
|
||||||
|
// AuthorURL is the author's website
|
||||||
|
AuthorURL = "https://sneak.berlin"
|
||||||
|
// License is the program license
|
||||||
|
License = "WTFPL"
|
||||||
|
// RepoURL is the git repository URL
|
||||||
|
RepoURL = "https://git.eeqj.de/sneak/routewatch"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommitURL returns the URL to view the current commit
|
||||||
|
func CommitURL() string {
|
||||||
|
if GitRevision == "unknown" {
|
||||||
|
return RepoURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return RepoURL + "/commit/" + GitRevision
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user