Skip to content

Commit

Permalink
Bar & histo logscale (#98)
Browse files Browse the repository at this point in the history
Add graph-scaling support to bargraphs and histograms
  • Loading branch information
zix99 authored Jul 26, 2023
1 parent 5e10c1d commit 1f903ae
Show file tree
Hide file tree
Showing 17 changed files with 187 additions and 64 deletions.
12 changes: 10 additions & 2 deletions cmd/bargraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@ go run . bars -sz -m "\[(.+?)\].*\" (\d+)" -e "{$ {buckettime {1} year nginx} {2

func bargraphFunction(c *cli.Context) error {
var (
stacked = c.Bool("stacked")
sortName = c.String(helpers.DefaultSortFlag.Name)
stacked = c.Bool("stacked")
sortName = c.String(helpers.DefaultSortFlag.Name)
scaleName = c.String(helpers.ScaleFlag.Name)
)

vt := helpers.BuildVTermFromArguments(c)
counter := aggregation.NewSubKeyCounter()
writer := termrenderers.NewBarGraph(vt)
writer.Stacked = stacked
if c.IsSet(helpers.ScaleFlag.Name) {
if stacked {
return cli.Exit("Unable to set graph scale on stacked graphs", helpers.ExitCodeInvalidUsage)
}
writer.Scaler = helpers.BuildScalerOrFail(scaleName)
}

batcher := helpers.BuildBatcherFromArguments(c)
ext := helpers.BuildExtractorFromArguments(c, batcher)
Expand Down Expand Up @@ -72,6 +79,7 @@ func bargraphCommand() *cli.Command {
helpers.SnapshotFlag,
helpers.NoOutFlag,
helpers.CSVFlag,
helpers.ScaleFlag,
},
})
}
12 changes: 11 additions & 1 deletion cmd/bargraph_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
package cmd

import "testing"
import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestBarGraph(t *testing.T) {
testCommandSet(t, bargraphCommand(),
`-m "(.+) (\d+)" -e "{$ {1} {2}}" testdata/graph.txt`,
`-o - -m "(.+) (\d+)" -e "{$ {1} {2}}" testdata/graph.txt`,
`-o - -m "(.+) (\d+)" -e "{$ {1} {2}}" --scale log10 testdata/graph.txt`,
)
}

