Add status page enhancements with new metrics and footer
- Add GC statistics (run count, total/last pause, heap usage) - Add BGP peer count tracking from RIS Live OPEN/NOTIFICATION messages - Add route churn rate metric (announcements + withdrawals per second) - Add announcement and withdrawal counters - Add footer with attribution, license, and git revision - Embed git revision at build time via ldflags - Update HTML template to display all new metrics
This commit is contained in:
parent
1115954827
commit
c116b035bd
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/
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -469,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,
|
||||||
@ -483,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,
|
||||||
|
|||||||
@ -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.
|
||||||
@ -132,7 +134,8 @@ func New(logger *logger.Logger, metrics *metrics.Tracker) *Streamer {
|
|||||||
handlers: make([]*handlerInfo, 0),
|
handlers: make([]*handlerInfo, 0),
|
||||||
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>
|
||||||
@ -129,6 +166,30 @@
|
|||||||
<span class="metric-value" id="wire_mbits_per_sec">-</span>
|
<span class="metric-value" id="wire_mbits_per_sec">-</span>
|
||||||
</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>
|
||||||
@ -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
|
||||||
@ -86,12 +88,19 @@ func initTemplates() {
|
|||||||
|
|
||||||
// Create common template functions
|
// Create common template functions
|
||||||
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