Skip to content

Commit

Permalink
cli: Add JSON and Pretty Print formatting for `consul snapshot inspec…
Browse files Browse the repository at this point in the history
…t` (#9006)
  • Loading branch information
s-christoff authored Oct 29, 2020
1 parent a670f7a commit 79ce24e
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 123 deletions.
3 changes: 3 additions & 0 deletions .changelog/9006.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
cli: snapshot inspect command supports JSON output
```
114 changes: 114 additions & 0 deletions command/snapshot/inspect/formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package inspect

import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
"text/tabwriter"
)

const (
PrettyFormat string = "pretty"
JSONFormat string = "json"
)

type Formatter interface {
Format(*OutputFormat) (string, error)
}

func GetSupportedFormats() []string {
return []string{PrettyFormat, JSONFormat}
}

type prettyFormatter struct{}

func newPrettyFormatter() Formatter {
return &prettyFormatter{}
}
func NewFormatter(format string) (Formatter, error) {
switch format {
case PrettyFormat:
return newPrettyFormatter(), nil
case JSONFormat:
return newJSONFormatter(), nil
default:
return nil, fmt.Errorf("Unknown format: %s", format)
}
}

func (_ *prettyFormatter) Format(info *OutputFormat) (string, error) {
var b bytes.Buffer
tw := tabwriter.NewWriter(&b, 8, 8, 6, ' ', 0)

fmt.Fprintf(tw, " ID\t%s", info.Meta.ID)
fmt.Fprintf(tw, "\n Size\t%d", info.Meta.Size)
fmt.Fprintf(tw, "\n Index\t%d", info.Meta.Index)
fmt.Fprintf(tw, "\n Term\t%d", info.Meta.Term)
fmt.Fprintf(tw, "\n Version\t%d", info.Meta.Version)
fmt.Fprintf(tw, "\n")
fmt.Fprintln(tw, "\n Type\tCount\tSize\t")
fmt.Fprintf(tw, " %s\t%s\t%s\t", "----", "----", "----")
// For each different type generate new output
for _, s := range info.Stats {
fmt.Fprintf(tw, "\n %s\t%d\t%s\t", s.Name, s.Count, ByteSize(uint64(s.Sum)))
}
fmt.Fprintf(tw, "\n %s\t%s\t%s\t", "----", "----", "----")
fmt.Fprintf(tw, "\n Total\t\t%s\t", ByteSize(uint64(info.TotalSize)))

if err := tw.Flush(); err != nil {
return b.String(), err
}
return b.String(), nil
}

type jsonFormatter struct{}

func newJSONFormatter() Formatter {
return &jsonFormatter{}
}

func (_ *jsonFormatter) Format(info *OutputFormat) (string, error) {
b, err := json.MarshalIndent(info, "", " ")
if err != nil {
return "", fmt.Errorf("Failed to marshal original snapshot stats: %v", err)
}
return string(b), nil
}

const (
BYTE = 1 << (10 * iota)
KILOBYTE
MEGABYTE
GIGABYTE
TERABYTE
)

func ByteSize(bytes uint64) string {
unit := ""
value := float64(bytes)

switch {
case bytes >= TERABYTE:
unit = "TB"
value = value / TERABYTE
case bytes >= GIGABYTE:
unit = "GB"
value = value / GIGABYTE
case bytes >= MEGABYTE:
unit = "MB"
value = value / MEGABYTE
case bytes >= KILOBYTE:
unit = "KB"
value = value / KILOBYTE
case bytes >= BYTE:
unit = "B"
case bytes == 0:
return "0"
}

result := strconv.FormatFloat(value, 'f', 1, 64)
result = strings.TrimSuffix(result, ".0")
return result + unit
}
45 changes: 45 additions & 0 deletions command/snapshot/inspect/formatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package inspect

import (
"fmt"
"testing"

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

func TestFormat(t *testing.T) {
m := []typeStats{{
Name: "msg",
Sum: 1,
Count: 2,
}}
info := OutputFormat{
Meta: &MetadataInfo{
ID: "one",
Size: 2,
Index: 3,
Term: 4,
Version: 1,
},
Stats: m,
TotalSize: 1,
}

formatters := map[string]Formatter{
"pretty": newPrettyFormatter(),
// the JSON formatter ignores the showMeta
"json": newJSONFormatter(),
}

for fmtName, formatter := range formatters {
t.Run(fmtName, func(t *testing.T) {
actual, err := formatter.Format(&info)
require.NoError(t, err)

gName := fmt.Sprintf("%s", fmtName)

expected := golden(t, gName, actual)
require.Equal(t, expected, actual)
})
}
}
173 changes: 64 additions & 109 deletions command/snapshot/inspect/snapshot_inspect.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package inspect

import (
"bytes"
"flag"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
"text/tabwriter"

"github.com/hashicorp/consul/agent/consul/fsm"
"github.com/hashicorp/consul/agent/structs"
Expand All @@ -28,16 +25,41 @@ func New(ui cli.Ui) *cmd {
}

type cmd struct {
UI cli.Ui
flags *flag.FlagSet
help string
UI cli.Ui
flags *flag.FlagSet
help string
format string
}

func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(
&c.format,
"format",
PrettyFormat,
fmt.Sprintf("Output format {%s}", strings.Join(GetSupportedFormats(), "|")))

c.help = flags.Usage(help, c.flags)
}

// MetadataInfo is used for passing information
// through the formatter
type MetadataInfo struct {
ID string
Size int64
Index uint64
Term uint64
Version raft.SnapshotVersion
}

// OutputFormat is used for passing information
// through the formatter
type OutputFormat struct {
Meta *MetadataInfo
Stats []typeStats
TotalSize int
}

func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
c.UI.Error(err.Error())
Expand Down Expand Up @@ -84,38 +106,37 @@ func (c *cmd) Run(args []string) int {
c.UI.Error(fmt.Sprintf("Error extracting snapshot data: %s", err))
return 1
}
// Outputs the original style of inspect information
legacy, err := c.legacyStats(meta)
if err != nil {
c.UI.Error(fmt.Sprintf("Error outputting snapshot data: %s", err))
}
c.UI.Info(legacy.String())

// Outputs the more detailed snapshot information
enhanced, err := c.readStats(stats, totalSize)
formatter, err := NewFormatter(c.format)
if err != nil {
c.UI.Error(fmt.Sprintf("Error outputting enhanced snapshot data: %s", err))
return 1
}
c.UI.Info(enhanced.String())
//Generate structs for the formatter with information we read in
metaformat := &MetadataInfo{
ID: meta.ID,
Size: meta.Size,
Index: meta.Index,
Term: meta.Term,
Version: meta.Version,
}

return 0
}
//Restructures stats given above to be human readable
formattedStats := generatetypeStats(stats)

// legacyStats outputs the expected stats from the original snapshot
// inspect command
func (c *cmd) legacyStats(meta *raft.SnapshotMeta) (bytes.Buffer, error) {
var b bytes.Buffer
tw := tabwriter.NewWriter(&b, 0, 2, 6, ' ', 0)
fmt.Fprintf(tw, "ID\t%s\n", meta.ID)
fmt.Fprintf(tw, "Size\t%d\n", meta.Size)
fmt.Fprintf(tw, "Index\t%d\n", meta.Index)
fmt.Fprintf(tw, "Term\t%d\n", meta.Term)
fmt.Fprintf(tw, "Version\t%d\n", meta.Version)
if err := tw.Flush(); err != nil {
return b, err
in := &OutputFormat{
Meta: metaformat,
Stats: formattedStats,
TotalSize: totalSize,
}
return b, nil
out, err := formatter.Format(in)
if err != nil {
c.UI.Error(err.Error())
return 1
}

c.UI.Output(out)
return 0
}

type typeStats struct {
Expand All @@ -124,6 +145,19 @@ type typeStats struct {
Count int
}

func generatetypeStats(info map[structs.MessageType]typeStats) []typeStats {
ss := make([]typeStats, 0, len(info))

for _, s := range info {
ss = append(ss, s)
}

// Sort the stat slice
sort.Slice(ss, func(i, j int) bool { return ss[i].Sum > ss[j].Sum })

return ss
}

// countingReader helps keep track of the bytes we have read
// when reading snapshots
type countingReader struct {
Expand Down Expand Up @@ -171,85 +205,6 @@ func enhance(file io.Reader) (map[structs.MessageType]typeStats, int, error) {

}

// readStats takes the information generated from enhance and creates human
// readable output from it
func (c *cmd) readStats(stats map[structs.MessageType]typeStats, totalSize int) (bytes.Buffer, error) {
// Output stats in size-order
ss := make([]typeStats, 0, len(stats))

for _, s := range stats {
ss = append(ss, s)
}

// Sort the stat slice
sort.Slice(ss, func(i, j int) bool { return ss[i].Sum > ss[j].Sum })

var b bytes.Buffer

tw := tabwriter.NewWriter(&b, 8, 8, 6, ' ', 0)
fmt.Fprintln(tw, "\n Type\tCount\tSize\t")
fmt.Fprintf(tw, " %s\t%s\t%s\t", "----", "----", "----")
// For each different type generate new output
for _, s := range ss {
fmt.Fprintf(tw, "\n %s\t%d\t%s\t", s.Name, s.Count, ByteSize(uint64(s.Sum)))
}
fmt.Fprintf(tw, "\n %s\t%s\t%s\t", "----", "----", "----")
fmt.Fprintf(tw, "\n Total\t\t%s\t", ByteSize(uint64(totalSize)))

if err := tw.Flush(); err != nil {
c.UI.Error(fmt.Sprintf("Error rendering snapshot info: %s", err))
return b, err
}

return b, nil

}

// ByteSize returns a human-readable byte string of the form 10MB, 12.5KB, and so forth. The following units are available:
// TB: Terabyte
// GB: Gigabyte
// MB: Megabyte
// KB: Kilobyte
// B: Byte
// The unit that results in the smallest number greater than or equal to 1 is always chosen.
// From https://github.com/cloudfoundry/bytefmt/blob/master/bytes.go

const (
BYTE = 1 << (10 * iota)
KILOBYTE
MEGABYTE
GIGABYTE
TERABYTE
)

func ByteSize(bytes uint64) string {
unit := ""
value := float64(bytes)

switch {
case bytes >= TERABYTE:
unit = "TB"
value = value / TERABYTE
case bytes >= GIGABYTE:
unit = "GB"
value = value / GIGABYTE
case bytes >= MEGABYTE:
unit = "MB"
value = value / MEGABYTE
case bytes >= KILOBYTE:
unit = "KB"
value = value / KILOBYTE
case bytes >= BYTE:
unit = "B"
case bytes == 0:
return "0"
}

result := strconv.FormatFloat(value, 'f', 1, 64)
result = strings.TrimSuffix(result, ".0")
return result + unit
}

func (c *cmd) Synopsis() string {
return synopsis
}
Expand Down
Loading

0 comments on commit 79ce24e

Please sign in to comment.