Skip to content

Commit 4f38a6c

Browse files
authored
Merge pull request #1872 from c9s/c9s/refactor-dynamic-metrics
FEATURE: [dynamic] dynamic nested metric from struct
2 parents 594bd3e + 416c0aa commit 4f38a6c

File tree

2 files changed

+164
-72
lines changed

2 files changed

+164
-72
lines changed

pkg/dynamic/metric.go

+112-72
Original file line numberDiff line numberDiff line change
@@ -5,113 +5,153 @@ import (
55
"reflect"
66
"regexp"
77
"strings"
8+
"sync"
89

910
"github.com/prometheus/client_golang/prometheus"
1011

1112
"github.com/c9s/bbgo/pkg/fixedpoint"
12-
"github.com/c9s/bbgo/pkg/types"
1313
)
1414

15-
var dynamicStrategyConfigMetrics = map[string]any{}
15+
var matchFirstCapRE = regexp.MustCompile("(.)([A-Z][a-z]+)")
16+
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
1617

17-
func InitializeConfigMetrics(id, instanceId string, s types.StrategyID) error {
18-
matchFirstCapRE := regexp.MustCompile("(.)([A-Z][a-z]+)")
19-
matchAllCap := regexp.MustCompile("([a-z0-9])([A-Z])")
18+
var dynamicStrategyConfigMetrics = map[string]*prometheus.GaugeVec{}
19+
var dynamicStrategyConfigMetricsMutex sync.Mutex
2020

21-
tv := reflect.TypeOf(s).Elem()
22-
sv := reflect.Indirect(reflect.ValueOf(s))
21+
func getOrCreateMetric(id, fieldName string) (*prometheus.GaugeVec, string, error) {
22+
metricName := id + "_config_" + fieldName
23+
24+
dynamicStrategyConfigMetricsMutex.Lock()
25+
metric, ok := dynamicStrategyConfigMetrics[metricName]
26+
defer dynamicStrategyConfigMetricsMutex.Unlock()
27+
28+
if !ok {
29+
metric = prometheus.NewGaugeVec(
30+
prometheus.GaugeOpts{
31+
Name: metricName,
32+
Help: id + " config value of " + fieldName,
33+
},
34+
[]string{"strategy_type", "strategy_id", "symbol"},
35+
)
36+
37+
if err := prometheus.Register(metric); err != nil {
38+
return nil, "", fmt.Errorf("unable to register metrics on field %+v, error: %+v", fieldName, err)
39+
}
40+
41+
dynamicStrategyConfigMetrics[metricName] = metric
42+
}
43+
44+
return metric, metricName, nil
45+
}
46+
47+
func toSnakeCase(input string) string {
48+
input = matchFirstCapRE.ReplaceAllString(input, "${1}_${2}")
49+
input = matchAllCap.ReplaceAllString(input, "${1}_${2}")
50+
return strings.ToLower(input)
51+
}
52+
53+
func castToFloat64(valInf any) (float64, bool) {
54+
var val float64
55+
switch tt := valInf.(type) {
56+
57+
case fixedpoint.Value:
58+
val = tt.Float64()
59+
case *fixedpoint.Value:
60+
if tt != nil {
61+
val = tt.Float64()
62+
}
63+
case float64:
64+
val = tt
65+
case int:
66+
val = float64(tt)
67+
case int32:
68+
val = float64(tt)
69+
case int64:
70+
val = float64(tt)
71+
case bool:
72+
if tt {
73+
val = 1.0
74+
} else {
75+
val = 0.0
76+
}
77+
default:
78+
return 0.0, false
79+
}
80+
81+
return val, true
82+
}
83+
84+
func InitializeConfigMetrics(id, instanceId string, st any) error {
85+
_, err := initializeConfigMetricsWithFieldPrefix(id, instanceId, "", st)
86+
return err
87+
}
88+
89+
func initializeConfigMetricsWithFieldPrefix(id, instanceId, fieldPrefix string, st any) ([]string, error) {
90+
var metricNames []string
91+
tv := reflect.TypeOf(st).Elem()
92+
93+
vv := reflect.ValueOf(st)
94+
if vv.IsNil() {
95+
return nil, nil
96+
}
97+
98+
sv := reflect.Indirect(vv)
2399

24100
symbolField := sv.FieldByName("Symbol")
25101
hasSymbolField := symbolField.IsValid()
26102

27-
nextStructField:
28103
for i := 0; i < tv.NumField(); i++ {
29104
field := tv.Field(i)
105+
if !field.IsExported() {
106+
continue
107+
}
108+
30109
jsonTag := field.Tag.Get("json")
31110
if jsonTag == "" {
32-
continue nextStructField
111+
continue
33112
}
34113

35114
tagAttrs := strings.Split(jsonTag, ",")
36115
if len(tagAttrs) == 0 {
37-
continue nextStructField
116+
continue
38117
}
39118

40-
fieldName := tagAttrs[0]
41-
fieldName = matchFirstCapRE.ReplaceAllString(fieldName, "${1}_${2}")
42-
fieldName = matchAllCap.ReplaceAllString(fieldName, "${1}_${2}")
43-
fieldName = strings.ToLower(fieldName)
119+
fieldName := fieldPrefix + toSnakeCase(tagAttrs[0])
120+
if field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct {
121+
subMetricNames, err := initializeConfigMetricsWithFieldPrefix(id, instanceId, fieldName+"_", sv.Field(i).Interface())
122+
if err != nil {
123+
return nil, err
124+
}
44125

45-
isStr := false
126+
metricNames = append(metricNames, subMetricNames...)
127+
continue
128+
}
46129

47130
val := 0.0
48131
valInf := sv.Field(i).Interface()
49-
switch tt := valInf.(type) {
50-
case string:
51-
isStr = true
52-
53-
case fixedpoint.Value:
54-
val = tt.Float64()
55-
case *fixedpoint.Value:
56-
if tt != nil {
57-
val = tt.Float64()
58-
}
59-
case float64:
60-
val = tt
61-
case int:
62-
val = float64(tt)
63-
case int32:
64-
val = float64(tt)
65-
case int64:
66-
val = float64(tt)
67-
case bool:
68-
if tt {
69-
val = 1.0
70-
} else {
71-
val = 0.0
72-
}
73-
default:
74-
continue nextStructField
75-
}
76-
77-
if isStr {
78-
continue nextStructField
132+
val, ok := castToFloat64(valInf)
133+
if !ok {
134+
continue
79135
}
80136

81137
symbol := ""
82138
if hasSymbolField {
83139
symbol = symbolField.String()
84140
}
85141

86-
metricName := id + "_config_" + fieldName
87-
anyMetric, ok := dynamicStrategyConfigMetrics[metricName]
88-
if !ok {
89-
gaugeMetric := prometheus.NewGaugeVec(
90-
prometheus.GaugeOpts{
91-
Name: metricName,
92-
Help: id + " config value of " + field.Name,
93-
},
94-
[]string{"strategy_type", "strategy_id", "symbol"},
95-
)
96-
if err := prometheus.Register(gaugeMetric); err != nil {
97-
return fmt.Errorf("unable to register metrics on field %+v, error: %+v", field.Name, err)
98-
}
99-
100-
anyMetric = gaugeMetric
101-
dynamicStrategyConfigMetrics[metricName] = anyMetric
142+
metric, metricName, err := getOrCreateMetric(id, fieldName)
143+
if err != nil {
144+
return nil, err
102145
}
103146

104-
if anyMetric != nil {
105-
switch metric := anyMetric.(type) {
106-
case *prometheus.GaugeVec:
107-
metric.With(prometheus.Labels{
108-
"strategy_type": id,
109-
"strategy_id": instanceId,
110-
"symbol": symbol,
111-
}).Set(val)
112-
}
113-
}
147+
metric.With(prometheus.Labels{
148+
"strategy_type": id,
149+
"strategy_id": instanceId,
150+
"symbol": symbol,
151+
}).Set(val)
152+
153+
metricNames = append(metricNames, metricName)
114154
}
115155

116-
return nil
156+
return metricNames, nil
117157
}

pkg/dynamic/metric_test.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package dynamic
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
8+
"github.com/c9s/bbgo/pkg/fixedpoint"
9+
. "github.com/c9s/bbgo/pkg/testing/testhelper"
10+
)
11+
12+
func TestInitializeConfigMetrics(t *testing.T) {
13+
type Bar struct {
14+
Enabled bool `json:"enabled"`
15+
}
16+
type Foo struct {
17+
MinMarginLevel fixedpoint.Value `json:"minMarginLevel"`
18+
Bar *Bar `json:"bar"`
19+
20+
// this field should be ignored
21+
ignoredField string
22+
23+
ignoredFieldInt int
24+
}
25+
26+
t.Run("general", func(t *testing.T) {
27+
metricNames, err := initializeConfigMetricsWithFieldPrefix("test", "test-01", "", &Foo{
28+
MinMarginLevel: Number(1.4),
29+
Bar: &Bar{
30+
Enabled: true,
31+
},
32+
})
33+
34+
if assert.NoError(t, err) {
35+
assert.Len(t, metricNames, 2)
36+
assert.Equal(t, "test_config_min_margin_level", metricNames[0])
37+
assert.Equal(t, "test_config_bar_enabled", metricNames[1], "nested struct field as a metric")
38+
}
39+
})
40+
41+
t.Run("nil struct field", func(t *testing.T) {
42+
metricNames, err := initializeConfigMetricsWithFieldPrefix("test", "test-01", "", &Foo{
43+
MinMarginLevel: Number(1.4),
44+
})
45+
46+
if assert.NoError(t, err) {
47+
assert.Len(t, metricNames, 1)
48+
assert.Equal(t, "test_config_min_margin_level", metricNames[0])
49+
}
50+
})
51+
52+
}

0 commit comments

Comments
 (0)