This commit is contained in:
2025-05-21 21:34:06 -07:00
parent 2ad2aef700
commit 0e1fc6a88d
+145 -48
View File
@@ -11,6 +11,7 @@ import (
"flag"
"fmt"
"math"
"math/rand"
"net"
"os"
"os/exec"
@@ -33,9 +34,9 @@ var (
labelB = flag.String("labelB", "Cox cable direct", "label for ifaceB")
hostCSV = flag.String("hosts",
"1.1.1.1,8.8.8.8,8.8.4.4,google.com,github.com,"+
"8.8.8.8,8.8.4.4,google.com,github.com,"+
"console.aws.amazon.com,console.cloud.google.com,"+
"fast.com,cloudflare.com,datavi.be",
"fast.com,datavi.be,captive.apple.com",
"comma-separated reachability hosts")
logFile = flag.String("logfile", "/tmp/rtnetmon.log", "path to log file")
@@ -51,12 +52,20 @@ const (
statsHistory = 300
screenRefresh = 500 * time.Millisecond // Still used as backup refresh rate
// Unicode characters for the meter
greenDot = '🟢' // Unicode green circle
redDot = '🔴' // Unicode red circle
emptyDot = '' // Unicode white circle
meterWidth = 6 // Fixed width of the meter
maxMeterValue = 10 // Maximum value for the meter
// ASCII characters for the meter
meterStart = '['
meterEnd = ']'
meterFill = '='
meterEmpty = ' '
meterWidth = 7 // Fixed width of the meter (including brackets)
meterFillWidth = 5 // Width of the fillable area (meterWidth - 2 for brackets)
maxMeterValue = 10 // Maximum value for the meter
hostW = 30 // Increased width for host column
numW = 7
stdW = 8
nW = 6
lostW = 7 // Width for the lost column
)
/*────────────────── runtime struct ─────────────*/
@@ -70,7 +79,8 @@ type InterfaceStatus struct {
DroppedCount int
LastDrop, LastPing time.Time
SpinFrame int
MeterValue int // Current value for the packet loss meter
MeterValue int // Current value for the packet loss meter
LostPackets map[string]int // Track lost packets per host
mu sync.RWMutex
}
@@ -79,12 +89,42 @@ type InterfaceStatus struct {
var (
cBrightGreen = tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
cGreen = tcell.StyleDefault.Foreground(tcell.ColorGreen)
cDimGreen = tcell.StyleDefault.Foreground(tcell.ColorGreen).Dim(true)
cYellow = tcell.StyleDefault.Foreground(tcell.ColorYellow)
cOrange = tcell.StyleDefault.Foreground(tcell.ColorOrange)
cRed = tcell.StyleDefault.Foreground(tcell.ColorRed)
cBrightRed = tcell.StyleDefault.Foreground(tcell.ColorRed).Bold(true)
cDefault = tcell.StyleDefault
// Rainbow colors for the spinner
cRainbow = []tcell.Style{
tcell.StyleDefault.Foreground(tcell.ColorRed),
tcell.StyleDefault.Foreground(tcell.ColorOrange),
tcell.StyleDefault.Foreground(tcell.ColorYellow),
tcell.StyleDefault.Foreground(tcell.ColorGreen),
tcell.StyleDefault.Foreground(tcell.ColorBlue),
tcell.StyleDefault.Foreground(tcell.ColorPurple),
}
)
// Get the appropriate meter style based on the value
func getMeterStyle(value int) tcell.Style {
switch {
case value <= 0:
return cBrightGreen
case value == 1:
return cGreen
case value == 2:
return cDimGreen
case value == 3:
return cYellow
case value == 4:
return cOrange
default:
return cRed
}
}
func styleLatency(ms float64) tcell.Style {
switch {
case ms < 50:
@@ -265,13 +305,6 @@ func ifaceHealthy(st *InterfaceStatus) bool {
return true
}
const (
hostW = 30 // Increased width for host column
numW = 7
stdW = 8
nW = 6
)
func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int {
put(scr, 0, y, hline(w), cDefault)
@@ -283,17 +316,21 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int {
}
st.mu.RLock()
header := fmt.Sprintf("%s: %s — %s", st.Name, st.Label, st.IPInfo)
spin := spins[st.SpinFrame]
spinChar := spins[st.SpinFrame]
spinnerFrame := st.SpinFrame
meterValue := st.MeterValue
st.mu.RUnlock()
// Create the meter - fixed width of 6 characters
meter := createMeter(meterValue, meterWidth)
// Create the meter with appropriate style
meter, meterStyle := createMeter(meterValue, meterWidth)
// Get rainbow color for spinner (cycle through colors)
spinnerStyle := cRainbow[spinnerFrame%len(cRainbow)]
put(scr, 0, y+1, "== ", cDefault)
put(scr, 3, y+1, string(spin)+" ", cDefault)
put(scr, 5, y+1, meter+" ", cDefault) // Add the meter after the spinner
put(scr, 5+meterWidth+1, y+1, header, style) // Move the header after the meter
put(scr, 3, y+1, string(spinChar)+" ", spinnerStyle) // Colorful spinner
put(scr, 5, y+1, meter+" ", meterStyle) // Colored meter
put(scr, 5+meterWidth+1, y+1, header, style) // Move the header after the meter
put(scr, 0, y+2, hline(w), cDefault)
y += 4
@@ -347,9 +384,9 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int {
put(scr, 0, y, "TCP Connect Stats:", cDefault)
y++
// header row
headerRow := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*s",
headerRow := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*s %*s",
hostW, "Host", numW, "last", numW, "min", numW, "avg",
numW, "max", stdW, "stddev", nW, "n")
numW, "max", stdW, "stddev", nW, "n", lostW, "lost")
put(scr, 0, y, headerRow, cDefault)
y++
@@ -361,7 +398,12 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int {
last := hist[len(hist)-1]
mi, ma, av, sd := minMaxAvgStd(hist)
row := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*d",
// Get host name without port for lost packets lookup
hostName := strings.Split(hp, ":")[0]
lost := st.LostPackets[hostName]
// Add lost column
row := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*d %*d",
hostW, hp,
numW, fmt.Sprintf("%.0fms", last),
numW, fmt.Sprintf("%.0fms", mi),
@@ -369,6 +411,7 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int {
numW, fmt.Sprintf("%.0fms", ma),
stdW, fmt.Sprintf("%.0fms", sd),
nW, len(hist),
lostW, lost,
)
put(scr, 0, y, row, cDefault)
// colourise individual numbers
@@ -377,6 +420,12 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int {
put(scr, hostW+1+numW*2+2, y, fmt.Sprintf("%*s", numW, fmt.Sprintf("%.0fms", av)), styleLatency(av))
put(scr, hostW+1+numW*3+3, y, fmt.Sprintf("%*s", numW, fmt.Sprintf("%.0fms", ma)), styleLatency(ma))
put(scr, hostW+1+numW*4+4, y, fmt.Sprintf("%*s", stdW, fmt.Sprintf("%.0fms", sd)), cDefault)
// Colorize the lost packets column
lostStyle := cDefault
if lost > 0 {
lostStyle = cRed
}
put(scr, hostW+1+numW*4+4+stdW+1+nW+1, y, fmt.Sprintf("%*d", lostW, lost), lostStyle)
y++
}
y++
@@ -385,13 +434,22 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int {
put(scr, 0, y, fmt.Sprintf("Total ICMP Requests: %d", st.TotalICMPReq), cDefault)
y++
put(scr, 0, y, fmt.Sprintf("Total ICMP Replies: %d", st.TotalICMPRep), cDefault)
y++
// Add a line showing lost packets
lost := st.TotalICMPReq - st.TotalICMPRep
lostStyle := cDefault
if lost > 0 {
lostStyle = cRed
}
put(scr, 0, y, fmt.Sprintf("Total ICMP Lost: %d", lost), lostStyle)
y += 2
st.mu.RUnlock()
return y
}
// Create a visual meter using green and red dots
func createMeter(value, width int) string {
// Create a visual meter using ASCII characters
// Returns the meter string and the appropriate style
func createMeter(value, width int) (string, tcell.Style) {
if value > maxMeterValue {
value = maxMeterValue
}
@@ -399,33 +457,48 @@ func createMeter(value, width int) string {
value = 0
}
// Calculate how many dots of each color to show
redDots := value
if redDots > width {
redDots = width
// Calculate the number of fill characters to show
// Map value (0-maxMeterValue) to fill width (0-meterFillWidth)
fillCount := value * meterFillWidth / maxMeterValue
if fillCount > meterFillWidth {
fillCount = meterFillWidth
}
greenDots := width - redDots
// Build the meter string
var b strings.Builder
// Add red dots for unreplied packets
for i := 0; i < redDots; i++ {
b.WriteRune(redDot)
// Add opening bracket
b.WriteRune(meterStart)
// Add fill characters
for i := 0; i < fillCount; i++ {
b.WriteRune(meterFill)
}
// Add green dots for available capacity
for i := 0; i < greenDots; i++ {
b.WriteRune(greenDot)
// Add empty spaces
for i := 0; i < meterFillWidth-fillCount; i++ {
b.WriteRune(meterEmpty)
}
return b.String()
// Add closing bracket
b.WriteRune(meterEnd)
// Get the appropriate style for this meter value
style := getMeterStyle(value)
return b.String(), style
}
/*────────────────── goroutine loops ─────────────*/
func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
logf("Starting reachability monitoring for %s with %d hosts", st.Name, len(hosts))
// Add random offset to avoid clustering at 1-second intervals
randomOffset := time.Duration(100+rand.Intn(800)) * time.Millisecond
logf("Reachability monitoring for %s will start after %v offset", st.Name, randomOffset)
time.Sleep(randomOffset)
tk := time.NewTicker(time.Second)
defer tk.Stop()
for {
@@ -469,6 +542,10 @@ func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
} else {
st.DroppedCount++
st.LastDrop = time.Now()
// Track lost packets per host
st.LostPackets[host]++
// For failed pings, we don't decrease the meter value
// Trigger UI update on ping failure
select {
@@ -521,8 +598,15 @@ func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
}
}
}
func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
logf("Starting packet loss monitoring for %s with %d hosts", st.Name, len(hosts))
// Add random offset to avoid clustering at periodic intervals
randomOffset := time.Duration(100+rand.Intn(800)) * time.Millisecond
logf("Packet loss monitoring for %s will start after %v offset", st.Name, randomOffset)
time.Sleep(randomOffset)
tk := time.NewTicker(packetLossPeriod)
defer tk.Stop()
for {
@@ -585,8 +669,15 @@ func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
}
}
}
func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
logf("Starting TCP monitoring for %s with %d hosts", st.Name, len(hosts))
// Add random offset to avoid clustering at 1-second intervals
randomOffset := time.Duration(100+rand.Intn(800)) * time.Millisecond
logf("TCP monitoring for %s will start after %v offset", st.Name, randomOffset)
time.Sleep(randomOffset)
tk := time.NewTicker(time.Second)
defer tk.Stop()
for {
@@ -702,9 +793,9 @@ func uiLoop(ctx context.Context, scr tcell.Screen, a, b *InterfaceStatus, start
var (
reachHosts []string
packetLossHosts = []string{"github.com", "google.com", "1.1.1.1", "8.8.8.8"}
packetLossHosts = []string{"github.com", "google.com", "8.8.8.8", "captive.apple.com"}
tcpTestHosts = []string{
"datavi.be:443", "fast.com:443", "cloudflare.com:443",
"datavi.be:443", "fast.com:443",
"console.aws.amazon.com:443", "console.cloud.google.com:443",
}
)
@@ -725,6 +816,11 @@ func logf(format string, v ...interface{}) {
func main() {
flag.Parse()
logf("Starting rtnetmon")
// Seed the random number generator
rand.Seed(time.Now().UnixNano())
logf("Random number generator seeded")
reachHosts = strings.Split(*hostCSV, ",")
logf("Monitoring interfaces %s and %s", *ifaceA, *ifaceB)
@@ -735,13 +831,14 @@ func main() {
newStatus := func(name, label string) *InterfaceStatus {
logf("Initializing status for interface %s", name)
return &InterfaceStatus{
Name: name,
Label: label,
IPInfo: fetchIPInfo(name),
Reachable: map[string]bool{},
Loss: map[string]float64{},
TCP: map[string][]float64{},
MeterValue: 0, // Initialize meter value to 0
Name: name,
Label: label,
IPInfo: fetchIPInfo(name),
Reachable: map[string]bool{},
Loss: map[string]float64{},
TCP: map[string][]float64{},
MeterValue: 0, // Initialize meter value to 0
LostPackets: map[string]int{}, // Initialize lost packets map
}
}
a, b := newStatus(*ifaceA, *labelA), newStatus(*ifaceB, *labelB)