Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ mping
- 📊 **Real-time statistics**: Live success rates, response times, and packet loss
- 🖥️ **Interactive TUI**: Clean terminal interface with sortable results
- ⚡ **High performance**: Concurrent probing with configurable intervals
- 📁 **Batch mode**: Non-interactive mode for scripting and automation
- 📁 **Batch mode**: Non-interactive mode for scripting and automation, with JSON/CSV output and exit codes
- ⏱️ **Outage measurement**: Records every outage with start/end time and duration — ideal for failover and maintenance tests
- 🛣️ **Path trace (MTR)**: Press `p` in the TUI to see per-hop loss and latency to the selected host
- 📈 **History at a glance**: Sparkline of recent probes and RTT p50/p95/p99 per host

## Supported Protocols

Expand Down Expand Up @@ -108,6 +111,52 @@ Batch mode details:
- Progress dots are written to stderr only when it is a terminal, so stdout stays clean for pipes. The failure beep is disabled in batch mode.
- `-I, --interface` selects the source interface, same as the interactive mode.

## Outage Measurement

mping turns consecutive failures into **outages** and records when each one started and ended. This answers the question that matters during failover tests and maintenance windows: *how long was it down?*

- An outage starts when **3 or more consecutive probes** are lost (shorter runs count as isolated packet loss) and ends at the next successful probe. Its start is the send time of the first lost probe.
- Duration is `lost probes × interval`, so use a short interval (e.g. `-i 100`) for sub-second precision.
- The host list's **LastFailTime** column also shows the outage duration: `16:44:20 (DOWN 5.20s)` (red, counting up) while a host is down, and `16:44:15 (0.50s)` (yellow) once it has recovered.
- The TUI host detail panel (`v`) shows the outage count, total downtime, the longest outage, and the most recent outages.
- When you quit the TUI (and in `mping batch`), a timeline of all outages across targets is printed:

```
┌──────────────────────────────────────────────────────────────────────────────────┐
│ Outages (2) │
├──────────────┬──────────────┬──────────┬──────┬─────────────────────┬────────────┤
│ START │ END │ DURATION │ LOST │ HOST │ LAST ERROR │
├──────────────┼──────────────┼──────────┼──────┼─────────────────────┼────────────┤
│ 14:03:05.120 │ 14:03:41.870 │ 36.750s │ 368 │ core-sw1(10.0.0.1) │ timeout │
│ 14:03:06.002 │ (ongoing) │ 1.010s │ 10 │ 10.0.0.254 │ timeout │
└──────────────┴──────────────┴──────────┴──────┴─────────────────────┴────────────┘
```

- `mping batch -o json` includes `outages[]`, `outage_count`, `total_downtime_ms`, and `longest_outage_ms` per target.
- Probe results can arrive out of order (a timeout is reported after a later probe already succeeded), so results are applied in send order after `timeout + interval`. Outages therefore appear in the TUI with that delay.

## Path Trace (MTR)

Press `p` in the TUI to show a live, MTR-style trace of the path to the selected host. The trace follows the selection. Any target works — for `tcp://`, `https://`, `dns://`, … the host part is traced.

Notes:
- Loss at an intermediate hop that does not continue to the destination is usually ICMP rate limiting on that router, not real loss; the path view points this out.
- Raw sockets are used when available (root / `cap_net_raw`). Otherwise mping falls back to unprivileged ICMP sockets: on macOS this works fully, on Linux only the destination may be visible.
- In the TUI, path probing runs at most once per second so that router rate limiting does not show up as fake loss.

## History and Percentiles

The host list has a **History** column with the last 20 probes (oldest on the left). The bar height shows how much slower than usual a probe was: the RTT increase over the host's median, on a fixed millisecond scale. So +20ms looks the same on a 10ms LAN host and on a 150ms overseas host.

| Mark | Meaning |
|------|---------|
| green `▁` | within +5ms of the median |
| green `▂▃▄▅` | +5ms / +10ms / +20ms / +50ms slower |
| yellow `▆▇█` | +100ms / +200ms / +500ms slower |
| red `×` | no reply (timeout / connection failure) |

Latency spikes and loss bursts are visible at a glance. The host details and JSON/CSV output include RTT p50/p95/p99 over the last 100 probes.

## Target Expansion

mping supports powerful target expansion syntax for monitoring large numbers of hosts efficiently:
Expand Down Expand Up @@ -270,6 +319,7 @@ mping tcp://google.com:443 https://google.com ntp://time.google.com 8.8.8.8

Available Commands:
batch Disables TUI and performs probing for a set number of iterations
config management config
help Help about any command

Flags:
Expand Down
10 changes: 6 additions & 4 deletions internal/command/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,12 @@ mping batch --max-loss 20 -f hosts.txt || echo "some targets are unhealthy"`,

// Create ProbeManager and MetricsManager
probeManager := prober.NewProbeManager(cfg.Prober, cfg.Default)
metricsManager := stats.NewMetricsManager()
// The terminal bell would corrupt machine-readable output and is
// pointless in non-interactive runs.
metricsManager.SetBeepEnabled(false)
metricsManager := stats.NewMetricsManagerWithOptions(stats.Options{
SettleTime: stats.SettleTimeFor(_interval, _timeout),
// The terminal bell would corrupt machine-readable output and
// is pointless in non-interactive runs.
DisableBeep: true,
})

// Add targets
err = probeManager.AddTargets(hosts...)
Expand Down
4 changes: 3 additions & 1 deletion internal/command/mping.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ mping dns://8.8.8.8/google.com`,

// Create ProbeManager and MetricsManager
probeManager := prober.NewProbeManager(cfg.Prober, cfg.Default)
metricsManager := stats.NewMetricsManager()
metricsManager := stats.NewMetricsManagerWithOptions(stats.Options{
SettleTime: stats.SettleTimeFor(_interval, _timeout),
})

// Add targets
err = probeManager.AddTargets(hosts...)
Expand Down
7 changes: 7 additions & 0 deletions internal/stats/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ func (th *TargetHistory) GetSuccessRateInPeriod(duration time.Duration) float64
return float64(successCount) / float64(len(entries)) * 100.0
}

// clone returns a deep copy of the history
func (th *TargetHistory) clone() *TargetHistory {
c := *th
c.entries = append([]HistoryEntry(nil), th.entries...)
return &c
}

// Clear clears the history
func (th *TargetHistory) Clear() {
th.index = 0
Expand Down
3 changes: 2 additions & 1 deletion internal/stats/history_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ func TestMetricsWithHistory(t *testing.T) {
// Record failure
mm.Failed(host, time.Now(), "timeout")

// Test consecutive failures
// GetMetrics returns a snapshot, so fetch again to observe the failure
metrics = mm.GetMetrics(host)
if metrics.GetConsecutiveFailures() != 1 {
t.Errorf("Expected 1 consecutive failure, got %d", metrics.GetConsecutiveFailures())
}
Expand Down
7 changes: 6 additions & 1 deletion internal/stats/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ type Metrics interface {
GetConsecutiveFailures() int
GetConsecutiveSuccesses() int
GetSuccessRateInPeriod(duration time.Duration) float64
GetRTTPercentiles(ps ...float64) []time.Duration

// Outage tracking
GetOutages() []Outage
GetLastOutage() (Outage, bool)
GetOutageSummary() OutageSummary
}

// MetricsProvider provides external API for metrics access
Expand All @@ -37,7 +43,6 @@ type MetricsProvider interface {
type MetricsSystemManager interface {
ResetAllMetrics()
ToggleBeep()
SetBeepEnabled(enabled bool)
IsBeepEnabled() bool
}

Expand Down
Loading
Loading