From aa2f3fd95d853300d06cc4e6eab1c955fd376b97 Mon Sep 17 00:00:00 2001 From: Mohammad Rajabloo Date: Fri, 16 Aug 2024 18:28:05 +0330 Subject: [PATCH 1/3] refactor and add event system --- event.go | 131 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 4 +- go.sum | 22 ++++++-- logger.go | 58 ++++++++++---------- options.go | 16 +++--- vabastegi.go | 152 ++++++++++++++++++++++++++++++++------------------- 6 files changed, 284 insertions(+), 99 deletions(-) create mode 100644 event.go diff --git a/event.go b/event.go new file mode 100644 index 0000000..f9bb121 --- /dev/null +++ b/event.go @@ -0,0 +1,131 @@ +package vabastegi + +import "time" + +// Event is what Vabastegi event look like. +type Event interface { + event() // it's private to prevent outside implementation. +} + +// EventHandlers is list of EventHandler. +type EventHandlers []EventHandler + +// Publish passed event using event handlers. +func (e EventHandlers) Publish(event Event) { + for _, handler := range e { + handler.OnEvent(event) + } +} + +// EventHandler used if you need to handle the events. +type EventHandler interface { + OnEvent(event Event) +} + +func (p *OnBuildsExecuting) event() {} +func (p *OnBuildsExecuted) event() {} +func (p *OnBuildExecuting) event() {} +func (p *OnBuildExecuted) event() {} +func (p *OnShutdownExecuting) event() {} +func (p *OnShutdownExecuted) event() {} +func (p *OnApplicationShutdownExecuting) event() {} +func (p *OnApplicationShutdownExecuted) event() {} +func (p *OnLog) event() {} + +// OnBuildsExecuting is emitted before a Builds is executed. +type OnBuildsExecuting struct { + // BuildAt is the time build happened. + BuildAt time.Time +} + +// OnBuildsExecuted is emitted after a Builds has been executed. +type OnBuildsExecuted struct { + // Runtime specifies how long it took to run this hook. + Runtime time.Duration + + // Err is non-nil if the hook failed to execute. + Err error +} + +// OnBuildExecuting is emitted before a Build is executed. +type OnBuildExecuting struct { + // BuildAt is the time build happened. + BuildAt time.Time + + // ProviderName is the name of the function that will be executed. + ProviderName string + + // CallerPath is the path of provider if from. + CallerPath string +} + +// OnBuildExecuted is emitted after a Provider has been executed. +type OnBuildExecuted struct { + // ProviderName is the name of the function that was executed. + ProviderName string + + // CallerPath is the path of provider if from. + CallerPath string + + // Runtime specifies how long it took to run this hook. + Runtime time.Duration + + // Err is non-nil if the hook failed to execute. + Err error +} + +// OnShutdownExecuting is emitted before a Shutdown is executed. +type OnShutdownExecuting struct { + // ShutdownAt is the time shutdown happened. + ShutdownAt time.Time + + // ProviderName is the name of the function that will be executed. + ProviderName string + + // CallerPath is the path of provider if from. + CallerPath string +} + +// OnShutdownExecuted is emitted after a Shutdown has been executed. +type OnShutdownExecuted struct { + // ProviderName is the name of the function that was executed. + ProviderName string + + // CallerPath is the path of provider if from. + CallerPath string + + // Runtime specifies how long it took to run this hook. + Runtime time.Duration + + // Err is non-nil if the hook failed to execute. + Err error +} + +// OnApplicationShutdownExecuting is emitted before the application Shutdown is executed. +type OnApplicationShutdownExecuting struct { + // ShutdownAt is the time shutdown happened. + ShutdownAt time.Time + + // Reason is the reason for shutdown the application. + Reason string +} + +// OnApplicationShutdownExecuted is emitted after the application Shutdown has been executed. +type OnApplicationShutdownExecuted struct { + // Reason is the reason for shutdown the application. + Reason string + + // Runtime specifies how long it took to run this hook. + Runtime time.Duration + + // Err is non-nil if the hook failed to execute. + Err error +} + +// OnLog is used if a log event is sent. +type OnLog struct { + LogAt time.Time + Level logLevel + Message string + Args []interface{} +} diff --git a/go.mod b/go.mod index 0e4c844..6a10f26 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,4 @@ module github.com/mrsoftware/vabastegi go 1.18.0 -require github.com/mattn/go-runewidth v0.0.15 - -require github.com/rivo/uniseg v0.2.0 // indirect +require github.com/mrsoftware/errors v0.1.0 diff --git a/go.sum b/go.sum index 9d3d381..daa0986 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,18 @@ -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/mrsoftware/errors v0.1.0 h1:5MSHsrsqlBMPbNzwobVt39IpgsVee7LuSe+n8aQWPsI= +github.com/mrsoftware/errors v0.1.0/go.mod h1:iHqx83gamUM9jhiV/rWZuVZe54NVqtqkIDnvZHywSM8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logger.go b/logger.go index e586774..9fb8be2 100644 --- a/logger.go +++ b/logger.go @@ -37,46 +37,46 @@ const ( ErrorLogLevel ) -var _ Logger = &IOLogger{} +var _ EventHandler = &EventLogger{} -// Logger is what Vabastegi use as logger. -type Logger interface { - Debugf(message string, args ...interface{}) - Infof(message string, args ...interface{}) - Errorf(message string, args ...interface{}) -} - -// NewIOLogger create new instance of IOLogger. -func NewIOLogger(writer io.Writer, level logLevel) Logger { - return &IOLogger{writer: writer, level: level} -} - -// IOLogger is a Logger that write to passed io.Writer. -type IOLogger struct { +// EventLogger is use event of vabastegi and log them. +type EventLogger struct { writer io.Writer level logLevel } -func (i *IOLogger) IsEnable(level logLevel) bool { - return level >= i.level +// NewEventLogger create new instance of EventLogger. +func NewEventLogger(writer io.Writer, level logLevel) *EventLogger { + return &EventLogger{writer: writer, level: level} } -func (i *IOLogger) Debugf(message string, args ...interface{}) { - i.log(DebugLogLevel, message, args...) +func (l *EventLogger) IsEnable(level logLevel) bool { + return level >= l.level } -func (i *IOLogger) Infof(message string, args ...interface{}) { - i.log(InfoLogLevel, message, args...) -} +func (l *EventLogger) log(level logLevel, message string, args ...interface{}) { + if !l.IsEnable(level) { + return + } -func (i *IOLogger) Errorf(message string, args ...interface{}) { - i.log(ErrorLogLevel, message, args...) + fmt.Fprintf(l.writer, "[Vabastegi] ["+level.String()+"] "+message+"\n", args...) } -func (i *IOLogger) log(level logLevel, message string, args ...interface{}) { - if !i.IsEnable(level) { - return +func (l *EventLogger) OnEvent(event Event) { + switch e := event.(type) { + case *OnBuildExecuted: + if e.Err != nil { + l.log(ErrorLogLevel, e.ProviderName+" ✕") + + return + } + + l.log(InfoLogLevel, e.ProviderName+" ✓") + case *OnShutdownExecuting: + l.log(InfoLogLevel, "Shutting Down %s", e.ProviderName) + case *OnApplicationShutdownExecuting: + l.log(InfoLogLevel, "Shutting Down Application: %s", e.Reason) + case *OnLog: + l.log(e.Level, e.Message, e.Args...) } - - fmt.Fprintf(i.writer, "[Vabastegi] ["+level.String()+"] "+message+"\n", args...) } diff --git a/options.go b/options.go index e80dc3e..cea9727 100644 --- a/options.go +++ b/options.go @@ -2,21 +2,14 @@ package vabastegi // Options of Vabastegi. type Options struct { - Logger Logger GracefulShutdown bool AppName string + EventHandlers EventHandlers } // Option of Vabastegi. type Option func(options *Options) -// WithLogger provide logger for Vabastegi. -func WithLogger(logger Logger) Option { - return func(options *Options) { - options.Logger = logger - } -} - // WithGraceFullShutdown used if you need gracefully shutdown for your application. func WithGraceFullShutdown(active bool) Option { return func(options *Options) { @@ -30,3 +23,10 @@ func WithAppName(appName string) Option { options.AppName = appName } } + +// WithEventHandlers register event handlers for vabastegi events. +func WithEventHandlers(handlers ...EventHandler) Option { + return func(options *Options) { + options.EventHandlers = append(options.EventHandlers, handlers...) + } +} diff --git a/vabastegi.go b/vabastegi.go index d630627..be0e050 100644 --- a/vabastegi.go +++ b/vabastegi.go @@ -8,36 +8,38 @@ import ( "runtime" "strings" "sync" + "time" + + "github.com/mrsoftware/errors" ) -// Provider is what a pkgs use to create dependency. +// Provider is a dependency provider for application. type Provider[T any] func(context.Context, *App[T]) error // App is the dependency injection manger. type App[T any] struct { waitGroup sync.WaitGroup - errList []error - onShutdown []shoutDown + errors *errors.MultiError + onShutdown []func(ctx context.Context) error Hub T options Options backgroundTasksCount int -} - -type shoutDown struct { - fn func(ctx context.Context) error - name string + graceFullOnce sync.Once } // New instance of App Dependency management. func New[T any](options ...Option) *App[T] { - app := App[T]{options: Options{}} + app := App[T]{ + errors: errors.NewMultiError(), + options: Options{EventHandlers: make(EventHandlers, 0)}, + } app.UpdateOptions(options...) return &app } // UpdateOptions is used if you want to change any options for App. -func (a *App[T]) UpdateOptions(options ...Option) { +func (a *App[ـ]) UpdateOptions(options ...Option) { for _, option := range options { option(&a.options) } @@ -45,15 +47,18 @@ func (a *App[T]) UpdateOptions(options ...Option) { if a.options.GracefulShutdown { a.registerGracefulShutdown() } - - // app required a logger. - if a.options.Logger == nil { - a.options.Logger = NewIOLogger(os.Stdout, InfoLogLevel) - } } // Builds the dependency structure of your app. -func (a *App[T]) Builds(ctx context.Context, providers ...Provider[T]) error { +func (a *App[T]) Builds(ctx context.Context, providers ...Provider[T]) (err error) { + startAt := time.Now() + + a.options.EventHandlers.Publish(&OnBuildsExecuting{BuildAt: startAt}) + + defer func() { + a.options.EventHandlers.Publish(&OnApplicationShutdownExecuted{Runtime: time.Now().Sub(startAt), Err: err}) + }() + for _, provider := range providers { err := a.Build(ctx, provider) if err == nil { @@ -70,28 +75,28 @@ func (a *App[T]) Builds(ctx context.Context, providers ...Provider[T]) error { // Build use the provider to set a dependency. func (a *App[T]) Build(ctx context.Context, provider Provider[T]) (err error) { - logMessage := a.getFuncName(provider, 0) + startAt := time.Now() - defer func() { - if err != nil { - logMessage = logMessage + " ✕" - } else { - logMessage = logMessage + " ✓" - } + a.options.EventHandlers.Publish(&OnBuildExecuting{ + ProviderName: a.getProviderName(provider, 0), + CallerPath: a.getProviderName(provider, -1), + BuildAt: startAt, + }) - a.options.Logger.Infof(logMessage) + defer func() { + a.options.EventHandlers.Publish(&OnBuildExecuted{ + ProviderName: a.getProviderName(provider, 0), + CallerPath: a.getProviderName(provider, -1), + Runtime: time.Now().Sub(startAt), + Err: err, + }) }() return provider(ctx, a) } -// Logger of application. -func (a *App[T]) Logger() Logger { - return a.options.Logger -} - // RunTask in background. -func (a *App[T]) RunTask(fn func()) { +func (a *App[ـ]) RunTask(fn func()) { go fn() a.backgroundTasksCount++ @@ -100,27 +105,31 @@ func (a *App[T]) RunTask(fn func()) { } // Wait for background task to done or any shutdown signal. -func (a *App[T]) Wait() error { +func (a *App[ـ]) Wait() error { a.waitGroup.Wait() - // todo: handle merging and returning all errors. - if len(a.errList) != 0 { - return a.errList[0] - } - - return nil + return a.errors.Err() } // Shutdown ths application. -func (a *App[T]) Shutdown(ctx context.Context, reason string) { - a.options.Logger.Infof("Shutting Down( %s ) ...", reason) +func (a *App[ـ]) Shutdown(ctx context.Context, reason string) { + startAt := time.Now() - for _, shutdown := range a.onShutdown { - a.options.Logger.Infof("Shutting Down %s", shutdown.name) + a.options.EventHandlers.Publish(&OnApplicationShutdownExecuting{ + Reason: reason, + ShutdownAt: startAt, + }) - if err := shutdown.fn(ctx); err != nil { - a.errList = append(a.errList, err) - } + defer func() { + a.options.EventHandlers.Publish(&OnApplicationShutdownExecuted{ + Reason: reason, + Runtime: time.Now().Sub(startAt), + Err: a.errors.Err(), + }) + }() + + for _, fn := range a.onShutdown { + a.errors.Add(a.shutdown(ctx, fn)) } for i := 0; i < a.backgroundTasksCount; i++ { @@ -128,23 +137,56 @@ func (a *App[T]) Shutdown(ctx context.Context, reason string) { } } +func (a *App[ـ]) shutdown(ctx context.Context, fn func(context.Context) error) (err error) { + startAt := time.Now() + + a.options.EventHandlers.Publish(&OnShutdownExecuting{ + ProviderName: a.getProviderName(fn, 1), + CallerPath: a.getProviderName(fn, -1), + ShutdownAt: startAt, + }) + + defer func() { + a.options.EventHandlers.Publish(&OnShutdownExecuted{ + ProviderName: a.getProviderName(fn, 1), + CallerPath: a.getProviderName(fn, -1), + Runtime: time.Now().Sub(startAt), + Err: err, + }) + }() + + return fn(ctx) +} + // OnShutdown register any method for Shutdown method. -func (a *App[T]) OnShutdown(fn func(ctx context.Context) error) { - a.onShutdown = append(a.onShutdown, shoutDown{fn: fn, name: a.getFuncName(fn, 1)}) +func (a *App[ـ]) OnShutdown(fn func(ctx context.Context) error) { + a.onShutdown = append(a.onShutdown, fn) } -func (a *App[T]) getFuncName(creator interface{}, index int) string { - parts := strings.Split(runtime.FuncForPC(reflect.ValueOf(creator).Pointer()).Name(), ".") +func (a *App[ـ]) getProviderName(creator interface{}, index int) string { + reference := runtime.FuncForPC(reflect.ValueOf(creator).Pointer()).Name() + if index == -1 { + return reference + } + + parts := strings.Split(reference, ".") return parts[len(parts)-(1+index)] } -func (a *App[T]) registerGracefulShutdown() { - interruptChan := make(chan os.Signal, 1) - signal.Notify(interruptChan, os.Interrupt) +// Log the message. +func (a *App[ـ]) Log(level logLevel, message string, args ...interface{}) { + a.options.EventHandlers.Publish(&OnLog{LogAt: time.Now(), Level: level, Message: message, Args: args}) +} + +func (a *App[ـ]) registerGracefulShutdown() { + a.graceFullOnce.Do(func() { + interruptChan := make(chan os.Signal, 1) + signal.Notify(interruptChan, os.Interrupt) - go func() { - appSignal := <-interruptChan - a.Shutdown(context.Background(), appSignal.String()) - }() + go func() { + appSignal := <-interruptChan + a.Shutdown(context.Background(), appSignal.String()) + }() + }) } From f2c97592af0d36e7e6129442ad2cd3012b509a76 Mon Sep 17 00:00:00 2001 From: Mohammad Rajabloo Date: Sun, 25 Aug 2024 22:39:51 +0330 Subject: [PATCH 2/3] fix IgnoreError to support new logger --- error.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/error.go b/error.go index 68ec9f2..bd9e7fd 100644 --- a/error.go +++ b/error.go @@ -8,7 +8,7 @@ import ( func IgnoreError[T any](provider Provider[T]) Provider[T] { return func(ctx context.Context, app *App[T]) error { if err := provider(ctx, app); err != nil { - app.Logger().Errorf("privider error ignored: %s", err.Error()) + app.Log(ErrorLogLevel, "provider error ignored: %s", err) } return nil From ee9c9dd663663b31294a3a80e3bd1ecc09a03d71 Mon Sep 17 00:00:00 2001 From: Mohammad Rajabloo Date: Sat, 21 Sep 2024 15:50:39 +0330 Subject: [PATCH 3/3] use time.Since instead of time.Now().Sub --- vabastegi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vabastegi.go b/vabastegi.go index be0e050..0eb07d0 100644 --- a/vabastegi.go +++ b/vabastegi.go @@ -56,7 +56,7 @@ func (a *App[T]) Builds(ctx context.Context, providers ...Provider[T]) (err erro a.options.EventHandlers.Publish(&OnBuildsExecuting{BuildAt: startAt}) defer func() { - a.options.EventHandlers.Publish(&OnApplicationShutdownExecuted{Runtime: time.Now().Sub(startAt), Err: err}) + a.options.EventHandlers.Publish(&OnApplicationShutdownExecuted{Runtime: time.Since(startAt), Err: err}) }() for _, provider := range providers {