diff --git a/README.md b/README.md index c2ac5b3..d116a55 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ 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 in your current session install-probe Join the community powered Globalping platform by running a Docker container. version Print the version number of Globalping CLI @@ -307,6 +308,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-27 11:56:46 | ping google.com +> https://www.jsdelivr.com/globalping?measurement=itcR65tYCqbouXib +- | 2024-03-27 11:57:01 | dns google.com from last +> https://www.jsdelivr.com/globalping?measurement=kWc5UBK9A6G4RUYM +2 | 2024-03-27 11:57:20 | traceroute google.com from New York --limit 2 +> https://www.jsdelivr.com/globalping?measurement=Yz7A1UifUonZsC3C +3 | 2024-03-27 11:57:37 | mtr google.com from New York --limit 2 +> https://www.jsdelivr.com/globalping?measurement=SX1NBgfDKiabM1vZ +4 | 2024-03-27 11:57:52 | http google.com from London,Belgium --limit 2 --method get --ci +> https://www.jsdelivr.com/globalping?measurement=eclwFSYX0zgU10Cs +``` + #### 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..66bbe15 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 @@ -78,6 +82,7 @@ func (r *Root) getLocations() ([]globalping.Locations, error) { if mId == "" { mId = strings.TrimSpace(fromArr[0]) } else { + r.ctx.IsLocationFromSession = true r.ctx.RecordToSession = false } return []globalping.Locations{{Magic: mId}}, nil @@ -182,19 +187,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 +209,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 +237,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 +287,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..9053d1a 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,11 @@ 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.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) } @@ -68,6 +69,7 @@ func (r *Root) RunDNS(cmd *cobra.Command, args []string) error { return err } + defer r.UpdateHistory() r.ctx.RecordToSession = true opts := &globalping.MeasurementCreate{ @@ -100,7 +102,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..870c259 100644 --- a/cmd/dns_test.go +++ b/cmd/dns_test.go @@ -37,10 +37,13 @@ func Test_Execute_DNS_Default(t *testing.T) { viewerMock := mocks.NewMockViewer(ctrl) viewerMock.EXPECT().Output(measurementID1, expectedOpts).Times(1).Return(nil) + timeMock := mocks.NewMockTime(ctrl) + timeMock.EXPECT().Now().Return(defaultCurrentTime).AnyTimes() + w := new(bytes.Buffer) printer := view.NewPrinter(nil, w, w) - ctx := createDefaultContext() - root := NewRoot(printer, ctx, viewerMock, nil, gbMock, nil) + ctx := createDefaultContext("dns") + root := NewRoot(printer, ctx, viewerMock, timeMock, gbMock, nil) os.Args = []string{"globalping", "dns", "jsdelivr.com", "from", "Berlin", @@ -67,6 +70,15 @@ func Test_Execute_DNS_Default(t *testing.T) { b, err := os.ReadFile(getMeasurementsPath()) assert.NoError(t, err) - expectedHistory := []byte(measurementID1 + "\n") - assert.Equal(t, expectedHistory, b) + expectedHistory := measurementID1 + "\n" + assert.Equal(t, expectedHistory, string(b)) + + b, err = os.ReadFile(getHistoryPath()) + assert.NoError(t, err) + expectedHistory = createDefaultExpectedHistoryLogItem( + "1", + measurementID1, + "dns jsdelivr.com from Berlin --limit 2 --type MX --resolver 1.1.1.1 --port 99 --protocol tcp --trace", + ) + assert.Equal(t, expectedHistory, string(b)) } diff --git a/cmd/history.go b/cmd/history.go new file mode 100644 index 0000000..e19534a --- /dev/null +++ b/cmd/history.go @@ -0,0 +1,246 @@ +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 ( + ErrReadHistory = errors.New("failed to read history") +) + +var ( + invalidHistoryItemErr = "invalid history item: %s" + saveToHistoryErr = "failed to save to history: %s" +) + +const ( + // ||