mirror of
https://github.com/turbot/steampipe.git
synced 2025-12-19 18:12:43 -05:00
193 lines
5.4 KiB
Go
193 lines
5.4 KiB
Go
package display
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/spf13/viper"
|
|
pconstants "github.com/turbot/pipe-fittings/v2/constants"
|
|
"github.com/turbot/steampipe/v2/pkg/query/queryresult"
|
|
"golang.org/x/text/language"
|
|
"golang.org/x/text/message"
|
|
)
|
|
|
|
func DisplayTiming(result *queryresult.Result, rowCount int) {
|
|
// show timing
|
|
timingResult := getTiming(result, rowCount)
|
|
if viper.GetString(pconstants.ArgTiming) != pconstants.ArgOff && timingResult != nil {
|
|
str := buildTimingString(timingResult)
|
|
if viper.GetBool(pconstants.ConfigKeyInteractive) {
|
|
fmt.Println(str)
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, str)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getTiming(result *queryresult.Result, count int) *queryresult.TimingResult {
|
|
timingConfig := viper.GetString(pconstants.ArgTiming)
|
|
|
|
if timingConfig == pconstants.ArgOff || timingConfig == "false" {
|
|
return nil
|
|
}
|
|
// now we have iterated the rows, get the timing
|
|
timingResult := <-result.Timing.Stream
|
|
// set rows returned
|
|
timingResult.RowsReturned = int64(count)
|
|
|
|
if timingConfig != pconstants.ArgVerbose {
|
|
timingResult.Scans = nil
|
|
}
|
|
return timingResult
|
|
}
|
|
|
|
func buildTimingString(timingResult *queryresult.TimingResult) string {
|
|
var sb strings.Builder
|
|
// large numbers should be formatted with commas
|
|
p := message.NewPrinter(language.English)
|
|
|
|
sb.WriteString(fmt.Sprintf("\nTime: %s.", getDurationString(timingResult.DurationMs, p)))
|
|
sb.WriteString(p.Sprintf(" Rows returned: %d.", timingResult.RowsReturned))
|
|
totalRowsFetched := timingResult.UncachedRowsFetched + timingResult.CachedRowsFetched
|
|
if totalRowsFetched == 0 {
|
|
// maybe there was an error retrieving timing - just display the basics
|
|
return sb.String()
|
|
}
|
|
|
|
sb.WriteString(" Rows fetched: ")
|
|
if totalRowsFetched == 0 {
|
|
sb.WriteString("0")
|
|
} else {
|
|
|
|
// calculate the number of cached rows fetched
|
|
|
|
sb.WriteString(p.Sprintf("%d", totalRowsFetched))
|
|
|
|
// were all cached
|
|
if timingResult.UncachedRowsFetched == 0 {
|
|
sb.WriteString(" (cached)")
|
|
} else if timingResult.CachedRowsFetched > 0 {
|
|
sb.WriteString(p.Sprintf(" (%d cached)", timingResult.CachedRowsFetched))
|
|
}
|
|
}
|
|
|
|
sb.WriteString(p.Sprintf(". Hydrate calls: %d.", timingResult.HydrateCalls))
|
|
if timingResult.ScanCount > 1 {
|
|
sb.WriteString(p.Sprintf(" Scans: %d.", timingResult.ScanCount))
|
|
}
|
|
if timingResult.ConnectionCount > 1 {
|
|
sb.WriteString(p.Sprintf(" Connections: %d.", timingResult.ConnectionCount))
|
|
}
|
|
|
|
if viper.GetString(pconstants.ArgTiming) == pconstants.ArgVerbose && len(timingResult.Scans) > 0 {
|
|
if err := getVerboseTimingString(&sb, p, timingResult); err != nil {
|
|
log.Printf("[WARN] Error getting verbose timing: %v", err)
|
|
}
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func getDurationString(durationMs int64, p *message.Printer) string {
|
|
if durationMs < 500 {
|
|
return p.Sprintf("%dms", durationMs)
|
|
} else {
|
|
seconds := float64(durationMs) / 1000
|
|
return p.Sprintf("%.1fs", seconds)
|
|
}
|
|
}
|
|
|
|
func getVerboseTimingString(sb *strings.Builder, p *message.Printer, timingResult *queryresult.TimingResult) error {
|
|
scans := timingResult.Scans
|
|
|
|
// keep track of empty scans and do not include them separately in scan list
|
|
emptyScanCount := 0
|
|
scanCount := 0
|
|
// is this all scans or just the slowest
|
|
if len(scans) == int(timingResult.ScanCount) {
|
|
sb.WriteString("\n\nScans:\n")
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf("\n\nSlowest %d scans:\n", len(scans)))
|
|
}
|
|
|
|
for _, scan := range scans {
|
|
if scan.RowsFetched == 0 {
|
|
emptyScanCount++
|
|
continue
|
|
}
|
|
scanCount++
|
|
|
|
cacheString := ""
|
|
if scan.CacheHit {
|
|
cacheString = " (cached)"
|
|
}
|
|
qualsString := formatQuals(scan)
|
|
limitString := ""
|
|
if scan.Limit != nil {
|
|
limitString = p.Sprintf(" Limit: %d.", *scan.Limit)
|
|
}
|
|
|
|
timeString := getDurationString(scan.DurationMs, p)
|
|
rowsFetchedString := p.Sprintf("%d", scan.RowsFetched)
|
|
|
|
sb.WriteString(p.Sprintf(" %d) %s.%s: Time: %s. Fetched: %s%s. Hydrates: %d.%s%s\n", scanCount, scan.Table, scan.Connection, timeString, rowsFetchedString, cacheString, scan.HydrateCalls, qualsString, limitString))
|
|
}
|
|
if emptyScanCount > 0 {
|
|
|
|
sb.WriteString(fmt.Sprintf(" %d…%d) Zero rows fetched.\n", scanCount+1, scanCount+emptyScanCount))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func formatQuals(scan *queryresult.ScanMetadataRow) string {
|
|
if len(scan.Quals) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var b strings.Builder
|
|
for _, qual := range scan.Quals {
|
|
operator := qual.Operator
|
|
valueStr := formatQualValue(qual.Value)
|
|
|
|
if operator == "=" {
|
|
|
|
// Use reflection to check if qual.Value is an array or a slice
|
|
val := reflect.ValueOf(qual.Value)
|
|
|
|
if val.Kind() == reflect.Array || val.Kind() == reflect.Slice {
|
|
// Change operator to IN if it was "=" and the value is an array or slice
|
|
if operator == "=" {
|
|
operator = " IN "
|
|
}
|
|
|
|
// Build the string of array elements
|
|
valueElements := make([]string, val.Len())
|
|
for i := 0; i < val.Len(); i++ {
|
|
valueElements[i] = fmt.Sprintf("%s", formatQualValue(val.Index(i).Interface()))
|
|
}
|
|
valueStr = fmt.Sprintf("(%s)", strings.Join(valueElements, ", "))
|
|
} else {
|
|
// Use the original value if it's not an array or slice
|
|
valueStr = fmt.Sprintf("%v", qual.Value)
|
|
}
|
|
}
|
|
|
|
b.WriteString(fmt.Sprintf("%s%s%s, ", qual.Column, operator, valueStr))
|
|
}
|
|
|
|
// Remove the trailing comma and space
|
|
trimmedResult := strings.TrimRight(b.String(), ", ")
|
|
|
|
return fmt.Sprintf(" Quals: %s.", trimmedResult)
|
|
}
|
|
|
|
func formatQualValue(val any) string {
|
|
if str, ok := val.(string); ok {
|
|
return fmt.Sprintf("'%s'", str)
|
|
}
|
|
return fmt.Sprintf("%v", val)
|
|
}
|