Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infinite ping #86

Merged
merged 27 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f577a85
Add --infinite flag
radulucut Jan 15, 2024
3bbc717
Add infinite output view & refactoring
radulucut Jan 17, 2024
ca86281
Fix Sent count
radulucut Jan 17, 2024
b5da380
More docs
jimaek Jan 17, 2024
b9e9d4e
Add tests + refactoring
radulucut Jan 18, 2024
20c83e7
Update formating, fix test
radulucut Jan 18, 2024
0c774f7
Fix test
radulucut Jan 18, 2024
1ae9a0f
Update Go version and dependencies
radulucut Jan 19, 2024
f527b86
Update table format
radulucut Jan 19, 2024
a99d83d
Add table truncation
radulucut Jan 23, 2024
2e3e18d
Fix limit flag
radulucut Jan 23, 2024
a291e5f
Parse and handle in-progress measurement
radulucut Jan 25, 2024
225d384
Add more tests
radulucut Jan 26, 2024
45d9151
Add support for share & summary + fixes & tests
radulucut Jan 30, 2024
8542e5a
Add combined mdev, change --infinite + --latency
radulucut Jan 31, 2024
180a8ba
Update ping.go
MartinKolarik Feb 2, 2024
3ca1801
Update mdev logic & term padding, fix history
radulucut Feb 5, 2024
940e0a4
Fix tests
radulucut Feb 5, 2024
416ea8f
Refactor code structure to improve testability
radulucut Feb 6, 2024
c741fd5
Add printer
radulucut Feb 7, 2024
7881888
Refactoring, ping tests & fixes
radulucut Feb 7, 2024
ea5c2b8
Skip test on windows
radulucut Feb 7, 2024
96a19a8
Update actions
MartinKolarik Feb 12, 2024
877c51a
Handle failed measurements
radulucut Feb 12, 2024
5d3121e
Add time for in progress measurements
radulucut Feb 13, 2024
b80540d
Replace github.com/golang/mock with go.uber.org/mock
radulucut Feb 13, 2024
9be6fae
Update sleep durations in ping_test.go
radulucut Feb 13, 2024
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
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,37 @@ Max: 7.413 ms
Avg: 7.359 ms
```

#### Continuous non-stop measurements

> [!IMPORTANT]
> Currently this feature is limited to the ping command

You can use the `--infinite` flag to continuously ping a host, just like on Linux or MacOS.
Note that while it looks like a single measurement, in actuality its multiple measurements from the same probes combined into a single output.
This means that eventually you will run out of credits and the test will stop.

```bash
globalping ping cdn.jsdelivr.net from Europe --infinite
> EU, SE, Stockholm, ASN:42708, GleSYS AB
PING cdn.jsdelivr.net (151.101.1.229)
151.101.1.229: icmp_seq=1 ttl=59 time=0.85 ms
151.101.1.229: icmp_seq=2 ttl=59 time=5.86 ms
^C
```

If you select multiple probes when using `--infinite` the output will change to a summary comparison table.

```bash
globalping ping cdn.jsdelivr.net from Europe --limit 5 --infinite
Location | Loss | Sent | Last | Avg | Min | Max
EU, DE, Falkenstein, ASN:24940, Hetzner Online GmbH | 0.00% | 21 | 5.43 ms | 5.71 ms | 5.30 ms | 11.98 ms
EU, NL, Rotterdam, ASN:210630, IncogNET LLC | 0.00% | 21 | 1.76 ms | 1.81 ms | 1.76 ms | 1.96 ms
EU, LU, Luxembourg, ASN:53667, FranTech Solutions | 0.00% | 21 | 5.14 ms | 13.03 ms | 4.80 ms | 75.71 ms
EU, ES, Madrid, ASN:20473, The Constant Company, LLC | 0.00% | 21 | 0.67 ms | 0.73 ms | 0.59 ms | 1.08 ms
MartinKolarik marked this conversation as resolved.
Show resolved Hide resolved
EU, DE, Frankfurt, ASN:16276, OVH SAS | 0.00% | 21 | 1.47 ms | 1.43 ms | 1.35 ms | 1.51 ms
^C
```

#### Learn about available flags

Most commands have shared and unique flags. We recommend that you familiarize yourself with these so that you can run and automate your network tests in powerful ways.
Expand Down
45 changes: 32 additions & 13 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
)

var ApiUrl = "https://api.globalping.io/v1/measurements"
var PacketsMax = 16

// Post measurement to Globalping API - boolean indicates whether to print CLI help on error
func PostAPI(measurement model.PostMeasurement) (model.PostResponse, bool, error) {
Expand Down Expand Up @@ -88,20 +89,38 @@ func PostAPI(measurement model.PostMeasurement) (model.PostResponse, bool, error
return data, false, nil
}

func DecodeTimings(cmd string, timings json.RawMessage) (model.Timings, error) {
var data model.Timings
func DecodeDNSTimings(timings json.RawMessage) (*model.DNSTimings, error) {
t := &model.DNSTimings{}
err := json.Unmarshal(timings, t)
if err != nil {
return nil, errors.New("invalid timings format returned (other)")
}
return t, nil
}

if cmd == "ping" {
err := json.Unmarshal(timings, &data.Arr)
if err != nil {
return model.Timings{}, errors.New("invalid timings format returned (ping)")
}
} else {
err := json.Unmarshal(timings, &data.Interface)
if err != nil {
return model.Timings{}, errors.New("invalid timings format returned (other)")
}
func DecodeHTTPTimings(timings json.RawMessage) (*model.HTTPTimings, error) {
t := &model.HTTPTimings{}
err := json.Unmarshal(timings, t)
if err != nil {
return nil, errors.New("invalid timings format returned (other)")
}
return t, nil
}

return data, nil
func DecodePingTimings(timings json.RawMessage) ([]model.PingTiming, error) {
t := []model.PingTiming{}
err := json.Unmarshal(timings, &t)
if err != nil {
return nil, errors.New("invalid timings format returned (ping)")
}
return t, nil
}

func DecodePingStats(stats json.RawMessage) (*model.PingStats, error) {
s := &model.PingStats{}
err := json.Unmarshal(stats, s)
if err != nil {
return nil, errors.New("invalid stats format returned")
}
return s, nil
}
36 changes: 18 additions & 18 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,15 @@ func testGetPing(t *testing.T) {

assert.Equal(t, "PING", res.Results[0].Result.RawOutput)
assert.Equal(t, "1.1.1.1", res.Results[0].Result.ResolvedAddress)
assert.Equal(t, 27.088, res.Results[0].Result.Stats["avg"])
assert.Equal(t, 28.193, res.Results[0].Result.Stats["max"])
assert.Equal(t, 24.891, res.Results[0].Result.Stats["min"])
assert.Equal(t, float64(3), res.Results[0].Result.Stats["total"])
assert.Equal(t, float64(3), res.Results[0].Result.Stats["rcv"])
assert.Equal(t, float64(0), res.Results[0].Result.Stats["loss"])
assert.Equal(t, float64(0), res.Results[0].Result.Stats["drop"])
stats, err := client.DecodePingStats(res.Results[0].Result.StatsRaw)
assert.NoError(t, err)
assert.Equal(t, float64(27.088), stats.Avg)
assert.Equal(t, float64(28.193), stats.Max)
assert.Equal(t, float64(24.891), stats.Min)
assert.Equal(t, 3, stats.Total)
assert.Equal(t, 3, stats.Rcv)
assert.Equal(t, 0, stats.Drop)
assert.Equal(t, float64(0), stats.Loss)
}

func testGetTraceroute(t *testing.T) {
Expand Down Expand Up @@ -415,9 +417,8 @@ func testGetDns(t *testing.T) {
assert.IsType(t, json.RawMessage{}, res.Results[0].Result.TimingsRaw)

// Test timings
timings, _ := client.DecodeTimings("dns", res.Results[0].Result.TimingsRaw)
assert.Equal(t, float64(15), timings.Interface["total"])
assert.Nil(t, timings.Arr)
timings, _ := client.DecodeDNSTimings(res.Results[0].Result.TimingsRaw)
assert.Equal(t, float64(15), timings.Total)
}

func testGetMtr(t *testing.T) {
Expand Down Expand Up @@ -647,12 +648,11 @@ func testGetHttp(t *testing.T) {
assert.IsType(t, json.RawMessage{}, res.Results[0].Result.TimingsRaw)

// Test timings
timings, _ := client.DecodeTimings("dns", res.Results[0].Result.TimingsRaw)
assert.Nil(t, timings.Arr)
assert.Equal(t, float64(583), timings.Interface["total"])
assert.Equal(t, float64(18), timings.Interface["download"])
assert.Equal(t, float64(450), timings.Interface["firstByte"])
assert.Equal(t, float64(24), timings.Interface["dns"])
assert.Equal(t, float64(70), timings.Interface["tls"])
assert.Equal(t, float64(19), timings.Interface["tcp"])
timings, _ := client.DecodeHTTPTimings(res.Results[0].Result.TimingsRaw)
assert.Equal(t, 583, timings.Total)
assert.Equal(t, 18, timings.Download)
assert.Equal(t, 450, timings.FirstByte)
assert.Equal(t, 24, timings.DNS)
assert.Equal(t, 70, timings.TLS)
assert.Equal(t, 19, timings.TCP)
}
83 changes: 51 additions & 32 deletions cmd/ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,55 +39,74 @@ Examples:
ping jsdelivr.com from aws+montreal --latency

# Ping jsdelivr.com from a probe in ASN 123 with json output
ping jsdelivr.com from 123 --json`,
ping jsdelivr.com from 123 --json

