Skip to content

Commit

Permalink
feat: add stats option to analyze command for performance insights (#…
Browse files Browse the repository at this point in the history
…1237)

* feat: add stats option to analyze command for performance insights

Introduced a new feature to the analyze command that enables users to print detailed performance statistics of each analyzer. This enhancement aids in debugging and understanding the time taken by various components during analysis, providing valuable insights for performance optimization.

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

* feat: enhance analysis command with statistics option

Refactored the analysis command to support an enhanced statistics option, enabling users to opt-in for detailed performance metrics of the analysis process. This change introduces a more flexible approach to handling statistics, allowing for a clearer separation between the analysis output and performance metrics, thereby improving the usability and insights provided to the user.

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

---------

Signed-off-by: Matthis Holleville <matthish29@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: Aris Boutselis <arisboutselis08@gmail.com>
  • Loading branch information
3 people authored Oct 30, 2024
1 parent 87565a0 commit 3eec9bb
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 42 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,22 @@ _Analysis with custom headers_
k8sgpt analyze --explain --custom-headers CustomHeaderKey:CustomHeaderValue
```

_Print analysis stats_

```
k8sgpt analyze -s
The stats mode allows for debugging and understanding the time taken by an analysis by displaying the statistics of each analyzer.
- Analyzer Ingress took 47.125583ms
- Analyzer PersistentVolumeClaim took 53.009167ms
- Analyzer CronJob took 57.517792ms
- Analyzer Deployment took 156.6205ms
- Analyzer Node took 160.109833ms
- Analyzer ReplicaSet took 245.938333ms
- Analyzer StatefulSet took 448.0455ms
- Analyzer Pod took 5.662594708s
- Analyzer Service took 38.583359166s
```

</details>

## LLM AI Backends
Expand Down
10 changes: 10 additions & 0 deletions cmd/analyze/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ var (
interactiveMode bool
customAnalysis bool
customHeaders []string
withStats bool
)

// AnalyzeCmd represents the problems command
Expand All @@ -63,6 +64,7 @@ var AnalyzeCmd = &cobra.Command{
withDoc,
interactiveMode,
customHeaders,
withStats,
)

if err != nil {
Expand All @@ -88,6 +90,12 @@ var AnalyzeCmd = &cobra.Command{
color.Red("Error: %v", err)
os.Exit(1)
}

if withStats {
statsData := config.PrintStats()
fmt.Println(string(statsData))
}

fmt.Println(string(output_data))

if interactiveMode && explain {
Expand Down Expand Up @@ -146,4 +154,6 @@ func init() {
AnalyzeCmd.Flags().StringSliceVarP(&customHeaders, "custom-headers", "r", []string{}, "Custom Headers, <key>:<value> (e.g CustomHeaderKey:CustomHeaderValue AnotherHeader:AnotherValue)")
// label selector flag
AnalyzeCmd.Flags().StringVarP(&labelSelector, "selector", "L", "", "Label selector (label query) to filter on, supports '=', '==', and '!='. (e.g. -L key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.")
// print stats
AnalyzeCmd.Flags().BoolVarP(&withStats, "with-stat", "s", false, "Print analysis stats. This option disables errors display.")
}
90 changes: 49 additions & 41 deletions pkg/analysis/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import (
"encoding/base64"
"errors"
"fmt"
"reflect"
"strings"
"sync"
"time"

"github.com/fatih/color"
openapi_v2 "github.com/google/gnostic/openapiv2"
Expand Down Expand Up @@ -50,6 +50,8 @@ type Analysis struct {
MaxConcurrency int
AnalysisAIProvider string // The name of the AI Provider used for this analysis
WithDoc bool
WithStats bool
Stats []common.AnalysisStats
}

type (
Expand Down Expand Up @@ -82,6 +84,7 @@ func NewAnalysis(
withDoc bool,
interactiveMode bool,
httpHeaders []string,
withStats bool,
) (*Analysis, error) {
// Get kubernetes client from viper.
kubecontext := viper.GetString("kubecontext")
Expand Down Expand Up @@ -112,6 +115,7 @@ func NewAnalysis(
Explain: explain,
MaxConcurrency: maxConcurrency,
WithDoc: withDoc,
WithStats: withStats,
}
if !explain {
// Return early if AI use was not requested.
Expand Down Expand Up @@ -243,22 +247,10 @@ func (a *Analysis) RunAnalysis() {
var mutex sync.Mutex
// if there are no filters selected and no active_filters then run coreAnalyzer
if len(a.Filters) == 0 && len(activeFilters) == 0 {
for _, analyzer := range coreAnalyzerMap {
for name, analyzer := range coreAnalyzerMap {
wg.Add(1)
semaphore <- struct{}{}
go func(analyzer common.IAnalyzer, wg *sync.WaitGroup, semaphore chan struct{}) {
defer wg.Done()
results, err := analyzer.Analyze(analyzerConfig)
if err != nil {
mutex.Lock()
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", reflect.TypeOf(analyzer).Name(), err))
mutex.Unlock()
}
mutex.Lock()
a.Results = append(a.Results, results...)
mutex.Unlock()
<-semaphore
}(analyzer, &wg, semaphore)
go a.executeAnalyzer(analyzer, name, analyzerConfig, semaphore, &wg, &mutex)

}
wg.Wait()
Expand All @@ -270,19 +262,7 @@ func (a *Analysis) RunAnalysis() {
if analyzer, ok := analyzerMap[filter]; ok {
semaphore <- struct{}{}
wg.Add(1)
go func(analyzer common.IAnalyzer, filter string) {
defer wg.Done()
results, err := analyzer.Analyze(analyzerConfig)
if err != nil {
mutex.Lock()
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
mutex.Unlock()
}
mutex.Lock()
a.Results = append(a.Results, results...)
mutex.Unlock()
<-semaphore
}(analyzer, filter)
go a.executeAnalyzer(analyzer, filter, analyzerConfig, semaphore, &wg, &mutex)
} else {
a.Errors = append(a.Errors, fmt.Sprintf("\"%s\" filter does not exist. Please run k8sgpt filters list.", filter))
}
Expand All @@ -296,24 +276,52 @@ func (a *Analysis) RunAnalysis() {
if analyzer, ok := analyzerMap[filter]; ok {
semaphore <- struct{}{}
wg.Add(1)
go func(analyzer common.IAnalyzer, filter string) {
defer wg.Done()
results, err := analyzer.Analyze(analyzerConfig)
if err != nil {
mutex.Lock()
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
mutex.Unlock()
}
mutex.Lock()
a.Results = append(a.Results, results...)
mutex.Unlock()
<-semaphore
}(analyzer, filter)
go a.executeAnalyzer(analyzer, filter, analyzerConfig, semaphore, &wg, &mutex)
}
}
wg.Wait()
}

func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, analyzerConfig common.Analyzer, semaphore chan struct{}, wg *sync.WaitGroup, mutex *sync.Mutex) {
defer wg.Done()

var startTime time.Time
var elapsedTime time.Duration

// Start the timer
if a.WithStats {
startTime = time.Now()
}

// Run the analyzer
results, err := analyzer.Analyze(analyzerConfig)

// Measure the time taken
if a.WithStats {
elapsedTime = time.Since(startTime)
}
stat := common.AnalysisStats{
Analyzer: filter,
DurationTime: elapsedTime,
}

mutex.Lock()
defer mutex.Unlock()

if err != nil {
if a.WithStats {
a.Stats = append(a.Stats, stat)
}
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
} else {
if a.WithStats {
a.Stats = append(a.Stats, stat)
}
a.Results = append(a.Results, results...)
}
<-semaphore
}

func (a *Analysis) GetAIResults(output string, anonymize bool) error {
if len(a.Results) == 0 {
return nil
Expand Down
12 changes: 12 additions & 0 deletions pkg/analysis/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ func (a *Analysis) jsonOutput() ([]byte, error) {
return output, nil
}

func (a *Analysis) PrintStats() []byte {
var output strings.Builder

output.WriteString(color.YellowString("The stats mode allows for debugging and understanding the time taken by an analysis by displaying the statistics of each analyzer.\n"))

for _, stat := range a.Stats {
output.WriteString(fmt.Sprintf("- Analyzer %s took %s \n", color.YellowString(stat.Analyzer), stat.DurationTime))
}

return []byte(output.String())
}

func (a *Analysis) textOutput() ([]byte, error) {
var output strings.Builder

Expand Down
6 changes: 6 additions & 0 deletions pkg/common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package common

import (
"context"
"time"

trivy "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
openapi_v2 "github.com/google/gnostic/openapiv2"
Expand Down Expand Up @@ -80,6 +81,11 @@ type Result struct {
ParentObject string `json:"parentObject"`
}

type AnalysisStats struct {
Analyzer string `json:"analyzer"`
DurationTime time.Duration `json:"durationTime"`
}

type Failure struct {
Text string
KubernetesDoc string
Expand Down
4 changes: 3 additions & 1 deletion pkg/server/analyze/analyze.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package analyze

import (
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"context"
json "encoding/json"

schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
)

Expand Down Expand Up @@ -31,6 +32,7 @@ func (h *Handler) Analyze(ctx context.Context, i *schemav1.AnalyzeRequest) (
false, // Kubernetes Doc disabled in server mode
false, // Interactive mode disabled in server mode
[]string{}, //TODO: add custom http headers in server mode
false, // with stats disable
)
config.Context = ctx // Replace context for correct timeouts.
if err != nil {
Expand Down

0 comments on commit 3eec9bb

Please sign in to comment.