cli: Add JSON and Pretty Print formatting for `consul snapshot inspect` (#9006)

This commit is contained in:
s-christoff 2020-10-29 11:31:14 -05:00 committed by GitHub
parent a670f7a098
commit 79ce24e9fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 355 additions and 125 deletions

3
.changelog/9006.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
cli: snapshot inspect command supports JSON output
```

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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"
@ -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())
@ -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())
return 0
}
// 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
//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 b, nil
//Restructures stats given above to be human readable
formattedStats := generatetypeStats(stats)
in := &OutputFormat{
Meta: metaformat,
Stats: formattedStats,
TotalSize: totalSize,
}
out, err := formatter.Format(in)
if err != nil {
c.UI.Error(err.Error())
return 1
}
c.UI.Output(out)
return 0
}
type typeStats struct {
@ -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 {
@ -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
}

View File

@ -1,9 +1,8 @@
ID 2-13-1602222343947
Size 5141
Index 13
Term 2
Version 1
ID 2-13-1602222343947
Size 5141
Index 13
Term 2
Version 1
Type Count Size
---- ---- ----

View File

@ -0,0 +1,18 @@
ID 2-12-1603319127176
Size 5133
Index 12
Term 2
Version 1
Type Count Size
---- ---- ----
Register 3 1.8KB
ConnectCA 1 1.2KB
ConnectCAProviderState 1 1.1KB
Index 11 313B
ConnectCAConfig 1 247B
Autopilot 1 199B
SystemMetadata 1 68B
ChunkingState 1 12B
---- ---- ----
Total 5KB

View File

@ -0,0 +1,17 @@
{
"Meta": {
"ID": "one",
"Size": 2,
"Index": 3,
"Term": 4,
"Version": 1
},
"Stats": [
{
"Name": "msg",
"Sum": 1,
"Count": 2
}
],
"TotalSize": 1
}

View File

@ -0,0 +1,11 @@
ID one
Size 2
Index 3
Term 4
Version 1
Type Count Size
---- ---- ----
msg 2 1B
---- ---- ----
Total 1B

View File

@ -26,6 +26,8 @@ The following fields are displayed when inspecting a snapshot:
- `Version` - The snapshot format version. This only refers to the structure of
the snapshot, not the data contained within.
- Each data type, size, and count within the read snapshot.
## Usage
Usage: `consul snapshot inspect [options] FILE`
@ -36,11 +38,11 @@ To inspect a snapshot from the file "backup.snap":
```shell-session
$ consul snapshot inspect backup.snap
ID 2-5-1477944140022
Size 667
Index 5
Term 2
Version 1
ID 2-13-1603221729747
Size 5141
Index 13
Term 2
Version 1
Type Count Size
---- ---- ----
@ -48,14 +50,80 @@ Version 1
ConnectCA 1 1.2KB
ConnectCAProviderState 1 1.1KB
Index 12 344B
AutopilotRequest 1 199B
Autopilot 1 199B
ConnectCAConfig 1 197B
FederationState 1 139B
SystemMetadata 1 68B
ChunkingState 1 12B
---- ---- ----
Total 5KB
Total 5KB
```
To enhance a snapshot inespection from "backup.snap":
```shell-session
$ consul snapshot inspect -format=json backup.snap
{
"Meta": {
"ID": "2-13-1603221729747",
"Size": 5141,
"Index": 13,
"Term": 2,
"Version": 1
},
"Stats": [
{
"Name": "Register",
"Sum": 1750,
"Count": 3
},
{
"Name": "ConnectCA",
"Sum": 1258,
"Count": 1
},
{
"Name": "ConnectCAProviderState",
"Sum": 1174,
"Count": 1
},
{
"Name": "Index",
"Sum": 344,
"Count": 12
},
{
"Name": "Autopilot",
"Sum": 199,
"Count": 1
},
{
"Name": "ConnectCAConfig",
"Sum": 197,
"Count": 1
},
{
"Name": "FederationState",
"Sum": 139,
"Count": 1
},
{
"Name": "SystemMetadata",
"Sum": 68,
"Count": 1
},
{
"Name": "ChunkingState",
"Sum": 12,
"Count": 1
}
],
"TotalSize": 5141
}
```
Please see the [HTTP API](/api/snapshot) documentation for
more details about snapshot internals.
#### Command Options
- `-format` - Optional, allows from changing the output to JSON. Parameters accepted are "pretty" and "JSON".