Skip to content
This repository has been archived by the owner on Jun 12, 2024. It is now read-only.

Commit

Permalink
changed display from reason to opportunity
Browse files Browse the repository at this point in the history
  • Loading branch information
pnickolov committed Sep 22, 2021
1 parent 6dc2ea6 commit c428f6f
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 61 deletions.
29 changes: 24 additions & 5 deletions app/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,31 @@ type AppMetrics struct {
// TODO: add network traffic, esp. indication of traffic
}

type AppFlag int

const (
F_WRITEABLE_VOLUME = iota
F_RESOURCE_SPEC
F_SINGLE_REPLICA
F_MANY_REPLICAS
F_TRAFFIC
F_UTILIZATION
F_BURST
F_MAIN_CONTAINER
)

func (f AppFlag) String() string {
return []string{"V", "R", "S", "M", "T", "U", "B", "C"}[f]
}

type AppAnalysis struct {
Rating int `yaml:"rating"` // how suitable for optimization
Confidence int `yaml:"confidence"` // how confident is the rating
MainContainer string `yaml:"main_container"` // container to optimize or empty if not identified
Pros []string `yaml:"pros"` // list of pros for optimization
Cons []string `yaml:"cons"` // list of cons for optimizatoin
Rating int `yaml:"rating"` // how suitable for optimization
Confidence int `yaml:"confidence"` // how confident is the rating
MainContainer string `yaml:"main_container"` // container to optimize or empty if not identified
Flags map[AppFlag]bool `yaml:"flags"` // flags
Opportunities []string `yaml:"opportunities"` // list of optimization opportunities
Cautions []string `yaml:"cautions"` // list of concerns/cautions
Blockers []string `yaml:"blockers"` // list of blockers prevention optimization
}

