From 794f2311eff605b497eaac04551aa3475357478b Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 6 Mar 2022 18:49:21 -0600 Subject: [PATCH 01/34] feat: add log of previous run's task hashes cleared with every run and repopulated even for cached tasks, plain format with single hash per line --- cli/internal/cache/cache_fs.go | 43 +++++++++++++++++++++++++++++++--- cli/internal/run/run.go | 2 ++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/cli/internal/cache/cache_fs.go b/cli/internal/cache/cache_fs.go index 90187e907e870..560ca93a6d4f4 100644 --- a/cli/internal/cache/cache_fs.go +++ b/cli/internal/cache/cache_fs.go @@ -1,11 +1,14 @@ package cache import ( + "bufio" "encoding/json" "fmt" - "runtime" - "path/filepath" "io/ioutil" + "os" + "path/filepath" + "runtime" + "github.com/vercel/turborepo/cli/internal/analytics" "github.com/vercel/turborepo/cli/internal/config" "github.com/vercel/turborepo/cli/internal/fs" @@ -67,7 +70,7 @@ func (f *fsCache) logFetch(hit bool, hash string, duration int) { func (f *fsCache) Put(target, hash string, duration int, files []string) error { g := new(errgroup.Group) - numDigesters := runtime.NumCPU() + numDigesters := runtime.NumCPU() fileQueue := make(chan string, numDigesters) for i := 0; i < numDigesters; i++ { @@ -151,3 +154,37 @@ func ReadCacheMetaFile(path string) (*CacheMetadata, error) { } return &config, nil } + +// AppendHashesFile adds a hash to a file at path +// Note: naively assuming locks are not needed +func AppendHashesFile(path string, hash string) error { + file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + + defer file.Close() + + if _, err = file.WriteString(hash + "\n"); err != nil { + return err + } + + return nil +} + +// ReadHashesFile reads hashes stored line by line from a file at path +func ReadHashesFile(path string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + + defer file.Close() + + var hashes []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + hashes = append(hashes, scanner.Text()) + } + return hashes, scanner.Err() +} diff --git a/cli/internal/run/run.go b/cli/internal/run/run.go index 0c5a4b8ae4c46..75be37ec7d1ae 100644 --- a/cli/internal/run/run.go +++ b/cli/internal/run/run.go @@ -331,6 +331,7 @@ func (c *RunCommand) runOperation(g *completeGraph, rs *runSpec, backend *api.La } analyticsClient := analytics.NewClient(goctx, analyticsSink, c.Config.Logger.Named("analytics")) defer analyticsClient.CloseWithTimeout(50 * time.Millisecond) + os.Remove(filepath.Join(rs.Opts.cacheFolder, "last-run.log")) turboCache := cache.New(c.Config, analyticsClient) defer turboCache.Shutdown() @@ -532,6 +533,7 @@ func (c *RunCommand) runOperation(g *completeGraph, rs *runSpec, backend *api.La } logFileName := filepath.Join(pack.Dir, ".turbo", fmt.Sprintf("turbo-%v.log", task)) targetLogger.Debug("log file", "path", filepath.Join(rs.Opts.cwd, logFileName)) + cache.AppendHashesFile(filepath.Join(rs.Opts.cacheFolder, "last-run.log"), hash) // Cache --------------------------------------------- var hit bool From 7a41d354fb2e9e02a910abdee82c9c095a152f51 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 6 Mar 2022 18:49:34 -0600 Subject: [PATCH 02/34] feat: add logs command to review log replays --- cli/cmd/turbo/main.go | 3 + cli/internal/run/logs.go | 380 ++++++++++++++++++ .../docs/reference/command-line-reference.mdx | 96 +++++ 3 files changed, 479 insertions(+) create mode 100644 cli/internal/run/logs.go diff --git a/cli/cmd/turbo/main.go b/cli/cmd/turbo/main.go index 69c994c677f8c..83e43cd65e0cd 100644 --- a/cli/cmd/turbo/main.go +++ b/cli/cmd/turbo/main.go @@ -74,6 +74,9 @@ func main() { return &run.RunCommand{Config: cf, Ui: ui, Processes: processes}, nil }, + "logs": func() (cli.Command, error) { + return &run.LogsCommand{Config: cf, Ui: ui}, nil + }, "prune": func() (cli.Command, error) { return &prune.PruneCommand{Config: cf, Ui: ui}, nil }, diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go new file mode 100644 index 0000000000000..3d1fb57fc402c --- /dev/null +++ b/cli/internal/run/logs.go @@ -0,0 +1,380 @@ +package run + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/vercel/turborepo/cli/internal/cache" + "github.com/vercel/turborepo/cli/internal/config" + "github.com/vercel/turborepo/cli/internal/fs" + "github.com/vercel/turborepo/cli/internal/globby" + "github.com/vercel/turborepo/cli/internal/ui" + "github.com/vercel/turborepo/cli/internal/util" + + "github.com/fatih/color" + "github.com/hashicorp/go-hclog" + "github.com/mitchellh/cli" + "github.com/pkg/errors" +) + +// LogsCommand is a Command implementation that allows the user to view log replays +type LogsCommand struct { + Config *config.Config + Ui *cli.ColoredUi +} + +// hashMetadata represents the files and duration metadata associated with +// a hash of a task +type hashMetadata struct { + Hash string + ReplayPaths []string + Duration int +} + +// Synopsis of run command +func (c *LogsCommand) Synopsis() string { + return "Review the most recently run tasks logs" +} + +// Help returns information about the `run` command +func (c *LogsCommand) Help() string { + helpText := ` +Usage: turbo logs [--list] [] + + Review the most recently run tasks logs. + +Options: + --help Show this message. + --list List the hashes available for viewing from the cache. + --list-format Specify output format for --list. Use hash to only + show the hashes. Use relevant to include metadata + relevant to sorting if any. (default hash) + --all Show all results, not just results from the most + recent run command execution. + --sort Set mode to order results. Use task to order by the + previous run's execution order. Use duration to order + by time taken (lowest to highest). Use alnum to order + alphanumerically. Normally defaults to task. If + specific hashes are given, defaults to their order. + If --all, defaults to alnum. (default task) + --reverse Reverse order while sorting. + --progress Set type of replay output (standard|reduced). + Use reduced to hide cached task logs. + (default standard) + --cache-dir Specify local filesystem cache directory. + (default "./node_modules/.cache/turbo") + --last-run-path Specify path to last run file to load. + (default <--cache-dir>/last-run.log) +` + return strings.TrimSpace(helpText) +} + +// Run finds and replays task logs in the monorepo +func (c *LogsCommand) Run(args []string) int { + logsOptions, err := parseLogsArgs(args, c.Ui) + if err != nil { + c.logError(c.Config.Logger, "", err) + return 1 + } + + c.Config.Logger.Trace("lastRunPath", "value", logsOptions.lastRunPath) + c.Config.Logger.Trace("list", "value", logsOptions.listOnly) + c.Config.Logger.Trace("sort", "value", logsOptions.sortType) + c.Config.Logger.Trace("reverse", "value", logsOptions.reverseSort) + + var lastRunHashes []string + if logsOptions.sortType == "task" || !logsOptions.includeAll { + if !fs.FileExists(logsOptions.lastRunPath) { + c.logError(c.Config.Logger, "", fmt.Errorf("failed to resolve last run file: %v", logsOptions.lastRunPath)) + return 1 + } + lastRunHashes, err = cache.ReadHashesFile(logsOptions.lastRunPath) + if err != nil { + c.logError(c.Config.Logger, "", err) + return 1 + } + } + c.Config.Logger.Trace("lastRunHashes", "value", lastRunHashes) + + specificHashes := lastRunHashes + if len(logsOptions.queryHashes) > 0 { + specificHashes = logsOptions.queryHashes + } + + // find and collect cached hashes and durations from metadata json + var hashes []hashMetadata + if len(specificHashes) > 0 { + for _, hash := range specificHashes { + replayPaths := globby.GlobFiles(filepath.Join(logsOptions.cacheFolder, hash, ".turbo"), []string{"turbo-*.log"}, []string{}) + metadataPath := filepath.Join(logsOptions.cacheFolder, hash+"-meta.json") + metadata, err := cache.ReadCacheMetaFile(metadataPath) + duration := -1 + if err == nil { + duration = metadata.Duration + } else { + c.logWarning(c.Config.Logger, "", fmt.Errorf("cannot read metadata file: %v: %w", metadataPath, err)) + } + hashes = append(hashes, hashMetadata{ + Hash: hash, + ReplayPaths: replayPaths, + Duration: duration, + }) + } + } else { + metadataPaths := globby.GlobFiles(logsOptions.cacheFolder, []string{"*-meta.json"}, []string{}) + for _, metadataPath := range metadataPaths { + metadata, err := cache.ReadCacheMetaFile(metadataPath) + if err != nil { + c.logWarning(c.Config.Logger, "", fmt.Errorf("cannot read metadata file: %v: %w", metadataPath, err)) + continue + } + replayPaths := globby.GlobFiles(filepath.Join(logsOptions.cacheFolder, metadata.Hash, ".turbo"), []string{"turbo-*.log"}, []string{}) + hashes = append(hashes, hashMetadata{ + Hash: metadata.Hash, + ReplayPaths: replayPaths, + Duration: metadata.Duration, + }) + } + } + c.Config.Logger.Trace("hashes before sort", "value", hashes) + + // sort task list + cmp := createAlnumComparator(hashes, logsOptions.reverseSort) + if logsOptions.sortType == "duration" { + cmp = createDurationComparator(hashes, logsOptions.reverseSort) + } else if logsOptions.sortType == "task" { + cmp = createReferenceIndexComparator(hashes, lastRunHashes, logsOptions.reverseSort) + } else if logsOptions.sortType == "query" { + cmp = createReferenceIndexComparator(hashes, logsOptions.queryHashes, logsOptions.reverseSort) + } + sort.SliceStable(hashes, cmp) + + // output replay logs from sorted task list + for _, hash := range hashes { + if logsOptions.listOnly { + if logsOptions.listType == "relevant" { + if logsOptions.sortType == "duration" { + c.Ui.Output(fmt.Sprintf("%v %v", hash.Hash, hash.Duration)) + continue + } + } + c.Ui.Output(hash.Hash) + continue + } + if len(hash.ReplayPaths) == 0 { + c.Ui.Output("No logs found to replay.") + } + for _, replayPath := range hash.ReplayPaths { + file, err := os.Open(replayPath) + if err != nil { + c.logWarning(c.Config.Logger, "", fmt.Errorf("error reading logs: %w", err)) + continue + } + defer file.Close() + scan := bufio.NewScanner(file) + if logsOptions.progressType == "reduced" { + scan.Scan() + c.Ui.Output(ui.StripAnsi(string(scan.Bytes()))) + } else { + for scan.Scan() { + c.Ui.Output(ui.StripAnsi(string(scan.Bytes()))) + } + } + } + } + + return 0 +} + +func createDurationComparator(hashes []hashMetadata, reverse bool) func(int, int) bool { + if reverse { + return func(i, j int) bool { + return hashes[i].Duration > hashes[j].Duration + } + } + return func(i, j int) bool { + return hashes[i].Duration <= hashes[j].Duration + } +} + +func createAlnumComparator(hashes []hashMetadata, reverse bool) func(int, int) bool { + if reverse { + return func(i, j int) bool { + return hashes[i].Hash > hashes[j].Hash + } + } + return func(i, j int) bool { + return hashes[i].Hash <= hashes[j].Hash + } +} + +func createReferenceIndexComparator(hashes []hashMetadata, refHashes []string, reverse bool) func(int, int) bool { + hashToIndex := make(map[string]int) + for i, hash := range refHashes { + hashToIndex[hash] = i + } + if reverse { + return func(i, j int) bool { + return hashToIndex[hashes[i].Hash] > hashToIndex[hashes[j].Hash] + } + } + return func(i, j int) bool { + return hashToIndex[hashes[i].Hash] <= hashToIndex[hashes[j].Hash] + } +} + +// LogsOptions holds the current run operations configuration +type LogsOptions struct { + // Current working directory + cwd string + // Cache folder + cacheFolder string + // Only output task hashes + listOnly bool + // Adjust output format + // hash - only show hash + // relevant - include relevant metadata + listType string + // Show all results, not only from the last run + includeAll bool + // Path to last run file + lastRunPath string + // Order by + // task - last run's execution order + // duration - duration of each task (low to high) + // alnum - alphanumerically on hash string + // query - match order of queryHashes + sortType string + // True to reverse output order + reverseSort bool + // List of requested hashes to retrieve + // in user-provided order + queryHashes []string + // Replay task logs output mode + progressType string +} + +func getDefaultLogsOptions() *LogsOptions { + return &LogsOptions{ + listOnly: false, + listType: "hash", + includeAll: false, + sortType: "task", + reverseSort: false, + progressType: "standard", + } +} + +func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { + var logsOptions = getDefaultLogsOptions() + + cwd, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("invalid working directory: %w", err) + } + logsOptions.cwd = cwd + + unresolvedCacheFolder := filepath.FromSlash("./node_modules/.cache/turbo") + unresolvedLastRunPath := "" + unresolvedSortType := "" + queryHashesSet := make(util.Set) + + for _, arg := range args { + if strings.HasPrefix(arg, "--") { + switch { + case strings.HasPrefix(arg, "--cwd="): + if len(arg[len("--cwd="):]) > 0 { + logsOptions.cwd = arg[len("--cwd="):] + } else { + logsOptions.cwd = cwd + } + case arg == "--list": + logsOptions.listOnly = true + case strings.HasPrefix(arg, "--list-format="): + if len(arg[len("--list-format="):]) > 0 { + logsOptions.listType = arg[len("--list-format="):] + } + case arg == "--all": + logsOptions.includeAll = true + case strings.HasPrefix(arg, "--sort="): + if len(arg[len("--sort="):]) > 0 { + unresolvedSortType = arg[len("--sort="):] + if unresolvedSortType != "task" && unresolvedSortType != "duration" && unresolvedSortType != "alnum" { + return nil, fmt.Errorf("invalid value %v for --sort CLI flag. This should be task, duration, or alnum", unresolvedSortType) + } + } + case arg == "--reverse": + logsOptions.reverseSort = true + case strings.HasPrefix(arg, "--cache-dir"): + unresolvedCacheFolder = arg[len("--cache-dir="):] + case strings.HasPrefix(arg, "--last-run-path="): + unresolvedLastRunPath = arg[len("--last-run-path="):] + case strings.HasPrefix(arg, "--progress"): + if len(arg[len("--progress="):]) > 0 { + progressType := arg[len("--progress="):] + if progressType != "standard" && progressType != "reduced" { + output.Warn(fmt.Sprintf("[WARNING] unknown value %v for --progress CLI flag. Falling back to standard", progressType)) + progressType = "standard" + } + logsOptions.progressType = progressType + } + default: + return nil, errors.New(fmt.Sprintf("unknown flag: %v", arg)) + } + } else if !strings.HasPrefix(arg, "-") { + if !queryHashesSet.Includes(arg) { + queryHashesSet.Add(arg) + logsOptions.queryHashes = append(logsOptions.queryHashes, arg) + } + } + } + + // We can only set sortType once we know what the default should + // be and whether or not it has been overridden + if len(logsOptions.queryHashes) > 0 && unresolvedSortType == "" { + unresolvedSortType = "query" + } + if logsOptions.includeAll && unresolvedSortType == "" { + unresolvedSortType = "alnum" + } + if unresolvedSortType != "" { + logsOptions.sortType = unresolvedSortType + } + + // We can only set this cache folder after we know actual cwd + logsOptions.cacheFolder = filepath.Join(logsOptions.cwd, unresolvedCacheFolder) + // We can only set lastRunPath after we know the final cacheFolder path + // and whether or not it has been overridden + if unresolvedLastRunPath == "" { + unresolvedLastRunPath = filepath.Join(logsOptions.cacheFolder, "last-run.log") + } + logsOptions.lastRunPath = unresolvedLastRunPath + + return logsOptions, nil +} + +// logWarning logs an error and outputs it to the UI as a warning. +func (c *LogsCommand) logWarning(log hclog.Logger, prefix string, err error) { + log.Warn(prefix, "warning", err) + + if prefix != "" { + prefix = " " + prefix + ": " + } + + c.Ui.Error(fmt.Sprintf("%s%s%s", ui.WARNING_PREFIX, prefix, color.YellowString(" %v", err))) +} + +// logError logs an error and outputs it to the UI. +func (c *LogsCommand) logError(log hclog.Logger, prefix string, err error) { + log.Error(prefix, "error", err) + + if prefix != "" { + prefix += ": " + } + + c.Ui.Error(fmt.Sprintf("%s%s%s", ui.ERROR_PREFIX, prefix, color.RedString(" %v", err))) +} diff --git a/docs/pages/docs/reference/command-line-reference.mdx b/docs/pages/docs/reference/command-line-reference.mdx index bcb04927e8c8f..aeaf48b7cce4c 100644 --- a/docs/pages/docs/reference/command-line-reference.mdx +++ b/docs/pages/docs/reference/command-line-reference.mdx @@ -450,3 +450,99 @@ Unlink the current directory from the Remote Cache. ## `turbo bin` Get the path to the Turbo binary. + +## `turbo logs ` + +Review the most recently run tasks logs. Looks for cached logs in [`--cache-dir`](#--cache-dir) and previous run history in [`--last-run-path`](#--last-run-path). + +### Options + +#### `--cache-dir` + +`type: string` + +Defaults to `./node_modules/.cache/turbo`. Specify local filesystem cache directory. + +```sh +turbo logs --cache-dir="./my-cache" +``` + +#### `--last-run-path` + +`type: string` + +Defaults to `<--cache-dir>/last-run.log`. Specify local filesystem log file. Not for general use cases. Useful for processing backed up or manually-created last run log files. + +```sh +turbo logs --last-run-path="./my-cache/my-log.txt" +turbo logs --cache-dir="./my-cache" --last-run-path="./my-logs/my-log.txt" +``` + +#### `--list` + +`type: boolean` + +Defaults to `false`. Only list the hashes available for viewing from the cache. + +```sh +turbo logs --list +``` + +#### `--list-format` + +`type: string` + +Defaults to `hash`. Specify output format for [`--list`](#--list). Use `hash` to only show the hashes. Use `relevant` to include metadata relevant to sorting if any. + +```sh +turbo logs --list --list-format=hash +turbo logs --list --list-format=relevant --sort=duration +``` + +#### `--all` + +`type: boolean` + +Defaults to `false`. Show all results, not just results from the most recent run command execution. + +```sh +turbo logs --all +turbo logs --list --all +``` + +#### `--sort` + +`type: string` + +Defaults to `task`. Set mode to order results. Use `task` to sort by the previous run's execution order (requires the file at [`--last-run-path`](#--last-run-path) to exist). Use `duration` to sort by time taken per task hash (lowest to highest). Use `alnum` to order by hash alphanumerically. Normally defaults to task. If specific hashes are given, defaults to the order they are given in. If [`--all`](#--all), defaults to alnum. + +```sh +turbo logs --sort=task +turbo logs --sort=duration +turbo logs --sort=alnum +turbo logs hash1 hash2 hash3 +turbo logs --list --sort=duration +``` + +#### `--reverse` + +`type: boolean` + +Defaults to `false`. Reverse order while sorting. + +```sh +turbo logs --sort=task --reverse +turbo logs hash1 hash2 hash3 --reverse +turbo logs --list --sort=duration --reverse +``` + +#### `--progress` + +`type: string` + +Defaults to `standard`. Set type of replay output. Use `standard` for normal output. Use `reduced` to hide cached task logs and only show what changed. Has no effect when [`--list`](#--list) is given. + +```shell +turbo logs --progress=standard +turbo logs --progress=reduced +``` From 2c92ee9b4178564c83b174d3c3f2d1ef5aa194d9 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 13 Mar 2022 15:19:35 -0500 Subject: [PATCH 03/34] refactor: match refactor in run command --- cli/internal/run/logs.go | 41 ++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 3d1fb57fc402c..4181f2d37bd80 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -62,9 +62,9 @@ Options: specific hashes are given, defaults to their order. If --all, defaults to alnum. (default task) --reverse Reverse order while sorting. - --progress Set type of replay output (standard|reduced). - Use reduced to hide cached task logs. - (default standard) + --output-logs Set type of replay output logs. Use full to show all + output. Use hash-only to show only the turbo-computed + task hash lines. (default full) --cache-dir Specify local filesystem cache directory. (default "./node_modules/.cache/turbo") --last-run-path Specify path to last run file to load. @@ -176,7 +176,7 @@ func (c *LogsCommand) Run(args []string) int { } defer file.Close() scan := bufio.NewScanner(file) - if logsOptions.progressType == "reduced" { + if logsOptions.outputLogsMode == "hash" { scan.Scan() c.Ui.Output(ui.StripAnsi(string(scan.Bytes()))) } else { @@ -255,17 +255,19 @@ type LogsOptions struct { // in user-provided order queryHashes []string // Replay task logs output mode - progressType string + // full - show all, + // hash - only show task hash + outputLogsMode string } func getDefaultLogsOptions() *LogsOptions { return &LogsOptions{ - listOnly: false, - listType: "hash", - includeAll: false, - sortType: "task", - reverseSort: false, - progressType: "standard", + listOnly: false, + listType: "hash", + includeAll: false, + sortType: "task", + reverseSort: false, + outputLogsMode: "full", } } @@ -313,14 +315,17 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { unresolvedCacheFolder = arg[len("--cache-dir="):] case strings.HasPrefix(arg, "--last-run-path="): unresolvedLastRunPath = arg[len("--last-run-path="):] - case strings.HasPrefix(arg, "--progress"): - if len(arg[len("--progress="):]) > 0 { - progressType := arg[len("--progress="):] - if progressType != "standard" && progressType != "reduced" { - output.Warn(fmt.Sprintf("[WARNING] unknown value %v for --progress CLI flag. Falling back to standard", progressType)) - progressType = "standard" + case strings.HasPrefix(arg, "--output-logs"): + outputLogsMode := arg[len("--output-logs="):] + if len(outputLogsMode) > 0 { + switch outputLogsMode { + case "full": + logsOptions.outputLogsMode = outputLogsMode + case "hash-only": + logsOptions.outputLogsMode = "hash" + default: + output.Warn(fmt.Sprintf("[WARNING] unknown value %v for --output-logs CLI flag. Falling back to full", outputLogsMode)) } - logsOptions.progressType = progressType } default: return nil, errors.New(fmt.Sprintf("unknown flag: %v", arg)) From 52e61afa0a6026c0ddb8d2d18c34fc30be45e256 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 13 Mar 2022 15:59:18 -0500 Subject: [PATCH 04/34] feat: add info logging with color --- cli/internal/run/logs.go | 7 +++++++ cli/internal/ui/ui.go | 1 + 2 files changed, 8 insertions(+) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 4181f2d37bd80..d1c669f29dd8e 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -383,3 +383,10 @@ func (c *LogsCommand) logError(log hclog.Logger, prefix string, err error) { c.Ui.Error(fmt.Sprintf("%s%s%s", ui.ERROR_PREFIX, prefix, color.RedString(" %v", err))) } + +// logInfo logs an info message and outputs it to the UI. +func (c *LogsCommand) logInfo(log hclog.Logger, message string) { + log.Info(message) + + c.Ui.Info(fmt.Sprintf("%s%s", ui.INFO_PREFIX, color.BlueString(" %v", message))) +} diff --git a/cli/internal/ui/ui.go b/cli/internal/ui/ui.go index c4c9852ee0579..d69dcd53eb823 100644 --- a/cli/internal/ui/ui.go +++ b/cli/internal/ui/ui.go @@ -18,6 +18,7 @@ var IsTTY = isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdo var IsCI = os.Getenv("CI") == "true" || os.Getenv("BUILD_NUMBER") == "true" || os.Getenv("TEAMCITY_VERSION") != "" var gray = color.New(color.Faint) var bold = color.New(color.Bold) +var INFO_PREFIX = color.New(color.Bold, color.FgBlue, color.ReverseVideo).Sprint(" INFO ") var ERROR_PREFIX = color.New(color.Bold, color.FgRed, color.ReverseVideo).Sprint(" ERROR ") var WARNING_PREFIX = color.New(color.Bold, color.FgYellow, color.ReverseVideo).Sprint(" WARNING ") From aea2fd5853296678e38be3fad16bd9ed1d21dba0 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 13 Mar 2022 16:01:08 -0500 Subject: [PATCH 05/34] feat: suggest --all flag if no last run log --- cli/internal/run/logs.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index d1c669f29dd8e..67d6914c92c33 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -90,6 +90,10 @@ func (c *LogsCommand) Run(args []string) int { if logsOptions.sortType == "task" || !logsOptions.includeAll { if !fs.FileExists(logsOptions.lastRunPath) { c.logError(c.Config.Logger, "", fmt.Errorf("failed to resolve last run file: %v", logsOptions.lastRunPath)) + metadataPaths := globby.GlobFiles(logsOptions.cacheFolder, []string{"*-meta.json"}, []string{}) + if len(metadataPaths) > 0 { + c.logInfo(c.Config.Logger, "other logs found, use --all to view them") + } return 1 } lastRunHashes, err = cache.ReadHashesFile(logsOptions.lastRunPath) From 4038ba2eb202c9b0c6a882eb931d07f6e7d34b39 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 13 Mar 2022 16:02:20 -0500 Subject: [PATCH 06/34] fix: specify which hash did not have replay logs --- cli/internal/run/logs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 67d6914c92c33..7c08c6a311af8 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -170,7 +170,7 @@ func (c *LogsCommand) Run(args []string) int { continue } if len(hash.ReplayPaths) == 0 { - c.Ui.Output("No logs found to replay.") + c.logInfo(c.Config.Logger, fmt.Sprintf("%v: no logs found to replay", hash.Hash)) } for _, replayPath := range hash.ReplayPaths { file, err := os.Open(replayPath) From 99bfbc9cf8de17a01471e6eef9a2c99567bf5373 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 13 Mar 2022 16:06:52 -0500 Subject: [PATCH 07/34] docs: update --progress to --output-logs flag --- docs/pages/docs/reference/command-line-reference.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/pages/docs/reference/command-line-reference.mdx b/docs/pages/docs/reference/command-line-reference.mdx index aeaf48b7cce4c..9854e95ffbbe4 100644 --- a/docs/pages/docs/reference/command-line-reference.mdx +++ b/docs/pages/docs/reference/command-line-reference.mdx @@ -536,13 +536,13 @@ turbo logs hash1 hash2 hash3 --reverse turbo logs --list --sort=duration --reverse ``` -#### `--progress` +#### `--output-logs` `type: string` -Defaults to `standard`. Set type of replay output. Use `standard` for normal output. Use `reduced` to hide cached task logs and only show what changed. Has no effect when [`--list`](#--list) is given. +Defaults to `full`. Set type of replay output logs. Use `full` to show all replay logs output. Use `hash-only` to show only the turbo-computed task hash lines. Has no effect when [`--list`](#--list) is given. ```shell -turbo logs --progress=standard -turbo logs --progress=reduced +turbo logs --output-logs=full +turbo logs --output-logs=hash-only ``` From 48a93186b49e7f57095ce5c4403681fc9eea1a82 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 13 Mar 2022 16:56:50 -0500 Subject: [PATCH 08/34] fix: use more loose glob for replay logs --- cli/internal/run/logs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 7c08c6a311af8..c480f859e2b84 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -113,7 +113,7 @@ func (c *LogsCommand) Run(args []string) int { var hashes []hashMetadata if len(specificHashes) > 0 { for _, hash := range specificHashes { - replayPaths := globby.GlobFiles(filepath.Join(logsOptions.cacheFolder, hash, ".turbo"), []string{"turbo-*.log"}, []string{}) + replayPaths := globby.GlobFiles(filepath.Join(logsOptions.cacheFolder, hash), []string{"**/.turbo/turbo-*.log"}, []string{}) metadataPath := filepath.Join(logsOptions.cacheFolder, hash+"-meta.json") metadata, err := cache.ReadCacheMetaFile(metadataPath) duration := -1 From 06188990ae6cd2a842957a53d49beaf64976214a Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Tue, 15 Mar 2022 22:02:38 -0500 Subject: [PATCH 09/34] refactor: sort mode strings should be enums --- cli/internal/run/logs.go | 48 +++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index c480f859e2b84..58e60431a4702 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -35,6 +35,16 @@ type hashMetadata struct { Duration int } +type SortMode string + +const ( + TaskSort SortMode = "task" + DurationSort SortMode = "duration" + AlnumSort SortMode = "alnum" + QuerySort SortMode = "query" + NothingSort SortMode = "n/a" +) + // Synopsis of run command func (c *LogsCommand) Synopsis() string { return "Review the most recently run tasks logs" @@ -87,7 +97,7 @@ func (c *LogsCommand) Run(args []string) int { c.Config.Logger.Trace("reverse", "value", logsOptions.reverseSort) var lastRunHashes []string - if logsOptions.sortType == "task" || !logsOptions.includeAll { + if logsOptions.sortType == TaskSort || !logsOptions.includeAll { if !fs.FileExists(logsOptions.lastRunPath) { c.logError(c.Config.Logger, "", fmt.Errorf("failed to resolve last run file: %v", logsOptions.lastRunPath)) metadataPaths := globby.GlobFiles(logsOptions.cacheFolder, []string{"*-meta.json"}, []string{}) @@ -148,11 +158,11 @@ func (c *LogsCommand) Run(args []string) int { // sort task list cmp := createAlnumComparator(hashes, logsOptions.reverseSort) - if logsOptions.sortType == "duration" { + if logsOptions.sortType == DurationSort { cmp = createDurationComparator(hashes, logsOptions.reverseSort) - } else if logsOptions.sortType == "task" { + } else if logsOptions.sortType == TaskSort { cmp = createReferenceIndexComparator(hashes, lastRunHashes, logsOptions.reverseSort) - } else if logsOptions.sortType == "query" { + } else if logsOptions.sortType == QuerySort { cmp = createReferenceIndexComparator(hashes, logsOptions.queryHashes, logsOptions.reverseSort) } sort.SliceStable(hashes, cmp) @@ -161,7 +171,7 @@ func (c *LogsCommand) Run(args []string) int { for _, hash := range hashes { if logsOptions.listOnly { if logsOptions.listType == "relevant" { - if logsOptions.sortType == "duration" { + if logsOptions.sortType == DurationSort { c.Ui.Output(fmt.Sprintf("%v %v", hash.Hash, hash.Duration)) continue } @@ -252,7 +262,7 @@ type LogsOptions struct { // duration - duration of each task (low to high) // alnum - alphanumerically on hash string // query - match order of queryHashes - sortType string + sortType SortMode // True to reverse output order reverseSort bool // List of requested hashes to retrieve @@ -261,6 +271,7 @@ type LogsOptions struct { // Replay task logs output mode // full - show all, // hash - only show task hash + // TODO: refactor to use run.LogsMode outputLogsMode string } @@ -286,7 +297,7 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { unresolvedCacheFolder := filepath.FromSlash("./node_modules/.cache/turbo") unresolvedLastRunPath := "" - unresolvedSortType := "" + unresolvedSortType := NothingSort queryHashesSet := make(util.Set) for _, arg := range args { @@ -308,9 +319,16 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { logsOptions.includeAll = true case strings.HasPrefix(arg, "--sort="): if len(arg[len("--sort="):]) > 0 { - unresolvedSortType = arg[len("--sort="):] - if unresolvedSortType != "task" && unresolvedSortType != "duration" && unresolvedSortType != "alnum" { - return nil, fmt.Errorf("invalid value %v for --sort CLI flag. This should be task, duration, or alnum", unresolvedSortType) + inputSortType := arg[len("--sort="):] + switch inputSortType { + case "task": + unresolvedSortType = TaskSort + case "duration": + unresolvedSortType = DurationSort + case "alnum": + unresolvedSortType = AlnumSort + default: + return nil, fmt.Errorf("invalid value %v for --sort CLI flag. This should be task, duration, or alnum", inputSortType) } } case arg == "--reverse": @@ -344,13 +362,13 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { // We can only set sortType once we know what the default should // be and whether or not it has been overridden - if len(logsOptions.queryHashes) > 0 && unresolvedSortType == "" { - unresolvedSortType = "query" + if len(logsOptions.queryHashes) > 0 && unresolvedSortType == NothingSort { + unresolvedSortType = QuerySort } - if logsOptions.includeAll && unresolvedSortType == "" { - unresolvedSortType = "alnum" + if logsOptions.includeAll && unresolvedSortType == NothingSort { + unresolvedSortType = AlnumSort } - if unresolvedSortType != "" { + if unresolvedSortType != NothingSort { logsOptions.sortType = unresolvedSortType } From a4ced7f751fb7a6bbad4752c39fe9f2171a3b497 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Tue, 15 Mar 2022 22:22:20 -0500 Subject: [PATCH 10/34] feat: add time sorting --- cli/internal/run/logs.go | 44 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 58e60431a4702..53cba5d98a2a8 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -7,6 +7,7 @@ import ( "path/filepath" "sort" "strings" + "time" "github.com/vercel/turborepo/cli/internal/cache" "github.com/vercel/turborepo/cli/internal/config" @@ -33,6 +34,7 @@ type hashMetadata struct { Hash string ReplayPaths []string Duration int + Datetime time.Time } type SortMode string @@ -41,6 +43,7 @@ const ( TaskSort SortMode = "task" DurationSort SortMode = "duration" AlnumSort SortMode = "alnum" + DatetimeSort SortMode = "datetime" QuerySort SortMode = "query" NothingSort SortMode = "n/a" ) @@ -67,10 +70,11 @@ Options: recent run command execution. --sort Set mode to order results. Use task to order by the previous run's execution order. Use duration to order - by time taken (lowest to highest). Use alnum to order + by time taken (lowest to highest). Use datetime to + order by timestamp of the log file. Use alnum to order alphanumerically. Normally defaults to task. If specific hashes are given, defaults to their order. - If --all, defaults to alnum. (default task) + If --all, defaults to datetime. (default task) --reverse Reverse order while sorting. --output-logs Set type of replay output logs. Use full to show all output. Use hash-only to show only the turbo-computed @@ -120,6 +124,7 @@ func (c *LogsCommand) Run(args []string) int { } // find and collect cached hashes and durations from metadata json + // also get file modified datetime from filesystem var hashes []hashMetadata if len(specificHashes) > 0 { for _, hash := range specificHashes { @@ -132,10 +137,18 @@ func (c *LogsCommand) Run(args []string) int { } else { c.logWarning(c.Config.Logger, "", fmt.Errorf("cannot read metadata file: %v: %w", metadataPath, err)) } + file, err := os.Stat(metadataPath) + datetime := time.Time{} + if err == nil { + datetime = file.ModTime() + } else { + c.logWarning(c.Config.Logger, "", fmt.Errorf("cannot get modification time of metadata file: %v: %w", metadataPath, err)) + } hashes = append(hashes, hashMetadata{ Hash: hash, ReplayPaths: replayPaths, Duration: duration, + Datetime: datetime, }) } } else { @@ -146,11 +159,19 @@ func (c *LogsCommand) Run(args []string) int { c.logWarning(c.Config.Logger, "", fmt.Errorf("cannot read metadata file: %v: %w", metadataPath, err)) continue } + file, err := os.Stat(metadataPath) + datetime := time.Time{} + if err == nil { + datetime = file.ModTime() + } else { + c.logWarning(c.Config.Logger, "", fmt.Errorf("cannot get modification time of metadata file: %v: %w", metadataPath, err)) + } replayPaths := globby.GlobFiles(filepath.Join(logsOptions.cacheFolder, metadata.Hash, ".turbo"), []string{"turbo-*.log"}, []string{}) hashes = append(hashes, hashMetadata{ Hash: metadata.Hash, ReplayPaths: replayPaths, Duration: metadata.Duration, + Datetime: datetime, }) } } @@ -164,6 +185,8 @@ func (c *LogsCommand) Run(args []string) int { cmp = createReferenceIndexComparator(hashes, lastRunHashes, logsOptions.reverseSort) } else if logsOptions.sortType == QuerySort { cmp = createReferenceIndexComparator(hashes, logsOptions.queryHashes, logsOptions.reverseSort) + } else if logsOptions.sortType == DatetimeSort { + cmp = createDatetimeComparator(hashes, logsOptions.reverseSort) } sort.SliceStable(hashes, cmp) @@ -215,6 +238,17 @@ func createDurationComparator(hashes []hashMetadata, reverse bool) func(int, int } } +func createDatetimeComparator(hashes []hashMetadata, reverse bool) func(int, int) bool { + if reverse { + return func(i, j int) bool { + return hashes[i].Datetime.After(hashes[j].Datetime) + } + } + return func(i, j int) bool { + return hashes[i].Datetime.Before(hashes[j].Datetime) || hashes[i].Datetime.Equal(hashes[j].Datetime) + } +} + func createAlnumComparator(hashes []hashMetadata, reverse bool) func(int, int) bool { if reverse { return func(i, j int) bool { @@ -325,10 +359,12 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { unresolvedSortType = TaskSort case "duration": unresolvedSortType = DurationSort + case "datetime": + unresolvedSortType = DatetimeSort case "alnum": unresolvedSortType = AlnumSort default: - return nil, fmt.Errorf("invalid value %v for --sort CLI flag. This should be task, duration, or alnum", inputSortType) + return nil, fmt.Errorf("invalid value %v for --sort CLI flag. This should be task, duration, datetime, or alnum", inputSortType) } } case arg == "--reverse": @@ -366,7 +402,7 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { unresolvedSortType = QuerySort } if logsOptions.includeAll && unresolvedSortType == NothingSort { - unresolvedSortType = AlnumSort + unresolvedSortType = DatetimeSort } if unresolvedSortType != NothingSort { logsOptions.sortType = unresolvedSortType From a783f00725169f1b53e210260960c08e0fb96296 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sat, 19 Mar 2022 18:10:06 -0500 Subject: [PATCH 11/34] feat: store start time in metadata file start time is ignored in http cache --- cli/internal/cache/async_cache.go | 9 +++++--- cli/internal/cache/cache.go | 35 ++++++++++++++++++------------- cli/internal/cache/cache_fs.go | 26 +++++++++++++---------- cli/internal/cache/cache_http.go | 14 +++++++------ cli/internal/run/run.go | 4 ++-- 5 files changed, 51 insertions(+), 37 deletions(-) diff --git a/cli/internal/cache/async_cache.go b/cli/internal/cache/async_cache.go index 6b5b9244c382c..691a845c09b11 100644 --- a/cli/internal/cache/async_cache.go +++ b/cli/internal/cache/async_cache.go @@ -2,6 +2,7 @@ package cache import ( "sync" + "time" "github.com/vercel/turborepo/cli/internal/config" ) @@ -21,6 +22,7 @@ type asyncCache struct { type cacheRequest struct { target string key string + start time.Time duration int files []string } @@ -37,17 +39,18 @@ func newAsyncCache(realCache Cache, config *config.Config) Cache { return c } -func (c *asyncCache) Put(target string, key string, duration int, files []string) error { +func (c *asyncCache) Put(target string, key string, start time.Time, duration int, files []string) error { c.requests <- cacheRequest{ target: target, key: key, files: files, duration: duration, + start: start, } return nil } -func (c *asyncCache) Fetch(target string, key string, files []string) (bool, []string, int, error) { +func (c *asyncCache) Fetch(target string, key string, files []string) (bool, []string, time.Time, int, error) { return c.realCache.Fetch(target, key, files) } @@ -69,7 +72,7 @@ func (c *asyncCache) Shutdown() { // run implements the actual async logic. func (c *asyncCache) run() { for r := range c.requests { - c.realCache.Put(r.target, r.key, r.duration, r.files) + c.realCache.Put(r.target, r.key, r.start, r.duration, r.files) } c.wg.Done() } diff --git a/cli/internal/cache/cache.go b/cli/internal/cache/cache.go index e1ecb93685994..197133b00115d 100644 --- a/cli/internal/cache/cache.go +++ b/cli/internal/cache/cache.go @@ -3,6 +3,7 @@ package cache import ( "fmt" + "time" "github.com/vercel/turborepo/cli/internal/analytics" "github.com/vercel/turborepo/cli/internal/config" @@ -14,9 +15,9 @@ import ( type Cache interface { // Fetch returns true if there is a cache it. It is expected to move files // into their correct position as a side effect - Fetch(target string, hash string, files []string) (bool, []string, int, error) + Fetch(target string, hash string, files []string) (bool, []string, time.Time, int, error) // Put caches files for a given hash - Put(target string, hash string, duration int, files []string) error + Put(target string, hash string, start time.Time, duration int, files []string) error Clean(target string) CleanAll() Shutdown() @@ -25,11 +26,15 @@ type Cache interface { const cacheEventHit = "HIT" const cacheEventMiss = "MISS" +// notime is the time used to represent invalid or undefined time +var notime = time.Date(0, time.January, 0, 0, 0, 0, 0, time.UTC) + type CacheEvent struct { - Source string `mapstructure:"source"` - Event string `mapstructure:"event"` - Hash string `mapstructure:"hash"` - Duration int `mapstructure:"duration"` + Source string `mapstructure:"source"` + Event string `mapstructure:"event"` + Hash string `mapstructure:"hash"` + Start time.Time `mapstructure:"start"` + Duration int `mapstructure:"duration"` } // New creates a new cache @@ -64,8 +69,8 @@ type cacheMultiplexer struct { caches []Cache } -func (mplex cacheMultiplexer) Put(target string, key string, duration int, files []string) error { - return mplex.storeUntil(target, key, duration, files, len(mplex.caches)) +func (mplex cacheMultiplexer) Put(target string, key string, start time.Time, duration int, files []string) error { + return mplex.storeUntil(target, key, start, duration, files, len(mplex.caches)) } // storeUntil stores artifacts into higher priority caches than the given one. @@ -73,7 +78,7 @@ func (mplex cacheMultiplexer) Put(target string, key string, duration int, files // downloading from the RPC cache. // This is a little inefficient since we could write the file to plz-out then copy it to the dir cache, // but it's hard to fix that without breaking the cache abstraction. -func (mplex cacheMultiplexer) storeUntil(target string, key string, duration int, outputGlobs []string, stopAt int) error { +func (mplex cacheMultiplexer) storeUntil(target string, key string, start time.Time, duration int, outputGlobs []string, stopAt int) error { // Attempt to store on all caches simultaneously. g := &errgroup.Group{} for i, cache := range mplex.caches { @@ -82,7 +87,7 @@ func (mplex cacheMultiplexer) storeUntil(target string, key string, duration int } c := cache g.Go(func() error { - return c.Put(target, key, duration, outputGlobs) + return c.Put(target, key, start, duration, outputGlobs) }) } @@ -93,19 +98,19 @@ func (mplex cacheMultiplexer) storeUntil(target string, key string, duration int return nil } -func (mplex cacheMultiplexer) Fetch(target string, key string, files []string) (bool, []string, int, error) { +func (mplex cacheMultiplexer) Fetch(target string, key string, files []string) (bool, []string, time.Time, int, error) { // Retrieve from caches sequentially; if we did them simultaneously we could // easily write the same file from two goroutines at once. for i, cache := range mplex.caches { - if ok, actualFiles, duration, err := cache.Fetch(target, key, files); ok { + if ok, actualFiles, start, duration, err := cache.Fetch(target, key, files); ok { // Store this into other caches. We can ignore errors here because we know // we have previously successfully stored in a higher-priority cache, and so the overall // result is a success at fetching. Storing in lower-priority caches is an optimization. - mplex.storeUntil(target, key, duration, actualFiles, i) - return ok, actualFiles, duration, err + mplex.storeUntil(target, key, start, duration, actualFiles, i) + return ok, actualFiles, start, duration, err } } - return false, files, 0, nil + return false, files, notime, 0, nil } func (mplex cacheMultiplexer) Clean(target string) { diff --git a/cli/internal/cache/cache_fs.go b/cli/internal/cache/cache_fs.go index d624d9f46dd35..45dbfc6e7088b 100644 --- a/cli/internal/cache/cache_fs.go +++ b/cli/internal/cache/cache_fs.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "runtime" + "time" "github.com/vercel/turborepo/cli/internal/analytics" "github.com/vercel/turborepo/cli/internal/config" @@ -27,31 +28,31 @@ func newFsCache(config *config.Config, recorder analytics.Recorder) Cache { } // Fetch returns true if items are cached. It moves them into position as a side effect. -func (f *fsCache) Fetch(target, hash string, _unusedOutputGlobs []string) (bool, []string, int, error) { +func (f *fsCache) Fetch(target, hash string, _unusedOutputGlobs []string) (bool, []string, time.Time, int, error) { cachedFolder := filepath.Join(f.cacheDirectory, hash) // If it's not in the cache bail now if !fs.PathExists(cachedFolder) { - f.logFetch(false, hash, 0) - return false, nil, 0, nil + f.logFetch(false, hash, notime, 0) + return false, nil, notime, 0, nil } // Otherwise, copy it into position err := fs.RecursiveCopyOrLinkFile(cachedFolder, target, fs.DirPermissions, true, true) if err != nil { // TODO: what event to log here? - return false, nil, 0, fmt.Errorf("error moving artifact from cache into %v: %w", target, err) + return false, nil, notime, 0, fmt.Errorf("error moving artifact from cache into %v: %w", target, err) } meta, err := ReadCacheMetaFile(filepath.Join(f.cacheDirectory, hash+"-meta.json")) if err != nil { - return false, nil, 0, fmt.Errorf("error reading cache metadata: %w", err) + return false, nil, notime, 0, fmt.Errorf("error reading cache metadata: %w", err) } - f.logFetch(true, hash, meta.Duration) - return true, nil, meta.Duration, nil + f.logFetch(true, hash, meta.Start, meta.Duration) + return true, nil, meta.Start, meta.Duration, nil } -func (f *fsCache) logFetch(hit bool, hash string, duration int) { +func (f *fsCache) logFetch(hit bool, hash string, start time.Time, duration int) { var event string if hit { event = cacheEventHit @@ -63,11 +64,12 @@ func (f *fsCache) logFetch(hit bool, hash string, duration int) { Event: event, Hash: hash, Duration: duration, + Start: start, } f.recorder.LogEvent(payload) } -func (f *fsCache) Put(target, hash string, duration int, files []string) error { +func (f *fsCache) Put(target, hash string, start time.Time, duration int, files []string) error { g := new(errgroup.Group) numDigesters := runtime.NumCPU() @@ -102,6 +104,7 @@ func (f *fsCache) Put(target, hash string, duration int, files []string) error { WriteCacheMetaFile(filepath.Join(f.cacheDirectory, hash+"-meta.json"), &CacheMetadata{ Duration: duration, Hash: hash, + Start: start, }) return nil @@ -120,8 +123,9 @@ func (cache *fsCache) Shutdown() {} // CacheMetadata stores duration and hash information for a cache entry so that aggregate Time Saved calculations // can be made from artifacts from various caches type CacheMetadata struct { - Hash string `json:"hash"` - Duration int `json:"duration"` + Hash string `json:"hash"` + Duration int `json:"duration"` + Start time.Time `json:"start"` } // WriteCacheMetaFile writes cache metadata file at a path diff --git a/cli/internal/cache/cache_http.go b/cli/internal/cache/cache_http.go index 667c9277b1c3c..32dadff27c8e0 100644 --- a/cli/internal/cache/cache_http.go +++ b/cli/internal/cache/cache_http.go @@ -41,7 +41,8 @@ var mtime = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) // nobody is the usual uid / gid of the 'nobody' user. const nobody = 65534 -func (cache *httpCache) Put(target, hash string, duration int, files []string) error { +// start is ignored +func (cache *httpCache) Put(target, hash string, start time.Time, duration int, files []string) error { // if cache.writable { cache.requestLimiter.acquire() defer cache.requestLimiter.release() @@ -104,19 +105,19 @@ func (cache *httpCache) storeFile(tw *tar.Writer, name string) error { return err } -func (cache *httpCache) Fetch(target, key string, _unusedOutputGlobs []string) (bool, []string, int, error) { +func (cache *httpCache) Fetch(target, key string, _unusedOutputGlobs []string) (bool, []string, time.Time, int, error) { cache.requestLimiter.acquire() defer cache.requestLimiter.release() hit, files, duration, err := cache.retrieve(key) if err != nil { // TODO: analytics event? - return false, files, duration, fmt.Errorf("failed to retrieve files from HTTP cache: %w", err) + return false, files, notime, duration, fmt.Errorf("failed to retrieve files from HTTP cache: %w", err) } - cache.logFetch(hit, key, duration) - return hit, files, duration, err + cache.logFetch(hit, key, notime, duration) + return hit, files, notime, duration, err } -func (cache *httpCache) logFetch(hit bool, hash string, duration int) { +func (cache *httpCache) logFetch(hit bool, hash string, start time.Time, duration int) { var event string if hit { event = cacheEventHit @@ -128,6 +129,7 @@ func (cache *httpCache) logFetch(hit bool, hash string, duration int) { Event: event, Hash: hash, Duration: duration, + Start: start, } cache.recorder.LogEvent(payload) } diff --git a/cli/internal/run/run.go b/cli/internal/run/run.go index d1cb4d1a8d895..7f4d88fa84215 100644 --- a/cli/internal/run/run.go +++ b/cli/internal/run/run.go @@ -860,7 +860,7 @@ func (e *execContext) exec(pt *packageTask) error { // Cache --------------------------------------------- var hit bool if !e.rs.Opts.forceExecution { - hit, _, _, err = e.turboCache.Fetch(e.rs.Opts.cwd, hash, nil) + hit, _, _, _, err = e.turboCache.Fetch(e.rs.Opts.cwd, hash, nil) if err != nil { targetUi.Error(fmt.Sprintf("error fetching from cache: %s", err)) } else if hit { @@ -978,7 +978,7 @@ func (e *execContext) exec(pt *packageTask) error { targetLogger.Debug("caching output", "outputs", outputs) ignore := []string{} filesToBeCached := globby.GlobFiles(pt.pkg.Dir, outputs, ignore) - if err := e.turboCache.Put(pt.pkg.Dir, hash, int(time.Since(cmdTime).Milliseconds()), filesToBeCached); err != nil { + if err := e.turboCache.Put(pt.pkg.Dir, hash, cmdTime, int(time.Since(cmdTime).Milliseconds()), filesToBeCached); err != nil { e.logError(targetLogger, "", fmt.Errorf("error caching output: %w", err)) } } From 9802b2b2bc16ca0e8b09b2568f490dbca95b947d Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sat, 19 Mar 2022 18:15:01 -0500 Subject: [PATCH 12/34] refactor: sort by task start and end, not fs time --- cli/internal/run/logs.go | 89 +++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 53cba5d98a2a8..c78630e080c26 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -34,20 +34,25 @@ type hashMetadata struct { Hash string ReplayPaths []string Duration int - Datetime time.Time + Start time.Time + End time.Time } type SortMode string const ( - TaskSort SortMode = "task" - DurationSort SortMode = "duration" - AlnumSort SortMode = "alnum" - DatetimeSort SortMode = "datetime" - QuerySort SortMode = "query" - NothingSort SortMode = "n/a" + TaskSort SortMode = "task" + DurationSort SortMode = "duration" + AlnumSort SortMode = "alnum" + StartTimeSort SortMode = "start" + EndTimeSort SortMode = "end" + QuerySort SortMode = "query" + NothingSort SortMode = "n/a" ) +// notime is the time used to represent invalid or undefined time +var notime = time.Date(0, time.January, 0, 0, 0, 0, 0, time.UTC) + // Synopsis of run command func (c *LogsCommand) Synopsis() string { return "Review the most recently run tasks logs" @@ -70,11 +75,12 @@ Options: recent run command execution. --sort Set mode to order results. Use task to order by the previous run's execution order. Use duration to order - by time taken (lowest to highest). Use datetime to - order by timestamp of the log file. Use alnum to order - alphanumerically. Normally defaults to task. If - specific hashes are given, defaults to their order. - If --all, defaults to datetime. (default task) + by time taken (lowest to highest). Use start to order + by task start times. Use end to order by task end + times. Use alnum to order alphanumerically. Normally + defaults to task. If specific hashes are given, + defaults to their order. If --all, defaults to + start. (default task) --reverse Reverse order while sorting. --output-logs Set type of replay output logs. Use full to show all output. Use hash-only to show only the turbo-computed @@ -131,24 +137,20 @@ func (c *LogsCommand) Run(args []string) int { replayPaths := globby.GlobFiles(filepath.Join(logsOptions.cacheFolder, hash), []string{"**/.turbo/turbo-*.log"}, []string{}) metadataPath := filepath.Join(logsOptions.cacheFolder, hash+"-meta.json") metadata, err := cache.ReadCacheMetaFile(metadataPath) - duration := -1 + duration := 0 // in milliseconds + start := notime if err == nil { duration = metadata.Duration + start = metadata.Start } else { c.logWarning(c.Config.Logger, "", fmt.Errorf("cannot read metadata file: %v: %w", metadataPath, err)) } - file, err := os.Stat(metadataPath) - datetime := time.Time{} - if err == nil { - datetime = file.ModTime() - } else { - c.logWarning(c.Config.Logger, "", fmt.Errorf("cannot get modification time of metadata file: %v: %w", metadataPath, err)) - } hashes = append(hashes, hashMetadata{ Hash: hash, ReplayPaths: replayPaths, Duration: duration, - Datetime: datetime, + Start: start, + End: start.Add(time.Duration(duration * 1000)), }) } } else { @@ -159,19 +161,13 @@ func (c *LogsCommand) Run(args []string) int { c.logWarning(c.Config.Logger, "", fmt.Errorf("cannot read metadata file: %v: %w", metadataPath, err)) continue } - file, err := os.Stat(metadataPath) - datetime := time.Time{} - if err == nil { - datetime = file.ModTime() - } else { - c.logWarning(c.Config.Logger, "", fmt.Errorf("cannot get modification time of metadata file: %v: %w", metadataPath, err)) - } replayPaths := globby.GlobFiles(filepath.Join(logsOptions.cacheFolder, metadata.Hash, ".turbo"), []string{"turbo-*.log"}, []string{}) hashes = append(hashes, hashMetadata{ Hash: metadata.Hash, ReplayPaths: replayPaths, Duration: metadata.Duration, - Datetime: datetime, + Start: metadata.Start, + End: metadata.Start.Add(time.Duration(metadata.Duration * 1000)), }) } } @@ -185,8 +181,10 @@ func (c *LogsCommand) Run(args []string) int { cmp = createReferenceIndexComparator(hashes, lastRunHashes, logsOptions.reverseSort) } else if logsOptions.sortType == QuerySort { cmp = createReferenceIndexComparator(hashes, logsOptions.queryHashes, logsOptions.reverseSort) - } else if logsOptions.sortType == DatetimeSort { - cmp = createDatetimeComparator(hashes, logsOptions.reverseSort) + } else if logsOptions.sortType == StartTimeSort { + cmp = createStartTimeComparator(hashes, logsOptions.reverseSort) + } else if logsOptions.sortType == EndTimeSort { + cmp = createEndTimeComparator(hashes, logsOptions.reverseSort) } sort.SliceStable(hashes, cmp) @@ -238,14 +236,25 @@ func createDurationComparator(hashes []hashMetadata, reverse bool) func(int, int } } -func createDatetimeComparator(hashes []hashMetadata, reverse bool) func(int, int) bool { +func createStartTimeComparator(hashes []hashMetadata, reverse bool) func(int, int) bool { + if reverse { + return func(i, j int) bool { + return hashes[i].Start.After(hashes[j].Start) + } + } + return func(i, j int) bool { + return hashes[i].Start.Before(hashes[j].Start) || hashes[i].Start.Equal(hashes[j].Start) + } +} + +func createEndTimeComparator(hashes []hashMetadata, reverse bool) func(int, int) bool { if reverse { return func(i, j int) bool { - return hashes[i].Datetime.After(hashes[j].Datetime) + return hashes[i].End.After(hashes[j].End) } } return func(i, j int) bool { - return hashes[i].Datetime.Before(hashes[j].Datetime) || hashes[i].Datetime.Equal(hashes[j].Datetime) + return hashes[i].End.Before(hashes[j].End) || hashes[i].End.Equal(hashes[j].End) } } @@ -294,6 +303,8 @@ type LogsOptions struct { // Order by // task - last run's execution order // duration - duration of each task (low to high) + // start - start time of each task (oldest to newest) + // end - end time of each task (oldest to newest) // alnum - alphanumerically on hash string // query - match order of queryHashes sortType SortMode @@ -359,12 +370,14 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { unresolvedSortType = TaskSort case "duration": unresolvedSortType = DurationSort - case "datetime": - unresolvedSortType = DatetimeSort + case "start": + unresolvedSortType = StartTimeSort + case "end": + unresolvedSortType = EndTimeSort case "alnum": unresolvedSortType = AlnumSort default: - return nil, fmt.Errorf("invalid value %v for --sort CLI flag. This should be task, duration, datetime, or alnum", inputSortType) + return nil, fmt.Errorf("invalid value %v for --sort CLI flag. This should be task, duration, start, end, or alnum", inputSortType) } } case arg == "--reverse": @@ -402,7 +415,7 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { unresolvedSortType = QuerySort } if logsOptions.includeAll && unresolvedSortType == NothingSort { - unresolvedSortType = DatetimeSort + unresolvedSortType = StartTimeSort } if unresolvedSortType != NothingSort { logsOptions.sortType = unresolvedSortType From 1a8813a5991b6ceea995d2dba6df8b893c1dbcf9 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sat, 19 Mar 2022 21:25:48 -0500 Subject: [PATCH 13/34] feat: remove unnecessary len checks --- cli/internal/run/logs.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index c78630e080c26..90a7bf5a591de 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -363,7 +363,6 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { case arg == "--all": logsOptions.includeAll = true case strings.HasPrefix(arg, "--sort="): - if len(arg[len("--sort="):]) > 0 { inputSortType := arg[len("--sort="):] switch inputSortType { case "task": @@ -378,7 +377,6 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { unresolvedSortType = AlnumSort default: return nil, fmt.Errorf("invalid value %v for --sort CLI flag. This should be task, duration, start, end, or alnum", inputSortType) - } } case arg == "--reverse": logsOptions.reverseSort = true @@ -388,7 +386,6 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { unresolvedLastRunPath = arg[len("--last-run-path="):] case strings.HasPrefix(arg, "--output-logs"): outputLogsMode := arg[len("--output-logs="):] - if len(outputLogsMode) > 0 { switch outputLogsMode { case "full": logsOptions.outputLogsMode = outputLogsMode @@ -396,7 +393,6 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { logsOptions.outputLogsMode = "hash" default: output.Warn(fmt.Sprintf("[WARNING] unknown value %v for --output-logs CLI flag. Falling back to full", outputLogsMode)) - } } default: return nil, errors.New(fmt.Sprintf("unknown flag: %v", arg)) From 3152c6d315868fef76791b99888c0bddf79d641b Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sat, 19 Mar 2022 21:26:35 -0500 Subject: [PATCH 14/34] refactor: use switch for readability --- cli/internal/run/logs.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 90a7bf5a591de..87f843d0a33b2 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -175,15 +175,16 @@ func (c *LogsCommand) Run(args []string) int { // sort task list cmp := createAlnumComparator(hashes, logsOptions.reverseSort) - if logsOptions.sortType == DurationSort { + switch logsOptions.sortType { + case DurationSort: cmp = createDurationComparator(hashes, logsOptions.reverseSort) - } else if logsOptions.sortType == TaskSort { + case TaskSort: cmp = createReferenceIndexComparator(hashes, lastRunHashes, logsOptions.reverseSort) - } else if logsOptions.sortType == QuerySort { + case QuerySort: cmp = createReferenceIndexComparator(hashes, logsOptions.queryHashes, logsOptions.reverseSort) - } else if logsOptions.sortType == StartTimeSort { + case StartTimeSort: cmp = createStartTimeComparator(hashes, logsOptions.reverseSort) - } else if logsOptions.sortType == EndTimeSort { + case EndTimeSort: cmp = createEndTimeComparator(hashes, logsOptions.reverseSort) } sort.SliceStable(hashes, cmp) From d450cbc26ae50a71c50bcfed13a7dab9499586f3 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sat, 19 Mar 2022 21:33:31 -0500 Subject: [PATCH 15/34] refactor: rename and add more metadata points --- cli/internal/run/logs.go | 135 +++++++++++++++++++++++++++------------ 1 file changed, 94 insertions(+), 41 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 87f843d0a33b2..1af31a32bfcc7 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -50,6 +50,26 @@ const ( NothingSort SortMode = "n/a" ) +type MetadataName string + +const ( + DurationPoint MetadataName = "duration" + StartTimePoint MetadataName = "start" + EndTimePoint MetadataName = "end" +) + +func (m MetadataName) String() string { + switch m { + case DurationPoint: + return "Elapsed Time" + case StartTimePoint: + return "Start Time" + case EndTimePoint: + return "End Time" + } + return "" +} + // notime is the time used to represent invalid or undefined time var notime = time.Date(0, time.January, 0, 0, 0, 0, 0, time.UTC) @@ -68,9 +88,6 @@ Usage: turbo logs [--list] [] Options: --help Show this message. --list List the hashes available for viewing from the cache. - --list-format Specify output format for --list. Use hash to only - show the hashes. Use relevant to include metadata - relevant to sorting if any. (default hash) --all Show all results, not just results from the most recent run command execution. --sort Set mode to order results. Use task to order by the @@ -85,6 +102,9 @@ Options: --output-logs Set type of replay output logs. Use full to show all output. Use hash-only to show only the turbo-computed task hash lines. (default full) + --include-metadata Also show the specified data points for each task. + Can be specified multiple times. Possible options are + duration, start, and end. --cache-dir Specify local filesystem cache directory. (default "./node_modules/.cache/turbo") --last-run-path Specify path to last run file to load. @@ -129,15 +149,14 @@ func (c *LogsCommand) Run(args []string) int { specificHashes = logsOptions.queryHashes } - // find and collect cached hashes and durations from metadata json - // also get file modified datetime from filesystem + // find and collect cached hashes and durations (milliseconds) from metadata json var hashes []hashMetadata if len(specificHashes) > 0 { for _, hash := range specificHashes { replayPaths := globby.GlobFiles(filepath.Join(logsOptions.cacheFolder, hash), []string{"**/.turbo/turbo-*.log"}, []string{}) metadataPath := filepath.Join(logsOptions.cacheFolder, hash+"-meta.json") metadata, err := cache.ReadCacheMetaFile(metadataPath) - duration := 0 // in milliseconds + duration := 0 start := notime if err == nil { duration = metadata.Duration @@ -190,15 +209,25 @@ func (c *LogsCommand) Run(args []string) int { sort.SliceStable(hashes, cmp) // output replay logs from sorted task list + if logsOptions.listOnly && len(logsOptions.includeData) > 0 { + header := make([]string, 0, len(logsOptions.includeData)+1) + header = append(header, "hash") + for _, dataPoint := range logsOptions.includeData { + header = append(header, string(dataPoint)) + } + c.Ui.Output(strings.Join(header, ",")) + } for _, hash := range hashes { + extraDataPoints := make([]string, 0, len(logsOptions.includeData)) + for _, dataPoint := range logsOptions.includeData { + extraDataPoints = append(extraDataPoints, getDataPoint(dataPoint, hash)) + } if logsOptions.listOnly { - if logsOptions.listType == "relevant" { - if logsOptions.sortType == DurationSort { - c.Ui.Output(fmt.Sprintf("%v %v", hash.Hash, hash.Duration)) - continue - } + extraDataPointsValue := "" + if len(extraDataPoints) > 0 { + extraDataPointsValue = "," + strings.Join(extraDataPoints, ",") } - c.Ui.Output(hash.Hash) + c.Ui.Output(fmt.Sprintf("%v%v", hash.Hash, extraDataPointsValue)) continue } if len(hash.ReplayPaths) == 0 { @@ -220,12 +249,28 @@ func (c *LogsCommand) Run(args []string) int { c.Ui.Output(ui.StripAnsi(string(scan.Bytes()))) } } + for i, dataPointName := range logsOptions.includeData { + // fmt.Sprintf uses the MetadataName.String() method + c.logInfo(c.Config.Logger, fmt.Sprintf("%v: %v", dataPointName, extraDataPoints[i])) + } } } return 0 } +func getDataPoint(dataType MetadataName, hash hashMetadata) string { + switch dataType { + case DurationPoint: + return fmt.Sprintf("%v ms", hash.Duration) + case StartTimePoint: + return hash.Start.String() + case EndTimePoint: + return hash.End.String() + } + return "" +} + func createDurationComparator(hashes []hashMetadata, reverse bool) func(int, int) bool { if reverse { return func(i, j int) bool { @@ -293,10 +338,11 @@ type LogsOptions struct { cacheFolder string // Only output task hashes listOnly bool - // Adjust output format - // hash - only show hash - // relevant - include relevant metadata - listType string + // Additional data to output, options are + // duration - show task elapsed duration + // start - show task start time + // end - show task end time + includeData []MetadataName // Show all results, not only from the last run includeAll bool // Path to last run file @@ -324,7 +370,6 @@ type LogsOptions struct { func getDefaultLogsOptions() *LogsOptions { return &LogsOptions{ listOnly: false, - listType: "hash", includeAll: false, sortType: "task", reverseSort: false, @@ -357,27 +402,35 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { } case arg == "--list": logsOptions.listOnly = true - case strings.HasPrefix(arg, "--list-format="): - if len(arg[len("--list-format="):]) > 0 { - logsOptions.listType = arg[len("--list-format="):] + case strings.HasPrefix(arg, "--include-metadata="): + metadataName := arg[len("--include-metadata="):] + switch metadataName { + case "duration": + logsOptions.includeData = append(logsOptions.includeData, DurationPoint) + case "start": + logsOptions.includeData = append(logsOptions.includeData, StartTimePoint) + case "end": + logsOptions.includeData = append(logsOptions.includeData, EndTimePoint) + default: + return nil, fmt.Errorf("invalid value %v for --include-metadata CLI flag. This should be duration, start, or end", metadataName) } case arg == "--all": logsOptions.includeAll = true case strings.HasPrefix(arg, "--sort="): - inputSortType := arg[len("--sort="):] - switch inputSortType { - case "task": - unresolvedSortType = TaskSort - case "duration": - unresolvedSortType = DurationSort - case "start": - unresolvedSortType = StartTimeSort - case "end": - unresolvedSortType = EndTimeSort - case "alnum": - unresolvedSortType = AlnumSort - default: - return nil, fmt.Errorf("invalid value %v for --sort CLI flag. This should be task, duration, start, end, or alnum", inputSortType) + inputSortType := arg[len("--sort="):] + switch inputSortType { + case "task": + unresolvedSortType = TaskSort + case "duration": + unresolvedSortType = DurationSort + case "start": + unresolvedSortType = StartTimeSort + case "end": + unresolvedSortType = EndTimeSort + case "alnum": + unresolvedSortType = AlnumSort + default: + return nil, fmt.Errorf("invalid value %v for --sort CLI flag. This should be task, duration, start, end, or alnum", inputSortType) } case arg == "--reverse": logsOptions.reverseSort = true @@ -387,13 +440,13 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { unresolvedLastRunPath = arg[len("--last-run-path="):] case strings.HasPrefix(arg, "--output-logs"): outputLogsMode := arg[len("--output-logs="):] - switch outputLogsMode { - case "full": - logsOptions.outputLogsMode = outputLogsMode - case "hash-only": - logsOptions.outputLogsMode = "hash" - default: - output.Warn(fmt.Sprintf("[WARNING] unknown value %v for --output-logs CLI flag. Falling back to full", outputLogsMode)) + switch outputLogsMode { + case "full": + logsOptions.outputLogsMode = outputLogsMode + case "hash-only": + logsOptions.outputLogsMode = "hash" + default: + output.Warn(fmt.Sprintf("[WARNING] unknown value %v for --output-logs CLI flag. Falling back to full", outputLogsMode)) } default: return nil, errors.New(fmt.Sprintf("unknown flag: %v", arg)) From 7e998032a575b03fe8594358be220e4dc2d65ddd Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sat, 19 Mar 2022 21:52:39 -0500 Subject: [PATCH 16/34] feat: allow pass metadata opts as csv --- cli/internal/run/logs.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 1af31a32bfcc7..740931be90612 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -103,8 +103,8 @@ Options: output. Use hash-only to show only the turbo-computed task hash lines. (default full) --include-metadata Also show the specified data points for each task. - Can be specified multiple times. Possible options are - duration, start, and end. + Can be given multiple times or as a comma-separated + list. Options are duration, start, and end. --cache-dir Specify local filesystem cache directory. (default "./node_modules/.cache/turbo") --last-run-path Specify path to last run file to load. @@ -403,16 +403,19 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { case arg == "--list": logsOptions.listOnly = true case strings.HasPrefix(arg, "--include-metadata="): - metadataName := arg[len("--include-metadata="):] - switch metadataName { - case "duration": - logsOptions.includeData = append(logsOptions.includeData, DurationPoint) - case "start": - logsOptions.includeData = append(logsOptions.includeData, StartTimePoint) - case "end": - logsOptions.includeData = append(logsOptions.includeData, EndTimePoint) - default: - return nil, fmt.Errorf("invalid value %v for --include-metadata CLI flag. This should be duration, start, or end", metadataName) + rawMetadataNames := arg[len("--include-metadata="):] + metadataNames := strings.Split(rawMetadataNames, ",") + for _, metadataName := range metadataNames { + switch metadataName { + case "duration": + logsOptions.includeData = append(logsOptions.includeData, DurationPoint) + case "start": + logsOptions.includeData = append(logsOptions.includeData, StartTimePoint) + case "end": + logsOptions.includeData = append(logsOptions.includeData, EndTimePoint) + default: + return nil, fmt.Errorf("invalid value(s) %v for --include-metadata CLI flag. This should be duration, start, or end", rawMetadataNames) + } } case arg == "--all": logsOptions.includeAll = true From f9a722d34f0ac396d21aacbff3858705cf2c636f Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sat, 19 Mar 2022 21:59:49 -0500 Subject: [PATCH 17/34] docs: sync with logs command help --- .../docs/reference/command-line-reference.mdx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/pages/docs/reference/command-line-reference.mdx b/docs/pages/docs/reference/command-line-reference.mdx index 9854e95ffbbe4..9452fdf7f7c7a 100644 --- a/docs/pages/docs/reference/command-line-reference.mdx +++ b/docs/pages/docs/reference/command-line-reference.mdx @@ -488,17 +488,6 @@ Defaults to `false`. Only list the hashes available for viewing from the cache. turbo logs --list ``` -#### `--list-format` - -`type: string` - -Defaults to `hash`. Specify output format for [`--list`](#--list). Use `hash` to only show the hashes. Use `relevant` to include metadata relevant to sorting if any. - -```sh -turbo logs --list --list-format=hash -turbo logs --list --list-format=relevant --sort=duration -``` - #### `--all` `type: boolean` @@ -514,14 +503,14 @@ turbo logs --list --all `type: string` -Defaults to `task`. Set mode to order results. Use `task` to sort by the previous run's execution order (requires the file at [`--last-run-path`](#--last-run-path) to exist). Use `duration` to sort by time taken per task hash (lowest to highest). Use `alnum` to order by hash alphanumerically. Normally defaults to task. If specific hashes are given, defaults to the order they are given in. If [`--all`](#--all), defaults to alnum. +Defaults to `task`. Set mode to order results. Use `task` to sort by the previous run's execution order (requires the file at [`--last-run-path`](#--last-run-path) to exist). Use `duration` to sort by time taken per task hash (lowest to highest). Use `start` to order by task start times. Use `end` to order by task end times. Use `alnum` to order by hash alphanumerically. Normally defaults to task. If specific hashes are given, defaults to the order they are given in. If [`--all`](#--all), defaults to alnum. ```sh turbo logs --sort=task turbo logs --sort=duration turbo logs --sort=alnum turbo logs hash1 hash2 hash3 -turbo logs --list --sort=duration +turbo logs --list --sort=start ``` #### `--reverse` @@ -546,3 +535,14 @@ Defaults to `full`. Set type of replay output logs. Use `full` to show all repla turbo logs --output-logs=full turbo logs --output-logs=hash-only ``` + +#### `--include-metadata` + +`type: string` + +Also show the specified data points for each task. Can be given multiple times or as a comma-separated list. Options are duration, start, and end. + +```sh +turbo logs --list --include-metadata=duration +turbo logs --include-metadata=start,duration +``` From 173ca8f2ac5bebc0f79d8288868c2912c591294f Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 13:09:20 -0500 Subject: [PATCH 18/34] fix: sync with run.go see PR #990 --- cli/internal/run/logs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 740931be90612..a71ebcb2af09e 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -243,10 +243,10 @@ func (c *LogsCommand) Run(args []string) int { scan := bufio.NewScanner(file) if logsOptions.outputLogsMode == "hash" { scan.Scan() - c.Ui.Output(ui.StripAnsi(string(scan.Bytes()))) + c.Ui.Output(string(scan.Bytes())) } else { for scan.Scan() { - c.Ui.Output(ui.StripAnsi(string(scan.Bytes()))) + c.Ui.Output(string(scan.Bytes())) } } for i, dataPointName := range logsOptions.includeData { From ba95f0c0158ae822bdfe764ea073364c8545f153 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 13:17:43 -0500 Subject: [PATCH 19/34] refactor: use SortMode and run.go LogsMode enums --- cli/internal/run/logs.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index a71ebcb2af09e..ed21bc501a10d 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -241,7 +241,7 @@ func (c *LogsCommand) Run(args []string) int { } defer file.Close() scan := bufio.NewScanner(file) - if logsOptions.outputLogsMode == "hash" { + if logsOptions.outputLogsMode == HashLogs { scan.Scan() c.Ui.Output(string(scan.Bytes())) } else { @@ -363,17 +363,16 @@ type LogsOptions struct { // Replay task logs output mode // full - show all, // hash - only show task hash - // TODO: refactor to use run.LogsMode - outputLogsMode string + outputLogsMode LogsMode } func getDefaultLogsOptions() *LogsOptions { return &LogsOptions{ listOnly: false, includeAll: false, - sortType: "task", + sortType: TaskSort, reverseSort: false, - outputLogsMode: "full", + outputLogsMode: FullLogs, } } @@ -445,9 +444,9 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { outputLogsMode := arg[len("--output-logs="):] switch outputLogsMode { case "full": - logsOptions.outputLogsMode = outputLogsMode + logsOptions.outputLogsMode = FullLogs case "hash-only": - logsOptions.outputLogsMode = "hash" + logsOptions.outputLogsMode = HashLogs default: output.Warn(fmt.Sprintf("[WARNING] unknown value %v for --output-logs CLI flag. Falling back to full", outputLogsMode)) } From bafbeef36c5c41b354c6d9509fd3261f4bfc0709 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 13:25:26 -0500 Subject: [PATCH 20/34] fix: only print metadata once per hash not per log --- cli/internal/run/logs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index ed21bc501a10d..c8beb4ca39e9d 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -249,12 +249,12 @@ func (c *LogsCommand) Run(args []string) int { c.Ui.Output(string(scan.Bytes())) } } + } for i, dataPointName := range logsOptions.includeData { // fmt.Sprintf uses the MetadataName.String() method c.logInfo(c.Config.Logger, fmt.Sprintf("%v: %v", dataPointName, extraDataPoints[i])) } } - } return 0 } From 23bb051bc5de3777a8c99614ae9fb586ad750e4a Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 13:32:09 -0500 Subject: [PATCH 21/34] feat: sync with run.go synthesize output rather than unnecessarily opening logs --- cli/internal/run/logs.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index c8beb4ca39e9d..2811bb390b145 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -230,6 +230,9 @@ func (c *LogsCommand) Run(args []string) int { c.Ui.Output(fmt.Sprintf("%v%v", hash.Hash, extraDataPointsValue)) continue } + if logsOptions.outputLogsMode == HashLogs { + c.Ui.Output(fmt.Sprintf("cache hit, suppressing output %s", ui.Dim(hash.Hash))) + } else { if len(hash.ReplayPaths) == 0 { c.logInfo(c.Config.Logger, fmt.Sprintf("%v: no logs found to replay", hash.Hash)) } @@ -241,10 +244,6 @@ func (c *LogsCommand) Run(args []string) int { } defer file.Close() scan := bufio.NewScanner(file) - if logsOptions.outputLogsMode == HashLogs { - scan.Scan() - c.Ui.Output(string(scan.Bytes())) - } else { for scan.Scan() { c.Ui.Output(string(scan.Bytes())) } From 56b8bf16ee01d16f274747be9de9158a2fabd0ec Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 13:32:58 -0500 Subject: [PATCH 22/34] fix: HasPrefix check must match slice --- cli/internal/run/logs.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 2811bb390b145..bc8a2051e1fc5 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -233,27 +233,27 @@ func (c *LogsCommand) Run(args []string) int { if logsOptions.outputLogsMode == HashLogs { c.Ui.Output(fmt.Sprintf("cache hit, suppressing output %s", ui.Dim(hash.Hash))) } else { - if len(hash.ReplayPaths) == 0 { - c.logInfo(c.Config.Logger, fmt.Sprintf("%v: no logs found to replay", hash.Hash)) - } - for _, replayPath := range hash.ReplayPaths { - file, err := os.Open(replayPath) - if err != nil { - c.logWarning(c.Config.Logger, "", fmt.Errorf("error reading logs: %w", err)) - continue + if len(hash.ReplayPaths) == 0 { + c.logInfo(c.Config.Logger, fmt.Sprintf("%v: no logs found to replay", hash.Hash)) } - defer file.Close() - scan := bufio.NewScanner(file) + for _, replayPath := range hash.ReplayPaths { + file, err := os.Open(replayPath) + if err != nil { + c.logWarning(c.Config.Logger, "", fmt.Errorf("error reading logs: %w", err)) + continue + } + defer file.Close() + scan := bufio.NewScanner(file) for scan.Scan() { c.Ui.Output(string(scan.Bytes())) } } } - for i, dataPointName := range logsOptions.includeData { - // fmt.Sprintf uses the MetadataName.String() method - c.logInfo(c.Config.Logger, fmt.Sprintf("%v: %v", dataPointName, extraDataPoints[i])) - } + for i, dataPointName := range logsOptions.includeData { + // fmt.Sprintf uses the MetadataName.String() method + c.logInfo(c.Config.Logger, fmt.Sprintf("%v: %v", dataPointName, extraDataPoints[i])) } + } return 0 } @@ -439,7 +439,7 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { unresolvedCacheFolder = arg[len("--cache-dir="):] case strings.HasPrefix(arg, "--last-run-path="): unresolvedLastRunPath = arg[len("--last-run-path="):] - case strings.HasPrefix(arg, "--output-logs"): + case strings.HasPrefix(arg, "--output-logs="): outputLogsMode := arg[len("--output-logs="):] switch outputLogsMode { case "full": From 7a2285385d4344aed9621531a0328d7fb81afe80 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 13:46:04 -0500 Subject: [PATCH 23/34] Revert "feat: sync with run.go synthesize output rather than unnecessarily opening logs" This reverts commit 23bb051bc5de3777a8c99614ae9fb586ad750e4a. Necessary because ui output is not prefixed with package and task --- cli/internal/run/logs.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index bc8a2051e1fc5..4e718574d605f 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -230,20 +230,21 @@ func (c *LogsCommand) Run(args []string) int { c.Ui.Output(fmt.Sprintf("%v%v", hash.Hash, extraDataPointsValue)) continue } - if logsOptions.outputLogsMode == HashLogs { - c.Ui.Output(fmt.Sprintf("cache hit, suppressing output %s", ui.Dim(hash.Hash))) - } else { - if len(hash.ReplayPaths) == 0 { - c.logInfo(c.Config.Logger, fmt.Sprintf("%v: no logs found to replay", hash.Hash)) + if len(hash.ReplayPaths) == 0 { + c.logInfo(c.Config.Logger, fmt.Sprintf("%v: no logs found to replay", hash.Hash)) + } + for _, replayPath := range hash.ReplayPaths { + file, err := os.Open(replayPath) + if err != nil { + c.logWarning(c.Config.Logger, "", fmt.Errorf("error reading logs: %w", err)) + continue } - for _, replayPath := range hash.ReplayPaths { - file, err := os.Open(replayPath) - if err != nil { - c.logWarning(c.Config.Logger, "", fmt.Errorf("error reading logs: %w", err)) - continue - } - defer file.Close() - scan := bufio.NewScanner(file) + defer file.Close() + scan := bufio.NewScanner(file) + if logsOptions.outputLogsMode == HashLogs { + scan.Scan() + c.Ui.Output(string(scan.Bytes())) + } else { for scan.Scan() { c.Ui.Output(string(scan.Bytes())) } From fda4caa0b22988dd3b0e22da45a63ac2a9cebd2c Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 13:53:44 -0500 Subject: [PATCH 24/34] feat: output log line should reflect behavior when --output-logs=hash-only --- cli/internal/run/logs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 4e718574d605f..3cec387a99674 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -243,7 +243,7 @@ func (c *LogsCommand) Run(args []string) int { scan := bufio.NewScanner(file) if logsOptions.outputLogsMode == HashLogs { scan.Scan() - c.Ui.Output(string(scan.Bytes())) + c.Ui.Output(strings.ReplaceAll(string(scan.Bytes()), "replaying output", "suppressing output")) } else { for scan.Scan() { c.Ui.Output(string(scan.Bytes())) From 7c35c2f9f5f016ad570b0ca3ebf809ecd7de7500 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 14:00:12 -0500 Subject: [PATCH 25/34] feat: add all metadata option --- cli/internal/run/logs.go | 4 ++++ docs/pages/docs/reference/command-line-reference.mdx | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 3cec387a99674..f1169ed6dec76 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -412,6 +412,10 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { logsOptions.includeData = append(logsOptions.includeData, StartTimePoint) case "end": logsOptions.includeData = append(logsOptions.includeData, EndTimePoint) + case "all": + logsOptions.includeData = append(logsOptions.includeData, DurationPoint) + logsOptions.includeData = append(logsOptions.includeData, StartTimePoint) + logsOptions.includeData = append(logsOptions.includeData, EndTimePoint) default: return nil, fmt.Errorf("invalid value(s) %v for --include-metadata CLI flag. This should be duration, start, or end", rawMetadataNames) } diff --git a/docs/pages/docs/reference/command-line-reference.mdx b/docs/pages/docs/reference/command-line-reference.mdx index 75c3e396802f3..82ee3e0419b02 100644 --- a/docs/pages/docs/reference/command-line-reference.mdx +++ b/docs/pages/docs/reference/command-line-reference.mdx @@ -644,7 +644,7 @@ turbo logs --list --all `type: string` -Defaults to `task`. Set mode to order results. Use `task` to sort by the previous run's execution order (requires the file at [`--last-run-path`](#--last-run-path) to exist). Use `duration` to sort by time taken per task hash (lowest to highest). Use `start` to order by task start times. Use `end` to order by task end times. Use `alnum` to order by hash alphanumerically. Normally defaults to task. If specific hashes are given, defaults to the order they are given in. If [`--all`](#--all), defaults to alnum. +Defaults to `task`. Set mode to order results. Use `task` to sort by the previous run's execution order (requires the file at [`--last-run-path`](#--last-run-path) to exist). Use `duration` to sort by time taken per task hash (lowest to highest). Use `start` to order by task start times. Use `end` to order by task end times. Use `alnum` to order by hash alphanumerically. Normally defaults to task. If specific hashes are given, defaults to the order they are given in. If [`--all`](#--all), defaults to `alnum`. ```sh turbo logs --sort=task @@ -681,7 +681,7 @@ turbo logs --output-logs=hash-only `type: string` -Also show the specified data points for each task. Can be given multiple times or as a comma-separated list. Options are duration, start, and end. +Also show the specified data points for each task. Can be given multiple times or as a comma-separated list. Options are `duration`, `start`, `end`, and `all`. ```sh turbo logs --list --include-metadata=duration From dea58d522375b721d1906f66bcb8330a32886419 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 14:54:15 -0500 Subject: [PATCH 26/34] feat: warn if err on remove last run log --- cli/internal/run/run.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/internal/run/run.go b/cli/internal/run/run.go index ba91e22cb371c..35dbb52052024 100644 --- a/cli/internal/run/run.go +++ b/cli/internal/run/run.go @@ -650,7 +650,9 @@ func (c *RunCommand) executeTasks(g *completeGraph, rs *runSpec, engine *core.Sc } analyticsClient := analytics.NewClient(goctx, analyticsSink, c.Config.Logger.Named("analytics")) defer analyticsClient.CloseWithTimeout(50 * time.Millisecond) - os.Remove(filepath.Join(rs.Opts.cacheFolder, "last-run.log")) + if err := os.Remove(filepath.Join(rs.Opts.cacheFolder, "last-run.log")); err != nil { + c.Ui.Warn(fmt.Sprintf("Failed to clear last run log: %v", err)) + } turboCache := cache.New(c.Config, rs.Opts.remoteOnly, analyticsClient) defer turboCache.Shutdown() runState := NewRunState(rs.Opts, startAt) From b4f3cf779b6864a066a33a74dba36d36f3318bbf Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 15:18:33 -0500 Subject: [PATCH 27/34] feat: warn if err on updating last run log --- cli/internal/run/run.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/internal/run/run.go b/cli/internal/run/run.go index 35dbb52052024..a75d2b9174e5c 100644 --- a/cli/internal/run/run.go +++ b/cli/internal/run/run.go @@ -883,7 +883,9 @@ func (e *execContext) exec(pt *packageTask, deps dag.Set) error { return nil } // Cache --------------------------------------------- - cache.AppendHashesFile(filepath.Join(e.rs.Opts.cacheFolder, "last-run.log"), hash) + if err := cache.AppendHashesFile(filepath.Join(e.rs.Opts.cacheFolder, "last-run.log"), hash); err != nil { + targetUi.Warn(fmt.Sprintf("failed to update last run log: %v", err)) + } var hit bool if !e.rs.Opts.forceExecution { hit, _, _, _, err = e.turboCache.Fetch(e.rs.Opts.cwd, hash, nil) From 9f6e1dc6097db03a6902da6a288f0d5bb1cea373 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 15:39:53 -0500 Subject: [PATCH 28/34] fix: prefer all caps abbrevs --- cli/cmd/turbo/main.go | 4 ++-- cli/internal/run/logs.go | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cli/cmd/turbo/main.go b/cli/cmd/turbo/main.go index a42a74847d358..86d858b6f7352 100644 --- a/cli/cmd/turbo/main.go +++ b/cli/cmd/turbo/main.go @@ -49,7 +49,7 @@ func main() { } args = args[:argsEnd] - ui := ui.BuildColoredUi(colorMode); + ui := ui.BuildColoredUi(colorMode) c := cli.NewCLI("turbo", turboVersion) util.InitPrintf() @@ -80,7 +80,7 @@ func main() { nil }, "logs": func() (cli.Command, error) { - return &run.LogsCommand{Config: cf, Ui: ui}, nil + return &run.LogsCommand{Config: cf, UI: ui}, nil }, "prune": func() (cli.Command, error) { return &prune.PruneCommand{Config: cf, Ui: ui}, nil diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index f1169ed6dec76..4208e40dbcd36 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -25,7 +25,7 @@ import ( // LogsCommand is a Command implementation that allows the user to view log replays type LogsCommand struct { Config *config.Config - Ui *cli.ColoredUi + UI *cli.ColoredUi } // hashMetadata represents the files and duration metadata associated with @@ -115,7 +115,7 @@ Options: // Run finds and replays task logs in the monorepo func (c *LogsCommand) Run(args []string) int { - logsOptions, err := parseLogsArgs(args, c.Ui) + logsOptions, err := parseLogsArgs(args, c.UI) if err != nil { c.logError(c.Config.Logger, "", err) return 1 @@ -215,7 +215,7 @@ func (c *LogsCommand) Run(args []string) int { for _, dataPoint := range logsOptions.includeData { header = append(header, string(dataPoint)) } - c.Ui.Output(strings.Join(header, ",")) + c.UI.Output(strings.Join(header, ",")) } for _, hash := range hashes { extraDataPoints := make([]string, 0, len(logsOptions.includeData)) @@ -227,7 +227,7 @@ func (c *LogsCommand) Run(args []string) int { if len(extraDataPoints) > 0 { extraDataPointsValue = "," + strings.Join(extraDataPoints, ",") } - c.Ui.Output(fmt.Sprintf("%v%v", hash.Hash, extraDataPointsValue)) + c.UI.Output(fmt.Sprintf("%v%v", hash.Hash, extraDataPointsValue)) continue } if len(hash.ReplayPaths) == 0 { @@ -243,10 +243,10 @@ func (c *LogsCommand) Run(args []string) int { scan := bufio.NewScanner(file) if logsOptions.outputLogsMode == HashLogs { scan.Scan() - c.Ui.Output(strings.ReplaceAll(string(scan.Bytes()), "replaying output", "suppressing output")) + c.UI.Output(strings.ReplaceAll(string(scan.Bytes()), "replaying output", "suppressing output")) } else { for scan.Scan() { - c.Ui.Output(string(scan.Bytes())) + c.UI.Output(string(scan.Bytes())) } } } @@ -497,7 +497,7 @@ func (c *LogsCommand) logWarning(log hclog.Logger, prefix string, err error) { prefix = " " + prefix + ": " } - c.Ui.Error(fmt.Sprintf("%s%s%s", ui.WARNING_PREFIX, prefix, color.YellowString(" %v", err))) + c.UI.Error(fmt.Sprintf("%s%s%s", ui.WARNING_PREFIX, prefix, color.YellowString(" %v", err))) } // logError logs an error and outputs it to the UI. @@ -508,12 +508,12 @@ func (c *LogsCommand) logError(log hclog.Logger, prefix string, err error) { prefix += ": " } - c.Ui.Error(fmt.Sprintf("%s%s%s", ui.ERROR_PREFIX, prefix, color.RedString(" %v", err))) + c.UI.Error(fmt.Sprintf("%s%s%s", ui.ERROR_PREFIX, prefix, color.RedString(" %v", err))) } // logInfo logs an info message and outputs it to the UI. func (c *LogsCommand) logInfo(log hclog.Logger, message string) { log.Info(message) - c.Ui.Info(fmt.Sprintf("%s%s", ui.INFO_PREFIX, color.BlueString(" %v", message))) + c.UI.Info(fmt.Sprintf("%s%s", ui.INFO_PREFIX, color.BlueString(" %v", message))) } From 7284b9f03879ae00d547bb80fd7e586c6b9bf169 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 15:49:51 -0500 Subject: [PATCH 29/34] fix: prefer fmt.Errorf --- cli/internal/run/logs.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 4208e40dbcd36..8ae1776615c88 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -19,7 +19,6 @@ import ( "github.com/fatih/color" "github.com/hashicorp/go-hclog" "github.com/mitchellh/cli" - "github.com/pkg/errors" ) // LogsCommand is a Command implementation that allows the user to view log replays @@ -455,7 +454,7 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { output.Warn(fmt.Sprintf("[WARNING] unknown value %v for --output-logs CLI flag. Falling back to full", outputLogsMode)) } default: - return nil, errors.New(fmt.Sprintf("unknown flag: %v", arg)) + return nil, fmt.Errorf("unknown flag: %v", arg) } } else if !strings.HasPrefix(arg, "-") { if !queryHashesSet.Includes(arg) { From 3d887be89551c77b93c1df319b0a45727a47e006 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 15:55:21 -0500 Subject: [PATCH 30/34] refactor: unexport sortMode and metadataName --- cli/internal/run/logs.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 8ae1776615c88..b32939cd475a8 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -37,27 +37,27 @@ type hashMetadata struct { End time.Time } -type SortMode string +type sortMode string const ( - TaskSort SortMode = "task" - DurationSort SortMode = "duration" - AlnumSort SortMode = "alnum" - StartTimeSort SortMode = "start" - EndTimeSort SortMode = "end" - QuerySort SortMode = "query" - NothingSort SortMode = "n/a" + TaskSort sortMode = "task" + DurationSort sortMode = "duration" + AlnumSort sortMode = "alnum" + StartTimeSort sortMode = "start" + EndTimeSort sortMode = "end" + QuerySort sortMode = "query" + NothingSort sortMode = "n/a" ) -type MetadataName string +type metadataName string const ( - DurationPoint MetadataName = "duration" - StartTimePoint MetadataName = "start" - EndTimePoint MetadataName = "end" + DurationPoint metadataName = "duration" + StartTimePoint metadataName = "start" + EndTimePoint metadataName = "end" ) -func (m MetadataName) String() string { +func (m metadataName) String() string { switch m { case DurationPoint: return "Elapsed Time" @@ -250,7 +250,7 @@ func (c *LogsCommand) Run(args []string) int { } } for i, dataPointName := range logsOptions.includeData { - // fmt.Sprintf uses the MetadataName.String() method + // fmt.Sprintf uses the metadataName.String() method c.logInfo(c.Config.Logger, fmt.Sprintf("%v: %v", dataPointName, extraDataPoints[i])) } } @@ -258,7 +258,7 @@ func (c *LogsCommand) Run(args []string) int { return 0 } -func getDataPoint(dataType MetadataName, hash hashMetadata) string { +func getDataPoint(dataType metadataName, hash hashMetadata) string { switch dataType { case DurationPoint: return fmt.Sprintf("%v ms", hash.Duration) @@ -341,7 +341,7 @@ type LogsOptions struct { // duration - show task elapsed duration // start - show task start time // end - show task end time - includeData []MetadataName + includeData []metadataName // Show all results, not only from the last run includeAll bool // Path to last run file @@ -353,7 +353,7 @@ type LogsOptions struct { // end - end time of each task (oldest to newest) // alnum - alphanumerically on hash string // query - match order of queryHashes - sortType SortMode + sortType sortMode // True to reverse output order reverseSort bool // List of requested hashes to retrieve @@ -403,8 +403,8 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { case strings.HasPrefix(arg, "--include-metadata="): rawMetadataNames := arg[len("--include-metadata="):] metadataNames := strings.Split(rawMetadataNames, ",") - for _, metadataName := range metadataNames { - switch metadataName { + for _, metadataNameStr := range metadataNames { + switch metadataNameStr { case "duration": logsOptions.includeData = append(logsOptions.includeData, DurationPoint) case "start": From f25f42dd09dcbaf9d854180c03e422862d5f8857 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 15:57:03 -0500 Subject: [PATCH 31/34] lint: ignore errcheck where nothing to do --- cli/internal/cache/async_cache.go | 2 +- cli/internal/cache/cache.go | 2 +- cli/internal/cache/cache_fs.go | 4 ++-- cli/internal/run/logs.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/internal/cache/async_cache.go b/cli/internal/cache/async_cache.go index 5c17cc8bc7c4e..4527da363bd7b 100644 --- a/cli/internal/cache/async_cache.go +++ b/cli/internal/cache/async_cache.go @@ -75,7 +75,7 @@ func (c *asyncCache) Shutdown() { // run implements the actual async logic. func (c *asyncCache) run() { for r := range c.requests { - c.realCache.Put(r.target, r.key, r.start, r.duration, r.files) + c.realCache.Put(r.target, r.key, r.start, r.duration, r.files) //nolint:golint,errcheck // nothing to do } c.wg.Done() } diff --git a/cli/internal/cache/cache.go b/cli/internal/cache/cache.go index 6e0c8d6df30b2..27999058b1af0 100644 --- a/cli/internal/cache/cache.go +++ b/cli/internal/cache/cache.go @@ -110,7 +110,7 @@ func (mplex cacheMultiplexer) Fetch(target string, key string, files []string) ( // Store this into other caches. We can ignore errors here because we know // we have previously successfully stored in a higher-priority cache, and so the overall // result is a success at fetching. Storing in lower-priority caches is an optimization. - mplex.storeUntil(target, key, start, duration, actualFiles, i) + mplex.storeUntil(target, key, start, duration, actualFiles, i) //nolint:golint,errcheck // nothing to do return ok, actualFiles, start, duration, err } } diff --git a/cli/internal/cache/cache_fs.go b/cli/internal/cache/cache_fs.go index 871a62bea7ede..0acb1e48917b1 100644 --- a/cli/internal/cache/cache_fs.go +++ b/cli/internal/cache/cache_fs.go @@ -170,7 +170,7 @@ func AppendHashesFile(path string, hash string) error { return err } - defer file.Close() + defer file.Close() //nolint:golint,errcheck // nothing to do if _, err = file.WriteString(hash + "\n"); err != nil { return err @@ -186,7 +186,7 @@ func ReadHashesFile(path string) ([]string, error) { return nil, err } - defer file.Close() + defer file.Close() //nolint:golint,errcheck // nothing to do var hashes []string scanner := bufio.NewScanner(file) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index b32939cd475a8..20d0e0afb9475 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -238,7 +238,7 @@ func (c *LogsCommand) Run(args []string) int { c.logWarning(c.Config.Logger, "", fmt.Errorf("error reading logs: %w", err)) continue } - defer file.Close() + defer file.Close() //nolint:golint,errcheck // nothing to do scan := bufio.NewScanner(file) if logsOptions.outputLogsMode == HashLogs { scan.Scan() From 3d6c7aae1129fb6cbe6ba8907e356a1c93a1ee18 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 16:01:49 -0500 Subject: [PATCH 32/34] lint: ignore ALL_CAPS, add as todo --- cli/internal/ui/ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/internal/ui/ui.go b/cli/internal/ui/ui.go index bc57515ca5d89..0dbd4f68a1777 100644 --- a/cli/internal/ui/ui.go +++ b/cli/internal/ui/ui.go @@ -19,7 +19,7 @@ var IsTTY = isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdo var IsCI = os.Getenv("CI") == "true" || os.Getenv("BUILD_NUMBER") == "true" || os.Getenv("TEAMCITY_VERSION") != "" var gray = color.New(color.Faint) var bold = color.New(color.Bold) -var INFO_PREFIX = color.New(color.Bold, color.FgBlue, color.ReverseVideo).Sprint(" INFO ") +var INFO_PREFIX = color.New(color.Bold, color.FgBlue, color.ReverseVideo).Sprint(" INFO ") //nolint:golint // TODO: don't use ALL_CAPS in Go names; use CamelCase var ERROR_PREFIX = color.New(color.Bold, color.FgRed, color.ReverseVideo).Sprint(" ERROR ") var WARNING_PREFIX = color.New(color.Bold, color.FgYellow, color.ReverseVideo).Sprint(" WARNING ") From f6a039e5bba3fa997db418970a627dd6a3eda066 Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 16:09:23 -0500 Subject: [PATCH 33/34] refactor: unexport sortMode and metadataName enums --- cli/internal/run/logs.go | 80 ++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/cli/internal/run/logs.go b/cli/internal/run/logs.go index 20d0e0afb9475..b09fbbf8e489c 100644 --- a/cli/internal/run/logs.go +++ b/cli/internal/run/logs.go @@ -40,30 +40,30 @@ type hashMetadata struct { type sortMode string const ( - TaskSort sortMode = "task" - DurationSort sortMode = "duration" - AlnumSort sortMode = "alnum" - StartTimeSort sortMode = "start" - EndTimeSort sortMode = "end" - QuerySort sortMode = "query" - NothingSort sortMode = "n/a" + taskSort sortMode = "task" + durationSort sortMode = "duration" + alnumSort sortMode = "alnum" + startTimeSort sortMode = "start" + endTimeSort sortMode = "end" + querySort sortMode = "query" + nothingSort sortMode = "n/a" ) type metadataName string const ( - DurationPoint metadataName = "duration" - StartTimePoint metadataName = "start" - EndTimePoint metadataName = "end" + durationPoint metadataName = "duration" + startTimePoint metadataName = "start" + endTimePoint metadataName = "end" ) func (m metadataName) String() string { switch m { - case DurationPoint: + case durationPoint: return "Elapsed Time" - case StartTimePoint: + case startTimePoint: return "Start Time" - case EndTimePoint: + case endTimePoint: return "End Time" } return "" @@ -126,7 +126,7 @@ func (c *LogsCommand) Run(args []string) int { c.Config.Logger.Trace("reverse", "value", logsOptions.reverseSort) var lastRunHashes []string - if logsOptions.sortType == TaskSort || !logsOptions.includeAll { + if logsOptions.sortType == taskSort || !logsOptions.includeAll { if !fs.FileExists(logsOptions.lastRunPath) { c.logError(c.Config.Logger, "", fmt.Errorf("failed to resolve last run file: %v", logsOptions.lastRunPath)) metadataPaths := globby.GlobFiles(logsOptions.cacheFolder, []string{"*-meta.json"}, []string{}) @@ -194,15 +194,15 @@ func (c *LogsCommand) Run(args []string) int { // sort task list cmp := createAlnumComparator(hashes, logsOptions.reverseSort) switch logsOptions.sortType { - case DurationSort: + case durationSort: cmp = createDurationComparator(hashes, logsOptions.reverseSort) - case TaskSort: + case taskSort: cmp = createReferenceIndexComparator(hashes, lastRunHashes, logsOptions.reverseSort) - case QuerySort: + case querySort: cmp = createReferenceIndexComparator(hashes, logsOptions.queryHashes, logsOptions.reverseSort) - case StartTimeSort: + case startTimeSort: cmp = createStartTimeComparator(hashes, logsOptions.reverseSort) - case EndTimeSort: + case endTimeSort: cmp = createEndTimeComparator(hashes, logsOptions.reverseSort) } sort.SliceStable(hashes, cmp) @@ -260,11 +260,11 @@ func (c *LogsCommand) Run(args []string) int { func getDataPoint(dataType metadataName, hash hashMetadata) string { switch dataType { - case DurationPoint: + case durationPoint: return fmt.Sprintf("%v ms", hash.Duration) - case StartTimePoint: + case startTimePoint: return hash.Start.String() - case EndTimePoint: + case endTimePoint: return hash.End.String() } return "" @@ -369,7 +369,7 @@ func getDefaultLogsOptions() *LogsOptions { return &LogsOptions{ listOnly: false, includeAll: false, - sortType: TaskSort, + sortType: taskSort, reverseSort: false, outputLogsMode: FullLogs, } @@ -386,7 +386,7 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { unresolvedCacheFolder := filepath.FromSlash("./node_modules/.cache/turbo") unresolvedLastRunPath := "" - unresolvedSortType := NothingSort + unresolvedSortType := nothingSort queryHashesSet := make(util.Set) for _, arg := range args { @@ -406,15 +406,15 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { for _, metadataNameStr := range metadataNames { switch metadataNameStr { case "duration": - logsOptions.includeData = append(logsOptions.includeData, DurationPoint) + logsOptions.includeData = append(logsOptions.includeData, durationPoint) case "start": - logsOptions.includeData = append(logsOptions.includeData, StartTimePoint) + logsOptions.includeData = append(logsOptions.includeData, startTimePoint) case "end": - logsOptions.includeData = append(logsOptions.includeData, EndTimePoint) + logsOptions.includeData = append(logsOptions.includeData, endTimePoint) case "all": - logsOptions.includeData = append(logsOptions.includeData, DurationPoint) - logsOptions.includeData = append(logsOptions.includeData, StartTimePoint) - logsOptions.includeData = append(logsOptions.includeData, EndTimePoint) + logsOptions.includeData = append(logsOptions.includeData, durationPoint) + logsOptions.includeData = append(logsOptions.includeData, startTimePoint) + logsOptions.includeData = append(logsOptions.includeData, endTimePoint) default: return nil, fmt.Errorf("invalid value(s) %v for --include-metadata CLI flag. This should be duration, start, or end", rawMetadataNames) } @@ -425,15 +425,15 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { inputSortType := arg[len("--sort="):] switch inputSortType { case "task": - unresolvedSortType = TaskSort + unresolvedSortType = taskSort case "duration": - unresolvedSortType = DurationSort + unresolvedSortType = durationSort case "start": - unresolvedSortType = StartTimeSort + unresolvedSortType = startTimeSort case "end": - unresolvedSortType = EndTimeSort + unresolvedSortType = endTimeSort case "alnum": - unresolvedSortType = AlnumSort + unresolvedSortType = alnumSort default: return nil, fmt.Errorf("invalid value %v for --sort CLI flag. This should be task, duration, start, end, or alnum", inputSortType) } @@ -466,13 +466,13 @@ func parseLogsArgs(args []string, output cli.Ui) (*LogsOptions, error) { // We can only set sortType once we know what the default should // be and whether or not it has been overridden - if len(logsOptions.queryHashes) > 0 && unresolvedSortType == NothingSort { - unresolvedSortType = QuerySort + if len(logsOptions.queryHashes) > 0 && unresolvedSortType == nothingSort { + unresolvedSortType = querySort } - if logsOptions.includeAll && unresolvedSortType == NothingSort { - unresolvedSortType = StartTimeSort + if logsOptions.includeAll && unresolvedSortType == nothingSort { + unresolvedSortType = startTimeSort } - if unresolvedSortType != NothingSort { + if unresolvedSortType != nothingSort { logsOptions.sortType = unresolvedSortType } From 223588b7b8718ec79a46e6b12f4a9c512c53d18b Mon Sep 17 00:00:00 2001 From: chelkyl <14041823+chelkyl@users.noreply.github.com> Date: Sun, 24 Apr 2022 16:21:30 -0500 Subject: [PATCH 34/34] lint: missed ignore type --- cli/internal/ui/ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/internal/ui/ui.go b/cli/internal/ui/ui.go index 0dbd4f68a1777..5ed6f90b1f510 100644 --- a/cli/internal/ui/ui.go +++ b/cli/internal/ui/ui.go @@ -19,7 +19,7 @@ var IsTTY = isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdo var IsCI = os.Getenv("CI") == "true" || os.Getenv("BUILD_NUMBER") == "true" || os.Getenv("TEAMCITY_VERSION") != "" var gray = color.New(color.Faint) var bold = color.New(color.Bold) -var INFO_PREFIX = color.New(color.Bold, color.FgBlue, color.ReverseVideo).Sprint(" INFO ") //nolint:golint // TODO: don't use ALL_CAPS in Go names; use CamelCase +var INFO_PREFIX = color.New(color.Bold, color.FgBlue, color.ReverseVideo).Sprint(" INFO ") //nolint:golint,revive // TODO: don't use ALL_CAPS in Go names; use CamelCase var ERROR_PREFIX = color.New(color.Bold, color.FgRed, color.ReverseVideo).Sprint(" ERROR ") var WARNING_PREFIX = color.New(color.Bold, color.FgYellow, color.ReverseVideo).Sprint(" WARNING ")