Files
routewatch/internal/metrics/metrics.go
T
clawbot b89987e356
check / check (push) Canceled after 0s
Re-vendor the shared files from sneak/prompts at 0653579 (closes #52)
Takes .golangci.yml, .dockerignore, .gitignore, .editorconfig, the
workflow and REPO_POLICIES.md from sneak/prompts 0653579, with this
repository's own entries after the shared content. Lint (golangci-lint
v2.14.0) and test are Dockerfile phases that script/lint and script/test
build alone with a new CACHEBUST value, keeping Go's and golangci-lint's
caches; the image build depends on both. make fmt and make fmt-check also
format the markdown with prettier, pinned in yarn.lock. The agent
guidance moves to AGENTS.md. The Go code is fixed for what the new
configuration finds, and the AS and prefix detail reads now return the
rows' error after their loops.

Model: opus-5-5
2026-10-19 12:10:11 +02:00

246 lines
6.6 KiB
Go

// Package metrics provides centralized metrics tracking for the RouteWatch application
package metrics
import (
"sync"
"sync/atomic"
"time"
"github.com/rcrowley/go-metrics"
)
// Tracker provides centralized metrics tracking
type Tracker struct {
mu sync.RWMutex
registry metrics.Registry
connectedSince time.Time
isConnected atomic.Bool
reconnectCount atomic.Uint64
// Stream metrics (decompressed data)
messageCounter metrics.Counter
byteCounter metrics.Counter
messageRate metrics.Meter
byteRate metrics.Meter
// Wire bytes metrics (actual bytes on the wire, before decompression)
wireByteCounter metrics.Counter
wireByteRate metrics.Meter
// 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
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(),
announcementCounter: metrics.NewCounter(),
withdrawalCounter: metrics.NewCounter(),
churnRate: metrics.NewMeter(),
}
}
// SetConnected updates the connection status. Every connection after the
// first counts one reconnect.
func (t *Tracker) SetConnected(connected bool) {
t.isConnected.Store(connected)
if !connected {
return
}
t.mu.Lock()
// connectedSince is zero until the first connection.
connectedBefore := !t.connectedSince.IsZero()
t.connectedSince = time.Now()
t.mu.Unlock()
if connectedBefore {
t.reconnectCount.Add(1)
}
}
// GetReconnectCount returns the number of reconnections since startup
func (t *Tracker) GetReconnectCount() uint64 {
return t.reconnectCount.Load()
}
// IsConnected returns the current connection status
func (t *Tracker) IsConnected() bool {
return t.isConnected.Load()
}
// RecordMessage records a received message and its decompressed size
func (t *Tracker) RecordMessage(bytes int64) {
t.messageCounter.Inc(1)
t.byteCounter.Inc(bytes)
t.messageRate.Mark(1)
t.byteRate.Mark(bytes)
}
// RecordWireBytes records actual bytes received on the wire (before decompression)
func (t *Tracker) RecordWireBytes(bytes int64) {
t.wireByteCounter.Inc(bytes)
t.wireByteRate.Mark(bytes)
}
// GetStreamMetrics returns current streaming metrics
func (t *Tracker) GetStreamMetrics() StreamMetrics {
t.mu.RLock()
connectedSince := t.connectedSince
t.mu.RUnlock()
const bitsPerByte = 8
// Safely convert counters to uint64
msgCount := t.messageCounter.Count()
byteCount := t.byteCounter.Count()
wireByteCount := t.wireByteCounter.Count()
var totalMessages, totalBytes, totalWireBytes uint64
if msgCount >= 0 {
totalMessages = uint64(msgCount)
}
if byteCount >= 0 {
totalBytes = uint64(byteCount)
}
if wireByteCount >= 0 {
totalWireBytes = uint64(wireByteCount)
}
return StreamMetrics{
TotalMessages: totalMessages,
TotalBytes: totalBytes,
TotalWireBytes: totalWireBytes,
ConnectedSince: connectedSince,
Connected: t.isConnected.Load(),
MessagesPerSec: t.messageRate.Rate1(),
BitsPerSec: t.byteRate.Rate1() * bitsPerByte,
WireBitsPerSec: t.wireByteRate.Rate1() * bitsPerByte,
ReconnectCount: t.reconnectCount.Load(),
}
}
// RecordIPv4Update records an IPv4 route update
func (t *Tracker) RecordIPv4Update() {
t.ipv4UpdateRate.Mark(1)
}
// RecordIPv6Update records an IPv6 route update
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))
}
}
// 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{
IPv4UpdatesPerSec: t.ipv4UpdateRate.Rate1(),
IPv6UpdatesPerSec: t.ipv6UpdateRate.Rate1(),
}
}
// StreamMetrics contains streaming statistics
type StreamMetrics struct {
// TotalMessages is the total number of messages received since startup
TotalMessages uint64
// TotalBytes is the total number of decompressed bytes received since startup
TotalBytes uint64
// TotalWireBytes is the total number of bytes received on the wire (before
// decompression)
TotalWireBytes uint64
// ConnectedSince is the time when the current connection was established
ConnectedSince time.Time
// Connected indicates whether the stream is currently connected
Connected bool
// MessagesPerSec is the rate of messages received per second (1-minute average)
MessagesPerSec float64
// BitsPerSec is the rate of decompressed bits received per second (1-minute
// average)
BitsPerSec float64
// WireBitsPerSec is the rate of bits received on the wire per second
// (1-minute average)
WireBitsPerSec float64
// ReconnectCount is the number of reconnections since startup
ReconnectCount uint64
}
// RouteMetrics contains route update statistics
type RouteMetrics struct {
// IPv4UpdatesPerSec is the rate of IPv4 route updates per second (1-minute average)
IPv4UpdatesPerSec float64
// IPv6UpdatesPerSec is the rate of IPv6 route updates per second (1-minute average)
IPv6UpdatesPerSec float64
}