From a2659b15aa3d098f453d6da9605ec363baf71a09 Mon Sep 17 00:00:00 2001 From: tonysken Date: Fri, 24 Jul 2026 23:20:46 +0100 Subject: [PATCH] fix: handle float64 chosen by json and invalid metric --- pkg/gui/presentation/container_stats.go | 57 ++----- pkg/gui/presentation/container_stats_test.go | 160 ++++++++++++++++++- 2 files changed, 166 insertions(+), 51 deletions(-) diff --git a/pkg/gui/presentation/container_stats.go b/pkg/gui/presentation/container_stats.go index 534c9efa..34968b4e 100644 --- a/pkg/gui/presentation/container_stats.go +++ b/pkg/gui/presentation/container_stats.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "math" - "os" "reflect" "strconv" "strings" @@ -19,36 +18,6 @@ import ( "github.com/samber/lo" ) -// var SCHEMA_JSON = `{ -// "client_stats": { -// "cpu_stats": { -// "cpu_usage": { -// "total_usage": "nanoseconds", -// "percpu_usage": ["nanoseconds"], -// "usage_in_kernelmode": "nanoseconds", -// "usage_in_usermode": "nanoseconds" -// }, -// "system_cpu_usage": "nanoseconds" -// }, -// "precpu_stats": { -// "cpu_usage": { -// "total_usage": "nanoseconds", -// "percpu_usage": ["nanoseconds"], -// "usage_in_kernelmode": "nanoseconds", -// "usage_in_usermode": "nanoseconds" -// }, -// "system_cpu_usage": "nanoseconds" -// }, -// "memory_stats": { -// "stats": { -// "hierarchical_memory_limit": "bytes", -// "hierarchical_memsw_limit": "bytes" -// }, -// "limit": "bytes" -// } -// } -// }` - var PATHS_TO_CONVERT_BIGMETRICS = []map[string]string{ {"ClientStats.cpu_stats.cpu_usage.total_usage": "nanoseconds"}, {"ClientStats.cpu_stats.cpu_usage.percpu_usage": "nanoseconds"}, @@ -92,19 +61,14 @@ func RenderStats(userConfig *config.UserConfig, container *commands.Container, v var statsMap map[string]interface{} err = json.Unmarshal(statsJsonBytes, &statsMap) - b, _ := json.MarshalIndent(statsMap, "", " ") - _ = os.WriteFile("in.json", b, 0644) if err != nil { return "", err } - // err = convertBigMetricFromSchema(&statsMap) - // b, _ = json.MarshalIndent(statsMap, "", " ") - // _ = os.WriteFile("out.json", b, 0644) - // if err != nil { - // _ = os.WriteFile("out", []byte(err.Error()), 0644) - // return "", err - // } + err = convertBigMetricFromSchema(&statsMap) + if err != nil { + return "", err + } originalStats, err := utils.MarshalIntoYaml(statsMap) if err != nil { @@ -264,14 +228,19 @@ func convertBigMetricFromSchema(data *map[string]interface{}) error { } return err } - if reflect.TypeOf(metric.Interface()) == reflect.TypeOf(int64(0)) { - formattedMetric := utils.FormatBigMetric(metric.Interface().(int64), metricType) + if !metric.IsValid() { + continue + } + if reflect.TypeOf(metric.Interface()) == reflect.TypeOf(float64(0)) { + formattedMetric := utils.FormatBigMetric(int64(metric.Interface().(float64)), metricType) err = utils.SetObjectFieldByPath(data, fmt.Sprintf(".%s", path), formattedMetric) if err != nil { return err } - } else if reflect.TypeOf(metric.Interface()) == reflect.TypeOf([]int64{}) { - longIntSlice := metric.Interface().([]int64) + } else if reflect.TypeOf(metric.Interface()) == reflect.TypeOf([]float64{}) { + longIntSlice := lo.Map(metric.Interface().([]float64), func(val float64, index int) int64 { + return int64(val) + }) formattedMetric := lo.Map(longIntSlice, func(val int64, index int) string { return utils.FormatBigMetric(val, metricType) }) diff --git a/pkg/gui/presentation/container_stats_test.go b/pkg/gui/presentation/container_stats_test.go index 6a0e93ee..4479e071 100644 --- a/pkg/gui/presentation/container_stats_test.go +++ b/pkg/gui/presentation/container_stats_test.go @@ -1,6 +1,8 @@ package presentation import ( + "encoding/json" + "os" "testing" "github.com/stretchr/testify/assert" @@ -14,13 +16,44 @@ func TestConvertBigMetricFromSchema(t *testing.T) { expectedErr string } + inJSONBytes, err := os.ReadFile("testdata/stats.json") + if err != nil { + t.Fatalf("failed to read testdata/stats.json: %v", err) + } + loadInJSON := func() map[string]interface{} { + var m map[string]interface{} + if err := json.Unmarshal(inJSONBytes, &m); err != nil { + t.Fatalf("failed to unmarshal testdata/stats.json: %v", err) + } + return m + } + setNested := func(m map[string]interface{}, keys []string, val interface{}) { + for _, k := range keys[:len(keys)-1] { + m = m[k].(map[string]interface{}) + } + m[keys[len(keys)-1]] = val + } + + inJSONExpected := loadInJSON() + setNested(inJSONExpected, []string{"ClientStats", "memory_stats", "limit"}, "1.911GB") + setNested(inJSONExpected, []string{"ClientStats", "memory_stats", "stats", "hierarchical_memory_limit"}, "0") + setNested(inJSONExpected, []string{"ClientStats", "memory_stats", "stats", "hierarchical_memsw_limit"}, "0") + setNested(inJSONExpected, []string{"ClientStats", "cpu_stats", "cpu_usage", "total_usage"}, "30.283ms") + setNested(inJSONExpected, []string{"ClientStats", "cpu_stats", "cpu_usage", "usage_in_kernelmode"}, "15.664ms") + setNested(inJSONExpected, []string{"ClientStats", "cpu_stats", "cpu_usage", "usage_in_usermode"}, "14.619ms") + setNested(inJSONExpected, []string{"ClientStats", "cpu_stats", "system_cpu_usage"}, "25.994h") + setNested(inJSONExpected, []string{"ClientStats", "precpu_stats", "cpu_usage", "total_usage"}, "30.283ms") + setNested(inJSONExpected, []string{"ClientStats", "precpu_stats", "cpu_usage", "usage_in_kernelmode"}, "15.664ms") + setNested(inJSONExpected, []string{"ClientStats", "precpu_stats", "cpu_usage", "usage_in_usermode"}, "14.619ms") + setNested(inJSONExpected, []string{"ClientStats", "precpu_stats", "system_cpu_usage"}, "25.994h") + scenarios := []scenario{ { - name: "string schema: converts int64 bytes", + name: "string schema: converts float64 bytes", data: map[string]interface{}{ "ClientStats": map[string]interface{}{ "memory_stats": map[string]interface{}{ - "limit": int64(2048), + "limit": float64(2048), }, }, }, @@ -38,12 +71,12 @@ func TestConvertBigMetricFromSchema(t *testing.T) { "ClientStats": map[string]interface{}{ "cpu_stats": map[string]interface{}{ "cpu_usage": map[string]interface{}{ - "total_usage": int64(100000000000), - "percpu_usage": []int64{50000000000, 50000000000}, - "usage_in_kernelmode": int64(200000), - "usage_in_usermode": int64(200000), + "total_usage": float64(100000000000), + "percpu_usage": []float64{50000000000, 50000000000}, + "usage_in_kernelmode": float64(200000), + "usage_in_usermode": float64(200000), }, - "system_cpu_usage": int64(100000000000), + "system_cpu_usage": float64(100000000000), }, }, }, @@ -61,6 +94,119 @@ func TestConvertBigMetricFromSchema(t *testing.T) { }, }, }, + { + name: "empty data is a no-op", + data: map[string]interface{}{}, + expected: map[string]interface{}{}, + }, + { + name: "bytes below unit threshold stays as raw number", + data: map[string]interface{}{ + "ClientStats": map[string]interface{}{ + "memory_stats": map[string]interface{}{ + "limit": float64(500), + }, + }, + }, + expected: map[string]interface{}{ + "ClientStats": map[string]interface{}{ + "memory_stats": map[string]interface{}{ + "limit": "500", + }, + }, + }, + }, + { + name: "bytes at MB scale", + data: map[string]interface{}{ + "ClientStats": map[string]interface{}{ + "memory_stats": map[string]interface{}{ + "limit": float64(1500000), + }, + }, + }, + expected: map[string]interface{}{ + "ClientStats": map[string]interface{}{ + "memory_stats": map[string]interface{}{ + "limit": "1.431MB", + }, + }, + }, + }, + { + name: "converts deeply nested hierarchical memory limits", + data: map[string]interface{}{ + "ClientStats": map[string]interface{}{ + "memory_stats": map[string]interface{}{ + "stats": map[string]interface{}{ + "hierarchical_memory_limit": float64(1073741824), + "hierarchical_memsw_limit": float64(2147483648), + }, + }, + }, + }, + expected: map[string]interface{}{ + "ClientStats": map[string]interface{}{ + "memory_stats": map[string]interface{}{ + "stats": map[string]interface{}{ + "hierarchical_memory_limit": "1.000GB", + "hierarchical_memsw_limit": "2.000GB", + }, + }, + }, + }, + }, + { + name: "converts precpu_stats branch", + data: map[string]interface{}{ + "ClientStats": map[string]interface{}{ + "precpu_stats": map[string]interface{}{ + "cpu_usage": map[string]interface{}{ + "total_usage": float64(1500000), + "percpu_usage": []float64{1000, 2000000000}, + }, + "system_cpu_usage": float64(1500000000000), + }, + }, + }, + expected: map[string]interface{}{ + "ClientStats": map[string]interface{}{ + "precpu_stats": map[string]interface{}{ + "cpu_usage": map[string]interface{}{ + "total_usage": "1.500ms", + "percpu_usage": []string{"1.000µs", "2.000s"}, + }, + "system_cpu_usage": "1.500m", + }, + }, + }, + }, + { + name: "preserves fields not listed in PATHS_TO_CONVERT_BIGMETRICS", + data: map[string]interface{}{ + "ClientStats": map[string]interface{}{ + "memory_stats": map[string]interface{}{ + "limit": float64(2048), + "usage": float64(1024), + }, + "name": "my-container", + }, + }, + expected: map[string]interface{}{ + "ClientStats": map[string]interface{}{ + "memory_stats": map[string]interface{}{ + "limit": "2.000KB", + "usage": float64(1024), + }, + "name": "my-container", + }, + }, + }, + { + name: "converts realistic docker stats loaded from testdata/stats.json", + data: loadInJSON(), + expected: inJSONExpected, + }, } for _, s := range scenarios {