Skip to content

Commit 3eec9bb

Browse files
matthishollevilleAlexsJonesarbreezy
authored
feat: add stats option to analyze command for performance insights (#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 <[email protected]> * 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 <[email protected]> --------- Signed-off-by: Matthis Holleville <[email protected]> Co-authored-by: Alex Jones <[email protected]> Co-authored-by: Aris Boutselis <[email protected]>
1 parent 87565a0 commit 3eec9bb

File tree

6 files changed

+96
-42
lines changed

6 files changed

+96
-42
lines changed

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,22 @@ _Analysis with custom headers_
319319
k8sgpt analyze --explain --custom-headers CustomHeaderKey:CustomHeaderValue
320320
```
321321

322+
_Print analysis stats_
323+
324+
```
325+
k8sgpt analyze -s
326+
The stats mode allows for debugging and understanding the time taken by an analysis by displaying the statistics of each analyzer.
327+
- Analyzer Ingress took 47.125583ms
328+
- Analyzer PersistentVolumeClaim took 53.009167ms
329+
- Analyzer CronJob took 57.517792ms
330+
- Analyzer Deployment took 156.6205ms
331+
- Analyzer Node took 160.109833ms
332+
- Analyzer ReplicaSet took 245.938333ms
333+
- Analyzer StatefulSet took 448.0455ms
334+
- Analyzer Pod took 5.662594708s
335+
- Analyzer Service took 38.583359166s
336+
```
337+
322338
</details>
323339

324340
## LLM AI Backends

cmd/analyze/analyze.go

+10
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ var (
4040
interactiveMode bool
4141
customAnalysis bool
4242
customHeaders []string
43+
withStats bool
4344
)
4445

4546
// AnalyzeCmd represents the problems command
@@ -63,6 +64,7 @@ var AnalyzeCmd = &cobra.Command{
6364
withDoc,
6465
interactiveMode,
6566
customHeaders,
67+
withStats,
6668
)
6769

6870
if err != nil {
@@ -88,6 +90,12 @@ var AnalyzeCmd = &cobra.Command{
8890
color.Red("Error: %v", err)
8991
os.Exit(1)
9092
}
93+
94+
if withStats {
95+
statsData := config.PrintStats()
96+
fmt.Println(string(statsData))
97+
}
98+
9199
fmt.Println(string(output_data))
92100

93101
if interactiveMode && explain {
@@ -146,4 +154,6 @@ func init() {
146154
AnalyzeCmd.Flags().StringSliceVarP(&customHeaders, "custom-headers", "r", []string{}, "Custom Headers, <key>:<value> (e.g CustomHeaderKey:CustomHeaderValue AnotherHeader:AnotherValue)")
147155
// label selector flag
148156
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.")
157+
// print stats
158+
AnalyzeCmd.Flags().BoolVarP(&withStats, "with-stat", "s", false, "Print analysis stats. This option disables errors display.")
149159
}

pkg/analysis/analysis.go

+49-41
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import (
1818
"encoding/base64"
1919
"errors"
2020
"fmt"
21-
"reflect"
2221
"strings"
2322
"sync"
23+
"time"
2424

2525
"github.com/fatih/color"
2626
openapi_v2 "github.com/google/gnostic/openapiv2"
@@ -50,6 +50,8 @@ type Analysis struct {
5050
MaxConcurrency int
5151
AnalysisAIProvider string // The name of the AI Provider used for this analysis
5252
WithDoc bool
53+
WithStats bool
54+
Stats []common.AnalysisStats
5355
}
5456

5557
type (
@@ -82,6 +84,7 @@ func NewAnalysis(
8284
withDoc bool,
8385
interactiveMode bool,
8486
httpHeaders []string,
87+
withStats bool,
8588
) (*Analysis, error) {
8689
// Get kubernetes client from viper.
8790
kubecontext := viper.GetString("kubecontext")
@@ -112,6 +115,7 @@ func NewAnalysis(
112115
Explain: explain,
113116
MaxConcurrency: maxConcurrency,
114117
WithDoc: withDoc,
118+
WithStats: withStats,
115119
}
116120
if !explain {
117121
// Return early if AI use was not requested.
@@ -243,22 +247,10 @@ func (a *Analysis) RunAnalysis() {
243247
var mutex sync.Mutex
244248
// if there are no filters selected and no active_filters then run coreAnalyzer
245249
if len(a.Filters) == 0 && len(activeFilters) == 0 {
246-
for _, analyzer := range coreAnalyzerMap {
250+
for name, analyzer := range coreAnalyzerMap {
247251
wg.Add(1)
248252
semaphore <- struct{}{}
249-
go func(analyzer common.IAnalyzer, wg *sync.WaitGroup, semaphore chan struct{}) {
250-
defer wg.Done()
251-
results, err := analyzer.Analyze(analyzerConfig)
252-
if err != nil {
253-
mutex.Lock()
254-
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", reflect.TypeOf(analyzer).Name(), err))
255-
mutex.Unlock()
256-
}
257-
mutex.Lock()
258-
a.Results = append(a.Results, results...)
259-
mutex.Unlock()
260-
<-semaphore
261-
}(analyzer, &wg, semaphore)
253+
go a.executeAnalyzer(analyzer, name, analyzerConfig, semaphore, &wg, &mutex)
262254

263255
}
264256
wg.Wait()
@@ -270,19 +262,7 @@ func (a *Analysis) RunAnalysis() {
270262
if analyzer, ok := analyzerMap[filter]; ok {
271263
semaphore <- struct{}{}
272264
wg.Add(1)
273-
go func(analyzer common.IAnalyzer, filter string) {
274-
defer wg.Done()
275-
results, err := analyzer.Analyze(analyzerConfig)
276-
if err != nil {
277-
mutex.Lock()
278-
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
279-
mutex.Unlock()
280-
}
281-
mutex.Lock()
282-
a.Results = append(a.Results, results...)
283-
mutex.Unlock()
284-
<-semaphore
285-
}(analyzer, filter)
265+
go a.executeAnalyzer(analyzer, filter, analyzerConfig, semaphore, &wg, &mutex)
286266
} else {
287267
a.Errors = append(a.Errors, fmt.Sprintf("\"%s\" filter does not exist. Please run k8sgpt filters list.", filter))
288268
}
@@ -296,24 +276,52 @@ func (a *Analysis) RunAnalysis() {
296276
if analyzer, ok := analyzerMap[filter]; ok {
297277
semaphore <- struct{}{}
298278
wg.Add(1)
299-
go func(analyzer common.IAnalyzer, filter string) {
300-
defer wg.Done()
301-
results, err := analyzer.Analyze(analyzerConfig)
302-
if err != nil {
303-
mutex.Lock()
304-
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
305-
mutex.Unlock()
306-
}
307-
mutex.Lock()
308-
a.Results = append(a.Results, results...)
309-
mutex.Unlock()
310-
<-semaphore
311-
}(analyzer, filter)
279+
go a.executeAnalyzer(analyzer, filter, analyzerConfig, semaphore, &wg, &mutex)
312280
}
313281
}
314282
wg.Wait()
315283
}
316284

285+
func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, analyzerConfig common.Analyzer, semaphore chan struct{}, wg *sync.WaitGroup, mutex *sync.Mutex) {
286+
defer wg.Done()
287+
288+
var startTime time.Time
289+
var elapsedTime time.Duration
290+
291+
// Start the timer
292+
if a.WithStats {
293+
startTime = time.Now()
294+
}
295+
296+
// Run the analyzer
297+
results, err := analyzer.Analyze(analyzerConfig)
298+
299+
// Measure the time taken
300+
if a.WithStats {
301+
elapsedTime = time.Since(startTime)
302+
}
303+
stat := common.AnalysisStats{
304+
Analyzer: filter,
305+
DurationTime: elapsedTime,
306+
}
307+
308+
mutex.Lock()
309+
defer mutex.Unlock()
310+
311+
if err != nil {
312+
if a.WithStats {
313+
a.Stats = append(a.Stats, stat)
314+
}
315+
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
316+
} else {
317+
if a.WithStats {
318+
a.Stats = append(a.Stats, stat)
319+
}
320+
a.Results = append(a.Results, results...)
321+
}
322+
<-semaphore
323+
}
324+
317325
func (a *Analysis) GetAIResults(output string, anonymize bool) error {
318326
if len(a.Results) == 0 {
319327
return nil

pkg/analysis/output.go

+12
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ func (a *Analysis) jsonOutput() ([]byte, error) {
5555
return output, nil
5656
}
5757

58+
func (a *Analysis) PrintStats() []byte {
59+
var output strings.Builder
60+
61+
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"))
62+
63+
for _, stat := range a.Stats {
64+
output.WriteString(fmt.Sprintf("- Analyzer %s took %s \n", color.YellowString(stat.Analyzer), stat.DurationTime))
65+
}
66+
67+
return []byte(output.String())
68+
}
69+
5870
func (a *Analysis) textOutput() ([]byte, error) {
5971
var output strings.Builder
6072

pkg/common/types.go

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package common
1515

1616
import (
1717
"context"
18+
"time"
1819

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

84+
type AnalysisStats struct {
85+
Analyzer string `json:"analyzer"`
86+
DurationTime time.Duration `json:"durationTime"`
87+
}
88+
8389
type Failure struct {
8490
Text string
8591
KubernetesDoc string

pkg/server/analyze/analyze.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package analyze
22

33
import (
4-
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
54
"context"
65
json "encoding/json"
6+
7+
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
78
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
89
)
910

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

0 commit comments

Comments
 (0)