Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: [dynamic] dynamic nested metric from struct #1872

Merged
merged 2 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 112 additions & 72 deletions pkg/dynamic/metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,113 +5,153 @@ import (
"reflect"
"regexp"
"strings"
"sync"

"github.com/prometheus/client_golang/prometheus"

"github.com/c9s/bbgo/pkg/fixedpoint"
"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])")

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

tv := reflect.TypeOf(s).Elem()
sv := reflect.Indirect(reflect.ValueOf(s))
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{
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)
}

dynamicStrategyConfigMetrics[metricName] = metric
}

return metric, metricName, 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, 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()

nextStructField:
for i := 0; i < tv.NumField(); i++ {
field := tv.Field(i)
if !field.IsExported() {
continue
}

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)
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
}

isStr := false
metricNames = append(metricNames, subMetricNames...)
continue
}

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 := ""
if hasSymbolField {
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, metricName, err := getOrCreateMetric(id, fieldName)
if err != nil {
return nil, 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)

metricNames = append(metricNames, metricName)
}

return nil
return metricNames, nil
}
52 changes: 52 additions & 0 deletions pkg/dynamic/metric_test.go
Original file line number Diff line number Diff line change
@@ -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])
}
})

}
Loading