From 231561c92698b617ed386afa8f1432941f9e520e Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Fri, 30 May 2025 11:15:59 -0700 Subject: [PATCH] customizable subsystem loggers --- lifecycle.go | 44 +++++++++++++++++++++++++++++++++----------- subsystem.go | 14 ++++++++++++++ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/lifecycle.go b/lifecycle.go index 5c759f8..5d571f6 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -20,14 +20,24 @@ type LifecycleOpts struct { // Lifecycle represents the core application structure. Lifecycle manages // resources providing an orchestrator for setup and teardown of sub-systems. 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 + subsystems []*Subsystem setupTracker map[string]bool teardownTracker map[string]bool opts LifecycleOpts } -// NewLifecycle creates a new Lifecycle instance with the given subsystems. -func NewLifecycle(subsystems ...*Subsystem) *Lifecycle { +// NewLifecycle creates a new Lifecycle instance with a default logger and the +// given subsystems. It panics if any subsystem has a duplicate name. +func NewLifecycle(defaultLogger *slog.Logger, subsystems ...*Subsystem) *Lifecycle { + // Use the provided logger or initialize a default one + if defaultLogger == nil { + defaultLogger = slog.Default() + } + // Ensure subsystems are unique unique := make(map[string]bool) for _, sub := range subsystems { @@ -38,6 +48,7 @@ func NewLifecycle(subsystems ...*Subsystem) *Lifecycle { } return &Lifecycle{ + logger: defaultLogger, subsystems: subsystems, setupTracker: make(map[string]bool), teardownTracker: make(map[string]bool), @@ -57,14 +68,14 @@ func (app *Lifecycle) WithOpts(opts LifecycleOpts) *Lifecycle { func (app *Lifecycle) Setup() error { setupCount := 0 for _, sub := range app.subsystems { - if ok, err := app.setupSingle(sub); err != nil { + if ok, err := app.setupSingle(sub, app.logger); err != nil { return err } else if ok { setupCount++ } } - slog.Info("Lifecycle sub-systems initialized, backend setup complete", + app.logger.Info("Lifecycle sub-systems initialized, backend setup complete", "count", setupCount, "subsystems", mapToString(app.setupTracker)) return nil @@ -83,17 +94,25 @@ func (app *Lifecycle) Teardown() error { } } - slog.Info("All sub-systems torn down, backend teardown complete", + app.logger.Info("All sub-systems torn down, backend teardown complete", "count", teardownCount, "subsystems", mapToString(app.teardownTracker)) return nil } // Require adds sub-system(s) to the lifecycle and immediately runs any setup -// function. Relevant when a sub-system is not part of the main application +// functions. Relevant when a sub-system is not part of the main application // but may still be conditionally necessary. Any sub-systems that are already // set up are ignored. func (app *Lifecycle) Require(subsystems ...*Subsystem) error { + return app.RequireL(app.logger, subsystems...) +} + +// RequireL adds sub-system(s) to the lifecycle with a specific logger and +// immediately runs any setup functions. See Require for more details. +// This variation is useful when you need to set up sub-systems with a non- +// default logger. +func (app *Lifecycle) RequireL(logger *slog.Logger, subsystems ...*Subsystem) error { for _, sub := range subsystems { if sub == nil { return fmt.Errorf("sub-system is nil") @@ -101,7 +120,7 @@ func (app *Lifecycle) Require(subsystems ...*Subsystem) error { // Check if the sub-system is already set up if _, ok := app.setupTracker[sub.name]; ok { - slog.Warn("sub-system already set up, ignoring", "subsystem", sub.name) + app.logger.Warn("sub-system already set up, ignoring", "subsystem", sub.name) continue } @@ -109,22 +128,25 @@ func (app *Lifecycle) Require(subsystems ...*Subsystem) error { app.subsystems = append(app.subsystems, sub) // Run the setup function for the sub-system - if _, err := app.setupSingle(sub); err != nil { + if _, err := app.setupSingle(sub, logger); err != nil { return fmt.Errorf("error setting up required subsystem '%s': %w", sub.name, err) } } - slog.Info("New sub-systems initialized", "all", mapToString(app.setupTracker)) + app.logger.Info("New sub-systems initialized", "all", mapToString(app.setupTracker)) return nil } // setupSingle is a helper function to set up a single sub-system. -func (app *Lifecycle) setupSingle(sub *Subsystem) (bool, error) { +func (app *Lifecycle) setupSingle(sub *Subsystem, logger *slog.Logger) (bool, error) { if sub == nil { return false, fmt.Errorf("sub-system is nil") } + // Set the logger for the sub-system + sub.logger = logger + // Check if all dependencies are satisfied for _, dep := range sub.depends { if _, ok := app.setupTracker[dep.name]; !ok { @@ -132,7 +154,7 @@ func (app *Lifecycle) setupSingle(sub *Subsystem) (bool, error) { return false, fmt.Errorf("dependency '%s' not satisfied for '%s'", dep.name, sub.name) } else { // Attempt to set up the dependency - if _, err := app.setupSingle(dep); err != nil { + if _, err := app.setupSingle(dep, logger); err != nil { return false, fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep.name, sub.name, err) } diff --git a/subsystem.go b/subsystem.go index da72851..7da7e01 100644 --- a/subsystem.go +++ b/subsystem.go @@ -1,8 +1,14 @@ package app +import ( + "fmt" + "log/slog" +) + // Subsystem represents a sub-system of the application, with its setup and // teardown functions and dependencies. type Subsystem struct { + logger *slog.Logger name string setup setupFn teardown teardownFn @@ -28,3 +34,11 @@ func NewSubsystem(name string, opts SubsystemOpts) *Subsystem { depends: opts.Depends, } } + +// Logger returns the logger for the subsystem. +func (s *Subsystem) Logger() *slog.Logger { + if s.logger == nil { + panic(fmt.Sprintf("subsytem %s used before logger was initialized", s.name)) + } + return s.logger +}