diff --git a/README.md b/README.md index 0f4b9de464..786e265a1e 100644 --- a/README.md +++ b/README.md @@ -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 +``` + ## LLM AI Backends diff --git a/cmd/analyze/analyze.go b/cmd/analyze/analyze.go index 0be570ba70..105ebdf7c2 100644 --- a/cmd/analyze/analyze.go +++ b/cmd/analyze/analyze.go @@ -40,6 +40,7 @@ var ( interactiveMode bool customAnalysis bool customHeaders []string + withStats bool ) // AnalyzeCmd represents the problems command @@ -63,6 +64,7 @@ var AnalyzeCmd = &cobra.Command{ withDoc, interactiveMode, customHeaders, + withStats, ) if err != nil { @@ -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 { @@ -146,4 +154,6 @@ func init() { AnalyzeCmd.Flags().StringSliceVarP(&customHeaders, "custom-headers", "r", []string{}, "Custom Headers, : (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.") } diff --git a/pkg/analysis/analysis.go b/pkg/analysis/analysis.go index 1a28c9846a..79f46d5aea 100644 --- a/pkg/analysis/analysis.go +++ b/pkg/analysis/analysis.go @@ -18,9 +18,9 @@ import ( "encoding/base64" "errors" "fmt" - "reflect" "strings" "sync" + "time" "github.com/fatih/color" openapi_v2 "github.com/google/gnostic/openapiv2" @@ -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 ( @@ -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") @@ -112,6 +115,7 @@ func NewAnalysis( Explain: explain, MaxConcurrency: maxConcurrency, WithDoc: withDoc, + WithStats: withStats, } if !explain { // Return early if AI use was not requested. @@ -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() @@ -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)) } @@ -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 diff --git a/pkg/analysis/output.go b/pkg/analysis/output.go index d599671a04..4684aade7f 100644 --- a/pkg/analysis/output.go +++ b/pkg/analysis/output.go @@ -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 diff --git a/pkg/common/types.go b/pkg/common/types.go index 98c14140c5..6345a28169 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -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" @@ -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 diff --git a/pkg/server/analyze/analyze.go b/pkg/server/analyze/analyze.go index 3fc68b8b4b..0d61682abd 100644 --- a/pkg/server/analyze/analyze.go +++ b/pkg/server/analyze/analyze.go @@ -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" ) @@ -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 {