From d78b6cdc44ec621fd95933ed0b6fe6a188f93f31 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sat, 8 Dec 2018 12:44:10 -0500 Subject: [PATCH] Export metrics to a file (#122) --- cmd/analyze.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++--- cmd/build.go | 13 +------ cmd/root.go | 4 +- 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/cmd/analyze.go b/cmd/analyze.go index 184ef90..3e6604f 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "fmt" "github.com/fatih/color" "github.com/spf13/cobra" @@ -8,6 +9,7 @@ import ( "github.com/wagoodman/dive/image" "github.com/wagoodman/dive/ui" "github.com/wagoodman/dive/utils" + "io/ioutil" ) // doAnalyzeCmd takes a docker image tag, digest, or id and displays the @@ -32,14 +34,84 @@ func doAnalyzeCmd(cmd *cobra.Command, args []string) { cmd.Help() utils.Exit(1) } - color.New(color.Bold).Println("Analyzing Image") - result := fetchAndAnalyze(userImage) - fmt.Println(" Building cache...") - cache := filetree.NewFileTreeCache(result.RefTrees) - cache.Build() + run(userImage) +} - ui.Run(result, cache) +type export struct { + Layer []exportLayer `json:"layer"` + Image exportImage `json:"image"` +} + +type exportLayer struct { + Index int `json:"index"` + DigestID string `json:"digestId"` + SizeBytes uint64 `json:"sizeBytes"` + Command string `json:"command"` +} +type exportImage struct { + SizeBytes uint64 `json:"sizeBytes"` + InefficientBytes uint64 `json:"inefficientBytes"` + EfficiencyScore float64 `json:"efficiencyScore"` + InefficientFiles []inefficientFiles `json:"inefficientFiles"` +} + +type inefficientFiles struct { + Count int `json:"count"` + SizeBytes uint64 `json:"sizeBytes"` + File string `json:"file"` +} + +func newExport(analysis *image.AnalysisResult) *export { + data := export{} + data.Layer = make([]exportLayer, len(analysis.Layers)) + data.Image.InefficientFiles = make([]inefficientFiles, len(analysis.Inefficiencies)) + + // export layers in order + for revIdx := len(analysis.Layers) - 1; revIdx >= 0; revIdx-- { + layer := analysis.Layers[revIdx] + idx := (len(analysis.Layers) - 1) - revIdx + + data.Layer[idx] = exportLayer{ + Index: idx, + DigestID: layer.Id(), + SizeBytes: layer.Size(), + Command: layer.Command(), + } + } + + // export image info + data.Image.SizeBytes = 0 + for idx := 0; idx < len(analysis.Layers); idx++ { + data.Image.SizeBytes += analysis.Layers[idx].Size() + } + + data.Image.EfficiencyScore = analysis.Efficiency + + for idx := 0; idx < len(analysis.Inefficiencies); idx++ { + fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx] + data.Image.InefficientBytes += uint64(fileData.CumulativeSize) + + data.Image.InefficientFiles[idx] = inefficientFiles{ + Count: len(fileData.Nodes), + SizeBytes: uint64(fileData.CumulativeSize), + File: fileData.Path, + } + } + + return &data +} + +func exportStatistics(analysis *image.AnalysisResult) { + data := newExport(analysis) + payload, err := json.MarshalIndent(&data, "", " ") + if err != nil { + panic(err) + } + err = ioutil.WriteFile(exportFile, payload, 0644) + if err != nil { + panic(err) + } } func fetchAndAnalyze(imageID string) *image.AnalysisResult { @@ -60,3 +132,20 @@ func fetchAndAnalyze(imageID string) *image.AnalysisResult { } return result } + +func run(imageID string) { + color.New(color.Bold).Println("Analyzing Image") + result := fetchAndAnalyze(imageID) + + if exportFile != "" { + exportStatistics(result) + color.New(color.Bold).Println(fmt.Sprintf("Exported to %s", exportFile)) + utils.Exit(0) + } + + fmt.Println(" Building cache...") + cache := filetree.NewFileTreeCache(result.RefTrees) + cache.Build() + + ui.Run(result, cache) +} diff --git a/cmd/build.go b/cmd/build.go index 315e574..658ef11 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -1,12 +1,8 @@ package cmd import ( - "fmt" - "github.com/fatih/color" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/wagoodman/dive/filetree" - "github.com/wagoodman/dive/ui" "github.com/wagoodman/dive/utils" "io/ioutil" "os" @@ -47,12 +43,5 @@ func doBuildCmd(cmd *cobra.Command, args []string) { log.Fatal(err) } - color.New(color.Bold).Println("Analyzing Image") - result := fetchAndAnalyze(string(imageId)) - - fmt.Println(" Building cache...") - cache := filetree.NewFileTreeCache(result.RefTrees) - cache.Build() - - ui.Run(result, cache) + run(string(imageId)) } diff --git a/cmd/root.go b/cmd/root.go index aa97fea..df4ada5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,6 +15,7 @@ import ( ) var cfgFile string +var exportFile string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ @@ -42,8 +43,9 @@ func init() { cobra.OnInitialize(initLogging) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.dive.yaml, ~/.config/dive.yaml, or $XDG_CONFIG_HOME/dive.yaml)") - rootCmd.PersistentFlags().BoolP("version", "v", false, "display version number") + + rootCmd.Flags().StringVarP(&exportFile, "json", "j", "", "Skip the interactive TUI and write the layer analysis statistics to a given file.") } // initConfig reads in config file and ENV variables if set.