func TestBarGraphCantScaleAndStack(t *testing.T) {
err := testCommand(bargraphCommand(), "--stacked --scale log10 testdata/graph.txt")
assert.Error(t, err)
}
13 changes: 8 additions & 5 deletions cmd/histo.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,20 @@ func writeHistoOutput(writer *termrenderers.HistoWriter, counter *aggregation.Ma

func histoFunction(c *cli.Context) error {
var (
topItems = c.Int("n")
atLeast = c.Int64("atleast")
extra = c.Bool("extra")
all = c.Bool("all")
sortName = c.String(helpers.DefaultSortFlag.Name)
topItems = c.Int("n")
atLeast = c.Int64("atleast")
extra = c.Bool("extra")
all = c.Bool("all")
sortName = c.String(helpers.DefaultSortFlag.Name)
scalerName = c.String(helpers.ScaleFlag.Name)
)

vt := helpers.BuildVTermFromArguments(c)
counter := aggregation.NewCounter()
writer := termrenderers.NewHistogram(vt, topItems)
writer.ShowBar = c.Bool("bars") || extra
writer.ShowPercentage = c.Bool("percentage") || extra
writer.Scaler = helpers.BuildScalerOrFail(scalerName)

batcher := helpers.BuildBatcherFromArguments(c)
ext := helpers.BuildExtractorFromArguments(c, batcher)
Expand Down Expand Up @@ -130,6 +132,7 @@ func histogramCommand() *cli.Command {
helpers.SnapshotFlag,
helpers.NoOutFlag,
helpers.CSVFlag,
helpers.ScaleFlag,
},
})
}
2 changes: 2 additions & 0 deletions cmd/histo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ func TestHistogram(t *testing.T) {
testCommandSet(t, histogramCommand(),
`-m (\d+) testdata/log.txt`,
`-m (\d+) testdata/graph.txt`,
`-m (\d+) --all testdata/graph.txt`,
`-m (\d+) --scale log10 testdata/graph.txt`,
`-o - -m (\d+) testdata/graph.txt`,
`-z -m (\d+) testdata/log.txt.gz`,
)
Expand Down
Binary file modified docs/cli-help.md
Binary file not shown.
4 changes: 4 additions & 0 deletions docs/usage/aggregators.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ of an extracted match. That is to say, on every line a regex will be
matched (or not), and the matched groups can be used to extract and build
a key, that will act as the bucketing name.

Supports [alternative scales](#alternative-scales)

### Example

Extract HTTP verb, URL and status code. Key off of status code and verb.
Expand Down Expand Up @@ -78,6 +80,8 @@ rare help bargraph
Similar to histogram or table, bargraph can generate a stacked or grouped
bargraph by one or two keys.

Supports [alternative scales](#alternative-scales)

### Example

!!! note "Color Coded Keys"
Expand Down
21 changes: 15 additions & 6 deletions docs/usage/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,21 @@ Matched: 6 / 6 (Groups: 4)

# Or with Bars/percentages
./rare histo --match "(\d+)" -e "{1}" -x simple.log
1 3 [50.0%] ||||||||||||||||||||||||||||||||||||||||||||||||||
0 1 [16.7%] ||||||||||||||||
2 1 [16.7%] ||||||||||||||||
3 1 [16.7%] ||||||||||||||||
1 7 [58.3%] ||||||||||||||||||||||||||||||||||||||||||||||||||
3 2 [16.7%] ||||||||||||||
2 2 [16.7%] ||||||||||||||
0 1 [ 8.3%] |||||||

Matched: 6 / 6 (Groups: 4)
Matched: 12 / 12 (Groups: 4)

# Logarithmic Scale
./rare histo --match "(\d+)" -e "{1}" --scale log10 -x simple.log
1 7 [58.3%] ||||||||||||||||||||||||||||||||||||||||||
3 2 [16.7%] |||||||||||||||
2 2 [16.7%] |||||||||||||||
0 1 [ 8.3%]

Matched: 12 / 12 (Groups: 4)
```

## Nginx
Expand Down Expand Up @@ -106,7 +115,7 @@ This shows an example of how to bucket the values into size of `1000`. In this c
sense to see the histogram by number of bytes, but we might want to know the ratio of various orders-of-magnitudes.

```sh
$ rare histo -m '"(\w{3,4}) ([A-Za-z0-9/.]+).*" (\d{3}) (\d+)' -e "{bucket {4} 10000}" -n 10 access.log -b
$ rare histo -m '"(\w{3,4}) ([A-Za-z0-9/.]+).*" (\d{3}) (\d+)' -e "{bucket {4} 10000}" -n 10 -b access.log
0 144239 ||||||||||||||||||||||||||||||||||||||||||||||||||
190000 2599
10000 1290
Expand Down
4 changes: 3 additions & 1 deletion docs/usage/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,12 +356,14 @@ Colorizes the 2nd argument.

#### Bars

Syntax: `{bar {val} "maxVal" "length"}`
Syntax: `{bar {val} "maxVal" "length" ["scale"]}`

Note: If unicode is disabled, will use pipe character

Draws a "bar" with the length `(val / maxVal) * length`

Scale can be `linear`, `log10`, or `log2` (Default: `lienar`)

### Paths

Syntax: `{basename a/b/c}`, `{dirname a/b/c}`, `{extname a/b/c.jpg}`
Expand Down
26 changes: 21 additions & 5 deletions pkg/expressions/stdlib/drawing.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package stdlib
import (
"rare/pkg/color"
. "rare/pkg/expressions" //lint:ignore ST1001 Legacy
"rare/pkg/multiterm/termscaler"
"rare/pkg/multiterm/termunicode"
"strconv"
"strings"
Expand Down Expand Up @@ -48,26 +49,41 @@ func kfRepeat(args []KeyBuilderStage) (KeyBuilderStage, error) {
}), nil
}

// {bar {val} "maxVal" "len"}
// {bar {val} "maxVal" "len" ["scaler"]}
func kfBar(args []KeyBuilderStage) (KeyBuilderStage, error) {
if len(args) != 3 {
return stageErrArgCount(args, 3)
if !isArgCountBetween(args, 3, 4) {
return stageErrArgRange(args, "3-4")
}

maxVal, maxValOk := EvalStageInt64(args[1])
if !maxValOk {
return stageArgError(ErrNum, 1)
}
maxLen, maxLenOk := EvalStageInt64(args[2])
maxLen, maxLenOk := EvalStageInt(args[2])
if !maxLenOk {
return stageArgError(ErrNum, 2)
}

scaler := termscaler.ScalerLinear
if len(args) >= 4 {
if name, ok := EvalStaticStage(args[3]); ok {
var scalerOk bool
if scaler, scalerOk = termscaler.ScalerByName(name); !scalerOk {
return stageArgError(ErrEnum, 3)
}
} else {
return stageArgError(ErrConst, 3)
}
}

return KeyBuilderStage(func(context KeyBuilderContext) string {
val, err := strconv.ParseInt(args[0](context), 10, 64)
if err != nil {
return ErrorNum
}
return termunicode.BarString(val, maxVal, maxLen)

var sb strings.Builder
termunicode.BarWrite(&sb, scaler.Scale(val, 0, maxVal), maxLen)
return sb.String()
}), nil
}
15 changes: 11 additions & 4 deletions pkg/expressions/stdlib/drawing_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package stdlib

import (
"rare/pkg/multiterm/termunicode"
"rare/pkg/testutil"
"testing"
)

Expand Down Expand Up @@ -29,10 +31,15 @@ func TestAddingColor(t *testing.T) {
}

func TestBarGraph(t *testing.T) {
testExpression(t,
mockContext(),
"{bar 2 5 5}",
"██")
testutil.SwitchGlobal(&termunicode.UnicodeEnabled, false)
defer testutil.RestoreGlobals()

testExpression(t, mockContext(), "{bar 2 5 5}", "||")
testExpression(t, mockContext(), "{bar 10 100 10}", "|")
testExpression(t, mockContext(), "{bar 10 100 10 log10}", "|||||")
testExpressionErr(t, mockContext(), "{bar 2 {0} 5}", "<BAD-TYPE>", ErrNum)
testExpressionErr(t, mockContext(), "{bar 10 100 10 badlog}", "<ENUM>", ErrEnum)
testExpressionErr(t, mockContext(), "{bar 10 100 10 {0}}", "<CONST>", ErrConst)
}

func TestBarGraphErrorCases(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion pkg/multiterm/termrenderers/bargraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"rare/pkg/color"
"rare/pkg/humanize"
"rare/pkg/multiterm"
"rare/pkg/multiterm/termscaler"
"rare/pkg/multiterm/termunicode"
"strings"
)
Expand All @@ -26,6 +27,7 @@ type BarGraph struct {

BarSize int
Stacked bool
Scaler termscaler.Scaler
}

func NewBarGraph(term multiterm.MultilineTerm) *BarGraph {
Expand All @@ -34,6 +36,7 @@ func NewBarGraph(term multiterm.MultilineTerm) *BarGraph {
maxKeyLength: 4,
Stacked: false,
BarSize: 50,
Scaler: termscaler.ScalerLinear,
}
}

Expand Down Expand Up @@ -143,7 +146,7 @@ func (s *BarGraph) writeBarGrouped(idx int, key string, vals ...int64) {
sb.WriteString(strings.Repeat(" ", s.maxKeyLength+2))
}
color.Write(&sb, color.GroupColors[i%len(color.GroupColors)], func(w io.StringWriter) {
termunicode.BarWrite(w, vals[i], s.maxLineVal, int64(s.BarSize))
termunicode.BarWrite(w, s.Scaler.Scale(vals[i], 0, s.maxLineVal), s.BarSize)
})
sb.WriteString(" ")
sb.WriteString(humanize.Hi(vals[i]))
Expand Down
22 changes: 21 additions & 1 deletion pkg/multiterm/termrenderers/bargraph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package termrenderers

import (
"rare/pkg/multiterm"
"rare/pkg/multiterm/termscaler"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -25,6 +26,25 @@ func TestBargraphRendering(t *testing.T) {
assert.Equal(t, "abc", v.Get(5))
}

func TestBargraphRenderingLog10(t *testing.T) {
v := multiterm.NewVirtualTerm()
bg := NewBarGraph(v)
bg.Stacked = false
bg.Scaler = termscaler.ScalerLog10

bg.SetKeys("a", "b")
bg.WriteBar(0, "test", 10, 100)
bg.WriteBar(1, "tes2", 50, 500)
bg.WriteFooter(0, "abc")

assert.Equal(t, " 0 a 1 b", v.Get(0))
assert.Equal(t, "test ████████████████▊ 10", v.Get(1))
assert.Equal(t, " █████████████████████████████████▍ 100", v.Get(2))
assert.Equal(t, "tes2 ████████████████████████████▎ 50", v.Get(3))
assert.Equal(t, " █████████████████████████████████████████████ 500", v.Get(4))
assert.Equal(t, "abc", v.Get(5))
}

func TestBargraphStackedRendering(t *testing.T) {
v := multiterm.NewVirtualTerm()
bg := NewBarGraph(v)
Expand Down Expand Up @@ -59,7 +79,7 @@ func TestBargraphBadSubkeys(t *testing.T) {
assert.Equal(t, "abc", v.Get(4))
}

func TestBargraphUnicode(t *testing.T) {
func TestBargraphUnicodeStacked(t *testing.T) {
v := multiterm.NewVirtualTerm()
bg := NewBarGraph(v)
bg.Stacked = true
Expand Down
8 changes: 7 additions & 1 deletion pkg/multiterm/termrenderers/histoWriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package termrenderers

import (
"fmt"
"io"
"rare/pkg/color"
"rare/pkg/humanize"
"rare/pkg/multiterm"
"rare/pkg/multiterm/termscaler"
"rare/pkg/multiterm/termunicode"
"strings"
)
Expand All @@ -23,13 +25,15 @@ type HistoWriter struct {

ShowBar bool
ShowPercentage bool
Scaler termscaler.Scaler
}

func NewHistogram(term multiterm.MultilineTerm, maxLines int) *HistoWriter {
return &HistoWriter{
writer: term,
ShowBar: true,
ShowPercentage: true,
Scaler: termscaler.ScalerLinear,
textSpacing: 16,
items: make([]histoPair, maxLines),
}
Expand Down Expand Up @@ -98,7 +102,9 @@ func (s *HistoWriter) writeLine(line int, key string, val int64) {

if s.ShowBar && s.maxVal > 0 {
sb.WriteString(" ")
sb.WriteString(color.Wrap(color.Blue, termunicode.BarString(val, s.maxVal, 50)))
color.Write(&sb, color.Blue, func(w io.StringWriter) {
termunicode.BarWrite(w, s.Scaler.Scale(val, 0, s.maxVal), 50)
})
}

s.writer.WriteForLine(line, sb.String())
Expand Down
10 changes: 10 additions & 0 deletions pkg/multiterm/termscaler/scale.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,21 @@ func Bucket(buckets int, unitVal float64) int {
return int(unitVal * float64(buckets-1))
}

// Return [0, maxLen]
func LengthVal(maxLen int, unitVal float64) int {
return int(unitVal * float64(maxLen))
}

// Return [0, bucket-1]
func (s Scaler) Bucket(buckets int, val, min, max int64) int {
return Bucket(buckets, s.Scale(val, min, max))
}

// Return [0, maxLen]
func (s Scaler) LengthVal(maxLen int, val, min, max int64) int {
return LengthVal(maxLen, s.Scale(val, min, max))
}

// Returns scaled bucket values. May return less buckets than requested for small or high-curve ranges (won't return dupes)
func (s Scaler) ScaleKeys(buckets, min, max int64) []int64 {
minf10, maxf10 := s.remapMinMax(min, max)
Expand Down
Loading

0 comments on commit 1f903ae

Please sign in to comment.