From 433283fd2e66a675d2e8c470c418c5ea57b2e6d7 Mon Sep 17 00:00:00 2001 From: Radu Lucut Date: Fri, 22 Mar 2024 16:39:56 +0200 Subject: [PATCH 1/7] feat: history command --- README.md | 26 ++++-- cmd/common.go | 32 ++++--- cmd/common_test.go | 10 +-- cmd/dns.go | 24 +++-- cmd/dns_test.go | 2 +- cmd/history.go | 185 ++++++++++++++++++++++++++++++++++++++ cmd/http.go | 32 ++++--- cmd/http_test.go | 6 +- cmd/install_probe_test.go | 7 +- cmd/mtr.go | 19 +++- cmd/mtr_test.go | 2 +- cmd/ping.go | 10 ++- cmd/ping_test.go | 39 ++++---- cmd/root.go | 24 ++--- cmd/traceroute.go | 18 +++- cmd/traceroute_test.go | 2 +- cmd/utils_test.go | 7 +- view/context.go | 3 + view/output.go | 4 +- 19 files changed, 363 insertions(+), 89 deletions(-) create mode 100644 cmd/history.go diff --git a/README.md b/README.md index c2ac5b3..3f3f57e 100644 --- a/README.md +++ b/README.md @@ -93,18 +93,12 @@ Measurement Commands: Additional Commands: completion Generate the autocompletion script for the specified shell help Help about any command + history Show the history of your measurements install-probe Join the community powered Globalping platform by running a Docker container. version Print the version number of Globalping CLI Flags: - -C, --ci Disable realtime terminal updates and color suitable for CI and scripting (default false) - -F, --from string Comma-separated list of location values to match against or a measurement ID - For example, the partial or full name of a continent, region (e.g eastern europe), country, US state, city or network - Or use [@1 | first, @2 ... @-2, @-1 | last | previous] to run with the probes from previous measurements. (default "world") -h, --help help for globalping - -J, --json Output results in JSON format (default false) - --latency Output only the stats of a measurement (default false). Only applies to the dns, http and ping commands - -L, --limit int Limit the number of probes to use (default 1) Use "globalping [command] --help" for more information about a command. ``` @@ -307,6 +301,24 @@ Madrid, ES, EU, EDGOO NETWORKS LLC (AS47787) | 22 | 0.00% | 0.24 ^C ``` +#### History + +You can view the history of your measurements by running the `history` command. + +```bash +globalping history +1 | 2024-03-22 16:20:30 | ping google.com from last +> https://www.jsdelivr.com/globalping?measurement=gdS0v9h5eTIxKEOk +2 | 2024-03-22 16:09:10 | traceroute google.com from New York --limit 2 +> https://www.jsdelivr.com/globalping?measurement=P4ZA7IcX04K359XN +3 | 2024-03-22 16:08:44 | mtr google.com from New York --limit 2 +> https://www.jsdelivr.com/globalping?measurement=ePITYQJZhg9yn8NE +4 | 2024-03-22 16:08:19 | http google.com from London,Belgium --limit 2 --method get --ci +> https://www.jsdelivr.com/globalping?measurement=Sq8OEuRYNs6s147G +5 | 2024-03-22 16:07:45 | dns google.com from New York --limit 2 +> https://www.jsdelivr.com/globalping?measurement=OMFclzhYExXTFJDV +``` + #### 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. diff --git a/cmd/common.go b/cmd/common.go index c2f4769..b00ec8a 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -19,9 +19,13 @@ import ( ) var ( - ErrorNoPreviousMeasurements = errors.New("no previous measurements found") - ErrInvalidIndex = errors.New("invalid index") - ErrIndexOutOfRange = errors.New("index out of range") + ErrNoPreviousMeasurements = errors.New("no previous measurements found") + ErrInvalidIndex = errors.New("invalid index") + ErrIndexOutOfRange = errors.New("index out of range") +) +var ( + saveIdToSessionErr = "failed to save measurement ID: %s" + readMeasuremetsErr = "failed to read previous measurements: %s" ) var SESSION_PATH string @@ -182,19 +186,19 @@ func getIdFromSession(index int) (string, error) { f, err := os.Open(getMeasurementsPath()) if err != nil { if errors.Is(err, fs.ErrNotExist) { - return "", ErrorNoPreviousMeasurements + return "", ErrNoPreviousMeasurements } - return "", fmt.Errorf("failed to open previous measurements file: %s", err) + return "", fmt.Errorf(readMeasuremetsErr, err) } defer f.Close() // Read ids from the end of the file if index < 0 { fStats, err := f.Stat() if err != nil { - return "", fmt.Errorf("failed to read previous measurements: %s", err) + return "", fmt.Errorf(readMeasuremetsErr, err) } if fStats.Size() == 0 { - return "", ErrorNoPreviousMeasurements + return "", ErrNoPreviousMeasurements } scanner := backscanner.New(f, int(fStats.Size()-1)) // -1 to skip last newline for { @@ -204,7 +208,7 @@ func getIdFromSession(index int) (string, error) { if err == io.EOF { return "", ErrIndexOutOfRange } - return "", fmt.Errorf("failed to read previous measurements: %s", err) + return "", fmt.Errorf(readMeasuremetsErr, err) } if index == 0 { return string(b), nil @@ -232,20 +236,20 @@ func saveIdToSession(id string) error { if errors.Is(err, fs.ErrNotExist) { err := os.Mkdir(getSessionPath(), 0755) if err != nil { - return fmt.Errorf("failed to save measurement ID: %s", err) + return fmt.Errorf(saveIdToSessionErr, err) } } else { - return fmt.Errorf("failed to save measurement ID: %s", err) + return fmt.Errorf(saveIdToSessionErr, err) } } f, err := os.OpenFile(getMeasurementsPath(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - return fmt.Errorf("failed to save measurement ID: %s", err) + return fmt.Errorf(saveIdToSessionErr, err) } defer f.Close() _, err = f.WriteString(id + "\n") if err != nil { - return fmt.Errorf("failed to save measurement ID: %s", err) + return fmt.Errorf(saveIdToSessionErr, err) } return nil } @@ -282,3 +286,7 @@ func getSessionId() string { func getMeasurementsPath() string { return filepath.Join(getSessionPath(), "measurements") } + +func getHistoryPath() string { + return filepath.Join(getSessionPath(), "history") +} diff --git a/cmd/common_test.go b/cmd/common_test.go index e295799..06d5be6 100644 --- a/cmd/common_test.go +++ b/cmd/common_test.go @@ -23,7 +23,7 @@ func Test_UpdateContext(t *testing.T) { } func test_updateContext_NoArg(t *testing.T) { - ctx := &view.Context{} + ctx := createDefaultContext("ping") printer := view.NewPrinter(nil, nil, nil) root := NewRoot(printer, ctx, nil, nil, nil, nil) @@ -35,7 +35,7 @@ func test_updateContext_NoArg(t *testing.T) { } func test_updateContext_Country(t *testing.T) { - ctx := &view.Context{} + ctx := createDefaultContext("ping") printer := view.NewPrinter(nil, nil, nil) root := NewRoot(printer, ctx, nil, nil, nil, nil) @@ -48,7 +48,7 @@ func test_updateContext_Country(t *testing.T) { // Check if country with whitespace is parsed correctly func test_updateContext_CountryWhitespace(t *testing.T) { - ctx := &view.Context{} + ctx := createDefaultContext("ping") printer := view.NewPrinter(nil, nil, nil) root := NewRoot(printer, ctx, nil, nil, nil, nil) @@ -60,7 +60,7 @@ func test_updateContext_CountryWhitespace(t *testing.T) { } func test_updateContext_NoTarget(t *testing.T) { - ctx := &view.Context{} + ctx := createDefaultContext("ping") printer := view.NewPrinter(nil, nil, nil) root := NewRoot(printer, ctx, nil, nil, nil, nil) @@ -73,7 +73,7 @@ func test_uodateContext_CIEnv(t *testing.T) { t.Setenv("CI", "true") defer t.Setenv("CI", oldCI) - ctx := &view.Context{} + ctx := createDefaultContext("ping") printer := view.NewPrinter(nil, nil, nil) root := NewRoot(printer, ctx, nil, nil, nil, nil) diff --git a/cmd/dns.go b/cmd/dns.go index ebc0c7e..601962f 100644 --- a/cmd/dns.go +++ b/cmd/dns.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/jsdelivr/globalping-cli/globalping" + "github.com/jsdelivr/globalping-cli/view" "github.com/spf13/cobra" ) @@ -53,11 +54,17 @@ Using the dig format @resolver. For example: // dns specific flags flags := dnsCmd.Flags() - flags.StringVar(&r.ctx.Protocol, "protocol", "", "Specifies the protocol to use for the DNS query (TCP or UDP) (default \"udp\")") - flags.IntVar(&r.ctx.Port, "port", 0, "Send the query to a non-standard port on the server (default 53)") - flags.StringVar(&r.ctx.Resolver, "resolver", "", "Resolver is the hostname or IP address of the name server to use (default empty)") - flags.StringVar(&r.ctx.QueryType, "type", "", "Specifies the type of DNS query to perform (default \"A\")") - flags.BoolVar(&r.ctx.Trace, "trace", false, "Toggle tracing of the delegation path from the root name servers (default false)") + flags.StringVarP(&r.ctx.From, "from", "F", r.ctx.From, fromShortDesc) + flags.IntVarP(&r.ctx.Limit, "limit", "L", r.ctx.Limit, limitShortDesc) + flags.BoolVarP(&r.ctx.ToJSON, "json", "J", r.ctx.ToJSON, jsonShortDesc) + flags.BoolVarP(&r.ctx.CIMode, "ci", "C", r.ctx.CIMode, ciModeShortDesc) + flags.BoolVar(&r.ctx.ToLatency, "latency", r.ctx.ToLatency, latencyShortDesc) + flags.BoolVar(&r.ctx.Share, "share", r.ctx.Share, shareShortDesc) + flags.StringVar(&r.ctx.Protocol, "protocol", r.ctx.Protocol, "Specifies the protocol to use for the DNS query (TCP or UDP) (default \"udp\")") + flags.IntVar(&r.ctx.Port, "port", r.ctx.Port, "Send the query to a non-standard port on the server (default 53)") + flags.StringVar(&r.ctx.Resolver, "resolver", r.ctx.Resolver, "Resolver is the hostname or IP address of the name server to use (default empty)") + flags.StringVar(&r.ctx.QueryType, "type", r.ctx.QueryType, "Specifies the type of DNS query to perform (default \"A\")") + flags.BoolVar(&r.ctx.Trace, "trace", r.ctx.Trace, "Toggle tracing of the delegation path from the root name servers (default false)") r.Cmd.AddCommand(dnsCmd) } @@ -100,7 +107,12 @@ func (r *Root) RunDNS(cmd *cobra.Command, args []string) error { } r.ctx.MeasurementsCreated++ - + hm := &view.HistoryItem{ + Id: res.ID, + Status: globalping.StatusInProgress, + StartedAt: r.time.Now(), + } + r.ctx.History.Push(hm) if r.ctx.RecordToSession { r.ctx.RecordToSession = false err := saveIdToSession(res.ID) diff --git a/cmd/dns_test.go b/cmd/dns_test.go index e5b0baa..f1a18d5 100644 --- a/cmd/dns_test.go +++ b/cmd/dns_test.go @@ -39,7 +39,7 @@ func Test_Execute_DNS_Default(t *testing.T) { w := new(bytes.Buffer) printer := view.NewPrinter(nil, w, w) - ctx := createDefaultContext() + ctx := createDefaultContext("dns") root := NewRoot(printer, ctx, viewerMock, nil, gbMock, nil) os.Args = []string{"globalping", "dns", "jsdelivr.com", diff --git a/cmd/history.go b/cmd/history.go new file mode 100644 index 0000000..70a67df --- /dev/null +++ b/cmd/history.go @@ -0,0 +1,185 @@ +package cmd + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/fs" + "os" + "strconv" + "strings" + "time" + + "github.com/icza/backscanner" + "github.com/jsdelivr/globalping-cli/view" + "github.com/spf13/cobra" +) + +var ( + readHistoryErr = "failed to read history: %s" + invalidHistoryItemErr = "invalid history item: %s" + saveToHistoryErr = "failed to save to history: %s" +) + +const ( + // |