From b9b9559880bc2a620eecfabc209b19da288d63fb Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 19 Dec 2024 14:02:55 +0800 Subject: [PATCH 1/2] dynamic: refactor dynamic metric --- pkg/dynamic/metric.go | 144 ++++++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 69 deletions(-) diff --git a/pkg/dynamic/metric.go b/pkg/dynamic/metric.go index 1b393e798..82f5db85a 100644 --- a/pkg/dynamic/metric.go +++ b/pkg/dynamic/metric.go @@ -12,70 +12,94 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -var dynamicStrategyConfigMetrics = map[string]any{} +var matchFirstCapRE = regexp.MustCompile("(.)([A-Z][a-z]+)") +var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + +var dynamicStrategyConfigMetrics = map[string]*prometheus.GaugeVec{} + +func getOrCreateMetric(id, fieldName string) (*prometheus.GaugeVec, error) { + metricName := id + "_config_" + fieldName + metric, ok := dynamicStrategyConfigMetrics[metricName] + if !ok { + metric = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: metricName, + Help: id + " config value of " + fieldName, + }, + []string{"strategy_type", "strategy_id", "symbol"}, + ) + + if err := prometheus.Register(metric); err != nil { + return nil, fmt.Errorf("unable to register metrics on field %+v, error: %+v", fieldName, err) + } + } -func InitializeConfigMetrics(id, instanceId string, s types.StrategyID) error { - matchFirstCapRE := regexp.MustCompile("(.)([A-Z][a-z]+)") - matchAllCap := regexp.MustCompile("([a-z0-9])([A-Z])") + return metric, nil +} + +func toSnakeCase(input string) string { + input = matchFirstCapRE.ReplaceAllString(input, "${1}_${2}") + input = matchAllCap.ReplaceAllString(input, "${1}_${2}") + return strings.ToLower(input) +} + +func castToFloat64(valInf any) (float64, bool) { + var val float64 + switch tt := valInf.(type) { + + case fixedpoint.Value: + val = tt.Float64() + case *fixedpoint.Value: + if tt != nil { + val = tt.Float64() + } + case float64: + val = tt + case int: + val = float64(tt) + case int32: + val = float64(tt) + case int64: + val = float64(tt) + case bool: + if tt { + val = 1.0 + } else { + val = 0.0 + } + default: + return 0.0, false + } + return val, true +} + +func InitializeConfigMetrics(id, instanceId string, s types.StrategyID) error { tv := reflect.TypeOf(s).Elem() sv := reflect.Indirect(reflect.ValueOf(s)) symbolField := sv.FieldByName("Symbol") hasSymbolField := symbolField.IsValid() -nextStructField: for i := 0; i < tv.NumField(); i++ { field := tv.Field(i) jsonTag := field.Tag.Get("json") if jsonTag == "" { - continue nextStructField + continue } tagAttrs := strings.Split(jsonTag, ",") if len(tagAttrs) == 0 { - continue nextStructField + continue } - fieldName := tagAttrs[0] - fieldName = matchFirstCapRE.ReplaceAllString(fieldName, "${1}_${2}") - fieldName = matchAllCap.ReplaceAllString(fieldName, "${1}_${2}") - fieldName = strings.ToLower(fieldName) - - isStr := false + fieldName := toSnakeCase(tagAttrs[0]) val := 0.0 valInf := sv.Field(i).Interface() - switch tt := valInf.(type) { - case string: - isStr = true - - case fixedpoint.Value: - val = tt.Float64() - case *fixedpoint.Value: - if tt != nil { - val = tt.Float64() - } - case float64: - val = tt - case int: - val = float64(tt) - case int32: - val = float64(tt) - case int64: - val = float64(tt) - case bool: - if tt { - val = 1.0 - } else { - val = 0.0 - } - default: - continue nextStructField - } - - if isStr { - continue nextStructField + val, ok := castToFloat64(valInf) + if !ok { + continue } symbol := "" @@ -83,34 +107,16 @@ nextStructField: symbol = symbolField.String() } - metricName := id + "_config_" + fieldName - anyMetric, ok := dynamicStrategyConfigMetrics[metricName] - if !ok { - gaugeMetric := prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: metricName, - Help: id + " config value of " + field.Name, - }, - []string{"strategy_type", "strategy_id", "symbol"}, - ) - if err := prometheus.Register(gaugeMetric); err != nil { - return fmt.Errorf("unable to register metrics on field %+v, error: %+v", field.Name, err) - } - - anyMetric = gaugeMetric - dynamicStrategyConfigMetrics[metricName] = anyMetric + metric, err := getOrCreateMetric(id, fieldName) + if err != nil { + return err } - if anyMetric != nil { - switch metric := anyMetric.(type) { - case *prometheus.GaugeVec: - metric.With(prometheus.Labels{ - "strategy_type": id, - "strategy_id": instanceId, - "symbol": symbol, - }).Set(val) - } - } + metric.With(prometheus.Labels{ + "strategy_type": id, + "strategy_id": instanceId, + "symbol": symbol, + }).Set(val) } return nil From 416c0aaf4ff8aa57c107c019bed75a81b2c29c3e Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 19 Dec 2024 14:31:11 +0800 Subject: [PATCH 2/2] dynamic: support nested metric fields --- pkg/dynamic/metric.go | 56 ++++++++++++++++++++++++++++++-------- pkg/dynamic/metric_test.go | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 pkg/dynamic/metric_test.go diff --git a/pkg/dynamic/metric.go b/pkg/dynamic/metric.go index 82f5db85a..c126b8376 100644 --- a/pkg/dynamic/metric.go +++ b/pkg/dynamic/metric.go @@ -5,21 +5,26 @@ import ( "reflect" "regexp" "strings" + "sync" "github.com/prometheus/client_golang/prometheus" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" ) var matchFirstCapRE = regexp.MustCompile("(.)([A-Z][a-z]+)") var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") var dynamicStrategyConfigMetrics = map[string]*prometheus.GaugeVec{} +var dynamicStrategyConfigMetricsMutex sync.Mutex -func getOrCreateMetric(id, fieldName string) (*prometheus.GaugeVec, error) { +func getOrCreateMetric(id, fieldName string) (*prometheus.GaugeVec, string, error) { metricName := id + "_config_" + fieldName + + dynamicStrategyConfigMetricsMutex.Lock() metric, ok := dynamicStrategyConfigMetrics[metricName] + defer dynamicStrategyConfigMetricsMutex.Unlock() + if !ok { metric = prometheus.NewGaugeVec( prometheus.GaugeOpts{ @@ -30,11 +35,13 @@ func getOrCreateMetric(id, fieldName string) (*prometheus.GaugeVec, error) { ) if err := prometheus.Register(metric); err != nil { - return nil, fmt.Errorf("unable to register metrics on field %+v, error: %+v", fieldName, err) + return nil, "", fmt.Errorf("unable to register metrics on field %+v, error: %+v", fieldName, err) } + + dynamicStrategyConfigMetrics[metricName] = metric } - return metric, nil + return metric, metricName, nil } func toSnakeCase(input string) string { @@ -74,15 +81,31 @@ func castToFloat64(valInf any) (float64, bool) { return val, true } -func InitializeConfigMetrics(id, instanceId string, s types.StrategyID) error { - tv := reflect.TypeOf(s).Elem() - sv := reflect.Indirect(reflect.ValueOf(s)) +func InitializeConfigMetrics(id, instanceId string, st any) error { + _, err := initializeConfigMetricsWithFieldPrefix(id, instanceId, "", st) + return err +} + +func initializeConfigMetricsWithFieldPrefix(id, instanceId, fieldPrefix string, st any) ([]string, error) { + var metricNames []string + tv := reflect.TypeOf(st).Elem() + + vv := reflect.ValueOf(st) + if vv.IsNil() { + return nil, nil + } + + sv := reflect.Indirect(vv) symbolField := sv.FieldByName("Symbol") hasSymbolField := symbolField.IsValid() for i := 0; i < tv.NumField(); i++ { field := tv.Field(i) + if !field.IsExported() { + continue + } + jsonTag := field.Tag.Get("json") if jsonTag == "" { continue @@ -93,7 +116,16 @@ func InitializeConfigMetrics(id, instanceId string, s types.StrategyID) error { continue } - fieldName := toSnakeCase(tagAttrs[0]) + fieldName := fieldPrefix + toSnakeCase(tagAttrs[0]) + if field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct { + subMetricNames, err := initializeConfigMetricsWithFieldPrefix(id, instanceId, fieldName+"_", sv.Field(i).Interface()) + if err != nil { + return nil, err + } + + metricNames = append(metricNames, subMetricNames...) + continue + } val := 0.0 valInf := sv.Field(i).Interface() @@ -107,9 +139,9 @@ func InitializeConfigMetrics(id, instanceId string, s types.StrategyID) error { symbol = symbolField.String() } - metric, err := getOrCreateMetric(id, fieldName) + metric, metricName, err := getOrCreateMetric(id, fieldName) if err != nil { - return err + return nil, err } metric.With(prometheus.Labels{ @@ -117,7 +149,9 @@ func InitializeConfigMetrics(id, instanceId string, s types.StrategyID) error { "strategy_id": instanceId, "symbol": symbol, }).Set(val) + + metricNames = append(metricNames, metricName) } - return nil + return metricNames, nil } diff --git a/pkg/dynamic/metric_test.go b/pkg/dynamic/metric_test.go new file mode 100644 index 000000000..807dc3481 --- /dev/null +++ b/pkg/dynamic/metric_test.go @@ -0,0 +1,52 @@ +package dynamic + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + . "github.com/c9s/bbgo/pkg/testing/testhelper" +) + +func TestInitializeConfigMetrics(t *testing.T) { + type Bar struct { + Enabled bool `json:"enabled"` + } + type Foo struct { + MinMarginLevel fixedpoint.Value `json:"minMarginLevel"` + Bar *Bar `json:"bar"` + + // this field should be ignored + ignoredField string + + ignoredFieldInt int + } + + t.Run("general", func(t *testing.T) { + metricNames, err := initializeConfigMetricsWithFieldPrefix("test", "test-01", "", &Foo{ + MinMarginLevel: Number(1.4), + Bar: &Bar{ + Enabled: true, + }, + }) + + if assert.NoError(t, err) { + assert.Len(t, metricNames, 2) + assert.Equal(t, "test_config_min_margin_level", metricNames[0]) + assert.Equal(t, "test_config_bar_enabled", metricNames[1], "nested struct field as a metric") + } + }) + + t.Run("nil struct field", func(t *testing.T) { + metricNames, err := initializeConfigMetricsWithFieldPrefix("test", "test-01", "", &Foo{ + MinMarginLevel: Number(1.4), + }) + + if assert.NoError(t, err) { + assert.Len(t, metricNames, 1) + assert.Equal(t, "test_config_min_margin_level", metricNames[0]) + } + }) + +}