# Continuously ping google.com from New York
ping google.com from New York --infinite`,
RunE: func(cmd *cobra.Command, args []string) error {
// Create context
err := createContext(cmd.CalledAs(), args)
if err != nil {
return err
}

// Make post struct
opts = model.PostMeasurement{
Type: "ping",
Target: ctx.Target,
Limit: ctx.Limit,
InProgressUpdates: inProgressUpdates(ctx.CI),
Options: &model.MeasurementOptions{
Packets: packets,
},
if ctx.Infinite {
ctx.Packets = 1 // We sent only one packet first and overwrite it later based on the view type
for {
ctx.From, err = ping(cmd)
if err != nil {
return err
}
}
}
isPreviousMeasurementId := false
opts.Locations, isPreviousMeasurementId, err = createLocations(ctx.From)
if err != nil {
_, err = ping(cmd)
return err
},
}

func ping(cmd *cobra.Command) (string, error) {
opts = model.PostMeasurement{
Type: "ping",
Target: ctx.Target,
Limit: ctx.Limit,
InProgressUpdates: inProgressUpdates(ctx.CI),
Options: &model.MeasurementOptions{
Packets: ctx.Packets,
},
}
locations, isPreviousMeasurementId, err := createLocations(ctx.From)
if err != nil {
cmd.SilenceUsage = true
return "", err
}
opts.Locations = locations

res, showHelp, err := client.PostAPI(opts)
if err != nil {
if !showHelp {
cmd.SilenceUsage = true
return err
}
return "", err
}

res, showHelp, err := client.PostAPI(opts)
// Save measurement ID to history
if !isPreviousMeasurementId {
err := saveMeasurementID(res.ID)
if err != nil {
if !showHelp {
cmd.SilenceUsage = true
}
return err
}

// Save measurement ID to history
if !isPreviousMeasurementId {
err := saveMeasurementID(res.ID)
if err != nil {
fmt.Printf("Warning: %s\n", err)
}
fmt.Printf("Warning: %s\n", err)
}
}

if ctx.Infinite {
err = view.OutputInfinite(res.ID, &ctx)
} else {
view.OutputResults(res.ID, ctx, opts)
return nil
},
}
return res.ID, err
}

func init() {
rootCmd.AddCommand(pingCmd)

// ping specific flags
pingCmd.Flags().IntVar(&packets, "packets", 0, "Specifies the desired amount of ECHO_REQUEST packets to be sent (default 3)")
pingCmd.Flags().IntVar(&ctx.Packets, "packets", 0, "Specifies the desired amount of ECHO_REQUEST packets to be sent (default 3)")
pingCmd.Flags().BoolVar(&ctx.Infinite, "infinite", false, "Continuously send ping request to a target (default false)")
}
48 changes: 36 additions & 12 deletions model/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package model

import "encoding/json"

// Modeled from https://github.com/jsdelivr/globalping/blob/master/docs/measurement/get.md
// Modeled from https://www.jsdelivr.com/docs/api.globalping.io

type ProbeData struct {
Continent string `json:"continent"`
Expand All @@ -16,19 +16,42 @@ type ProbeData struct {
}

type ResultData struct {
Status string `json:"status"`
RawOutput string `json:"rawOutput"`
RawHeaders string `json:"rawHeaders"`
RawBody string `json:"rawBody"`
ResolvedAddress string `json:"resolvedAddress"`
ResolvedHostname string `json:"resolvedHostname"`
Stats map[string]interface{} `json:"stats,omitempty"`
TimingsRaw json.RawMessage `json:"timings,omitempty"`
Status string `json:"status"`
RawOutput string `json:"rawOutput"`
RawHeaders string `json:"rawHeaders"`
RawBody string `json:"rawBody"`
ResolvedAddress string `json:"resolvedAddress"`
ResolvedHostname string `json:"resolvedHostname"`
StatsRaw json.RawMessage `json:"stats,omitempty"`
TimingsRaw json.RawMessage `json:"timings,omitempty"`
}

type Timings struct {
Arr []map[string]interface{}
Interface map[string]interface{}
type PingStats struct {
Min float64 `json:"min"` // The lowest rtt value.
Avg float64 `json:"avg"` // The average rtt value.
Max float64 `json:"max"` // The highest rtt value.
Total int `json:"total"` // The number of sent packets.
Rcv int `json:"rcv"` // The number of received packets.
Drop int `json:"drop"` // The number of dropped packets (total - rcv).
Loss float64 `json:"loss"` // The percentage of dropped packets.
}

type PingTiming struct {
RTT float64 `json:"rtt"` // The round-trip time for this packet.
TTL int `json:"ttl"` // The packet time-to-live value.
}

type DNSTimings struct {
Total float64 `json:"total"` // The total query time in milliseconds.
}

type HTTPTimings struct {
Total int `json:"total"` // The total HTTP request time
DNS int `json:"dns"` // The time required to perform the DNS lookup.
TCP int `json:"tcp"` // The time from performing the DNS lookup to establishing the TCP connection.
TLS int `json:"tls"` // The time from establishing the TCP connection to establishing the TLS session.
FirstByte int `json:"firstByte"` // The time from establishing the TCP/TLS connection to the first response byte.
Download int `json:"download"` // The time from the first byte to downloading the whole response.
}

// Nested structs
Expand All @@ -44,6 +67,7 @@ type GetMeasurement struct {
Status string `json:"status"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Target string `json:"target"`
ProbesCount int `json:"probesCount"`
Results []MeasurementResponse `json:"results"`
}
36 changes: 25 additions & 11 deletions model/root.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
package model

import "github.com/pterm/pterm"

// Used in thc client TUI
type Context struct {
Cmd string
Target string
From string
Limit int
Resolver string
// JsonOutput is a flag that determines whether the output should be in JSON format.
JsonOutput bool
// Latency is a flag that outputs only stats of a measurement
Latency bool
// CI flag is used to determine whether the output should be in a format that is easy to parse by a CI tool
CI bool
// Full output
Full bool
// Display share message
Share bool

Limit int
Packets int // Number of packets to send

JsonOutput bool // JsonOutput is a flag that determines whether the output should be in JSON format.
Latency bool // Latency is a flag that outputs only stats of a measurement
CI bool // CI flag is used to determine whether the output should be in a format that is easy to parse by a CI tool
Full bool // Full output
Share bool // Display share message
Infinite bool // Infinite flag

Area *pterm.AreaPrinter
Stats []MeasurementStats
}

type MeasurementStats struct {
Sent int // Number of packets sent
Lost int // Number of packets lost
Loss float64 // Percentage of packets lost
Last float64 // Last RTT
Min float64 // Minimum RTT
Avg float64 // Average RTT
Max float64 // Maximum RTT
}
Loading