diff --git a/fsm/default.go b/fsm/default.go new file mode 100644 index 00000000..28500cda --- /dev/null +++ b/fsm/default.go @@ -0,0 +1,126 @@ +package fsm + +import ( + "context" + "fmt" + "sync" +) + +type state struct { + body interface{} + name string +} + +var _ State = &state{} + +func (s *state) Name() string { + return s.name +} + +func (s *state) Body() interface{} { + return s.body +} + +// fsm is a finite state machine +type fsm struct { + statesMap map[string]StateFunc + current string + statesOrder []string + opts Options + mu sync.Mutex +} + +// NewFSM creates a new finite state machine having the specified initial state +// with specified options +func NewFSM(opts ...Option) *fsm { + return &fsm{ + statesMap: map[string]StateFunc{}, + opts: NewOptions(opts...), + } +} + +// Current returns the current state +func (f *fsm) Current() string { + f.mu.Lock() + s := f.current + f.mu.Unlock() + return s +} + +// Current returns the current state +func (f *fsm) Reset() { + f.mu.Lock() + f.current = f.opts.Initial + f.mu.Unlock() +} + +// State adds state to fsm +func (f *fsm) State(state string, fn StateFunc) { + f.mu.Lock() + f.statesMap[state] = fn + f.statesOrder = append(f.statesOrder, state) + f.mu.Unlock() +} + +// Start runs state machine with provided data +func (f *fsm) Start(ctx context.Context, args interface{}, opts ...Option) (interface{}, error) { + var err error + + f.mu.Lock() + options := f.opts + + for _, opt := range opts { + opt(&options) + } + + sopts := []StateOption{StateDryRun(options.DryRun)} + + cstate := options.Initial + states := make(map[string]StateFunc, len(f.statesMap)) + for k, v := range f.statesMap { + states[k] = v + } + f.current = cstate + f.mu.Unlock() + + var s State + s = &state{name: cstate, body: args} + nstate := s.Name() + + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + fn, ok := states[nstate] + if !ok { + return nil, fmt.Errorf(`state "%s" %w`, nstate, ErrInvalidState) + } + f.mu.Lock() + f.current = nstate + f.mu.Unlock() + + // wrap the handler func + for i := len(options.Wrappers); i > 0; i-- { + fn = options.Wrappers[i-1](fn) + } + + s, err = fn(ctx, s, sopts...) + + switch { + case err != nil: + return s.Body(), err + case s.Name() == StateEnd: + return s.Body(), nil + case s.Name() == "": + for idx := range f.statesOrder { + if f.statesOrder[idx] == nstate && len(f.statesOrder) > idx+1 { + nstate = f.statesOrder[idx+1] + } + } + default: + nstate = s.Name() + } + } + } +} diff --git a/fsm/fsm.go b/fsm/fsm.go index c404f9a1..2cee6b1c 100644 --- a/fsm/fsm.go +++ b/fsm/fsm.go @@ -3,8 +3,6 @@ package fsm // import "go.unistack.org/micro/v3/fsm" import ( "context" "errors" - "fmt" - "sync" ) var ( @@ -12,170 +10,20 @@ var ( StateEnd = "end" ) -// Options struct holding fsm options -type Options struct { - // DryRun mode - DryRun bool - // Initial state - Initial string - // HooksBefore func slice runs in order before state - HooksBefore []HookBeforeFunc - // HooksAfter func slice runs in order after state - HooksAfter []HookAfterFunc +type State interface { + Name() string + Body() interface{} } -// HookBeforeFunc func signature -type HookBeforeFunc func(ctx context.Context, state string, args interface{}) - -// HookAfterFunc func signature -type HookAfterFunc func(ctx context.Context, state string, args interface{}) - -// Option func signature -type Option func(*Options) - -// StateOptions holds state options -type StateOptions struct { - DryRun bool -} - -// StateDryRun says that state executes in dry run mode -func StateDryRun(b bool) StateOption { - return func(o *StateOptions) { - o.DryRun = b - } -} - -// StateOption func signature -type StateOption func(*StateOptions) - -// InitialState sets init state for state machine -func InitialState(initial string) Option { - return func(o *Options) { - o.Initial = initial - } -} - -// HookBefore provides hook func slice -func HookBefore(fns ...HookBeforeFunc) Option { - return func(o *Options) { - o.HooksBefore = fns - } -} - -// HookAfter provides hook func slice -func HookAfter(fns ...HookAfterFunc) Option { - return func(o *Options) { - o.HooksAfter = fns - } -} +// StateWrapper wraps the StateFunc and returns the equivalent +type StateWrapper func(StateFunc) StateFunc // StateFunc called on state transition and return next step and error -type StateFunc func(ctx context.Context, args interface{}, opts ...StateOption) (string, interface{}, error) +type StateFunc func(ctx context.Context, state State, opts ...StateOption) (State, error) -// FSM is a finite state machine -type FSM struct { - mu sync.Mutex - statesMap map[string]StateFunc - statesOrder []string - opts *Options - current string -} - -// New creates a new finite state machine having the specified initial state -// with specified options -func New(opts ...Option) *FSM { - options := &Options{} - - for _, opt := range opts { - opt(options) - } - - return &FSM{ - statesMap: map[string]StateFunc{}, - opts: options, - } -} - -// Current returns the current state -func (f *FSM) Current() string { - f.mu.Lock() - defer f.mu.Unlock() - return f.current -} - -// Current returns the current state -func (f *FSM) Reset() { - f.mu.Lock() - f.current = f.opts.Initial - f.mu.Unlock() -} - -// State adds state to fsm -func (f *FSM) State(state string, fn StateFunc) { - f.mu.Lock() - f.statesMap[state] = fn - f.statesOrder = append(f.statesOrder, state) - f.mu.Unlock() -} - -// Init initialize fsm and check states - -// Start runs state machine with provided data -func (f *FSM) Start(ctx context.Context, args interface{}, opts ...Option) (interface{}, error) { - var err error - var ok bool - var fn StateFunc - var nstate string - - f.mu.Lock() - options := f.opts - - for _, opt := range opts { - opt(options) - } - - sopts := []StateOption{StateDryRun(options.DryRun)} - - cstate := options.Initial - states := make(map[string]StateFunc, len(f.statesMap)) - for k, v := range f.statesMap { - states[k] = v - } - f.current = cstate - f.mu.Unlock() - - for { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - fn, ok = states[cstate] - if !ok { - return nil, fmt.Errorf(`state "%s" %w`, cstate, ErrInvalidState) - } - f.mu.Lock() - f.current = cstate - f.mu.Unlock() - for _, fn := range options.HooksBefore { - fn(ctx, cstate, args) - } - nstate, args, err = fn(ctx, args, sopts...) - for _, fn := range options.HooksAfter { - fn(ctx, cstate, args) - } - switch { - case err != nil: - return args, err - case nstate == StateEnd: - return args, nil - case nstate == "": - for idx := range f.statesOrder { - if f.statesOrder[idx] == cstate && len(f.statesOrder) > idx+1 { - nstate = f.statesOrder[idx+1] - } - } - } - cstate = nstate - } - } +type FSM interface { + Start(context.Context, interface{}, ...Option) (interface{}, error) + Current() string + Reset() + State(string, StateFunc) } diff --git a/fsm/fsm_test.go b/fsm/fsm_test.go index 3064f7ad..ad7d706e 100644 --- a/fsm/fsm_test.go +++ b/fsm/fsm_test.go @@ -1,63 +1,72 @@ package fsm import ( - "bytes" "context" "fmt" "testing" + + "go.unistack.org/micro/v3/logger" ) func TestFSMStart(t *testing.T) { ctx := context.TODO() - buf := bytes.NewBuffer(nil) - pfb := func(_ context.Context, state string, _ interface{}) { - fmt.Fprintf(buf, "before state %s\n", state) + + if err := logger.DefaultLogger.Init(); err != nil { + t.Fatal(err) } - pfa := func(_ context.Context, state string, _ interface{}) { - fmt.Fprintf(buf, "after state %s\n", state) + + wrapper := func(next StateFunc) StateFunc { + return func(sctx context.Context, s State, opts ...StateOption) (State, error) { + sctx = logger.NewContext(sctx, logger.Fields("state", s.Name())) + return next(sctx, s, opts...) + } } - f := New(InitialState("1"), HookBefore(pfb), HookAfter(pfa)) - f1 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) { - args := req.(map[string]interface{}) + + f := NewFSM(InitialState("1"), WrapState(wrapper)) + f1 := func(sctx context.Context, s State, opts ...StateOption) (State, error) { + _, ok := logger.FromContext(sctx) + if !ok { + t.Fatal("f1 context does not have logger") + } + args := s.Body().(map[string]interface{}) if v, ok := args["request"].(string); !ok || v == "" { - return "", nil, fmt.Errorf("empty request") + return nil, fmt.Errorf("empty request") } - return "2", map[string]interface{}{"response": "test2"}, nil + return &state{name: "", body: map[string]interface{}{"response": "state1"}}, nil } - f2 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) { - args := req.(map[string]interface{}) - if v, ok := args["response"].(string); !ok || v == "" { - return "", nil, fmt.Errorf("empty response") + f2 := func(sctx context.Context, s State, opts ...StateOption) (State, error) { + _, ok := logger.FromContext(sctx) + if !ok { + t.Fatal("f2 context does not have logger") } - return "", map[string]interface{}{"response": "test"}, nil + args := s.Body().(map[string]interface{}) + if v, ok := args["response"].(string); !ok || v == "" { + return nil, fmt.Errorf("empty response") + } + return &state{name: "", body: map[string]interface{}{"response": "state2"}}, nil } - f3 := func(_ context.Context, req interface{}, _ ...StateOption) (string, interface{}, error) { - args := req.(map[string]interface{}) - if v, ok := args["response"].(string); !ok || v == "" { - return "", nil, fmt.Errorf("empty response") + f3 := func(sctx context.Context, s State, opts ...StateOption) (State, error) { + _, ok := logger.FromContext(sctx) + if !ok { + t.Fatal("f3 context does not have logger") } - return StateEnd, map[string]interface{}{"response": "test_last"}, nil + args := s.Body().(map[string]interface{}) + if v, ok := args["response"].(string); !ok || v == "" { + return nil, fmt.Errorf("empty response") + } + return &state{name: StateEnd, body: map[string]interface{}{"response": "state3"}}, nil } f.State("1", f1) f.State("2", f2) f.State("3", f3) - rsp, err := f.Start(ctx, map[string]interface{}{"request": "test1"}) + rsp, err := f.Start(ctx, map[string]interface{}{"request": "state"}) if err != nil { t.Fatal(err) } args := rsp.(map[string]interface{}) if v, ok := args["response"].(string); !ok || v == "" { t.Fatalf("nil rsp: %#+v", args) - } else if v != "test_last" { + } else if v != "state3" { t.Fatalf("invalid rsp %#+v", args) } - - if !bytes.Contains(buf.Bytes(), []byte(`before state 1`)) || - !bytes.Contains(buf.Bytes(), []byte(`before state 2`)) || - !bytes.Contains(buf.Bytes(), []byte(`after state 1`)) || - !bytes.Contains(buf.Bytes(), []byte(`after state 2`)) || - !bytes.Contains(buf.Bytes(), []byte(`after state 3`)) || - !bytes.Contains(buf.Bytes(), []byte(`after state 3`)) { - t.Fatalf("fsm not works properly or hooks error, buf: %s", buf.Bytes()) - } } diff --git a/fsm/options.go b/fsm/options.go new file mode 100644 index 00000000..93259429 --- /dev/null +++ b/fsm/options.go @@ -0,0 +1,52 @@ +package fsm + +// Options struct holding fsm options +type Options struct { + // Initial state + Initial string + // Wrappers runs before state + Wrappers []StateWrapper + // DryRun mode + DryRun bool +} + +// Option func signature +type Option func(*Options) + +// StateOptions holds state options +type StateOptions struct { + DryRun bool +} + +// StateDryRun says that state executes in dry run mode +func StateDryRun(b bool) StateOption { + return func(o *StateOptions) { + o.DryRun = b + } +} + +// StateOption func signature +type StateOption func(*StateOptions) + +// InitialState sets init state for state machine +func InitialState(initial string) Option { + return func(o *Options) { + o.Initial = initial + } +} + +// WrapState adds a state Wrapper to a list of options passed into the fsm +func WrapState(w StateWrapper) Option { + return func(o *Options) { + o.Wrappers = append(o.Wrappers, w) + } +} + +// NewOptions returns new Options struct filled by passed Option +func NewOptions(opts ...Option) Options { + options := Options{} + for _, o := range opts { + o(&options) + } + return options +} diff --git a/logger/default.go b/logger/default.go index d1961c33..45be6c64 100644 --- a/logger/default.go +++ b/logger/default.go @@ -12,10 +12,8 @@ import ( ) type defaultLogger struct { - enc *json.Encoder - logFunc LogFunc - logfFunc LogfFunc - opts Options + enc *json.Encoder + opts Options sync.RWMutex } @@ -27,10 +25,6 @@ func (l *defaultLogger) Init(opts ...Option) error { } l.enc = json.NewEncoder(l.opts.Out) // wrap the Log func - for i := len(l.opts.Wrappers); i > 0; i-- { - l.logFunc = l.opts.Wrappers[i-1].Log(l.logFunc) - l.logfFunc = l.opts.Wrappers[i-1].Logf(l.logfFunc) - } l.Unlock() return nil } @@ -47,17 +41,10 @@ func (l *defaultLogger) Clone(opts ...Option) Logger { o(&oldopts) } - oldopts.Wrappers = newopts.Wrappers l.Lock() - cl := &defaultLogger{opts: oldopts, logFunc: l.logFunc, logfFunc: l.logfFunc, enc: json.NewEncoder(l.opts.Out)} + cl := &defaultLogger{opts: oldopts, enc: json.NewEncoder(l.opts.Out)} l.Unlock() - // wrap the Log func - for i := len(newopts.Wrappers); i > 0; i-- { - cl.logFunc = newopts.Wrappers[i-1].Log(cl.logFunc) - cl.logfFunc = newopts.Wrappers[i-1].Logf(cl.logfFunc) - } - return cl } @@ -75,15 +62,17 @@ func (l *defaultLogger) Level(level Level) { } func (l *defaultLogger) Fields(fields ...interface{}) Logger { + l.RLock() nl := &defaultLogger{opts: l.opts, enc: l.enc} if len(fields) == 0 { + l.RUnlock() return nl } else if len(fields)%2 != 0 { fields = fields[:len(fields)-1] } - nl.logFunc = l.logFunc - nl.logfFunc = l.logfFunc + nl.opts.Fields = copyFields(l.opts.Fields) nl.opts.Fields = append(nl.opts.Fields, fields...) + l.RUnlock() return nl } @@ -143,27 +132,27 @@ func (l *defaultLogger) Fatal(ctx context.Context, args ...interface{}) { } func (l *defaultLogger) Infof(ctx context.Context, msg string, args ...interface{}) { - l.logfFunc(ctx, InfoLevel, msg, args...) + l.Logf(ctx, InfoLevel, msg, args...) } func (l *defaultLogger) Errorf(ctx context.Context, msg string, args ...interface{}) { - l.logfFunc(ctx, ErrorLevel, msg, args...) + l.Logf(ctx, ErrorLevel, msg, args...) } func (l *defaultLogger) Debugf(ctx context.Context, msg string, args ...interface{}) { - l.logfFunc(ctx, DebugLevel, msg, args...) + l.Logf(ctx, DebugLevel, msg, args...) } func (l *defaultLogger) Warnf(ctx context.Context, msg string, args ...interface{}) { - l.logfFunc(ctx, WarnLevel, msg, args...) + l.Logf(ctx, WarnLevel, msg, args...) } func (l *defaultLogger) Tracef(ctx context.Context, msg string, args ...interface{}) { - l.logfFunc(ctx, TraceLevel, msg, args...) + l.Logf(ctx, TraceLevel, msg, args...) } func (l *defaultLogger) Fatalf(ctx context.Context, msg string, args ...interface{}) { - l.logfFunc(ctx, FatalLevel, msg, args...) + l.Logf(ctx, FatalLevel, msg, args...) os.Exit(1) } @@ -236,8 +225,6 @@ func NewLogger(opts ...Option) Logger { l := &defaultLogger{ opts: NewOptions(opts...), } - l.logFunc = l.Log - l.logfFunc = l.Logf l.enc = json.NewEncoder(l.opts.Out) return l } diff --git a/logger/logger_test.go b/logger/logger_test.go index dd61487e..b9b8146d 100644 --- a/logger/logger_test.go +++ b/logger/logger_test.go @@ -32,7 +32,33 @@ func TestFields(t *testing.T) { if err := l.Init(); err != nil { t.Fatal(err) } - l.Fields("key", "val").Info(ctx, "message") + + nl := l.Fields("key", "val") + + nl.Info(ctx, "message") + if !bytes.Contains(buf.Bytes(), []byte(`"key":"val"`)) { + t.Fatalf("logger fields not works, buf contains: %s", buf.Bytes()) + } +} + +func TestFromContextWithFields(t *testing.T) { + ctx := context.TODO() + buf := bytes.NewBuffer(nil) + var ok bool + l := NewLogger(WithLevel(TraceLevel), WithOutput(buf)) + if err := l.Init(); err != nil { + t.Fatal(err) + } + nl := l.Fields("key", "val") + + ctx = NewContext(ctx, nl) + + l, ok = FromContext(ctx) + if !ok { + t.Fatalf("context does not have logger") + } + + l.Info(ctx, "message") if !bytes.Contains(buf.Bytes(), []byte(`"key":"val"`)) { t.Fatalf("logger fields not works, buf contains: %s", buf.Bytes()) } @@ -110,39 +136,3 @@ func TestLogger(t *testing.T) { t.Fatalf("logger error, buf %s", buf.Bytes()) } } - -func TestLoggerWrapper(t *testing.T) { - ctx := context.TODO() - buf := bytes.NewBuffer(nil) - l := NewLogger(WithLevel(TraceLevel), WithOutput(buf)) - if err := l.Init(WrapLogger(NewOmitWrapper())); err != nil { - t.Fatal(err) - } - type secret struct { - Name string - Passw string `logger:"omit"` - } - s := &secret{Name: "name", Passw: "secret"} - l.Errorf(ctx, "test %#+v", s) - if !bytes.Contains(buf.Bytes(), []byte(`logger.secret{Name:\"name\", Passw:\"\"}"`)) { - t.Fatalf("omit not works, struct: %v, output: %s", s, buf.Bytes()) - } -} - -func TestOmitLoggerWrapper(t *testing.T) { - ctx := context.TODO() - buf := bytes.NewBuffer(nil) - l := NewOmitLogger(NewLogger(WithLevel(TraceLevel), WithOutput(buf))) - if err := l.Init(); err != nil { - t.Fatal(err) - } - type secret struct { - Name string - Passw string `logger:"omit"` - } - s := &secret{Name: "name", Passw: "secret"} - l.Errorf(ctx, "test %#+v", s) - if !bytes.Contains(buf.Bytes(), []byte(`logger.secret{Name:\"name\", Passw:\"\"}"`)) { - t.Fatalf("omit not works, struct: %v, output: %s", s, buf.Bytes()) - } -} diff --git a/logger/options.go b/logger/options.go index 4918eb45..7ea04e74 100644 --- a/logger/options.go +++ b/logger/options.go @@ -19,8 +19,6 @@ type Options struct { Fields []interface{} // Name holds the logger name Name string - // Wrappers logger wrapper that called before actual Log/Logf function - Wrappers []Wrapper // The logging level the logger should log Level Level // CallerSkipCount number of frmaes to skip @@ -83,10 +81,3 @@ func WithName(n string) Option { o.Name = n } } - -// WrapLogger adds a logger Wrapper to a list of options passed into the logger -func WrapLogger(w Wrapper) Option { - return func(o *Options) { - o.Wrappers = append(o.Wrappers, w) - } -} diff --git a/logger/wrapper.go b/logger/wrapper.go deleted file mode 100644 index e7a9f79c..00000000 --- a/logger/wrapper.go +++ /dev/null @@ -1,166 +0,0 @@ -package logger // import "go.unistack.org/micro/v3/logger/wrapper" - -import ( - "context" - "reflect" - - rutil "go.unistack.org/micro/v3/util/reflect" -) - -// LogFunc function used for Log method -type LogFunc func(ctx context.Context, level Level, args ...interface{}) - -// LogfFunc function used for Logf method -type LogfFunc func(ctx context.Context, level Level, msg string, args ...interface{}) - -type Wrapper interface { - // Log logs message with needed level - Log(LogFunc) LogFunc - // Logf logs message with needed level - Logf(LogfFunc) LogfFunc -} - -var _ Logger = &omitLogger{} - -type omitLogger struct { - l Logger -} - -func NewOmitLogger(l Logger) Logger { - return &omitLogger{l: l} -} - -func (w *omitLogger) Init(opts ...Option) error { - return w.l.Init(append(opts, WrapLogger(NewOmitWrapper()))...) -} - -func (w *omitLogger) V(level Level) bool { - return w.l.V(level) -} - -func (w *omitLogger) Level(level Level) { - w.l.Level(level) -} - -func (w *omitLogger) Clone(opts ...Option) Logger { - return w.l.Clone(opts...) -} - -func (w *omitLogger) Options() Options { - return w.l.Options() -} - -func (w *omitLogger) Fields(fields ...interface{}) Logger { - return w.l.Fields(fields...) -} - -func (w *omitLogger) Info(ctx context.Context, args ...interface{}) { - w.l.Info(ctx, args...) -} - -func (w *omitLogger) Trace(ctx context.Context, args ...interface{}) { - w.l.Trace(ctx, args...) -} - -func (w *omitLogger) Debug(ctx context.Context, args ...interface{}) { - w.l.Debug(ctx, args...) -} - -func (w *omitLogger) Warn(ctx context.Context, args ...interface{}) { - w.l.Warn(ctx, args...) -} - -func (w *omitLogger) Error(ctx context.Context, args ...interface{}) { - w.l.Error(ctx, args...) -} - -func (w *omitLogger) Fatal(ctx context.Context, args ...interface{}) { - w.l.Fatal(ctx, args...) -} - -func (w *omitLogger) Infof(ctx context.Context, msg string, args ...interface{}) { - w.l.Infof(ctx, msg, args...) -} - -func (w *omitLogger) Tracef(ctx context.Context, msg string, args ...interface{}) { - w.l.Tracef(ctx, msg, args...) -} - -func (w *omitLogger) Debugf(ctx context.Context, msg string, args ...interface{}) { - w.l.Debugf(ctx, msg, args...) -} - -func (w *omitLogger) Warnf(ctx context.Context, msg string, args ...interface{}) { - w.l.Warnf(ctx, msg, args...) -} - -func (w *omitLogger) Errorf(ctx context.Context, msg string, args ...interface{}) { - w.l.Errorf(ctx, msg, args...) -} - -func (w *omitLogger) Fatalf(ctx context.Context, msg string, args ...interface{}) { - w.l.Fatalf(ctx, msg, args...) -} - -func (w *omitLogger) Log(ctx context.Context, level Level, args ...interface{}) { - w.l.Log(ctx, level, args...) -} - -func (w *omitLogger) Logf(ctx context.Context, level Level, msg string, args ...interface{}) { - w.l.Logf(ctx, level, msg, args...) -} - -func (w *omitLogger) String() string { - return w.l.String() -} - -type omitWrapper struct{} - -func NewOmitWrapper() Wrapper { - return &omitWrapper{} -} - -func getArgs(args []interface{}) []interface{} { - nargs := make([]interface{}, 0, len(args)) - var err error - for _, arg := range args { - val := reflect.ValueOf(arg) - if val.Kind() == reflect.Ptr { - val = val.Elem() - } - narg := arg - if val.Kind() != reflect.Struct { - nargs = append(nargs, narg) - continue - } - - if narg, err = rutil.Zero(arg); err != nil { - nargs = append(nargs, narg) - continue - } - - rutil.CopyDefaults(narg, arg) - if flds, ferr := rutil.StructFields(narg); ferr == nil { - for _, fld := range flds { - if tv, ok := fld.Field.Tag.Lookup("logger"); ok && tv == "omit" { - fld.Value.Set(reflect.Zero(fld.Value.Type())) - } - } - } - - nargs = append(nargs, narg) - } - return nargs -} - -func (w *omitWrapper) Log(fn LogFunc) LogFunc { - return func(ctx context.Context, level Level, args ...interface{}) { - fn(ctx, level, getArgs(args)...) - } -} - -func (w *omitWrapper) Logf(fn LogfFunc) LogfFunc { - return func(ctx context.Context, level Level, msg string, args ...interface{}) { - fn(ctx, level, msg, getArgs(args)...) - } -}