diff --git a/app/model/model.go b/app/model/model.go index 82e1286..f0b74cb 100644 --- a/app/model/model.go +++ b/app/model/model.go @@ -11,13 +11,14 @@ type AppMetadata struct { } type AppSettings struct { - Replicas int - HpaEnabled bool - VpaEnabled bool - MpaEnabled bool - HpaMinReplicas int - HpaMaxReplicas int - WriteableVolume bool + Replicas int `yaml:"-"` + HpaEnabled bool `yaml:"-"` + VpaEnabled bool `yaml:"-"` + MpaEnabled bool `yaml:"-"` + HpaMinReplicas int `yaml:"-"` + HpaMaxReplicas int `yaml:"-"` + WriteableVolume bool `yaml:"writeable_volume"` + QosClass string `yaml:"qos_class"` // TODO: consider adding replicas stats (min/max/avg/median) } @@ -30,36 +31,36 @@ type AppContainerResourceInfo struct { } type AppContainer struct { - Name string + Name string `yaml:"name"` Cpu struct { - AppContainerResourceInfo - SecondsThrottled float64 // average rate across instances/time - Shares float64 // alt source for Cpu.Request, in CPU shares (1000-1024 per core) - } + AppContainerResourceInfo `yaml:"resource"` + SecondsThrottled float64 `yaml:"seconds_throttled"` // average rate across instances/time + Shares float64 `yaml:"shares"` // alt source for Cpu.Request, in CPU shares (1000-1024 per core) + } `yaml:"cpu"` Memory struct { - AppContainerResourceInfo - } - RestartCount float64 + AppContainerResourceInfo `yaml:"resource"` + } `yaml:"memory"` + RestartCount float64 `yaml:"restart_count"` // yaml: don't omit empty, since 0 is a valid value } type AppMetrics struct { - AverageReplicas float64 - CpuUtilization float64 // aka Saturation, % - MemoryUtilization float64 + AverageReplicas float64 `yaml:"average_replicas"` + CpuUtilization float64 `yaml:"cpu_saturation"` // aka Saturation, % + MemoryUtilization float64 `yaml:"memory_saturation"` // TODO: add network traffic, esp. indication of traffic } type AppOpportunity struct { - Rating int // how suitable for optimization - Confidence int // how confident is the rating - Pros []string // list of pros for optimization - Cons []string // list of cons for optimizatoin + Rating int `yaml:"rating"` // how suitable for optimization + Confidence int `yaml:"confidence"` // how confident is the rating + Pros []string `yaml:"pros"` // list of pros for optimization + Cons []string `yaml:"cons"` // list of cons for optimizatoin } type App struct { - Metadata AppMetadata - Settings AppSettings - Containers []AppContainer - Metrics AppMetrics - Opportunity AppOpportunity + Metadata AppMetadata `yaml:"metadata"` + Settings AppSettings `yaml:"settings"` + Containers []AppContainer `yaml:"containers"` + Metrics AppMetrics `yaml:"metrics"` + Opportunity AppOpportunity `yaml:"analysis"` } diff --git a/cmd/ignite.go b/cmd/ignite.go index e9a985b..8ee01a6 100644 --- a/cmd/ignite.go +++ b/cmd/ignite.go @@ -120,9 +120,7 @@ func runIgnite(cmd *cobra.Command, args []string) { } display.WriteApp(table, app) } - fmt.Println("") - table.Render() - fmt.Println("") + display.WriteOut(table) if skipped > 0 { log.Infof("%v applications were not shown due to low rating. Use --show-all to see all apps", skipped) } diff --git a/cmd/output.go b/cmd/output.go index 14d939e..00b7292 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -8,25 +8,32 @@ package cmd import ( "fmt" "io" - appmodel "opsani-ignite/app/model" "strings" "github.com/olekukonko/tablewriter" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" + + appmodel "opsani-ignite/app/model" ) type AppTable struct { - tablewriter.Table // allows for adding methods locally + wr io.Writer + t tablewriter.Table // table writer, if used + yaml *yaml.Encoder // yaml encoder, if used } type DisplayMethods struct { WriteHeader func(table *AppTable) WriteApp func(table *AppTable, app *appmodel.App) + WriteOut func(table *AppTable) } func getDisplayMethods() map[string]DisplayMethods { return map[string]DisplayMethods{ - OUTPUT_TABLE: {(*AppTable).outputTableHeader, (*AppTable).outputTableApp}, - OUTPUT_DETAIL: {(*AppTable).outputDetailHeader, (*AppTable).outputDetailApp}, + OUTPUT_TABLE: {(*AppTable).outputTableHeader, (*AppTable).outputTableApp, (*AppTable).outputAnyTableOut}, + OUTPUT_DETAIL: {(*AppTable).outputDetailHeader, (*AppTable).outputDetailApp, (*AppTable).outputAnyTableOut}, + OUTPUT_YAML: {(*AppTable).outputYamlHeader, (*AppTable).outputYamlApp, (*AppTable).outputYamlOut}, } } @@ -72,14 +79,14 @@ func (table *AppTable) outputTableHeader() { const RIGHT = tablewriter.ALIGN_RIGHT const LEFT = tablewriter.ALIGN_LEFT - table.SetHeader([]string{"Rating", "Confidence", "Namespace", "Deployment", "Instances", "CPU", "Mem", "Reason"}) - table.SetColumnAlignment([]int{RIGHT, RIGHT, LEFT, LEFT, RIGHT, RIGHT, RIGHT, LEFT}) - table.SetFooter([]string{}) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetRowSeparator("") - table.SetHeaderLine(false) - table.SetBorder(false) + table.t.SetHeader([]string{"Rating", "Confidence", "Namespace", "Deployment", "Instances", "CPU", "Mem", "Reason"}) + table.t.SetColumnAlignment([]int{RIGHT, RIGHT, LEFT, LEFT, RIGHT, RIGHT, RIGHT, LEFT}) + table.t.SetFooter([]string{}) + table.t.SetCenterSeparator("") + table.t.SetColumnSeparator("") + table.t.SetRowSeparator("") + table.t.SetHeaderLine(false) + table.t.SetBorder(false) } func (table *AppTable) outputTableApp(app *appmodel.App) { @@ -99,16 +106,16 @@ func (table *AppTable) outputTableApp(app *appmodel.App) { for i := range rowColors { rowColors[i] = cellColors } - table.Rich(rowValues, rowColors) + table.t.Rich(rowValues, rowColors) } func (table *AppTable) outputDetailHeader() { - table.SetCenterSeparator("") - table.SetColumnSeparator(":") - table.SetRowSeparator("") - table.SetHeaderLine(false) - table.SetBorder(false) - table.SetAlignment(tablewriter.ALIGN_LEFT) + table.t.SetCenterSeparator("") + table.t.SetColumnSeparator(":") + table.t.SetRowSeparator("") + table.t.SetHeaderLine(false) + table.t.SetBorder(false) + table.t.SetAlignment(tablewriter.ALIGN_LEFT) } func (table *AppTable) outputDetailApp(app *appmodel.App) { @@ -121,30 +128,51 @@ func (table *AppTable) outputDetailApp(app *appmodel.App) { consColors = []tablewriter.Colors{[]int{0}, []int{tablewriter.FgRedColor}} } - table.Rich([]string{"Namespace", app.Metadata.Namespace}, nil) - table.Rich([]string{"Deployment", app.Metadata.Workload}, nil) - table.Rich([]string{"Kind", fmt.Sprintf("%v (%v)", app.Metadata.WorkloadKind, app.Metadata.WorkloadApiVersion)}, nil) + table.t.Rich([]string{"Namespace", app.Metadata.Namespace}, nil) + table.t.Rich([]string{"Deployment", app.Metadata.Workload}, nil) + table.t.Rich([]string{"Kind", fmt.Sprintf("%v (%v)", app.Metadata.WorkloadKind, app.Metadata.WorkloadApiVersion)}, nil) - table.Rich([]string{"Rating", fmt.Sprintf("%4d%%", app.Opportunity.Rating)}, appColors) - table.Rich([]string{"Confidence", fmt.Sprintf("%4d%%", app.Opportunity.Confidence)}, appColors) + table.t.Rich([]string{"Rating", fmt.Sprintf("%4d%%", app.Opportunity.Rating)}, appColors) + table.t.Rich([]string{"Confidence", fmt.Sprintf("%4d%%", app.Opportunity.Confidence)}, appColors) //table.Rich(blank, nil) if len(app.Opportunity.Pros) > 0 { - table.Rich([]string{"Pros", strings.Join(app.Opportunity.Pros, "\n")}, prosColors) + table.t.Rich([]string{"Pros", strings.Join(app.Opportunity.Pros, "\n")}, prosColors) } if len(app.Opportunity.Cons) > 0 { - table.Rich([]string{"Cons", strings.Join(app.Opportunity.Cons, "\n")}, consColors) + table.t.Rich([]string{"Cons", strings.Join(app.Opportunity.Cons, "\n")}, consColors) } //table.Rich(blank, nil) - table.Rich([]string{"Average Replica Count", fmt.Sprintf("%3.0f%%", app.Metrics.AverageReplicas)}, nil) - table.Rich([]string{"Container Count", fmt.Sprintf("%3d", len(app.Containers))}, nil) - table.Rich([]string{"CPU Utilization", fmt.Sprintf("%3.0f%%", app.Metrics.CpuUtilization)}, nil) - table.Rich([]string{"Memory Utilization", fmt.Sprintf("%3.0f%%", app.Metrics.MemoryUtilization)}, nil) + table.t.Rich([]string{"Average Replica Count", fmt.Sprintf("%3.0f%%", app.Metrics.AverageReplicas)}, nil) + table.t.Rich([]string{"Container Count", fmt.Sprintf("%3d", len(app.Containers))}, nil) + table.t.Rich([]string{"CPU Utilization", fmt.Sprintf("%3.0f%%", app.Metrics.CpuUtilization)}, nil) + table.t.Rich([]string{"Memory Utilization", fmt.Sprintf("%3.0f%%", app.Metrics.MemoryUtilization)}, nil) + + table.t.Rich(blank, nil) +} + +func (table *AppTable) outputAnyTableOut() { + fmt.Println("") + table.t.Render() + fmt.Println("") +} + +func (table *AppTable) outputYamlHeader() { + table.yaml = yaml.NewEncoder(table.wr) +} + +func (table *AppTable) outputYamlApp(app *appmodel.App) { + err := table.yaml.Encode(*app) + if err != nil { + log.Errorf("Failed to write app %v to yaml: %v", app.Metadata, err) + } +} - table.Rich(blank, nil) +func (table *AppTable) outputYamlOut() { + table.yaml.Close() } func newAppTable(wr io.Writer) *AppTable { - return &AppTable{*tablewriter.NewWriter(wr)} + return &AppTable{wr, *tablewriter.NewWriter(wr), nil} } diff --git a/cmd/root.go b/cmd/root.go index 917ddaa..31e3cad 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,12 +27,12 @@ var showDebug bool const ( OUTPUT_TABLE = "table" OUTPUT_DETAIL = "detail" - // TODO: add JSON and YAML + OUTPUT_YAML = "yaml" ) // constant table - format types, keep in sync with OUTPUT_xxx constants above func getOutputFormats() []string { - return []string{OUTPUT_TABLE, OUTPUT_DETAIL} + return []string{OUTPUT_TABLE, OUTPUT_DETAIL, OUTPUT_YAML} } // rootCmd represents the base command when called without any subcommands diff --git a/go.mod b/go.mod index 117a713..0add181 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,11 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/prometheus/client_golang v1.11.0 github.com/prometheus/common v0.26.0 - github.com/sirupsen/logrus v1.8.1 // indirect + github.com/sirupsen/logrus v1.8.1 github.com/spf13/cast v1.4.1 // indirect github.com/spf13/cobra v1.2.1 github.com/spf13/viper v1.8.1 golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect golang.org/x/text v0.3.7 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/sources/prometheus/containers.go b/sources/prometheus/containers.go index 12559f7..2e1e44f 100644 --- a/sources/prometheus/containers.go +++ b/sources/prometheus/containers.go @@ -222,7 +222,10 @@ func collectContainersInfo(ctx context.Context, promApi v1.API, app *appmodel.Ap if len(labels) != 1 || !ok { return warnings, fmt.Errorf("Query %q returned labels %v, expected %v", query, labels, []string{"container"}) } - app.Containers = append(app.Containers, appmodel.AppContainer{Name: string(name)}) + container := appmodel.AppContainer{Name: string(name)} + container.Cpu.Unit = "cores" + container.Memory.Unit = "bytes" + app.Containers = append(app.Containers, container) } // Get resource requests