latest
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user