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