diff --git a/cmd/api/wire_gen.go b/cmd/api/wire_gen.go index 3fa5198e..61775555 100644 --- a/cmd/api/wire_gen.go +++ b/cmd/api/wire_gen.go @@ -40,18 +40,18 @@ func initializeApp() (*application, func(), error) { paths := providers.ProvidePaths(config) logger := providers.ProvideLogger(paths) context := providers.ProvideContext(logger) - manager, err := providers.ProvideImageManager(paths, config) + manager, err := providers.ProvideImageManager(paths, config, logger) if err != nil { return nil, nil, err } - systemManager := providers.ProvideSystemManager(paths) - networkManager := providers.ProvideNetworkManager(paths, config) - devicesManager := providers.ProvideDeviceManager(paths) - volumesManager, err := providers.ProvideVolumeManager(paths, config) + systemManager := providers.ProvideSystemManager(paths, logger) + networkManager := providers.ProvideNetworkManager(paths, config, logger) + devicesManager := providers.ProvideDeviceManager(paths, logger) + volumesManager, err := providers.ProvideVolumeManager(paths, config, logger) if err != nil { return nil, nil, err } - instancesManager, err := providers.ProvideInstanceManager(paths, config, manager, systemManager, networkManager, devicesManager, volumesManager) + instancesManager, err := providers.ProvideInstanceManager(paths, config, manager, systemManager, networkManager, devicesManager, volumesManager, logger) if err != nil { return nil, nil, err } @@ -59,7 +59,7 @@ func initializeApp() (*application, func(), error) { if err != nil { return nil, nil, err } - ingressManager, err := providers.ProvideIngressManager(paths, config, instancesManager) + ingressManager, err := providers.ProvideIngressManager(paths, config, instancesManager, logger) if err != nil { return nil, nil, err } @@ -67,7 +67,7 @@ func initializeApp() (*application, func(), error) { if err != nil { return nil, nil, err } - resourcesManager, err := providers.ProvideResourceManager(context, config, paths, manager, instancesManager, volumesManager) + resourcesManager, err := providers.ProvideResourceManager(context, config, paths, manager, instancesManager, volumesManager, logger) if err != nil { return nil, nil, err } @@ -81,7 +81,7 @@ func initializeApp() (*application, func(), error) { if err != nil { return nil, nil, err } - registry, err := providers.ProvideRegistry(paths, manager) + registry, err := providers.ProvideRegistry(paths, manager, logger) if err != nil { return nil, nil, err } diff --git a/lib/providers/auto_standby_linux.go b/lib/providers/auto_standby_linux.go index d54d7aea..ee30fcd2 100644 --- a/lib/providers/auto_standby_linux.go +++ b/lib/providers/auto_standby_linux.go @@ -108,6 +108,9 @@ func ProvideAutoStandbyController(instanceManager instances.Manager, cfg *config return nil } + finish := trackInitialization(log, "auto-standby controller") + defer func() { finish(nil) }() + runtimeManager, ok := instanceManager.(autoStandbyRuntimeManager) if !ok { return nil diff --git a/lib/providers/auto_standby_unsupported.go b/lib/providers/auto_standby_unsupported.go index b0bffe6b..97cae7dc 100644 --- a/lib/providers/auto_standby_unsupported.go +++ b/lib/providers/auto_standby_unsupported.go @@ -11,6 +11,13 @@ import ( ) // ProvideAutoStandbyController is unavailable on non-Linux platforms. -func ProvideAutoStandbyController(instances.Manager, *config.Config, *slog.Logger) *autostandby.Controller { +func ProvideAutoStandbyController(_ instances.Manager, _ *config.Config, log *slog.Logger) *autostandby.Controller { + if log == nil { + return nil + } + + finish := trackInitialization(log, "auto-standby controller") + defer func() { finish(nil) }() + return nil } diff --git a/lib/providers/health_check.go b/lib/providers/health_check.go index 2aed224b..d945df2f 100644 --- a/lib/providers/health_check.go +++ b/lib/providers/health_check.go @@ -7,5 +7,8 @@ import ( ) func ProvideHealthCheckController(instanceManager instances.Manager, log *slog.Logger) *instances.HealthCheckController { + finish := trackInitialization(log, "health check controller") + defer func() { finish(nil) }() + return instances.NewHealthCheckController(instanceManager, log) } diff --git a/lib/providers/providers.go b/lib/providers/providers.go index 1b52e3e4..5a3c2b4a 100644 --- a/lib/providers/providers.go +++ b/lib/providers/providers.go @@ -56,6 +56,18 @@ func ProvideContext(log *slog.Logger) context.Context { return logger.AddToContext(context.Background(), log) } +func trackInitialization(log *slog.Logger, component string) func(error) { + startedAt := time.Now() + log.Info("application component initialization started", "component", component) + return func(err error) { + if err != nil { + log.Error("application component initialization failed", "component", component, "duration", time.Since(startedAt), "error", err) + return + } + log.Info("application component initialization completed", "component", component, "duration", time.Since(startedAt)) + } +} + // ProvideConfig provides the application configuration. // Panics if configuration is invalid (prevents startup with bad config). // Config path can be specified via CONFIG_PATH env var or defaults to platform-specific locations. @@ -77,29 +89,44 @@ func ProvidePaths(cfg *config.Config) *paths.Paths { } // ProvideImageManager provides the image manager -func ProvideImageManager(p *paths.Paths, cfg *config.Config) (images.Manager, error) { +func ProvideImageManager(p *paths.Paths, cfg *config.Config, log *slog.Logger) (_ images.Manager, err error) { + finish := trackInitialization(log, "image manager") + defer func() { finish(err) }() + meter := otel.GetMeterProvider().Meter("hypeman") return images.NewManager(p, cfg.Limits.MaxConcurrentBuilds, meter) } // ProvideSystemManager provides the system manager -func ProvideSystemManager(p *paths.Paths) system.Manager { +func ProvideSystemManager(p *paths.Paths, log *slog.Logger) system.Manager { + finish := trackInitialization(log, "system manager") + defer func() { finish(nil) }() + return system.NewManager(p) } // ProvideNetworkManager provides the network manager -func ProvideNetworkManager(p *paths.Paths, cfg *config.Config) network.Manager { +func ProvideNetworkManager(p *paths.Paths, cfg *config.Config, log *slog.Logger) network.Manager { + finish := trackInitialization(log, "network manager") + defer func() { finish(nil) }() + meter := otel.GetMeterProvider().Meter("hypeman") return network.NewManager(p, cfg, meter) } // ProvideDeviceManager provides the device manager -func ProvideDeviceManager(p *paths.Paths) devices.Manager { +func ProvideDeviceManager(p *paths.Paths, log *slog.Logger) devices.Manager { + finish := trackInitialization(log, "device manager") + defer func() { finish(nil) }() + return devices.NewManager(p) } // ProvideInstanceManager provides the instance manager -func ProvideInstanceManager(p *paths.Paths, cfg *config.Config, imageManager images.Manager, systemManager system.Manager, networkManager network.Manager, deviceManager devices.Manager, volumeManager volumes.Manager) (instances.Manager, error) { +func ProvideInstanceManager(p *paths.Paths, cfg *config.Config, imageManager images.Manager, systemManager system.Manager, networkManager network.Manager, deviceManager devices.Manager, volumeManager volumes.Manager, log *slog.Logger) (_ instances.Manager, err error) { + finish := trackInitialization(log, "instance manager") + defer func() { finish(err) }() + firecracker.SetCustomBinaryPath(cfg.Hypervisor.FirecrackerBinaryPath) if err := cloudhypervisor.SetDefaultVersion(cfg.Hypervisor.CloudHypervisorDefaultVersion); err != nil { return nil, fmt.Errorf("invalid cloud-hypervisor default version: %w", err) @@ -172,7 +199,10 @@ func snapshotDefaultsFromConfig(cfg *config.Config) instances.SnapshotPolicy { } // ProvideGuestMemoryController provides the active ballooning controller. -func ProvideGuestMemoryController(instanceManager instances.Manager, cfg *config.Config, log *slog.Logger) (guestmemory.Controller, error) { +func ProvideGuestMemoryController(instanceManager instances.Manager, cfg *config.Config, log *slog.Logger) (_ guestmemory.Controller, err error) { + finish := trackInitialization(log, "guest memory controller") + defer func() { finish(err) }() + pollInterval, err := parseRequiredDuration(cfg.Hypervisor.Memory.ActiveBallooning.PollInterval) if err != nil { return nil, fmt.Errorf("parse active ballooning poll interval: %w", err) @@ -217,7 +247,10 @@ func ProvideGuestMemoryController(instanceManager instances.Manager, cfg *config } // ProvideVolumeManager provides the volume manager -func ProvideVolumeManager(p *paths.Paths, cfg *config.Config) (volumes.Manager, error) { +func ProvideVolumeManager(p *paths.Paths, cfg *config.Config, log *slog.Logger) (_ volumes.Manager, err error) { + finish := trackInitialization(log, "volume manager") + defer func() { finish(err) }() + // Parse max total volume storage (empty or "0" means unlimited) var maxTotalVolumeStorage int64 if cfg.Limits.MaxTotalVolumeStorage != "" && cfg.Limits.MaxTotalVolumeStorage != "0" { @@ -241,12 +274,18 @@ func parseRequiredDuration(value string) (time.Duration, error) { } // ProvideRegistry provides the OCI registry for image push -func ProvideRegistry(p *paths.Paths, imageManager images.Manager) (*registry.Registry, error) { +func ProvideRegistry(p *paths.Paths, imageManager images.Manager, log *slog.Logger) (_ *registry.Registry, err error) { + finish := trackInitialization(log, "OCI registry") + defer func() { finish(err) }() + return registry.New(p, imageManager) } // ProvideResourceManager provides the resource manager for capacity tracking -func ProvideResourceManager(ctx context.Context, cfg *config.Config, p *paths.Paths, imageManager images.Manager, instanceManager instances.Manager, volumeManager volumes.Manager) (*resources.Manager, error) { +func ProvideResourceManager(ctx context.Context, cfg *config.Config, p *paths.Paths, imageManager images.Manager, instanceManager instances.Manager, volumeManager volumes.Manager, log *slog.Logger) (_ *resources.Manager, err error) { + finish := trackInitialization(log, "resource manager") + defer func() { finish(err) }() + mgr := resources.NewManager(cfg, p) // Managers implement the lister interfaces directly @@ -263,7 +302,10 @@ func ProvideResourceManager(ctx context.Context, cfg *config.Config, p *paths.Pa } // ProvideVMMetricsManager provides the VM metrics manager for utilization tracking -func ProvideVMMetricsManager(instanceManager instances.Manager, cfg *config.Config, log *slog.Logger) (*vm_metrics.Manager, error) { +func ProvideVMMetricsManager(instanceManager instances.Manager, cfg *config.Config, log *slog.Logger) (_ *vm_metrics.Manager, err error) { + finish := trackInitialization(log, "VM metrics manager") + defer func() { finish(err) }() + mgr := vm_metrics.NewManager() mgr.SetVMLabelBudget(cfg.Metrics.VMLabelBudget) mgr.SetLogger(log) @@ -316,7 +358,10 @@ func parseByteSize(value string) (int64, error) { } // ProvideIngressManager provides the ingress manager -func ProvideIngressManager(p *paths.Paths, cfg *config.Config, instanceManager instances.Manager) (ingress.Manager, error) { +func ProvideIngressManager(p *paths.Paths, cfg *config.Config, instanceManager instances.Manager, log *slog.Logger) (_ ingress.Manager, err error) { + finish := trackInitialization(log, "ingress manager") + defer func() { finish(err) }() + // Parse DNS provider - fail if invalid dnsProvider, err := ingress.ParseDNSProvider(cfg.ACME.DNSProvider) if err != nil { @@ -380,7 +425,10 @@ func ProvideIngressManager(p *paths.Paths, cfg *config.Config, instanceManager i } // ProvideBuilderManager provides the builders manager -func ProvideBuilderManager(p *paths.Paths, cfg *config.Config, instanceManager instances.Manager, volumeManager volumes.Manager, log *slog.Logger) (builders.Manager, error) { +func ProvideBuilderManager(p *paths.Paths, cfg *config.Config, instanceManager instances.Manager, volumeManager volumes.Manager, log *slog.Logger) (_ builders.Manager, err error) { + finish := trackInitialization(log, "builder manager") + defer func() { finish(err) }() + meter := otel.GetMeterProvider().Meter("hypeman") return builders.NewManager(p, builders.Config{ MaxCount: cfg.Builders.MaxCount, @@ -391,7 +439,10 @@ func ProvideBuilderManager(p *paths.Paths, cfg *config.Config, instanceManager i } // ProvideBuildManager provides the build manager -func ProvideBuildManager(p *paths.Paths, cfg *config.Config, instanceManager instances.Manager, volumeManager volumes.Manager, builderManager builders.Manager, imageManager images.Manager, networkManager network.Manager, log *slog.Logger) (builds.Manager, error) { +func ProvideBuildManager(p *paths.Paths, cfg *config.Config, instanceManager instances.Manager, volumeManager volumes.Manager, builderManager builders.Manager, imageManager images.Manager, networkManager network.Manager, log *slog.Logger) (_ builds.Manager, err error) { + finish := trackInitialization(log, "build manager") + defer func() { finish(err) }() + // Read CA cert file if specified var registryCACert string if cfg.Registry.CACertFile != "" { diff --git a/lib/providers/providers_test.go b/lib/providers/providers_test.go index 5ea519ba..9a03189e 100644 --- a/lib/providers/providers_test.go +++ b/lib/providers/providers_test.go @@ -1,6 +1,9 @@ package providers import ( + "bytes" + "errors" + "log/slog" "testing" "github.com/kernel/hypeman/cmd/api/config" @@ -9,6 +12,34 @@ import ( "github.com/stretchr/testify/require" ) +func TestTrackInitialization(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + log := slog.New(slog.NewJSONHandler(&output, nil)) + finish := trackInitialization(log, "test component") + finish(nil) + + logs := output.String() + assert.Contains(t, logs, `"msg":"application component initialization started"`) + assert.Contains(t, logs, `"msg":"application component initialization completed"`) + assert.Contains(t, logs, `"component":"test component"`) + assert.Contains(t, logs, `"duration":`) +} + +func TestTrackInitializationFailure(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + log := slog.New(slog.NewJSONHandler(&output, nil)) + finish := trackInitialization(log, "test component") + finish(errors.New("failed")) + + logs := output.String() + assert.Contains(t, logs, `"msg":"application component initialization failed"`) + assert.Contains(t, logs, `"error":"failed"`) +} + func TestSnapshotDefaultsFromConfigDisabledReturnsNilCompression(t *testing.T) { t.Parallel()