v0.3.1 background mode fixed, bug fixes and refactoring
This commit is contained in:
4
go.mod
4
go.mod
@ -3,8 +3,8 @@ module logwisp
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6
|
||||
github.com/lixenwraith/log v0.0.0-20250710012114-049926224b0e
|
||||
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497
|
||||
github.com/lixenwraith/log v0.0.0-20250710220042-139c522da9aa
|
||||
github.com/panjf2000/gnet/v2 v2.9.1
|
||||
github.com/valyala/fasthttp v1.63.0
|
||||
)
|
||||
|
||||
8
go.sum
8
go.sum
@ -6,10 +6,10 @@ 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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6 h1:qE4SpAJWFaLkdRyE0FjTPBBRYE7LOvcmRCB5p86W73Q=
|
||||
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6/go.mod h1:4wPJ3HnLrYrtUwTinngCsBgtdIXsnxkLa7q4KAIbwY8=
|
||||
github.com/lixenwraith/log v0.0.0-20250710012114-049926224b0e h1:WjYl/OIKxDCFA1In2W0bJbCGJ/Ub9X9DL+avZRNjXIQ=
|
||||
github.com/lixenwraith/log v0.0.0-20250710012114-049926224b0e/go.mod h1:KFE7B7m2pu5kAl0olDCvywlOqFJhanogAhTlVvlp8JE=
|
||||
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497 h1:ixTIdJSd945n/IhMRwGwQVmQnQ1nUr5z1wn31jXq9FU=
|
||||
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497/go.mod h1:y7kgDrWIFROWJJ6ASM/SPTRRAj27FjRGWh2SDLcdQ68=
|
||||
github.com/lixenwraith/log v0.0.0-20250710220042-139c522da9aa h1:lIo780MTgZXH5jF+Qr24fRDFQTQ3eu3OaaFYLP9ZkR0=
|
||||
github.com/lixenwraith/log v0.0.0-20250710220042-139c522da9aa/go.mod h1:lEjTIMWGW+XGn20x5Ec0qkNK4LCd7Y/04PII51crYKk=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
||||
|
||||
@ -4,9 +4,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"logwisp/src/internal/config"
|
||||
@ -17,13 +14,13 @@ import (
|
||||
)
|
||||
|
||||
// bootstrapService creates and initializes the log transport service
|
||||
func bootstrapService(ctx context.Context, cfg *config.Config, flagCfg *FlagConfig) (*service.Service, *service.HTTPRouter, error) {
|
||||
func bootstrapService(ctx context.Context, cfg *config.Config) (*service.Service, *service.HTTPRouter, error) {
|
||||
// Create service with logger dependency injection
|
||||
svc := service.New(ctx, logger)
|
||||
|
||||
// Create HTTP router if requested
|
||||
var router *service.HTTPRouter
|
||||
if flagCfg.UseRouter {
|
||||
if cfg.UseRouter {
|
||||
router = service.NewHTTPRouter(svc, logger)
|
||||
logger.Info("msg", "HTTP router mode enabled")
|
||||
}
|
||||
@ -42,7 +39,7 @@ func bootstrapService(ctx context.Context, cfg *config.Config, flagCfg *FlagConf
|
||||
}
|
||||
|
||||
// If using router mode, register HTTP sinks
|
||||
if flagCfg.UseRouter {
|
||||
if cfg.UseRouter {
|
||||
pipeline, err := svc.GetPipeline(pipelineCfg.Name)
|
||||
if err == nil && len(pipeline.HTTPSinks) > 0 {
|
||||
if err := router.RegisterPipeline(pipeline); err != nil {
|
||||
@ -54,7 +51,7 @@ func bootstrapService(ctx context.Context, cfg *config.Config, flagCfg *FlagConf
|
||||
}
|
||||
|
||||
successCount++
|
||||
displayPipelineEndpoints(pipelineCfg, flagCfg.UseRouter)
|
||||
displayPipelineEndpoints(pipelineCfg, cfg.UseRouter)
|
||||
}
|
||||
|
||||
if successCount == 0 {
|
||||
@ -68,42 +65,31 @@ func bootstrapService(ctx context.Context, cfg *config.Config, flagCfg *FlagConf
|
||||
return svc, router, nil
|
||||
}
|
||||
|
||||
// initializeLogger sets up the logger based on configuration and CLI flags
|
||||
func initializeLogger(cfg *config.Config, flagCfg *FlagConfig) error {
|
||||
// initializeLogger sets up the logger based on configuration
|
||||
func initializeLogger(cfg *config.Config) error {
|
||||
logger = log.NewLogger()
|
||||
|
||||
var configArgs []string
|
||||
|
||||
// Quiet mode suppresses ALL LogWisp logging (not sink outputs)
|
||||
if flagCfg.Quiet {
|
||||
if cfg.Quiet {
|
||||
// In quiet mode, disable ALL logging output
|
||||
configArgs = append(configArgs,
|
||||
"disable_file=true",
|
||||
"enable_stdout=false",
|
||||
"level=255") // Set to max level to suppress everything
|
||||
"level=255")
|
||||
|
||||
return logger.InitWithDefaults(configArgs...)
|
||||
}
|
||||
|
||||
// Determine output mode from CLI or config
|
||||
outputMode := cfg.Logging.Output
|
||||
if flagCfg.LogOutput != "" {
|
||||
outputMode = flagCfg.LogOutput
|
||||
}
|
||||
|
||||
// Determine log level
|
||||
level := cfg.Logging.Level
|
||||
if flagCfg.LogLevel != "" {
|
||||
level = flagCfg.LogLevel
|
||||
}
|
||||
levelValue, err := parseLogLevel(level)
|
||||
levelValue, err := parseLogLevel(cfg.Logging.Level)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid log level: %w", err)
|
||||
}
|
||||
configArgs = append(configArgs, fmt.Sprintf("level=%d", levelValue))
|
||||
|
||||
// Configure based on output mode
|
||||
switch outputMode {
|
||||
switch cfg.Logging.Output {
|
||||
case "none":
|
||||
configArgs = append(configArgs, "disable_file=true", "enable_stdout=false")
|
||||
|
||||
@ -121,15 +107,15 @@ func initializeLogger(cfg *config.Config, flagCfg *FlagConfig) error {
|
||||
|
||||
case "file":
|
||||
configArgs = append(configArgs, "enable_stdout=false")
|
||||
configureFileLogging(&configArgs, cfg, flagCfg)
|
||||
configureFileLogging(&configArgs, cfg)
|
||||
|
||||
case "both":
|
||||
configArgs = append(configArgs, "enable_stdout=true")
|
||||
configureFileLogging(&configArgs, cfg, flagCfg)
|
||||
configureConsoleTarget(&configArgs, cfg, flagCfg)
|
||||
configureFileLogging(&configArgs, cfg)
|
||||
configureConsoleTarget(&configArgs, cfg)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("invalid log output mode: %s", outputMode)
|
||||
return fmt.Errorf("invalid log output mode: %s", cfg.Logging.Output)
|
||||
}
|
||||
|
||||
// Apply format if specified
|
||||
@ -141,20 +127,8 @@ func initializeLogger(cfg *config.Config, flagCfg *FlagConfig) error {
|
||||
}
|
||||
|
||||
// configureFileLogging sets up file-based logging parameters
|
||||
func configureFileLogging(configArgs *[]string, cfg *config.Config, flagCfg *FlagConfig) {
|
||||
// CLI overrides
|
||||
if flagCfg.LogFile != "" {
|
||||
dir := filepath.Dir(flagCfg.LogFile)
|
||||
name := strings.TrimSuffix(filepath.Base(flagCfg.LogFile), filepath.Ext(flagCfg.LogFile))
|
||||
*configArgs = append(*configArgs,
|
||||
fmt.Sprintf("directory=%s", dir),
|
||||
fmt.Sprintf("name=%s", name))
|
||||
} else if flagCfg.LogDir != "" {
|
||||
*configArgs = append(*configArgs,
|
||||
fmt.Sprintf("directory=%s", flagCfg.LogDir),
|
||||
fmt.Sprintf("name=%s", cfg.Logging.File.Name))
|
||||
} else if cfg.Logging.File != nil {
|
||||
// Use config file settings
|
||||
func configureFileLogging(configArgs *[]string, cfg *config.Config) {
|
||||
if cfg.Logging.File != nil {
|
||||
*configArgs = append(*configArgs,
|
||||
fmt.Sprintf("directory=%s", cfg.Logging.File.Directory),
|
||||
fmt.Sprintf("name=%s", cfg.Logging.File.Name),
|
||||
@ -169,12 +143,10 @@ func configureFileLogging(configArgs *[]string, cfg *config.Config, flagCfg *Fla
|
||||
}
|
||||
|
||||
// configureConsoleTarget sets up console output parameters
|
||||
func configureConsoleTarget(configArgs *[]string, cfg *config.Config, flagCfg *FlagConfig) {
|
||||
func configureConsoleTarget(configArgs *[]string, cfg *config.Config) {
|
||||
target := "stderr" // default
|
||||
|
||||
if flagCfg.LogConsole != "" {
|
||||
target = flagCfg.LogConsole
|
||||
} else if cfg.Logging.Console != nil && cfg.Logging.Console.Target != "" {
|
||||
if cfg.Logging.Console != nil && cfg.Logging.Console.Target != "" {
|
||||
target = cfg.Logging.Console.Target
|
||||
}
|
||||
|
||||
@ -182,39 +154,11 @@ func configureConsoleTarget(configArgs *[]string, cfg *config.Config, flagCfg *F
|
||||
if target == "split" {
|
||||
*configArgs = append(*configArgs, "stdout_split_mode=true")
|
||||
*configArgs = append(*configArgs, "stdout_target=split")
|
||||
logger.Debug("msg", "Console output configured for split mode",
|
||||
"component", "bootstrap",
|
||||
"info_debug", "stdout",
|
||||
"warn_error", "stderr")
|
||||
} else {
|
||||
*configArgs = append(*configArgs, fmt.Sprintf("stdout_target=%s", target))
|
||||
}
|
||||
}
|
||||
|
||||
// isBackgroundProcess checks if we're already running in background
|
||||
func isBackgroundProcess() bool {
|
||||
return os.Getenv("LOGWISP_BACKGROUND") == "1"
|
||||
}
|
||||
|
||||
// runInBackground starts the process in background
|
||||
func runInBackground() error {
|
||||
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||
cmd.Env = append(os.Environ(), "LOGWISP_BACKGROUND=1")
|
||||
cmd.Stdin = nil
|
||||
// Respect quiet mode for background process output
|
||||
if !output.IsQuiet() {
|
||||
cmd.Stdout = os.Stdout // Keep stdout for logging
|
||||
cmd.Stderr = os.Stderr // Keep stderr for logging
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Print("Started LogWisp in background (PID: %d)\n", cmd.Process.Pid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseLogLevel(level string) (int, error) {
|
||||
switch strings.ToLower(level) {
|
||||
case "debug":
|
||||
|
||||
@ -1,140 +0,0 @@
|
||||
// FILE: src/cmd/logwisp/flags.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Command-line flags
|
||||
var (
|
||||
// General flags
|
||||
configFile = flag.String("config", "", "Config file path")
|
||||
useRouter = flag.Bool("router", false, "Use HTTP router for path-based routing")
|
||||
showVersion = flag.Bool("version", false, "Show version information")
|
||||
background = flag.Bool("background", false, "Run as background process")
|
||||
|
||||
// Logging flags
|
||||
logOutput = flag.String("log-output", "", "Log output: file, stdout, stderr, both, none (overrides config)")
|
||||
logLevel = flag.String("log-level", "", "Log level: debug, info, warn, error (overrides config)")
|
||||
logFile = flag.String("log-file", "", "Log file path (when using file output)")
|
||||
logDir = flag.String("log-dir", "", "Log directory (when using file output)")
|
||||
logConsole = flag.String("log-console", "", "Console target: stdout, stderr, split (overrides config)")
|
||||
|
||||
// Quiet mode flag
|
||||
quiet = flag.Bool("quiet", false, "Suppress all LogWisp logging output (sink outputs remain unaffected)")
|
||||
)
|
||||
|
||||
// FlagConfig holds parsed command-line flags
|
||||
type FlagConfig struct {
|
||||
ConfigFile string
|
||||
UseRouter bool
|
||||
ShowVersion bool
|
||||
Background bool
|
||||
LogOutput string
|
||||
LogLevel string
|
||||
LogFile string
|
||||
LogDir string
|
||||
LogConsole string
|
||||
Quiet bool
|
||||
}
|
||||
|
||||
// ParseFlags parses command-line arguments and returns configuration
|
||||
func ParseFlags() (*FlagConfig, error) {
|
||||
// Set custom usage before parsing
|
||||
flag.Usage = customUsage
|
||||
flag.Parse()
|
||||
|
||||
fc := &FlagConfig{
|
||||
ConfigFile: *configFile,
|
||||
UseRouter: *useRouter,
|
||||
ShowVersion: *showVersion,
|
||||
Background: *background,
|
||||
LogOutput: *logOutput,
|
||||
LogLevel: *logLevel,
|
||||
LogFile: *logFile,
|
||||
LogDir: *logDir,
|
||||
LogConsole: *logConsole,
|
||||
Quiet: *quiet,
|
||||
}
|
||||
|
||||
// Validate log-output flag if provided
|
||||
if fc.LogOutput != "" {
|
||||
validOutputs := map[string]bool{
|
||||
"file": true, "stdout": true, "stderr": true,
|
||||
"both": true, "none": true,
|
||||
}
|
||||
if !validOutputs[fc.LogOutput] {
|
||||
return nil, fmt.Errorf("invalid log-output: %s (valid: file, stdout, stderr, both, none)", fc.LogOutput)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate log-level flag if provided
|
||||
if fc.LogLevel != "" {
|
||||
validLevels := map[string]bool{
|
||||
"debug": true, "info": true, "warn": true, "error": true,
|
||||
}
|
||||
if !validLevels[strings.ToLower(fc.LogLevel)] {
|
||||
return nil, fmt.Errorf("invalid log-level: %s (valid: debug, info, warn, error)", fc.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate log-console flag if provided
|
||||
if fc.LogConsole != "" {
|
||||
validTargets := map[string]bool{
|
||||
"stdout": true, "stderr": true, "split": true,
|
||||
}
|
||||
if !validTargets[fc.LogConsole] {
|
||||
return nil, fmt.Errorf("invalid log-console: %s (valid: stdout, stderr, split)", fc.LogConsole)
|
||||
}
|
||||
}
|
||||
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func customUsage() {
|
||||
fmt.Fprintf(os.Stderr, "LogWisp - Multi-Pipeline Log Processing Service\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "Options:\n")
|
||||
|
||||
// General options
|
||||
fmt.Fprintf(os.Stderr, "\nGeneral:\n")
|
||||
fmt.Fprintf(os.Stderr, " -config string\n\tConfig file path\n")
|
||||
fmt.Fprintf(os.Stderr, " -router\n\tUse HTTP router for path-based routing\n")
|
||||
fmt.Fprintf(os.Stderr, " -version\n\tShow version information\n")
|
||||
fmt.Fprintf(os.Stderr, " -background\n\tRun as background process\n")
|
||||
|
||||
// Logging options
|
||||
fmt.Fprintf(os.Stderr, "\nLogging:\n")
|
||||
fmt.Fprintf(os.Stderr, " -log-output string\n\tLog output: file, stdout, stderr, both, none (overrides config)\n")
|
||||
fmt.Fprintf(os.Stderr, " -log-level string\n\tLog level: debug, info, warn, error (overrides config)\n")
|
||||
fmt.Fprintf(os.Stderr, " -log-file string\n\tLog file path (when using file output)\n")
|
||||
fmt.Fprintf(os.Stderr, " -log-dir string\n\tLog directory (when using file output)\n")
|
||||
fmt.Fprintf(os.Stderr, " -log-console string\n\tConsole target: stdout, stderr, split (overrides config)\n")
|
||||
fmt.Fprintf(os.Stderr, " -quiet\n\tSuppress all LogWisp logging output (sink outputs remain unaffected)\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\nExamples:\n")
|
||||
fmt.Fprintf(os.Stderr, " # Run with default config (logs to stderr)\n")
|
||||
fmt.Fprintf(os.Stderr, " %s\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, " # Run with file logging\n")
|
||||
fmt.Fprintf(os.Stderr, " %s --log-output file --log-dir /var/log/logwisp\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, " # Run with debug logging to both file and console\n")
|
||||
fmt.Fprintf(os.Stderr, " %s --log-output both --log-level debug\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, " # Run with custom config and override log level\n")
|
||||
fmt.Fprintf(os.Stderr, " %s --config /etc/logwisp.toml --log-level warn\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, " # Run in router mode with multiple pipelines\n")
|
||||
fmt.Fprintf(os.Stderr, " %s --router --config /etc/logwisp/multi-pipeline.toml\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Environment Variables:\n")
|
||||
fmt.Fprintf(os.Stderr, " LOGWISP_CONFIG_FILE Config file path\n")
|
||||
fmt.Fprintf(os.Stderr, " LOGWISP_CONFIG_DIR Config directory\n")
|
||||
fmt.Fprintf(os.Stderr, " LOGWISP_DISABLE_STATUS_REPORTER Disable periodic status reports (set to 1)\n")
|
||||
fmt.Fprintf(os.Stderr, " LOGWISP_BACKGROUND Internal use - background process marker\n")
|
||||
fmt.Fprintf(os.Stderr, "\nFor complete documentation, see: https://github.com/logwisp/logwisp/tree/main/doc\n")
|
||||
}
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
@ -19,62 +20,57 @@ import (
|
||||
var logger *log.Logger
|
||||
|
||||
func main() {
|
||||
// Parse flags first to get quiet mode early
|
||||
flagCfg, err := ParseFlags()
|
||||
// Emulates nohup
|
||||
signal.Ignore(syscall.SIGHUP)
|
||||
|
||||
// Load configuration with automatic CLI parsing
|
||||
cfg, err := config.Load(os.Args[1:])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
if strings.Contains(err.Error(), "not found") && cfg != nil && cfg.ConfigFile != "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: Config file not found: %s\n", cfg.ConfigFile)
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Error: Failed to load config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Initialize output handler with quiet mode
|
||||
InitOutputHandler(flagCfg.Quiet)
|
||||
// Initialize output handler
|
||||
InitOutputHandler(cfg.Quiet)
|
||||
|
||||
// Handle version flag
|
||||
if flagCfg.ShowVersion {
|
||||
// Handle version
|
||||
if cfg.ShowVersion {
|
||||
fmt.Println(version.String())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Handle background mode
|
||||
if flagCfg.Background && !isBackgroundProcess() {
|
||||
if err := runInBackground(); err != nil {
|
||||
// Background mode spawns a child with internal --background-daemon flag.
|
||||
if cfg.Background && !cfg.BackgroundDaemon {
|
||||
// Prepare arguments for the child process, including originals and daemon flag.
|
||||
args := append(os.Args[1:], "--background-daemon")
|
||||
|
||||
cmd := exec.Command(os.Args[0], args...)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
FatalError(1, "Failed to start background process: %v\n", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
Print("Started LogWisp in background (PID: %d)\n", cmd.Process.Pid)
|
||||
os.Exit(0) // The parent process exits successfully.
|
||||
}
|
||||
|
||||
// Set config file environment if specified
|
||||
if flagCfg.ConfigFile != "" {
|
||||
os.Setenv("LOGWISP_CONFIG_FILE", flagCfg.ConfigFile)
|
||||
}
|
||||
|
||||
// Load configuration with CLI overrides
|
||||
cfg, err := config.LoadWithCLI(os.Args[1:], flagCfg)
|
||||
if err != nil {
|
||||
if flagCfg.ConfigFile != "" && strings.Contains(err.Error(), "not found") {
|
||||
FatalError(2, "Config file not found: %s\n", flagCfg.ConfigFile)
|
||||
}
|
||||
FatalError(1, "Failed to load config: %v\n", err)
|
||||
|
||||
}
|
||||
|
||||
// DEBUG: Extra nil check
|
||||
if cfg == nil {
|
||||
FatalError(1, "Configuration is nil after loading\n")
|
||||
}
|
||||
|
||||
// Initialize logger with quiet mode awareness
|
||||
if err := initializeLogger(cfg, flagCfg); err != nil {
|
||||
// Initialize logger
|
||||
if err := initializeLogger(cfg); err != nil {
|
||||
FatalError(1, "Failed to initialize logger: %v\n", err)
|
||||
}
|
||||
defer shutdownLogger()
|
||||
|
||||
// Log startup information (respects quiet mode via logger config)
|
||||
// Log startup information
|
||||
logger.Info("msg", "LogWisp starting",
|
||||
"version", version.String(),
|
||||
"config_file", flagCfg.ConfigFile,
|
||||
"config_file", cfg.ConfigFile,
|
||||
"log_output", cfg.Logging.Output,
|
||||
"router_mode", flagCfg.UseRouter)
|
||||
"router_mode", cfg.UseRouter,
|
||||
"background_mode", cfg.Background)
|
||||
|
||||
// Create context for shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@ -85,14 +81,14 @@ func main() {
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
|
||||
|
||||
// Bootstrap the service
|
||||
svc, router, err := bootstrapService(ctx, cfg, flagCfg)
|
||||
svc, router, err := bootstrapService(ctx, cfg)
|
||||
if err != nil {
|
||||
logger.Error("msg", "Failed to bootstrap service", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Start status reporter if enabled
|
||||
if enableStatusReporter() {
|
||||
if !cfg.DisableStatusReporter {
|
||||
go statusReporter(svc)
|
||||
}
|
||||
|
||||
@ -139,11 +135,3 @@ func shutdownLogger() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enableStatusReporter() bool {
|
||||
// Status reporter can be disabled via environment variable
|
||||
if os.Getenv("LOGWISP_DISABLE_STATUS_REPORTER") == "1" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -29,7 +29,7 @@ func InitOutputHandler(quiet bool) {
|
||||
}
|
||||
|
||||
// Print writes to stdout if not in quiet mode
|
||||
func (o *OutputHandler) Print(format string, args ...interface{}) {
|
||||
func (o *OutputHandler) Print(format string, args ...any) {
|
||||
o.mu.RLock()
|
||||
defer o.mu.RUnlock()
|
||||
|
||||
@ -39,7 +39,7 @@ func (o *OutputHandler) Print(format string, args ...interface{}) {
|
||||
}
|
||||
|
||||
// Error writes to stderr if not in quiet mode
|
||||
func (o *OutputHandler) Error(format string, args ...interface{}) {
|
||||
func (o *OutputHandler) Error(format string, args ...any) {
|
||||
o.mu.RLock()
|
||||
defer o.mu.RUnlock()
|
||||
|
||||
@ -49,7 +49,7 @@ func (o *OutputHandler) Error(format string, args ...interface{}) {
|
||||
}
|
||||
|
||||
// FatalError writes to stderr and exits (respects quiet mode)
|
||||
func (o *OutputHandler) FatalError(code int, format string, args ...interface{}) {
|
||||
func (o *OutputHandler) FatalError(code int, format string, args ...any) {
|
||||
o.Error(format, args...)
|
||||
os.Exit(code)
|
||||
}
|
||||
@ -69,19 +69,19 @@ func (o *OutputHandler) SetQuiet(quiet bool) {
|
||||
}
|
||||
|
||||
// Helper functions for global output handler
|
||||
func Print(format string, args ...interface{}) {
|
||||
func Print(format string, args ...any) {
|
||||
if output != nil {
|
||||
output.Print(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Error(format string, args ...interface{}) {
|
||||
func Error(format string, args ...any) {
|
||||
if output != nil {
|
||||
output.Error(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func FatalError(code int, format string, args ...interface{}) {
|
||||
func FatalError(code int, format string, args ...any) {
|
||||
if output != nil {
|
||||
output.FatalError(code, format, args...)
|
||||
} else {
|
||||
|
||||
@ -2,10 +2,23 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
// Logging configuration
|
||||
Logging *LogConfig `toml:"logging"`
|
||||
// Top-level flags for application control
|
||||
UseRouter bool `toml:"router"`
|
||||
Background bool `toml:"background"`
|
||||
ShowVersion bool `toml:"version"`
|
||||
Quiet bool `toml:"quiet"`
|
||||
|
||||
// Pipeline configurations
|
||||
// Runtime behavior flags
|
||||
DisableStatusReporter bool `toml:"disable_status_reporter"`
|
||||
|
||||
// Internal flag indicating demonized child process
|
||||
BackgroundDaemon bool `toml:"background-daemon"`
|
||||
|
||||
// Configuration file path
|
||||
ConfigFile string `toml:"config"`
|
||||
|
||||
// Existing fields
|
||||
Logging *LogConfig `toml:"logging"`
|
||||
Pipelines []PipelineConfig `toml:"pipelines"`
|
||||
}
|
||||
|
||||
|
||||
@ -12,11 +12,24 @@ import (
|
||||
|
||||
// LoadContext holds all configuration sources
|
||||
type LoadContext struct {
|
||||
FlagConfig interface{} // Parsed command-line flags from main
|
||||
FlagConfig any // Parsed command-line flags from main
|
||||
}
|
||||
|
||||
func defaults() *Config {
|
||||
return &Config{
|
||||
// Top-level flag defaults
|
||||
UseRouter: false,
|
||||
Background: false,
|
||||
ShowVersion: false,
|
||||
Quiet: false,
|
||||
|
||||
// Runtime behavior defaults
|
||||
DisableStatusReporter: false,
|
||||
|
||||
// Child process indicator
|
||||
BackgroundDaemon: false,
|
||||
|
||||
// Existing defaults
|
||||
Logging: DefaultLogConfig(),
|
||||
Pipelines: []PipelineConfig{
|
||||
{
|
||||
@ -37,7 +50,7 @@ func defaults() *Config {
|
||||
Options: map[string]any{
|
||||
"port": 8080,
|
||||
"buffer_size": 1000,
|
||||
"stream_path": "/transport",
|
||||
"stream_path": "/stream",
|
||||
"status_path": "/status",
|
||||
"heartbeat": map[string]any{
|
||||
"enabled": true,
|
||||
@ -54,16 +67,15 @@ func defaults() *Config {
|
||||
}
|
||||
}
|
||||
|
||||
// LoadWithCLI loads config with CLI flag overrides
|
||||
func LoadWithCLI(cliArgs []string, flagCfg interface{}) (*Config, error) {
|
||||
configPath := GetConfigPath()
|
||||
|
||||
// Load is the single entry point for loading all configuration
|
||||
func Load(args []string) (*Config, error) {
|
||||
configPath, isExplicit := resolveConfigPath(args)
|
||||
// Build configuration with all sources
|
||||
cfg, err := lconfig.NewBuilder().
|
||||
WithDefaults(defaults()).
|
||||
WithEnvPrefix("LOGWISP_").
|
||||
WithFile(configPath).
|
||||
WithArgs(cliArgs).
|
||||
WithArgs(args).
|
||||
WithEnvTransform(customEnvTransform).
|
||||
WithSources(
|
||||
lconfig.SourceCLI,
|
||||
@ -74,29 +86,25 @@ func LoadWithCLI(cliArgs []string, flagCfg interface{}) (*Config, error) {
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") && configPath != "logwisp.toml" {
|
||||
// If explicit config file specified and not found, fail
|
||||
// Config file load errors
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if isExplicit {
|
||||
return nil, fmt.Errorf("config file not found: %s", configPath)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "not found") {
|
||||
// If the default config file is not found, it's not an error.
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Likely never happens
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("configuration builder returned nil config")
|
||||
}
|
||||
|
||||
// Scan into final config struct
|
||||
finalConfig := &Config{}
|
||||
if err := cfg.Scan("", finalConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan config: %w", err)
|
||||
}
|
||||
|
||||
// Ensure we have valid config even with defaults
|
||||
if finalConfig == nil {
|
||||
return nil, fmt.Errorf("configuration scan produced nil config")
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
finalConfig.ConfigFile = configPath
|
||||
}
|
||||
|
||||
// Ensure critical fields are not nil
|
||||
@ -104,14 +112,58 @@ func LoadWithCLI(cliArgs []string, flagCfg interface{}) (*Config, error) {
|
||||
finalConfig.Logging = DefaultLogConfig()
|
||||
}
|
||||
|
||||
// Apply any console target transformations here
|
||||
// Apply console target overrides if needed
|
||||
if err := applyConsoleTargetOverrides(finalConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to apply console target overrides: %w", err)
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
return finalConfig, finalConfig.validate()
|
||||
}
|
||||
|
||||
// resolveConfigPath returns the configuration file path
|
||||
func resolveConfigPath(args []string) (path string, isExplicit bool) {
|
||||
// 1. Check for --config flag in command-line arguments (highest precedence)
|
||||
for i, arg := range args {
|
||||
if (arg == "--config" || arg == "-c") && i+1 < len(args) {
|
||||
return args[i+1], true
|
||||
}
|
||||
if strings.HasPrefix(arg, "--config=") {
|
||||
return strings.TrimPrefix(arg, "--config="), true
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check environment variables
|
||||
if configFile := os.Getenv("LOGWISP_CONFIG_FILE"); configFile != "" {
|
||||
path = configFile
|
||||
if configDir := os.Getenv("LOGWISP_CONFIG_DIR"); configDir != "" {
|
||||
path = filepath.Join(configDir, configFile)
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
if configDir := os.Getenv("LOGWISP_CONFIG_DIR"); configDir != "" {
|
||||
return filepath.Join(configDir, "logwisp.toml"), true
|
||||
}
|
||||
|
||||
// 3. Check default user config location
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
configPath := filepath.Join(homeDir, ".config", "logwisp", "logwisp.toml")
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
return configPath, false // Found a default, but not explicitly set by user
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback to default in current directory
|
||||
return "logwisp.toml", false
|
||||
}
|
||||
|
||||
func customEnvTransform(path string) string {
|
||||
env := strings.ReplaceAll(path, ".", "_")
|
||||
env = strings.ToUpper(env)
|
||||
env = "LOGWISP_" + env
|
||||
return env
|
||||
}
|
||||
|
||||
// applyConsoleTargetOverrides centralizes console target configuration
|
||||
func applyConsoleTargetOverrides(cfg *Config) error {
|
||||
// Check environment variable for console target override
|
||||
@ -150,40 +202,3 @@ func applyConsoleTargetOverrides(cfg *Config) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfigPath returns the configuration file path
|
||||
func GetConfigPath() string {
|
||||
// Check if explicit config file was specified via flag or env
|
||||
if configFile := os.Getenv("LOGWISP_CONFIG_FILE"); configFile != "" {
|
||||
if filepath.IsAbs(configFile) {
|
||||
return configFile
|
||||
}
|
||||
if configDir := os.Getenv("LOGWISP_CONFIG_DIR"); configDir != "" {
|
||||
return filepath.Join(configDir, configFile)
|
||||
}
|
||||
return configFile
|
||||
}
|
||||
|
||||
if configDir := os.Getenv("LOGWISP_CONFIG_DIR"); configDir != "" {
|
||||
return filepath.Join(configDir, "logwisp.toml")
|
||||
}
|
||||
|
||||
// Default locations
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
configPath := filepath.Join(homeDir, ".config", "logwisp.toml")
|
||||
// Check if config exists in home directory
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
return configPath
|
||||
}
|
||||
}
|
||||
|
||||
// Return current directory default
|
||||
return "logwisp.toml"
|
||||
}
|
||||
|
||||
func customEnvTransform(path string) string {
|
||||
env := strings.ReplaceAll(path, ".", "_")
|
||||
env = strings.ToUpper(env)
|
||||
env = "LOGWISP_" + env
|
||||
return env
|
||||
}
|
||||
@ -79,11 +79,10 @@ func (w *fileWatcher) watch(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// FILE: src/internal/source/file_watcher.go
|
||||
func (w *fileWatcher) seekToEnd() error {
|
||||
file, err := os.Open(w.path)
|
||||
if err != nil {
|
||||
// For non-existent files, initialize position to 0
|
||||
// This allows watching files that don't exist yet
|
||||
if os.IsNotExist(err) {
|
||||
w.mu.Lock()
|
||||
w.position = 0
|
||||
@ -103,13 +102,13 @@ func (w *fileWatcher) seekToEnd() error {
|
||||
}
|
||||
|
||||
w.mu.Lock()
|
||||
// Only seek to end if position was never set (-1)
|
||||
// This preserves position = 0 for new files while allowing
|
||||
// directory-discovered files to start reading from current position
|
||||
defer w.mu.Unlock()
|
||||
|
||||
// Keep existing position (including 0)
|
||||
// First time initialization seeks to the end of the file
|
||||
if w.position == -1 {
|
||||
pos, err := file.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
w.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
w.position = pos
|
||||
@ -120,7 +119,6 @@ func (w *fileWatcher) seekToEnd() error {
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
w.inode = stat.Ino
|
||||
}
|
||||
w.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -171,35 +169,57 @@ func (w *fileWatcher) checkFile() error {
|
||||
w.inode = currentInode
|
||||
w.size = currentSize
|
||||
w.modTime = currentModTime
|
||||
// Keep position at 0 to read from beginning if this is a new file
|
||||
// or seek to end if we want to skip existing content
|
||||
if oldSize == 0 && w.position == 0 {
|
||||
// First time seeing this file, seek to end to skip existing content
|
||||
w.position = currentSize
|
||||
}
|
||||
// Position stays at 0 for new files
|
||||
w.mu.Unlock()
|
||||
return nil
|
||||
// Don't return here - continue to read content
|
||||
}
|
||||
|
||||
// Check for rotation
|
||||
rotated := false
|
||||
rotationReason := ""
|
||||
startPos := oldPos
|
||||
|
||||
if oldInode != 0 && currentInode != 0 && currentInode != oldInode {
|
||||
rotated = true
|
||||
rotationReason = "inode change"
|
||||
} else if currentSize < oldSize {
|
||||
// Rotation detection
|
||||
if currentSize < oldSize {
|
||||
// File was truncated
|
||||
rotated = true
|
||||
rotationReason = "size decrease"
|
||||
} else if currentModTime.Before(oldModTime) && currentSize <= oldSize {
|
||||
// Modification time went backwards (logrotate behavior)
|
||||
rotated = true
|
||||
rotationReason = "modification time reset"
|
||||
} else if oldPos > currentSize+1024 {
|
||||
// Our position is way beyond file size
|
||||
rotated = true
|
||||
rotationReason = "position beyond file size"
|
||||
} else if oldInode != 0 && currentInode != 0 && currentInode != oldInode {
|
||||
// Inode changed - distinguish between rotation and atomic save
|
||||
if currentSize == 0 {
|
||||
// Empty file with new inode = likely rotation
|
||||
rotated = true
|
||||
rotationReason = "inode change with empty file"
|
||||
} else if currentSize < oldPos {
|
||||
// New file is smaller than our position = rotation
|
||||
rotated = true
|
||||
rotationReason = "inode change with size less than position"
|
||||
} else {
|
||||
// Inode changed but file has content and size >= position
|
||||
// This is likely an atomic save by an editor
|
||||
// Update inode but keep position
|
||||
w.mu.Lock()
|
||||
w.inode = currentInode
|
||||
w.mu.Unlock()
|
||||
|
||||
w.logger.Debug("msg", "Atomic file update detected",
|
||||
"component", "file_watcher",
|
||||
"path", w.path,
|
||||
"old_inode", oldInode,
|
||||
"new_inode", currentInode,
|
||||
"position", oldPos,
|
||||
"size", currentSize)
|
||||
}
|
||||
}
|
||||
|
||||
startPos := oldPos
|
||||
if rotated {
|
||||
startPos = 0
|
||||
w.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user