diff --git a/README.md b/README.md index 3e688118..8bfb4803 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ See [rare.zdyn.net](https://rare.zdyn.net) or the [docs/ folder](docs/) for the * Aggregating and realtime summary (Don't have to wait for all data to be scanned) * Multi-threaded reading, parsing, and aggregation (It's fast) * Color-coded outputs (optionally) - * Pipe support (stdin for reading, stdout will disable color) eg. `tail -f | rare ...` + * Pipe support (stdin for reading, stdout will disable realtime) eg. `tail -f | rare ... > out` Take a look at [examples](docs/usage/examples.md) to see more of what *rare* does. diff --git a/cmd/analyze.go b/cmd/analyze.go index f10ecd11..f971c928 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -16,7 +16,7 @@ func humanf(arg interface{}) string { return color.Wrap(color.BrightWhite, humanize.Hf(arg)) } -func writeAggrOutput(writer *multiterm.TermWriter, aggr *aggregation.MatchNumerical, extra bool, quantiles []float64) int { +func writeAggrOutput(writer multiterm.MultilineTerm, aggr *aggregation.MatchNumerical, extra bool, quantiles []float64) int { writer.WriteForLinef(0, "Samples: %v", color.Wrap(color.BrightWhite, humanize.Hi(aggr.Count()))) writer.WriteForLinef(1, "Mean: %v", humanf(aggr.Mean())) writer.WriteForLinef(2, "StdDev: %v", humanf(aggr.StdDev())) @@ -59,8 +59,7 @@ func analyzeFunction(c *cli.Context) error { } aggr := aggregation.NewNumericalAggregator(&config) - writer := multiterm.New() - defer multiterm.ResetCursor() + writer := helpers.BuildVTermFromArguments(c) batcher := helpers.BuildBatcherFromArguments(c) ext := helpers.BuildExtractorFromArguments(c, batcher) @@ -102,6 +101,7 @@ func analyzeCommand() *cli.Command { Usage: "Adds a quantile to the output set. Requires --extra", Value: cli.NewStringSlice("90", "99", "99.9"), }, + helpers.SnapshotFlag, }, }) } diff --git a/cmd/bargraph.go b/cmd/bargraph.go index 6ad88665..5e88af48 100644 --- a/cmd/bargraph.go +++ b/cmd/bargraph.go @@ -3,7 +3,6 @@ package cmd import ( "rare/cmd/helpers" "rare/pkg/aggregation" - "rare/pkg/multiterm" "rare/pkg/multiterm/termrenderers" "github.com/urfave/cli/v2" @@ -20,8 +19,9 @@ func bargraphFunction(c *cli.Context) error { sortName = c.String(helpers.DefaultSortFlag.Name) ) + vt := helpers.BuildVTermFromArguments(c) counter := aggregation.NewSubKeyCounter() - writer := termrenderers.NewBarGraph(multiterm.New()) + writer := termrenderers.NewBarGraph(vt) writer.Stacked = stacked batcher := helpers.BuildBatcherFromArguments(c) @@ -63,6 +63,7 @@ func bargraphCommand() *cli.Command { Usage: "Display bargraph as stacked", }, helpers.DefaultSortFlag, + helpers.SnapshotFlag, }, }) } diff --git a/cmd/heatmap.go b/cmd/heatmap.go index 673578c4..1656633d 100644 --- a/cmd/heatmap.go +++ b/cmd/heatmap.go @@ -32,7 +32,8 @@ func heatmapFunction(c *cli.Context) error { rowSorter := helpers.BuildSorterOrFail(sortRows) colSorter := helpers.BuildSorterOrFail(sortCols) - writer := termrenderers.NewHeatmap(multiterm.New(), numRows, numCols) + vt := helpers.BuildVTermFromArguments(c) + writer := termrenderers.NewHeatmap(vt, numRows, numCols) writer.FixedMin = minFixed writer.FixedMax = maxFixed @@ -96,6 +97,7 @@ func heatmapCommand() *cli.Command { Usage: helpers.DefaultSortFlag.Usage, Value: helpers.DefaultSortFlag.Value, }, + helpers.SnapshotFlag, }, }) } diff --git a/cmd/helpers/output.go b/cmd/helpers/output.go new file mode 100644 index 00000000..1b60c101 --- /dev/null +++ b/cmd/helpers/output.go @@ -0,0 +1,25 @@ +package helpers + +import ( + "rare/pkg/multiterm" + "rare/pkg/multiterm/termstate" + + "github.com/urfave/cli/v2" +) + +var SnapshotFlag = &cli.BoolFlag{ + Name: "snapshot", + Usage: "In aggregators that support it, only output final results, and not progressive updates. Will enable automatically when piping output", +} + +func BuildVTerm(forceSnapshot bool) multiterm.MultilineTerm { + if forceSnapshot || termstate.IsPipedOutput() { + return multiterm.NewBufferedTerm() + } + return multiterm.New() +} + +func BuildVTermFromArguments(c *cli.Context) multiterm.MultilineTerm { + snapshot := c.Bool(SnapshotFlag.Name) + return BuildVTerm(snapshot) +} diff --git a/cmd/helpers/output_test.go b/cmd/helpers/output_test.go new file mode 100644 index 00000000..0fc333cf --- /dev/null +++ b/cmd/helpers/output_test.go @@ -0,0 +1,27 @@ +package helpers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v2" +) + +func TestBuildVTerm(t *testing.T) { + assert.NotNil(t, BuildVTerm(false)) + assert.NotNil(t, BuildVTerm(true)) +} + +func TestBuildVTermFromArgs(t *testing.T) { + app := cli.NewApp() + app.Flags = []cli.Flag{ + &cli.BoolFlag{ + Name: "snapshot", + }, + } + app.Action = func(ctx *cli.Context) error { + BuildVTermFromArguments(ctx) + return nil + } + app.Run([]string{"", "--snapshot"}) +} diff --git a/cmd/helpers/updatingAggregator.go b/cmd/helpers/updatingAggregator.go index cc15017d..baffcf2f 100644 --- a/cmd/helpers/updatingAggregator.go +++ b/cmd/helpers/updatingAggregator.go @@ -1,13 +1,11 @@ package helpers import ( - "fmt" "os" "os/signal" "rare/pkg/aggregation" "rare/pkg/extractor" "rare/pkg/logger" - "rare/pkg/multiterm" "sync" "time" ) @@ -18,7 +16,6 @@ import ( // writeOutput - triggered after a delay, only if there's an update // The two functions are guaranteed to never happen at the same time func RunAggregationLoop(ext *extractor.Extractor, aggregator aggregation.Aggregator, writeOutput func()) { - defer multiterm.ResetCursor() logger.DeferLogs() // Updater sync variables @@ -62,5 +59,4 @@ PROCESSING_LOOP: outputDone <- true writeOutput() - fmt.Println() } diff --git a/cmd/histo.go b/cmd/histo.go index 14fe6aa3..dd9d5bba 100644 --- a/cmd/histo.go +++ b/cmd/histo.go @@ -35,8 +35,9 @@ func histoFunction(c *cli.Context) error { sortName = c.String(helpers.DefaultSortFlag.Name) ) + vt := helpers.BuildVTermFromArguments(c) counter := aggregation.NewCounter() - writer := termrenderers.NewHistogram(multiterm.New(), topItems) + writer := termrenderers.NewHistogram(vt, topItems) writer.ShowBar = c.Bool("bars") || extra writer.ShowPercentage = c.Bool("percentage") || extra @@ -56,6 +57,8 @@ func histoFunction(c *cli.Context) error { writer.WriteFooter(1, batcher.StatusString()) }) + // Not deferred because of the `all` below to print out before it + // when in snapshot mode writer.Close() if all { @@ -118,6 +121,7 @@ func histogramCommand() *cli.Command { Value: 0, }, helpers.DefaultSortFlagWithDefault("value"), + helpers.SnapshotFlag, }, }) } diff --git a/cmd/histo_test.go b/cmd/histo_test.go index f4434cee..dce27519 100644 --- a/cmd/histo_test.go +++ b/cmd/histo_test.go @@ -15,8 +15,8 @@ func TestHistogram(t *testing.T) { } func TestHistogramRender(t *testing.T) { - out, eout, err := testCommandCapture(histogramCommand(), `-m "(\d+)" -e "{bucket {1} 10}" testdata/log.txt`) + out, eout, err := testCommandCapture(histogramCommand(), `--snapshot -m "(\d+)" -e "{bucket {1} 10}" testdata/log.txt`) assert.NoError(t, err) - assert.Contains(t, out, "Matched: 3 / 6 (Groups: 2)") + assert.Equal(t, out, "0 2 \n20 1 \n\n\n\nMatched: 3 / 6 (Groups: 2)\n96 B (0 B/s) \n") assert.Equal(t, "", eout) } diff --git a/cmd/tabulate.go b/cmd/tabulate.go index 76db1101..e463c57f 100644 --- a/cmd/tabulate.go +++ b/cmd/tabulate.go @@ -7,7 +7,6 @@ import ( "rare/pkg/color" "rare/pkg/expressions" "rare/pkg/humanize" - "rare/pkg/multiterm" "rare/pkg/multiterm/termrenderers" "github.com/urfave/cli/v2" @@ -32,7 +31,8 @@ func tabulateFunction(c *cli.Context) error { ) counter := aggregation.NewTable(delim) - writer := termrenderers.NewTable(multiterm.New(), numCols+2, numRows+2) + vt := helpers.BuildVTermFromArguments(c) + writer := termrenderers.NewTable(vt, numCols+2, numRows+2) batcher := helpers.BuildBatcherFromArguments(c) ext := helpers.BuildExtractorFromArguments(c, batcher) @@ -148,6 +148,7 @@ func tabulateCommand() *cli.Command { Usage: helpers.DefaultSortFlag.Usage, Value: "value", }, + helpers.SnapshotFlag, }, }) } diff --git a/docs/index.md b/docs/index.md index fdcedcf2..b07a1682 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,7 +24,7 @@ Supports various CLI-based graphing and metric formats (filter (grep-like), hist * Aggregating and realtime summary (Don't have to wait for all data to be scanned) * Multi-threaded reading, parsing, and aggregation (It's fast) * Color-coded outputs (optionally) - * Pipe support (stdin for reading, stdout will disable color) eg. `tail -f | rare ...` + * Pipe support (stdin for reading, stdout will disable realtime) eg. `tail -f | rare ... > out` Take a look at [examples](usage/examples.md) to see more of what *rare* does. diff --git a/pkg/color/coloring.go b/pkg/color/coloring.go index 0263046a..a5bd6f9d 100644 --- a/pkg/color/coloring.go +++ b/pkg/color/coloring.go @@ -3,7 +3,7 @@ package color import ( "fmt" "io" - "os" + "rare/pkg/multiterm/termstate" "strings" "unicode/utf8" ) @@ -56,10 +56,8 @@ var Enabled = true var GroupColors = [...]ColorCode{Red, Green, Yellow, Blue, Magenta, Cyan, BrightRed, BrightGreen, BrightYellow, BrightBlue, BrightMagenta, BrightCyan} func init() { - if fi, err := os.Stdout.Stat(); err == nil { - if (fi.Mode() & os.ModeCharDevice) == 0 { - Enabled = false - } + if termstate.IsPipedOutput() { + Enabled = false } } diff --git a/pkg/multiterm/bufferedterm.go b/pkg/multiterm/bufferedterm.go new file mode 100644 index 00000000..05c3082a --- /dev/null +++ b/pkg/multiterm/bufferedterm.go @@ -0,0 +1,19 @@ +package multiterm + +import "os" + +type BufferedTerm struct { + *VirtualTerm +} + +// NewBufferedTerm writes on Close +func NewBufferedTerm() *BufferedTerm { + return &BufferedTerm{ + NewVirtualTerm(), + } +} + +func (s *BufferedTerm) Close() { + s.WriteToOutput(os.Stdout) + s.VirtualTerm.Close() +} diff --git a/pkg/multiterm/bufferedterm_test.go b/pkg/multiterm/bufferedterm_test.go new file mode 100644 index 00000000..94e99d40 --- /dev/null +++ b/pkg/multiterm/bufferedterm_test.go @@ -0,0 +1,11 @@ +package multiterm + +import ( + "testing" +) + +func TestBufferedTerm(t *testing.T) { + vt := NewBufferedTerm() + vt.WriteForLine(0, "hello") + vt.Close() +} diff --git a/pkg/multiterm/cursor.go b/pkg/multiterm/cursor.go index ef76eeef..542f8c50 100644 --- a/pkg/multiterm/cursor.go +++ b/pkg/multiterm/cursor.go @@ -36,7 +36,3 @@ func showCursor() { func eraseRemainingLine() { fmt.Print(escape("[0K")) } - -func ResetCursor() { - showCursor() -} diff --git a/pkg/multiterm/intf.go b/pkg/multiterm/intf.go index 587d90fc..041219cc 100644 --- a/pkg/multiterm/intf.go +++ b/pkg/multiterm/intf.go @@ -2,5 +2,6 @@ package multiterm type MultilineTerm interface { WriteForLine(line int, s string) + WriteForLinef(line int, format string, args ...interface{}) Close() } diff --git a/pkg/multiterm/linetrim.go b/pkg/multiterm/linetrim.go index e1d75570..db899e2f 100644 --- a/pkg/multiterm/linetrim.go +++ b/pkg/multiterm/linetrim.go @@ -2,9 +2,7 @@ package multiterm import ( "io" - "os" - - "golang.org/x/term" + "rare/pkg/multiterm/termstate" ) var AutoTrim = true @@ -13,22 +11,8 @@ const defaultRows, defaultCols = 24, 80 var computedRows, computedCols = 0, 0 -func getTermRowsCols() (rows, cols int, ok bool) { - fd := int(os.Stdout.Fd()) - if !term.IsTerminal(fd) { - return 0, 0, false - } - - cols, rows, err := term.GetSize(fd) - if err != nil { - return 0, 0, false - } - - return rows, cols, true -} - func init() { - if rows, cols, ok := getTermRowsCols(); ok { + if rows, cols, ok := termstate.GetTermRowsCols(); ok { computedRows, computedCols = rows, cols } else { AutoTrim = false diff --git a/pkg/multiterm/multiterm.go b/pkg/multiterm/multiterm.go index e93b7abc..515e869f 100644 --- a/pkg/multiterm/multiterm.go +++ b/pkg/multiterm/multiterm.go @@ -41,6 +41,7 @@ func (s *TermWriter) WriteForLine(line int, text string) { func (s *TermWriter) Close() { s.goTo(s.maxLine) + fmt.Println() // Put cursor after last line if s.cursorHidden { showCursor() } diff --git a/pkg/multiterm/multiterm_test.go b/pkg/multiterm/multiterm_test.go index 17beec38..b95518a4 100644 --- a/pkg/multiterm/multiterm_test.go +++ b/pkg/multiterm/multiterm_test.go @@ -9,4 +9,6 @@ func TestBasicMultiterm(t *testing.T) { mt.WriteForLine(0, "Hello") mt.WriteForLine(1, "you") mt.WriteForLine(10, "There") + mt.WriteForLinef(5, "This is %s", "Test") + mt.Close() } diff --git a/pkg/multiterm/termstate/term.go b/pkg/multiterm/termstate/term.go new file mode 100644 index 00000000..e62683eb --- /dev/null +++ b/pkg/multiterm/termstate/term.go @@ -0,0 +1,32 @@ +package termstate + +import ( + "os" + + "golang.org/x/term" +) + +// Returns 'true' if output is being piped (Not char device) +func IsPipedOutput() bool { + if fi, err := os.Stdout.Stat(); err == nil { + if (fi.Mode() & os.ModeCharDevice) == 0 { + return true + } + } + return false +} + +// Gets size of the terminal +func GetTermRowsCols() (rows, cols int, ok bool) { + fd := int(os.Stdout.Fd()) + if !term.IsTerminal(fd) { + return 0, 0, false + } + + cols, rows, err := term.GetSize(fd) + if err != nil { + return 0, 0, false + } + + return rows, cols, true +} diff --git a/pkg/multiterm/termstate/term_test.go b/pkg/multiterm/termstate/term_test.go new file mode 100644 index 00000000..191d2c14 --- /dev/null +++ b/pkg/multiterm/termstate/term_test.go @@ -0,0 +1,16 @@ +package termstate + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsPipedOutput(t *testing.T) { + assert.True(t, IsPipedOutput()) +} + +func TestGetTerminalSize(t *testing.T) { + // Can't really test its output.. no idea where it'll be run in a test + GetTermRowsCols() +} diff --git a/pkg/multiterm/virtualterm.go b/pkg/multiterm/virtualterm.go index 87ca57cc..b3e42e59 100644 --- a/pkg/multiterm/virtualterm.go +++ b/pkg/multiterm/virtualterm.go @@ -1,6 +1,7 @@ package multiterm import ( + "fmt" "io" ) @@ -33,6 +34,10 @@ func (s *VirtualTerm) WriteForLine(line int, text string) { s.lines[line] = text } +func (s *VirtualTerm) WriteForLinef(line int, format string, args ...interface{}) { + s.WriteForLine(line, fmt.Sprintf(format, args...)) +} + // Close the virtual term. Doesn't ever really need to close, but useful in testing func (s *VirtualTerm) Close() { s.closed = true diff --git a/pkg/multiterm/virtualterm_test.go b/pkg/multiterm/virtualterm_test.go index 27807a8a..46a0f319 100644 --- a/pkg/multiterm/virtualterm_test.go +++ b/pkg/multiterm/virtualterm_test.go @@ -10,7 +10,7 @@ import ( func TestVirtualTerm(t *testing.T) { vt := NewVirtualTerm() vt.WriteForLine(0, "Hello") - vt.WriteForLine(2, "Thar") + vt.WriteForLinef(2, "Thar %s", "bob") assert.Equal(t, "Hello", vt.Get(0)) assert.Equal(t, "", vt.Get(1)) @@ -24,7 +24,7 @@ func TestVirtualTerm(t *testing.T) { var sb strings.Builder vt.WriteToOutput(&sb) - assert.Equal(t, "Hello\n\nThar\n", sb.String()) + assert.Equal(t, "Hello\n\nThar bob\n", sb.String()) // And close vt.Close()