Skip to content

Commit efb4014

Browse files
config: output config info into JSON (#830) (#831)
Signed-off-by: ti-chi-bot <[email protected]> Co-authored-by: djshow832 <[email protected]>
1 parent 967f4cc commit efb4014

File tree

7 files changed

+239
-55
lines changed

7 files changed

+239
-55
lines changed

cmd/tiproxy/main.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"runtime"
1010

11+
"github.com/pingcap/tiproxy/lib/config"
1112
"github.com/pingcap/tiproxy/lib/util/cmd"
1213
"github.com/pingcap/tiproxy/lib/util/errors"
1314
"github.com/pingcap/tiproxy/pkg/metrics"
@@ -28,15 +29,24 @@ func main() {
2829

2930
sctx := &sctx.Context{}
3031

31-
var deprecatedStr string
32+
var deprecatedStr, configInfo string
3233
rootCmd.PersistentFlags().StringVar(&sctx.ConfigFile, "config", "", "proxy config file path")
3334
rootCmd.PersistentFlags().StringVar(&deprecatedStr, "log_encoder", "", "deprecated and will be removed")
3435
rootCmd.PersistentFlags().StringVar(&deprecatedStr, "log_level", "", "deprecated and will be removed")
3536
rootCmd.PersistentFlags().StringVar(&sctx.AdvertiseAddr, "advertise-addr", "", "advertise address")
37+
rootCmd.PersistentFlags().StringVar(&configInfo, "config-info", "", "output config info and exit")
3638

3739
metrics.MaxProcsGauge.Set(float64(runtime.GOMAXPROCS(0)))
3840

3941
rootCmd.RunE = func(cmd *cobra.Command, _ []string) error {
42+
if configInfo != "" {
43+
info, err := config.ConfigInfo(configInfo)
44+
if err != nil {
45+
return err
46+
}
47+
cmd.Println(info)
48+
return nil
49+
}
4050
srv, err := server.NewServer(cmd.Context(), sctx)
4151
if err != nil {
4252
return errors.Wrapf(err, "fail to create server")

lib/config/balance.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ const (
1212
)
1313

1414
type Balance struct {
15-
LabelName string `yaml:"label-name,omitempty" toml:"label-name,omitempty" json:"label-name,omitempty"`
16-
Policy string `yaml:"policy,omitempty" toml:"policy,omitempty" json:"policy,omitempty"`
15+
LabelName string `yaml:"label-name,omitempty" toml:"label-name,omitempty" json:"label-name,omitempty" reloadable:"true"`
16+
Policy string `yaml:"policy,omitempty" toml:"policy,omitempty" json:"policy,omitempty" reloadable:"true"`
1717
}
1818

1919
func (b *Balance) Check() error {

lib/config/health.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ const (
2222
// Some general configurations of them may be exposed to users in the future.
2323
// We can use shorter durations to speed up unit tests.
2424
type HealthCheck struct {
25-
Enable bool `yaml:"enable" json:"enable" toml:"enable"`
26-
Interval time.Duration `yaml:"interval" json:"interval" toml:"interval"`
27-
MaxRetries int `yaml:"max-retries" json:"max-retries" toml:"max-retries"`
28-
RetryInterval time.Duration `yaml:"retry-interval" json:"retry-interval" toml:"retry-interval"`
29-
DialTimeout time.Duration `yaml:"dial-timeout" json:"dial-timeout" toml:"dial-timeout"`
30-
MetricsInterval time.Duration `yaml:"metrics-interval" json:"metrics-interval" toml:"metrics-interval"`
31-
MetricsTimeout time.Duration `yaml:"metrics-timeout" json:"metrics-timeout" toml:"metrics-timeout"`
25+
Enable bool `yaml:"enable" json:"enable" toml:"enable" reloadable:"false"`
26+
Interval time.Duration `yaml:"interval" json:"interval" toml:"interval" reloadable:"false"`
27+
MaxRetries int `yaml:"max-retries" json:"max-retries" toml:"max-retries" reloadable:"false"`
28+
RetryInterval time.Duration `yaml:"retry-interval" json:"retry-interval" toml:"retry-interval" reloadable:"false"`
29+
DialTimeout time.Duration `yaml:"dial-timeout" json:"dial-timeout" toml:"dial-timeout" reloadable:"false"`
30+
MetricsInterval time.Duration `yaml:"metrics-interval" json:"metrics-interval" toml:"metrics-interval" reloadable:"false"`
31+
MetricsTimeout time.Duration `yaml:"metrics-timeout" json:"metrics-timeout" toml:"metrics-timeout" reloadable:"false"`
3232
}
3333

3434
// NewDefaultHealthCheckConfig creates a default HealthCheck.

lib/config/output.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2025 PingCAP, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package config
5+
6+
import (
7+
"encoding/json"
8+
"reflect"
9+
"strconv"
10+
"strings"
11+
12+
"github.com/pingcap/tiproxy/lib/util/errors"
13+
)
14+
15+
type output struct {
16+
Component string
17+
Version string
18+
Parameters []field
19+
}
20+
21+
type field struct {
22+
Name string `json:"key"`
23+
Type string `json:"type"`
24+
DefaultValue any `json:"default_value"`
25+
HotReloadable bool `json:"hot_reloadable"`
26+
}
27+
28+
// ConfigInfo outputs the config info used for loading to the operation system.
29+
func ConfigInfo(format string) (string, error) {
30+
if !strings.EqualFold(format, "json") {
31+
return "", errors.New("only support json format")
32+
}
33+
fields := make([]field, 0, 256)
34+
cfg := NewConfig()
35+
res, err := formatValue(reflect.ValueOf(*cfg), "", true)
36+
if err != nil {
37+
return "", err
38+
}
39+
fields = append(fields, res...)
40+
41+
op := output{
42+
Component: "TiProxy Server",
43+
Version: "",
44+
Parameters: fields,
45+
}
46+
bytes, err := json.MarshalIndent(op, "", " ")
47+
return string(bytes), err
48+
}
49+
50+
func formatValue(v reflect.Value, name string, reloadable bool) ([]field, error) {
51+
if !v.IsValid() {
52+
return nil, errors.New("invalid value")
53+
}
54+
55+
if v.Kind() == reflect.Interface {
56+
return formatValue(v.Elem(), name, reloadable)
57+
}
58+
59+
if v.Kind() == reflect.Ptr {
60+
if v.IsNil() {
61+
return nil, nil
62+
}
63+
return formatValue(v.Elem(), name, reloadable)
64+
}
65+
66+
switch v.Kind() {
67+
case reflect.Bool:
68+
return []field{{Name: name, Type: "bool", DefaultValue: v.Bool(), HotReloadable: reloadable}}, nil
69+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
70+
return []field{{Name: name, Type: "int", DefaultValue: v.Int(), HotReloadable: reloadable}}, nil
71+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
72+
return []field{{Name: name, Type: "int", DefaultValue: v.Uint(), HotReloadable: reloadable}}, nil
73+
case reflect.Float32, reflect.Float64:
74+
return []field{{Name: name, Type: "float", DefaultValue: v.Float(), HotReloadable: reloadable}}, nil
75+
case reflect.String:
76+
return []field{{Name: name, Type: "string", DefaultValue: v.String(), HotReloadable: reloadable}}, nil
77+
case reflect.Slice, reflect.Array:
78+
return formatSliceOrArray(v, name, reloadable)
79+
case reflect.Struct:
80+
return formatStruct(v, name)
81+
case reflect.Map:
82+
return formatMap(v, name, reloadable)
83+
default:
84+
return nil, errors.Errorf("unsupported type %s", v.Kind().String())
85+
}
86+
}
87+
88+
func formatSliceOrArray(v reflect.Value, name string, reloadable bool) ([]field, error) {
89+
length := v.Len()
90+
elements := make([]string, 0, length)
91+
for i := 0; i < length; i++ {
92+
str, err := valueToString(v.Index(i))
93+
if err != nil {
94+
return nil, err
95+
}
96+
elements = append(elements, str)
97+
}
98+
return []field{{Name: name, Type: "json", DefaultValue: elements, HotReloadable: reloadable}}, nil
99+
}
100+
101+
func formatStruct(v reflect.Value, name string) ([]field, error) {
102+
typ := v.Type()
103+
fields := make([]field, 0, typ.NumField())
104+
105+
for i := 0; i < typ.NumField(); i++ {
106+
f := typ.Field(i)
107+
jsonName := strings.Split(f.Tag.Get("json"), ",")[0]
108+
switch jsonName {
109+
case "-":
110+
continue
111+
case "":
112+
jsonName = name
113+
default:
114+
if name != "" {
115+
jsonName = name + "." + jsonName
116+
}
117+
}
118+
reloadable := f.Tag.Get("reloadable") == "true"
119+
res, err := formatValue(v.Field(i), jsonName, reloadable)
120+
if err != nil {
121+
return nil, err
122+
}
123+
fields = append(fields, res...)
124+
}
125+
return fields, nil
126+
}
127+
128+
func formatMap(v reflect.Value, name string, reloadable bool) ([]field, error) {
129+
entries := make(map[string]string)
130+
if v.IsNil() || v.Len() == 0 {
131+
return []field{{Name: name, Type: "json", DefaultValue: entries, HotReloadable: reloadable}}, nil
132+
}
133+
134+
for _, key := range v.MapKeys() {
135+
val := v.MapIndex(key)
136+
keyStr, err := valueToString(key)
137+
if err != nil {
138+
return nil, err
139+
}
140+
valStr, err := valueToString(val)
141+
if err != nil {
142+
return nil, err
143+
}
144+
entries[keyStr] = valStr
145+
}
146+
return []field{{Name: name, Type: "json", DefaultValue: entries, HotReloadable: reloadable}}, nil
147+
}
148+
149+
func valueToString(v reflect.Value) (string, error) {
150+
switch v.Kind() {
151+
case reflect.String:
152+
return strconv.Quote(v.String()), nil
153+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
154+
return strconv.FormatInt(v.Int(), 10), nil
155+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
156+
return strconv.FormatUint(v.Uint(), 10), nil
157+
case reflect.Float32, reflect.Float64:
158+
return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil
159+
case reflect.Bool:
160+
return strconv.FormatBool(v.Bool()), nil
161+
default:
162+
return "", errors.Errorf("unsupported type %s", v.Kind().String())
163+
}
164+
}

lib/config/output_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2025 PingCAP, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package config
5+
6+
import (
7+
"encoding/json"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestOutput(t *testing.T) {
14+
str, err := ConfigInfo("json")
15+
require.NoError(t, err)
16+
require.NotEmpty(t, str)
17+
require.NoError(t, json.Unmarshal([]byte(str), &Config{}))
18+
}

0 commit comments

Comments
 (0)