type App struct {
Expand Down
83 changes: 68 additions & 15 deletions cmd/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This file is part of https://github.com/opsani/opsani-ignite
package cmd

import (
"fmt"
"math"
"sort"

Expand Down Expand Up @@ -245,12 +246,12 @@ func resourcesExplicitlyDefined(app *appmodel.App) (bool, string) {

// construct feedback message for human consumption
if !cpuGood && !memGood {
return false, "No resources defined (requests or limits required for cpu & memory resources)"
return false, "Resources not specified (request or limit for cpu and memory is required"
}
if !cpuGood {
return false, "CPU resources not defined (request or limit required)"
return false, "CPU resources not specified (request or limit is required)"
}
return false, "Memory resources not defined (request or limit required)"
return false, "Memory resources not specified (request or limit required)"
}

// --- App-level Analysis ----------------------------------------------------
Expand Down Expand Up @@ -282,53 +283,105 @@ func preAnalyzeApp(app *appmodel.App) {
}
}

func efficiencyImprovementEstimate(app *appmodel.App) string {
cpu := app.Metrics.CpuUtilization
mem := app.Metrics.MemoryUtilization
if cpu == 0 || mem == 0 {
return ""
}
if cpu >= 80 || mem >= 80 {
return ""
}
imp := (160 - cpu - mem) / 2.0
imp = float64(math.Round(imp/10) * 10)
if imp >= 60 {
return fmt.Sprintf("2x-%gx", 1+math.Round(100.0/imp*10)/10)
} else if imp > 20 {
return fmt.Sprintf("%0.f-%0.f%%", imp-20, imp)
} else {
return fmt.Sprintf("up to %0.f%%", imp)
}
}

func analyzeApp(app *appmodel.App) {
// finalize basis and prepare for analysis
preAnalyzeApp(app)

// start from current analysis
o := app.Analysis
if o.Flags == nil {
o.Flags = make(map[appmodel.AppFlag]bool)
}

// check main container
if app.Analysis.MainContainer != "" {
o.Flags[appmodel.F_MAIN_CONTAINER] = true
} else {
o.Blockers = append(o.Blockers, "Could not identify main container")
o.Flags[appmodel.F_MAIN_CONTAINER] = false
}

// having a writeable PVC disqualifies the app immediately (stateful)
if app.Settings.WriteableVolume {
o.Rating = -100
o.Confidence = 100
o.Cons = append(o.Cons, "Stateful: pods have writeable volumes")
o.Blockers = append(o.Blockers, "Stateful: pods have writeable volumes")
o.Flags[appmodel.F_WRITEABLE_VOLUME] = true
} else {
o.Flags[appmodel.F_WRITEABLE_VOLUME] = false
}

// missing resource specification (main container has no QoS)
if resGood, msg := resourcesExplicitlyDefined(app); resGood {
o.Pros = append(o.Pros, "Main container resources specified")
o.Flags[appmodel.F_RESOURCE_SPEC] = true
} else {
o.Rating = -100
o.Confidence = 100
o.Cons = append(o.Cons, msg)
o.Flags[appmodel.F_RESOURCE_SPEC] = false
o.Blockers = append(o.Blockers, msg)
}

// analyze utilization
o.Flags[appmodel.F_UTILIZATION] = app.Metrics.CpuUtilization > 0 && app.Metrics.MemoryUtilization > 0
utilBump := utilizationCombinedRating(app.Metrics.CpuUtilization, app.Metrics.MemoryUtilization)
if utilBump != 0 {
o.Rating += utilBump
o.Confidence += 30
if utilBump >= 30 {
o.Pros = append(o.Pros, "Resource utilization")
if app.Metrics.CpuUtilization >= 100 || app.Metrics.MemoryUtilization >= 100 {
o.Opportunities = append(o.Opportunities, "Improve performance/reliability")
o.Flags[appmodel.F_BURST] = true
} else if utilBump >= 30 {
effImpr := efficiencyImprovementEstimate(app)
if effImpr != "" {
effImpr = " by " + effImpr
}
o.Opportunities = append(o.Opportunities, fmt.Sprintf("Improve efficiency%v", effImpr))
o.Flags[appmodel.F_BURST] = false
} else if utilBump == 0 {
o.Cons = append(o.Cons, "Idle application")
o.Cautions = append(o.Cautions, "Idle application")
o.Flags[appmodel.F_BURST] = false
}
}

// analyze replica count
if app.Metrics.AverageReplicas <= 1 {
o.Rating -= 20
o.Confidence += 10
o.Cons = append(o.Cons, "Less than 2 replicas")
o.Cautions = append(o.Cautions, "Less than 2 replicas")
o.Flags[appmodel.F_SINGLE_REPLICA] = true
o.Flags[appmodel.F_MANY_REPLICAS] = false
} else if app.Metrics.AverageReplicas >= 7 {
o.Rating += 20
o.Confidence += 30
o.Pros = append(o.Pros, "7 or more replicas")
o.Flags[appmodel.F_SINGLE_REPLICA] = false
o.Flags[appmodel.F_MANY_REPLICAS] = true
} else if app.Metrics.AverageReplicas >= 3 {
o.Rating += 10
o.Confidence += 10
o.Flags[appmodel.F_SINGLE_REPLICA] = false
o.Flags[appmodel.F_MANY_REPLICAS] = false
}

// finalize blockers
if len(o.Blockers) > 0 {
o.Rating = -100
o.Confidence = 100
}

// bound rating and confidence
Expand Down
91 changes: 52 additions & 39 deletions cmd/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"io"
"math"
"sort"
"strings"

"github.com/olekukonko/tablewriter"
Expand Down Expand Up @@ -39,50 +40,61 @@ func getDisplayMethods() map[string]DisplayMethods {
}
}

func appReasonAndColor(app *appmodel.App) (string, int) {
var reason string
var color int // tablewriter colors
func appOpportunityAndColor(app *appmodel.App) (oppty string, color int) {
// note: color is among tablewriter colors

// handle unqualified apps
if !isQualifiedApp(app) {
if len(app.Analysis.Cons) > 0 {
reason = app.Analysis.Cons[0]
} else {
reason = "n/a"
}
color = 0 // keep default color (neutral); alt: tablewriter.FgRedColor
return
}

return reason, color
// list opportunities (usually one but allow for multiple)
if len(app.Analysis.Opportunities) > 0 {
oppty = strings.Join(app.Analysis.Opportunities, "\n")
} else {
oppty = "n/a"
}

// handle qualified apps depending on rating
// choose color depending on rating
if app.Analysis.Rating >= 50 {
if len(app.Analysis.Pros) > 0 {
reason = app.Analysis.Pros[0]
} else {
reason = "n/a"
}
color = tablewriter.FgGreenColor
} else {
if len(app.Analysis.Cons) > 0 {
reason = app.Analysis.Cons[0]
} else if len(app.Analysis.Pros) > 0 {
reason = app.Analysis.Pros[0]
} else {
reason = "n/a"
}
color = tablewriter.FgYellowColor
}

return reason, color
return
}

func flagsString(flags map[appmodel.AppFlag]bool) (ret string) {
type flagStruct struct {
flag appmodel.AppFlag
value bool
}

list := make([]flagStruct, 0, len(flags))
for f, v := range flags {
list = append(list, flagStruct{f, v})
}
sort.Slice(list, func(i, j int) bool {
return list[i].flag < list[j].flag
})
for _, e := range list {
if e.value {
ret += strings.ToUpper(e.flag.String())
} else {
ret += strings.ToLower(e.flag.String())
}
}
return
}

func (table *AppTable) outputTableHeader() {
const RIGHT = tablewriter.ALIGN_RIGHT
const LEFT = tablewriter.ALIGN_LEFT

table.t.SetHeader([]string{"Rating", "Confidence", "Namespace", "Deployment", "QoS Class", "Instances", "CPU", "Mem", "Reason"})
table.t.SetColumnAlignment([]int{RIGHT, RIGHT, LEFT, LEFT, LEFT, RIGHT, RIGHT, RIGHT, LEFT})
table.t.SetHeader([]string{"Namespace", "Deployment", "QoS Class", "Instances", "CPU", "Mem", "Opportunity", "Flags"})
table.t.SetColumnAlignment([]int{LEFT, LEFT, LEFT, RIGHT, RIGHT, RIGHT, LEFT, LEFT})
table.t.SetFooter([]string{})
table.t.SetCenterSeparator("")
table.t.SetColumnSeparator("")
Expand All @@ -92,17 +104,16 @@ func (table *AppTable) outputTableHeader() {
}

func (table *AppTable) outputTableApp(app *appmodel.App) {
reason, color := appReasonAndColor(app)
reason, color := appOpportunityAndColor(app)
rowValues := []string{
fmt.Sprintf("%d%%", app.Analysis.Rating),
fmt.Sprintf("%d%%", app.Analysis.Confidence),
app.Metadata.Namespace,
app.Metadata.Workload,
app.Settings.QosClass,
fmt.Sprintf("%.0fx%d", app.Metrics.AverageReplicas, len(app.Containers)),
fmt.Sprintf("%.0f%%", app.Metrics.CpuUtilization),
fmt.Sprintf("%.0f%%", app.Metrics.MemoryUtilization),
reason,
flagsString(app.Analysis.Flags),
}
cellColors := []int{color}
rowColors := make([]tablewriter.Colors, len(rowValues))
Expand All @@ -123,13 +134,11 @@ func (table *AppTable) outputDetailHeader() {

func (table *AppTable) outputDetailApp(app *appmodel.App) {
blank := []string{""}
_, appColor := appReasonAndColor(app)
_, appColor := appOpportunityAndColor(app)
appColors := []tablewriter.Colors{[]int{0}, []int{appColor}}
prosColors := []tablewriter.Colors{[]int{0}, []int{tablewriter.FgGreenColor}}
consColors := []tablewriter.Colors{[]int{0}, []int{tablewriter.FgYellowColor}}
if app.Analysis.Rating < 0 {
consColors = []tablewriter.Colors{[]int{0}, []int{tablewriter.FgRedColor}}
}
opportunityColors := []tablewriter.Colors{[]int{0}, []int{tablewriter.FgGreenColor}}
cautionColors := []tablewriter.Colors{[]int{0}, []int{tablewriter.FgYellowColor}}
blockerColors := []tablewriter.Colors{[]int{0}, []int{tablewriter.FgRedColor}}

table.t.Rich([]string{"Namespace", app.Metadata.Namespace}, nil)
table.t.Rich([]string{"Deployment", app.Metadata.Workload}, nil)
Expand All @@ -141,18 +150,22 @@ func (table *AppTable) outputDetailApp(app *appmodel.App) {
table.t.Rich([]string{"Confidence", fmt.Sprintf("%4d%%", app.Analysis.Confidence)}, appColors)

//table.Rich(blank, nil)
if len(app.Analysis.Pros) > 0 {
table.t.Rich([]string{"Pros", strings.Join(app.Analysis.Pros, "\n")}, prosColors)
if len(app.Analysis.Opportunities) > 0 {
table.t.Rich([]string{"Opportunities", strings.Join(app.Analysis.Opportunities, "\n")}, opportunityColors)
}
if len(app.Analysis.Cautions) > 0 {
table.t.Rich([]string{"Cautions", strings.Join(app.Analysis.Cautions, "\n")}, cautionColors)
}
if len(app.Analysis.Cons) > 0 {
table.t.Rich([]string{"Cons", strings.Join(app.Analysis.Cons, "\n")}, consColors)
if len(app.Analysis.Blockers) > 0 {
table.t.Rich([]string{"Blockers", strings.Join(app.Analysis.Blockers, "\n")}, blockerColors)
}

//table.Rich(blank, nil)
table.t.Rich([]string{"Average Replica Count", fmt.Sprintf("%3.0f%%", app.Metrics.AverageReplicas)}, nil)
table.t.Rich([]string{"Average Replica Count", fmt.Sprintf("%3.1g", 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([]string{"Opsani Flags", flagsString(app.Analysis.Flags)}, nil)

table.t.Rich(blank, nil)
}
Expand Down
2 changes: 0 additions & 2 deletions sources/prometheus/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,6 @@ func mapNamespace(ctx context.Context, promApi v1.API, namespace model.LabelValu
}
if err != nil {
log.Errorf("Failed to collect deployment details for app %v: %v\n", app.Metadata, err)
app.Analysis.Cons = append(app.Analysis.Cons, fmt.Sprintf("Failed to collect deployment details: %v", err))
}

//log.Tracef("%#v\n\n", app)
Expand Down Expand Up @@ -356,7 +355,6 @@ func collectSingleApp(ctx context.Context, promApi v1.API, namespace string, tim
}
if err != nil {
log.Errorf("Failed to collect deployment details for app %v: %v\n", app.Metadata, err)
app.Analysis.Cons = append(app.Analysis.Cons, fmt.Sprintf("Failed to collect deployment details: %v", err))
}

//log.Tracef("%#v\n\n", app)
Expand Down

0 comments on commit c428f6f

Please sign in to comment.