context helpers, rework logging to support ongoing inheritance
Leaves module logger property as purely an override, pulling logger information directly from the lifecycle instead.
This commit is contained in:
141
lifecycle.go
141
lifecycle.go
@@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -8,19 +9,23 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const lifecycleContextKey contextKey = "lifecycle"
|
||||
|
||||
// LifecycleOpts contains user-exposed options when defining a lifecycle.
|
||||
type LifecycleOpts struct {
|
||||
// DisableAutoload disables dependency autoloading (enabled by default).
|
||||
DisableAutoload bool
|
||||
|
||||
// Logger is the logger for the lifecycle. If not set, it will be initialized
|
||||
// by the lifecycle using the default logger (slog.Default).
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
// Lifecycle represents the core application structure. Lifecycle manages
|
||||
// resources providing an orchestrator for setup and teardown of modules.
|
||||
type Lifecycle struct {
|
||||
// Logger is the logger for the lifecycle. If not set, it will be initialized
|
||||
// by the lifecycle itself using the default logger.
|
||||
logger *slog.Logger
|
||||
|
||||
modules []*Module
|
||||
opts LifecycleOpts
|
||||
|
||||
@@ -33,46 +38,59 @@ type Lifecycle struct {
|
||||
// NewLifecycle creates a new Lifecycle instance with a default logger and the
|
||||
// given modules. It panics if any module has a duplicate name.
|
||||
func NewLifecycle(modules ...*Module) *Lifecycle {
|
||||
return NewLifecycleL(nil, modules...)
|
||||
}
|
||||
|
||||
// NewLifecycleL creates a new Lifecycle instance with a specific logger and the
|
||||
// given modules. See NewLifecycle for more details.
|
||||
func NewLifecycleL(defaultLogger *slog.Logger, modules ...*Module) *Lifecycle {
|
||||
// Use the provided logger or initialize a default one
|
||||
if defaultLogger == nil {
|
||||
defaultLogger = slog.Default()
|
||||
}
|
||||
|
||||
// Ensure modules are unique
|
||||
unique := make(map[string]bool)
|
||||
for _, mod := range modules {
|
||||
if _, exists := unique[mod.name]; exists {
|
||||
panic(fmt.Sprintf("duplicate module name: %s", mod.name))
|
||||
panic(fmt.Sprintf("duplicate module: %s", mod))
|
||||
}
|
||||
unique[mod.name] = true
|
||||
}
|
||||
|
||||
return &Lifecycle{
|
||||
logger: defaultLogger,
|
||||
modules: modules,
|
||||
setupTracker: make(map[string]int),
|
||||
teardownTracker: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
// LifecycleToContext adds the Lifecycle to the context.
|
||||
func LifecycleToContext(ctx context.Context, lifecycle *Lifecycle) context.Context {
|
||||
if lifecycle == nil {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, lifecycleContextKey, lifecycle)
|
||||
}
|
||||
|
||||
// LifecycleFromContext retrieves the Lifecycle from the context. Returns nil if not found.
|
||||
func LifecycleFromContext(ctx context.Context) *Lifecycle {
|
||||
if lifecycle, ok := ctx.Value(lifecycleContextKey).(*Lifecycle); ok {
|
||||
return lifecycle
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithOpts sets the options for the lifecycle.
|
||||
func (app *Lifecycle) WithOpts(opts LifecycleOpts) *Lifecycle {
|
||||
app.opts = opts
|
||||
return app
|
||||
}
|
||||
|
||||
// WithLogger sets the logger for the lifecycle. Panics if the logger is nil.
|
||||
func (app *Lifecycle) WithLogger(logger *slog.Logger) *Lifecycle {
|
||||
if logger == nil {
|
||||
panic("logger cannot be nil")
|
||||
}
|
||||
app.opts.Logger = logger
|
||||
return app
|
||||
}
|
||||
|
||||
// Logger returns the logger for the lifecycle.
|
||||
func (app *Lifecycle) Logger() *slog.Logger {
|
||||
if app.logger == nil {
|
||||
panic("lifecycle logger is not initialized")
|
||||
if app.opts.Logger == nil {
|
||||
app.opts.Logger = slog.Default()
|
||||
}
|
||||
return app.logger
|
||||
return app.opts.Logger
|
||||
}
|
||||
|
||||
// Setup initializes all modules in the order they were defined and checks
|
||||
@@ -81,12 +99,12 @@ func (app *Lifecycle) Logger() *slog.Logger {
|
||||
// of the application lifecycle to ensure all resources are cleaned up properly.
|
||||
func (app *Lifecycle) Setup() error {
|
||||
for _, mod := range app.modules {
|
||||
if err := app.setupSingle(mod, app.logger); err != nil {
|
||||
if err := app.setupSingle(nil, mod); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
app.logger.Info("Lifecycle modules initialized, backend setup complete",
|
||||
app.Logger().Info("Lifecycle modules initialized, backend setup complete",
|
||||
"modules", mapToString(app.setupTracker))
|
||||
|
||||
return nil
|
||||
@@ -105,11 +123,11 @@ func (app *Lifecycle) Teardown() error {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
app.logger.Error("Error tearing down modules", "failures", app.setupCount-app.teardownCount, "error", err)
|
||||
app.Logger().Error("Error tearing down modules", "failures", app.setupCount-app.teardownCount, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
app.logger.Info("All modules torn down, backend teardown complete",
|
||||
app.Logger().Info("All modules torn down, backend teardown complete",
|
||||
"modules", mapToString(app.teardownTracker))
|
||||
|
||||
return nil
|
||||
@@ -120,13 +138,13 @@ func (app *Lifecycle) Teardown() error {
|
||||
// but may still be conditionally necessary. Any modules that are already
|
||||
// set up are ignored.
|
||||
func (app *Lifecycle) Require(modules ...*Module) error {
|
||||
return app.RequireL(app.logger, modules...)
|
||||
return app.require(nil, false, modules...)
|
||||
}
|
||||
|
||||
// RequireUnique is the same as Require, but it returns an error if any requested
|
||||
// module is already set up--rather than ignoring it.
|
||||
func (app *Lifecycle) RequireUnique(modules ...*Module) error {
|
||||
return app.RequireUniqueL(app.logger, modules...)
|
||||
return app.require(nil, true, modules...)
|
||||
}
|
||||
|
||||
// RequireL adds module(s) to the lifecycle with a specific logger and
|
||||
@@ -135,21 +153,39 @@ func (app *Lifecycle) RequireUnique(modules ...*Module) error {
|
||||
// default logger.
|
||||
func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
|
||||
if logger == nil {
|
||||
logger = app.logger
|
||||
return fmt.Errorf("logger cannot be nil")
|
||||
}
|
||||
return app.require(logger, false, modules...)
|
||||
}
|
||||
|
||||
// RequireUniqueL is the same as RequireL, but it returns an error if any requested
|
||||
// module is already set up--rather than ignoring it.
|
||||
func (app *Lifecycle) RequireUniqueL(logger *slog.Logger, modules ...*Module) error {
|
||||
if logger == nil {
|
||||
return fmt.Errorf("logger cannot be nil")
|
||||
}
|
||||
return app.require(logger, true, modules...)
|
||||
}
|
||||
|
||||
// require is a helper function that attempts to add module(s) to the lifecycle
|
||||
// and immediately run any setup functions.
|
||||
func (app *Lifecycle) require(logger *slog.Logger, unique bool, modules ...*Module) error {
|
||||
if len(modules) == 0 {
|
||||
return fmt.Errorf("no modules to require")
|
||||
}
|
||||
|
||||
for _, mod := range modules {
|
||||
for i, mod := range modules {
|
||||
if mod == nil {
|
||||
return fmt.Errorf("module is nil")
|
||||
return fmt.Errorf("module %d is nil", i)
|
||||
}
|
||||
|
||||
// Check if the module is already set up
|
||||
// Check if the module has already been set up
|
||||
if _, ok := app.setupTracker[mod.name]; ok {
|
||||
app.logger.Warn("module already set up, ignoring", "module", mod.name)
|
||||
if unique {
|
||||
return fmt.Errorf("module %s is already set up, cannot require it again", mod)
|
||||
}
|
||||
|
||||
app.Logger().Warn("module already set up, ignoring", "module", mod)
|
||||
mod.loaded = true
|
||||
mod.logger = logger
|
||||
continue
|
||||
@@ -159,57 +195,40 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
|
||||
app.modules = append(app.modules, mod)
|
||||
|
||||
// Run the setup function for the module
|
||||
if err := app.setupSingle(mod, logger); err != nil {
|
||||
return fmt.Errorf("error setting up required module '%s': %w", mod.name, err)
|
||||
if err := app.setupSingle(logger, mod); err != nil {
|
||||
return fmt.Errorf("error setting up required module %s: %w", mod, err)
|
||||
}
|
||||
}
|
||||
|
||||
app.logger.Info("New modules initialized", "all", mapToString(app.setupTracker))
|
||||
|
||||
app.Logger().Info("New modules initialized", "all", mapToString(app.setupTracker))
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequireUniqueL is the same as RequireL, but it returns an error if any requested
|
||||
// module is already set up--rather than ignoring it.
|
||||
func (app *Lifecycle) RequireUniqueL(logger *slog.Logger, modules ...*Module) error {
|
||||
// Check if any module is already set up
|
||||
for _, mod := range modules {
|
||||
if mod == nil {
|
||||
return fmt.Errorf("module is nil")
|
||||
}
|
||||
|
||||
if _, ok := app.setupTracker[mod.name]; ok {
|
||||
return fmt.Errorf("module '%s' is already set up, cannot require it again", mod.name)
|
||||
}
|
||||
}
|
||||
|
||||
return app.RequireL(logger, modules...)
|
||||
}
|
||||
|
||||
// setupSingle is a helper function to set up a single module. Returns an error
|
||||
// if the module cannot be set up or if dependencies are not satisfied.
|
||||
func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) error {
|
||||
func (app *Lifecycle) setupSingle(logger *slog.Logger, mod *Module) error {
|
||||
if mod == nil {
|
||||
return fmt.Errorf("module is nil")
|
||||
}
|
||||
|
||||
// Set the logger for the module
|
||||
// Set the parent lifecycle and logger override
|
||||
mod.lifecycle = app
|
||||
mod.logger = logger
|
||||
|
||||
// Check if all dependencies are satisfied
|
||||
for _, dep := range mod.depends {
|
||||
if _, ok := app.setupTracker[dep]; !ok {
|
||||
if app.opts.DisableAutoload {
|
||||
return fmt.Errorf("dependency '%s' not satisfied for '%s'", dep, mod.name)
|
||||
return fmt.Errorf("dependency %s not satisfied for '%s'", dep, mod)
|
||||
} else {
|
||||
// Attempt to set up the dependency
|
||||
depmod, err := app.getModuleByName(dep)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting dependency '%s' for '%s': %w", dep, mod.name, err)
|
||||
return fmt.Errorf("error getting dependency '%s' for %s: %w", dep, mod, err)
|
||||
}
|
||||
|
||||
if err := app.setupSingle(depmod, logger); err != nil {
|
||||
return fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep, mod.name, err)
|
||||
if err := app.setupSingle(logger, depmod); err != nil {
|
||||
return fmt.Errorf("error setting up dependency %s for %s: %w", depmod, mod, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,7 +237,7 @@ func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) error {
|
||||
if mod.setup != nil {
|
||||
// Run the setup function for the module
|
||||
if err := mod.setup(mod); err != nil {
|
||||
return fmt.Errorf("error initializing '%s': %w", mod.name, err)
|
||||
return fmt.Errorf("error initializing %s: %w", mod, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,13 +257,13 @@ func (app *Lifecycle) teardownSingle(mod *Module) error {
|
||||
|
||||
// Check if the module was set up
|
||||
if _, ok := app.setupTracker[mod.name]; !ok {
|
||||
return fmt.Errorf("module '%s' is not set up, cannot tear down", mod.name)
|
||||
return fmt.Errorf("module %s is not set up, cannot tear down", mod)
|
||||
}
|
||||
|
||||
// Run the teardown function for the module
|
||||
if mod.teardown != nil {
|
||||
if err := mod.teardown(mod); err != nil {
|
||||
return fmt.Errorf("error tearing down '%s': %w", mod.name, err)
|
||||
return fmt.Errorf("error tearing down %s: %w", mod, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user