Files
go-omni/config/file.go

145 lines
3.3 KiB
Go

package config
import (
"context"
"errors"
"reflect"
"strings"
"sync"
"time"
"git.yuntuo.hk/k2safe/go-omni/logx"
)
func durations(v any, t reflect.Type) (any, error) {
if t == nil {
return v, nil
}
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t == reflect.TypeOf(time.Duration(0)) {
if s, ok := v.(string); ok {
d, err := time.ParseDuration(s)
return int64(d), err
}
return v, nil
}
if t.Kind() == reflect.Struct {
if m, ok := v.(map[string]any); ok {
for n := 0; n < t.NumField(); n++ {
field := t.Field(n)
name := strings.Split(field.Tag.Get("json"), ",")[0]
if name == "-" {
continue
}
if name == "" {
name = field.Name
}
if item, exists := m[name]; exists {
converted, err := durations(item, field.Type)
if err != nil {
return nil, err
}
m[name] = converted
}
}
}
}
if t.Kind() == reflect.Map && t.Key().Kind() == reflect.String {
if values, ok := v.(map[string]any); ok {
for key, value := range values {
converted, err := durations(value, t.Elem())
if err != nil {
return nil, err
}
values[key] = converted
}
}
}
if t.Kind() == reflect.Slice {
if items, ok := v.([]any); ok {
for n, item := range items {
converted, err := durations(item, t.Elem())
if err != nil {
return nil, err
}
items[n] = converted
}
}
}
return v, nil
}
func LoadFile(path, prefix string) (Config, error) {
cfg, err := DecodeFile(path, Default())
if err != nil {
return Config{}, err
}
return ApplyEnv(cfg, prefix)
}
// Reloader only accepts log.level changes. Static changes and invalid files
// leave the current snapshot and logger untouched. Watch does not own a goroutine.
type Reloader struct {
mu sync.Mutex
path, prefix string
current Config
logger *logx.Logger
}
func NewReloader(path, prefix string, logger *logx.Logger) (*Reloader, error) {
cfg, err := LoadFile(path, prefix)
if err != nil {
return nil, err
}
if err = logger.SetLevel(cfg.Log.Level); err != nil {
return nil, err
}
return &Reloader{path: path, prefix: prefix, current: cfg, logger: logger}, nil
}
func (r *Reloader) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
next, err := LoadFile(r.path, r.prefix)
if err != nil {
return err
}
comparable := next
comparable.Log.Level = r.current.Log.Level
if !reflect.DeepEqual(comparable, r.current) {
return errors.New("config: static fields changed; restart required")
}
if err = r.logger.SetLevel(next.Log.Level); err != nil {
return err
}
r.current = next
return nil
}
func (r *Reloader) Snapshot() Config {
r.mu.Lock()
defer r.mu.Unlock()
cfg := r.current
cfg.Response = r.current.Response.Clone()
cfg.Errors = r.current.Errors.Clone()
cfg.TrustedProxies = append([]string(nil), cfg.TrustedProxies...)
cfg.Log.RedactKeys = append([]string(nil), cfg.Log.RedactKeys...)
return cfg
}
func (r *Reloader) Watch(ctx context.Context, interval time.Duration, onError func(error)) error {
if ctx == nil || interval < 100*time.Millisecond {
return errors.New("config: context and interval >=100ms required")
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
if err := r.Reload(); err != nil && onError != nil {
onError(err)
}
}
}
}