Files
logwisp/cmd/lw/bootstrap.go
T
lixen dfd8a68988
Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
v0.29.0 the configuration engine, and lw --schema
2026-10-09 18:34:45 -04:00

223 lines
7.1 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"io"
"os"
_ "github.com/lixenwraith/logwisp/internal/source/console"
_ "github.com/lixenwraith/logwisp/internal/source/file"
_ "github.com/lixenwraith/logwisp/internal/source/httpchain"
_ "github.com/lixenwraith/logwisp/internal/source/null"
_ "github.com/lixenwraith/logwisp/internal/source/random"
_ "github.com/lixenwraith/logwisp/internal/source/tcpchain"
_ "github.com/lixenwraith/logwisp/internal/sink/console"
_ "github.com/lixenwraith/logwisp/internal/sink/file"
_ "github.com/lixenwraith/logwisp/internal/sink/http"
_ "github.com/lixenwraith/logwisp/internal/sink/httpchain"
_ "github.com/lixenwraith/logwisp/internal/sink/null"
_ "github.com/lixenwraith/logwisp/internal/sink/tcp"
_ "github.com/lixenwraith/logwisp/internal/sink/tcpchain"
"github.com/lixenwraith/logwisp/internal/config"
"github.com/lixenwraith/logwisp/internal/service"
"github.com/lixenwraith/logwisp/internal/version"
"github.com/lixenwraith/log"
"github.com/lixenwraith/log/sanitizer"
"github.com/lixenwraith/toml"
)
// bootstrapInitial handles initial service startup with status reporter
func bootstrapInitial(ctx context.Context, cfg *config.Config) (*service.Service, context.CancelFunc, error) {
svc, err := bootstrapService(ctx, cfg)
if err != nil {
return nil, nil, fmt.Errorf("failed to bootstrap service: %w", err)
}
if err := svc.Start(); err != nil {
svc.Shutdown()
return nil, nil, fmt.Errorf("failed to start service pipelines: %w", err)
}
var statusCancel context.CancelFunc
if cfg.StatusReporter {
statusCancel = startStatusReporter(ctx, svc)
}
return svc, statusCancel, nil
}
// errServiceStopped is a reload that failed after the old service stopped:
// no pipeline runs until a later reload succeeds.
var errServiceStopped = errors.New("old service stopped, new one failed to start")
// handleReload orchestrates the entire hot-reload process including status reporter lifecycle
func handleReload(ctx context.Context, newCfg *config.Config, oldSvc *service.Service, statusCancel context.CancelFunc) (*service.Service, *config.Config, context.CancelFunc, error) {
logger.Info("msg", "Starting configuration hot reload")
if err := config.ValidateConfig(newCfg); err != nil {
logger.Error("msg", "Invalid reload configuration, keeping old service running", "error", err)
return nil, nil, nil, err
}
// Bootstrap a new service to ensure it's valid before touching the old one
logger.Debug("msg", "Bootstrapping new service with updated config")
newService, err := bootstrapService(ctx, newCfg)
if err != nil {
logger.Error("msg", "Failed to bootstrap new service, keeping old service running", "error", err)
return nil, nil, nil, err
}
// Gracefully shut down the old service
if statusCancel != nil {
statusCancel()
}
if oldSvc != nil {
logger.Info("msg", "Shutting down old service before activating new one")
oldSvc.Shutdown()
}
// Start the new service
if err := newService.Start(); err != nil {
newService.Shutdown()
logger.Error("msg", "Failed to start new service pipelines after reload. The application may be in a non-functional state.", "error", err)
return nil, nil, nil, fmt.Errorf("%w: %w", errServiceStopped, err)
}
// Manage status reporter lifecycle
var newStatusCancel context.CancelFunc
if newCfg.StatusReporter {
newStatusCancel = startStatusReporter(ctx, newService)
}
logger.Info("msg", "Configuration hot reload completed successfully")
return newService, newCfg, newStatusCancel, nil
}
// bootstrapService creates and initializes the main log transport service and its pipelines
func bootstrapService(ctx context.Context, cfg *config.Config) (*service.Service, error) {
// Create service with logger dependency injection
svc, err := service.NewService(ctx, cfg, logger)
if err != nil {
logger.Error("msg", "Failed to initialize service",
"component", "bootstrap",
)
return nil, err
}
logger.Info("msg", "LogWisp started",
"version", version.Short(),
)
return svc, nil
}
// checkConfig builds every pipeline and plugin as a start would, so TLS files,
// credentials and options are loaded and their warnings print, then exits
// before anything binds or reads: plugins start goroutines only in Start.
func checkConfig(cfg *config.Config) int {
cfg.Logging.Output, cfg.Logging.Level = "stderr", "warn"
if err := initializeLogger(cfg); err == nil && logger.Start() == nil {
defer shutdownLogger()
}
if _, err := service.NewService(context.Background(), cfg, logger); err != nil {
fmt.Fprintf(os.Stderr, "configuration invalid: %v\n", err)
return 1
}
fmt.Printf("configuration ok: %d pipeline(s)\n", len(cfg.Pipelines))
return 0
}
// dumpConfig prints the effective configuration, flags and environment
// resolved into it, as a file lw -c reads back.
func dumpConfig(cfg *config.Config, w io.Writer) int {
config.Uninherit(cfg.Pipelines, cfg.Color)
data, err := toml.Marshal(cfg)
if err == nil {
_, err = w.Write(data)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return 1
}
return 0
}
// initializeLogger sets up the global logger based on the application's configuration
func initializeLogger(cfg *config.Config) error {
logger = log.NewLogger()
logCfg := log.DefaultConfig()
if cfg.Quiet {
// In quiet mode, disable ALL logging output
logCfg.Level = 255 // A level that disables all output
logCfg.EnableFile = false
logCfg.EnableConsole = false
return logger.ApplyConfig(logCfg)
}
// Determine log level
levelValue, err := log.Level(cfg.Logging.Level)
if err != nil {
return fmt.Errorf("invalid log level: %w", err)
}
logCfg.Level = levelValue
// Configure log format
if cfg.Logging.Format != "" {
logCfg.Format = cfg.Logging.Format
}
if cfg.Logging.Sanitization != "" {
logCfg.Sanitization = sanitizer.PolicyPreset(cfg.Logging.Sanitization)
}
// Configure based on output mode
switch cfg.Logging.Output {
case "none":
logCfg.EnableFile = false
logCfg.EnableConsole = false
case "stdout":
logCfg.EnableFile = false
logCfg.EnableConsole = true
logCfg.ConsoleTarget = "stdout"
case "stderr":
logCfg.EnableFile = false
logCfg.EnableConsole = true
logCfg.ConsoleTarget = "stderr"
case "split":
logCfg.EnableFile = false
logCfg.EnableConsole = true
logCfg.ConsoleTarget = "split"
case "file":
logCfg.EnableFile = true
logCfg.EnableConsole = false
configureFileLogging(logCfg, cfg)
case "all":
logCfg.EnableFile = true
logCfg.EnableConsole = true
logCfg.ConsoleTarget = "split"
configureFileLogging(logCfg, cfg)
default:
return fmt.Errorf("invalid log output mode: %s", cfg.Logging.Output)
}
return logger.ApplyConfig(logCfg)
}
// configureFileLogging sets up file-based logging parameters from the configuration
func configureFileLogging(logCfg *log.Config, cfg *config.Config) {
if cfg.Logging.File != nil {
logCfg.Directory = cfg.Logging.File.Directory
logCfg.Name = cfg.Logging.File.Name
logCfg.MaxSizeKB = cfg.Logging.File.MaxSizeMB * 1000
logCfg.MaxTotalSizeKB = cfg.Logging.File.MaxTotalSizeMB * 1000
if cfg.Logging.File.RetentionHours > 0 {
logCfg.RetentionPeriodHrs = cfg.Logging.File.RetentionHours
}
}
}