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
|
go 1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6
|
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497
|
||||||
github.com/lixenwraith/log v0.0.0-20250710012114-049926224b0e
|
github.com/lixenwraith/log v0.0.0-20250710220042-139c522da9aa
|
||||||
github.com/panjf2000/gnet/v2 v2.9.1
|
github.com/panjf2000/gnet/v2 v2.9.1
|
||||||
github.com/valyala/fasthttp v1.63.0
|
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/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 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
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-20250712170030-7d38402e0497 h1:ixTIdJSd945n/IhMRwGwQVmQnQ1nUr5z1wn31jXq9FU=
|
||||||
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6/go.mod h1:4wPJ3HnLrYrtUwTinngCsBgtdIXsnxkLa7q4KAIbwY8=
|
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497/go.mod h1:y7kgDrWIFROWJJ6ASM/SPTRRAj27FjRGWh2SDLcdQ68=
|
||||||
github.com/lixenwraith/log v0.0.0-20250710012114-049926224b0e h1:WjYl/OIKxDCFA1In2W0bJbCGJ/Ub9X9DL+avZRNjXIQ=
|
github.com/lixenwraith/log v0.0.0-20250710220042-139c522da9aa h1:lIo780MTgZXH5jF+Qr24fRDFQTQ3eu3OaaFYLP9ZkR0=
|
||||||
github.com/lixenwraith/log v0.0.0-20250710012114-049926224b0e/go.mod h1:KFE7B7m2pu5kAl0olDCvywlOqFJhanogAhTlVvlp8JE=
|
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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
||||||
|
|||||||
@ -4,9 +4,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"logwisp/src/internal/config"
|
"logwisp/src/internal/config"
|
||||||
@ -17,13 +14,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// bootstrapService creates and initializes the log transport service
|
// 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
|
// Create service with logger dependency injection
|
||||||
svc := service.New(ctx, logger)
|
svc := service.New(ctx, logger)
|
||||||
|
|
||||||
// Create HTTP router if requested
|
// Create HTTP router if requested
|
||||||
var router *service.HTTPRouter
|
var router *service.HTTPRouter
|
||||||
if flagCfg.UseRouter {
|
if cfg.UseRouter {
|
||||||
router = service.NewHTTPRouter(svc, logger)
|
router = service.NewHTTPRouter(svc, logger)
|
||||||
logger.Info("msg", "HTTP router mode enabled")
|
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 using router mode, register HTTP sinks
|
||||||
if flagCfg.UseRouter {
|
if cfg.UseRouter {
|
||||||
pipeline, err := svc.GetPipeline(pipelineCfg.Name)
|
pipeline, err := svc.GetPipeline(pipelineCfg.Name)
|
||||||
if err == nil && len(pipeline.HTTPSinks) > 0 {
|
if err == nil && len(pipeline.HTTPSinks) > 0 {
|
||||||
if err := router.RegisterPipeline(pipeline); err != nil {
|
if err := router.RegisterPipeline(pipeline); err != nil {
|
||||||
@ -54,7 +51,7 @@ func bootstrapService(ctx context.Context, cfg *config.Config, flagCfg *FlagConf
|
|||||||
}
|
}
|
||||||
|
|
||||||
successCount++
|
successCount++
|
||||||
displayPipelineEndpoints(pipelineCfg, flagCfg.UseRouter)
|
displayPipelineEndpoints(pipelineCfg, cfg.UseRouter)
|
||||||
}
|
}
|
||||||
|
|
||||||
if successCount == 0 {
|
if successCount == 0 {
|
||||||
@ -68,42 +65,31 @@ func bootstrapService(ctx context.Context, cfg *config.Config, flagCfg *FlagConf
|
|||||||
return svc, router, nil
|
return svc, router, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// initializeLogger sets up the logger based on configuration and CLI flags
|
// initializeLogger sets up the logger based on configuration
|
||||||
func initializeLogger(cfg *config.Config, flagCfg *FlagConfig) error {
|
func initializeLogger(cfg *config.Config) error {
|
||||||
logger = log.NewLogger()
|
logger = log.NewLogger()
|
||||||
|
|
||||||
var configArgs []string
|
var configArgs []string
|
||||||
|
|
||||||
// Quiet mode suppresses ALL LogWisp logging (not sink outputs)
|
if cfg.Quiet {
|
||||||
if flagCfg.Quiet {
|
|
||||||
// In quiet mode, disable ALL logging output
|
// In quiet mode, disable ALL logging output
|
||||||
configArgs = append(configArgs,
|
configArgs = append(configArgs,
|
||||||
"disable_file=true",
|
"disable_file=true",
|
||||||
"enable_stdout=false",
|
"enable_stdout=false",
|
||||||
"level=255") // Set to max level to suppress everything
|
"level=255")
|
||||||
|
|
||||||
return logger.InitWithDefaults(configArgs...)
|
return logger.InitWithDefaults(configArgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine output mode from CLI or config
|
|
||||||
outputMode := cfg.Logging.Output
|
|
||||||
if flagCfg.LogOutput != "" {
|
|
||||||
outputMode = flagCfg.LogOutput
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine log level
|
// Determine log level
|
||||||
level := cfg.Logging.Level
|
levelValue, err := parseLogLevel(cfg.Logging.Level)
|
||||||
if flagCfg.LogLevel != "" {
|
|
||||||
level = flagCfg.LogLevel
|
|
||||||
}
|
|
||||||
levelValue, err := parseLogLevel(level)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid log level: %w", err)
|
return fmt.Errorf("invalid log level: %w", err)
|
||||||
}
|
}
|
||||||
configArgs = append(configArgs, fmt.Sprintf("level=%d", levelValue))
|
configArgs = append(configArgs, fmt.Sprintf("level=%d", levelValue))
|
||||||
|
|
||||||
// Configure based on output mode
|
// Configure based on output mode
|
||||||
switch outputMode {
|
switch cfg.Logging.Output {
|
||||||
case "none":
|
case "none":
|
||||||
configArgs = append(configArgs, "disable_file=true", "enable_stdout=false")
|
configArgs = append(configArgs, "disable_file=true", "enable_stdout=false")
|
||||||
|
|
||||||
@ -121,15 +107,15 @@ func initializeLogger(cfg *config.Config, flagCfg *FlagConfig) error {
|
|||||||
|
|
||||||
case "file":
|
case "file":
|
||||||
configArgs = append(configArgs, "enable_stdout=false")
|
configArgs = append(configArgs, "enable_stdout=false")
|
||||||
configureFileLogging(&configArgs, cfg, flagCfg)
|
configureFileLogging(&configArgs, cfg)
|
||||||
|
|
||||||
case "both":
|
case "both":
|
||||||
configArgs = append(configArgs, "enable_stdout=true")
|
configArgs = append(configArgs, "enable_stdout=true")
|
||||||
configureFileLogging(&configArgs, cfg, flagCfg)
|
configureFileLogging(&configArgs, cfg)
|
||||||
configureConsoleTarget(&configArgs, cfg, flagCfg)
|
configureConsoleTarget(&configArgs, cfg)
|
||||||
|
|
||||||
default:
|
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
|
// Apply format if specified
|
||||||
@ -141,20 +127,8 @@ func initializeLogger(cfg *config.Config, flagCfg *FlagConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// configureFileLogging sets up file-based logging parameters
|
// configureFileLogging sets up file-based logging parameters
|
||||||
func configureFileLogging(configArgs *[]string, cfg *config.Config, flagCfg *FlagConfig) {
|
func configureFileLogging(configArgs *[]string, cfg *config.Config) {
|
||||||
// CLI overrides
|
if cfg.Logging.File != nil {
|
||||||
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
|
|
||||||
*configArgs = append(*configArgs,
|
*configArgs = append(*configArgs,
|
||||||
fmt.Sprintf("directory=%s", cfg.Logging.File.Directory),
|
fmt.Sprintf("directory=%s", cfg.Logging.File.Directory),
|
||||||
fmt.Sprintf("name=%s", cfg.Logging.File.Name),
|
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
|
// 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
|
target := "stderr" // default
|
||||||
|
|
||||||
if flagCfg.LogConsole != "" {
|
if cfg.Logging.Console != nil && cfg.Logging.Console.Target != "" {
|
||||||
target = flagCfg.LogConsole
|
|
||||||
} else if cfg.Logging.Console != nil && cfg.Logging.Console.Target != "" {
|
|
||||||
target = 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" {
|
if target == "split" {
|
||||||
*configArgs = append(*configArgs, "stdout_split_mode=true")
|
*configArgs = append(*configArgs, "stdout_split_mode=true")
|
||||||
*configArgs = append(*configArgs, "stdout_target=split")
|
*configArgs = append(*configArgs, "stdout_target=split")
|
||||||
logger.Debug("msg", "Console output configured for split mode",
|
|
||||||
"component", "bootstrap",
|
|
||||||
"info_debug", "stdout",
|
|
||||||
"warn_error", "stderr")
|
|
||||||
} else {
|
} else {
|
||||||
*configArgs = append(*configArgs, fmt.Sprintf("stdout_target=%s", target))
|
*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) {
|
func parseLogLevel(level string) (int, error) {
|
||||||
switch strings.ToLower(level) {
|
switch strings.ToLower(level) {
|
||||||
case "debug":
|
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"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@ -19,62 +20,57 @@ import (
|
|||||||
var logger *log.Logger
|
var logger *log.Logger
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Parse flags first to get quiet mode early
|
// Emulates nohup
|
||||||
flagCfg, err := ParseFlags()
|
signal.Ignore(syscall.SIGHUP)
|
||||||
|
|
||||||
|
// Load configuration with automatic CLI parsing
|
||||||
|
cfg, err := config.Load(os.Args[1:])
|
||||||
if err != nil {
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize output handler with quiet mode
|
// Initialize output handler
|
||||||
InitOutputHandler(flagCfg.Quiet)
|
InitOutputHandler(cfg.Quiet)
|
||||||
|
|
||||||
// Handle version flag
|
// Handle version
|
||||||
if flagCfg.ShowVersion {
|
if cfg.ShowVersion {
|
||||||
fmt.Println(version.String())
|
fmt.Println(version.String())
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle background mode
|
// Background mode spawns a child with internal --background-daemon flag.
|
||||||
if flagCfg.Background && !isBackgroundProcess() {
|
if cfg.Background && !cfg.BackgroundDaemon {
|
||||||
if err := runInBackground(); err != nil {
|
// 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)
|
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
|
// Initialize logger
|
||||||
if flagCfg.ConfigFile != "" {
|
if err := initializeLogger(cfg); err != nil {
|
||||||
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 {
|
|
||||||
FatalError(1, "Failed to initialize logger: %v\n", err)
|
FatalError(1, "Failed to initialize logger: %v\n", err)
|
||||||
}
|
}
|
||||||
defer shutdownLogger()
|
defer shutdownLogger()
|
||||||
|
|
||||||
// Log startup information (respects quiet mode via logger config)
|
// Log startup information
|
||||||
logger.Info("msg", "LogWisp starting",
|
logger.Info("msg", "LogWisp starting",
|
||||||
"version", version.String(),
|
"version", version.String(),
|
||||||
"config_file", flagCfg.ConfigFile,
|
"config_file", cfg.ConfigFile,
|
||||||
"log_output", cfg.Logging.Output,
|
"log_output", cfg.Logging.Output,
|
||||||
"router_mode", flagCfg.UseRouter)
|
"router_mode", cfg.UseRouter,
|
||||||
|
"background_mode", cfg.Background)
|
||||||
|
|
||||||
// Create context for shutdown
|
// Create context for shutdown
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@ -85,14 +81,14 @@ func main() {
|
|||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
|
||||||
|
|
||||||
// Bootstrap the service
|
// Bootstrap the service
|
||||||
svc, router, err := bootstrapService(ctx, cfg, flagCfg)
|
svc, router, err := bootstrapService(ctx, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("msg", "Failed to bootstrap service", "error", err)
|
logger.Error("msg", "Failed to bootstrap service", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start status reporter if enabled
|
// Start status reporter if enabled
|
||||||
if enableStatusReporter() {
|
if !cfg.DisableStatusReporter {
|
||||||
go statusReporter(svc)
|
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
|
// 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()
|
o.mu.RLock()
|
||||||
defer o.mu.RUnlock()
|
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
|
// 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()
|
o.mu.RLock()
|
||||||
defer o.mu.RUnlock()
|
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)
|
// 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...)
|
o.Error(format, args...)
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
@ -69,19 +69,19 @@ func (o *OutputHandler) SetQuiet(quiet bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for global output handler
|
// Helper functions for global output handler
|
||||||
func Print(format string, args ...interface{}) {
|
func Print(format string, args ...any) {
|
||||||
if output != nil {
|
if output != nil {
|
||||||
output.Print(format, args...)
|
output.Print(format, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Error(format string, args ...interface{}) {
|
func Error(format string, args ...any) {
|
||||||
if output != nil {
|
if output != nil {
|
||||||
output.Error(format, args...)
|
output.Error(format, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FatalError(code int, format string, args ...interface{}) {
|
func FatalError(code int, format string, args ...any) {
|
||||||
if output != nil {
|
if output != nil {
|
||||||
output.FatalError(code, format, args...)
|
output.FatalError(code, format, args...)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -2,10 +2,23 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Logging configuration
|
// Top-level flags for application control
|
||||||
Logging *LogConfig `toml:"logging"`
|
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"`
|
Pipelines []PipelineConfig `toml:"pipelines"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,11 +12,24 @@ import (
|
|||||||
|
|
||||||
// LoadContext holds all configuration sources
|
// LoadContext holds all configuration sources
|
||||||
type LoadContext struct {
|
type LoadContext struct {
|
||||||
FlagConfig interface{} // Parsed command-line flags from main
|
FlagConfig any // Parsed command-line flags from main
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaults() *Config {
|
func defaults() *Config {
|
||||||
return &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(),
|
Logging: DefaultLogConfig(),
|
||||||
Pipelines: []PipelineConfig{
|
Pipelines: []PipelineConfig{
|
||||||
{
|
{
|
||||||
@ -37,7 +50,7 @@ func defaults() *Config {
|
|||||||
Options: map[string]any{
|
Options: map[string]any{
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
"buffer_size": 1000,
|
"buffer_size": 1000,
|
||||||
"stream_path": "/transport",
|
"stream_path": "/stream",
|
||||||
"status_path": "/status",
|
"status_path": "/status",
|
||||||
"heartbeat": map[string]any{
|
"heartbeat": map[string]any{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@ -54,16 +67,15 @@ func defaults() *Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadWithCLI loads config with CLI flag overrides
|
// Load is the single entry point for loading all configuration
|
||||||
func LoadWithCLI(cliArgs []string, flagCfg interface{}) (*Config, error) {
|
func Load(args []string) (*Config, error) {
|
||||||
configPath := GetConfigPath()
|
configPath, isExplicit := resolveConfigPath(args)
|
||||||
|
|
||||||
// Build configuration with all sources
|
// Build configuration with all sources
|
||||||
cfg, err := lconfig.NewBuilder().
|
cfg, err := lconfig.NewBuilder().
|
||||||
WithDefaults(defaults()).
|
WithDefaults(defaults()).
|
||||||
WithEnvPrefix("LOGWISP_").
|
WithEnvPrefix("LOGWISP_").
|
||||||
WithFile(configPath).
|
WithFile(configPath).
|
||||||
WithArgs(cliArgs).
|
WithArgs(args).
|
||||||
WithEnvTransform(customEnvTransform).
|
WithEnvTransform(customEnvTransform).
|
||||||
WithSources(
|
WithSources(
|
||||||
lconfig.SourceCLI,
|
lconfig.SourceCLI,
|
||||||
@ -74,29 +86,25 @@ func LoadWithCLI(cliArgs []string, flagCfg interface{}) (*Config, error) {
|
|||||||
Build()
|
Build()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "not found") && configPath != "logwisp.toml" {
|
// Config file load errors
|
||||||
// If explicit config file specified and not found, fail
|
if strings.Contains(err.Error(), "not found") {
|
||||||
|
if isExplicit {
|
||||||
return nil, fmt.Errorf("config file not found: %s", configPath)
|
return nil, fmt.Errorf("config file not found: %s", configPath)
|
||||||
}
|
}
|
||||||
|
// If the default config file is not found, it's not an error.
|
||||||
if !strings.Contains(err.Error(), "not found") {
|
} else {
|
||||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
return nil, fmt.Errorf("failed to load config: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Likely never happens
|
// Scan into final config struct
|
||||||
if cfg == nil {
|
|
||||||
return nil, fmt.Errorf("configuration builder returned nil config")
|
|
||||||
}
|
|
||||||
|
|
||||||
finalConfig := &Config{}
|
finalConfig := &Config{}
|
||||||
if err := cfg.Scan("", finalConfig); err != nil {
|
if err := cfg.Scan("", finalConfig); err != nil {
|
||||||
return nil, fmt.Errorf("failed to scan config: %w", err)
|
return nil, fmt.Errorf("failed to scan config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have valid config even with defaults
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
if finalConfig == nil {
|
finalConfig.ConfigFile = configPath
|
||||||
return nil, fmt.Errorf("configuration scan produced nil config")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure critical fields are not nil
|
// Ensure critical fields are not nil
|
||||||
@ -104,14 +112,58 @@ func LoadWithCLI(cliArgs []string, flagCfg interface{}) (*Config, error) {
|
|||||||
finalConfig.Logging = DefaultLogConfig()
|
finalConfig.Logging = DefaultLogConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply any console target transformations here
|
// Apply console target overrides if needed
|
||||||
if err := applyConsoleTargetOverrides(finalConfig); err != nil {
|
if err := applyConsoleTargetOverrides(finalConfig); err != nil {
|
||||||
return nil, fmt.Errorf("failed to apply console target overrides: %w", err)
|
return nil, fmt.Errorf("failed to apply console target overrides: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate configuration
|
||||||
return finalConfig, finalConfig.validate()
|
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
|
// applyConsoleTargetOverrides centralizes console target configuration
|
||||||
func applyConsoleTargetOverrides(cfg *Config) error {
|
func applyConsoleTargetOverrides(cfg *Config) error {
|
||||||
// Check environment variable for console target override
|
// Check environment variable for console target override
|
||||||
@ -150,40 +202,3 @@ func applyConsoleTargetOverrides(cfg *Config) error {
|
|||||||
|
|
||||||
return nil
|
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 {
|
func (w *fileWatcher) seekToEnd() error {
|
||||||
file, err := os.Open(w.path)
|
file, err := os.Open(w.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// For non-existent files, initialize position to 0
|
|
||||||
// This allows watching files that don't exist yet
|
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
w.position = 0
|
w.position = 0
|
||||||
@ -103,13 +102,13 @@ func (w *fileWatcher) seekToEnd() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
// Only seek to end if position was never set (-1)
|
defer w.mu.Unlock()
|
||||||
// This preserves position = 0 for new files while allowing
|
|
||||||
// directory-discovered files to start reading from current position
|
// Keep existing position (including 0)
|
||||||
|
// First time initialization seeks to the end of the file
|
||||||
if w.position == -1 {
|
if w.position == -1 {
|
||||||
pos, err := file.Seek(0, io.SeekEnd)
|
pos, err := file.Seek(0, io.SeekEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.mu.Unlock()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.position = pos
|
w.position = pos
|
||||||
@ -120,7 +119,6 @@ func (w *fileWatcher) seekToEnd() error {
|
|||||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||||
w.inode = stat.Ino
|
w.inode = stat.Ino
|
||||||
}
|
}
|
||||||
w.mu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -171,35 +169,57 @@ func (w *fileWatcher) checkFile() error {
|
|||||||
w.inode = currentInode
|
w.inode = currentInode
|
||||||
w.size = currentSize
|
w.size = currentSize
|
||||||
w.modTime = currentModTime
|
w.modTime = currentModTime
|
||||||
// Keep position at 0 to read from beginning if this is a new file
|
// Position stays at 0 for new files
|
||||||
// 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
|
|
||||||
}
|
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
return nil
|
// Don't return here - continue to read content
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for rotation
|
// Check for rotation
|
||||||
rotated := false
|
rotated := false
|
||||||
rotationReason := ""
|
rotationReason := ""
|
||||||
|
startPos := oldPos
|
||||||
|
|
||||||
if oldInode != 0 && currentInode != 0 && currentInode != oldInode {
|
// Rotation detection
|
||||||
rotated = true
|
if currentSize < oldSize {
|
||||||
rotationReason = "inode change"
|
// File was truncated
|
||||||
} else if currentSize < oldSize {
|
|
||||||
rotated = true
|
rotated = true
|
||||||
rotationReason = "size decrease"
|
rotationReason = "size decrease"
|
||||||
} else if currentModTime.Before(oldModTime) && currentSize <= oldSize {
|
} else if currentModTime.Before(oldModTime) && currentSize <= oldSize {
|
||||||
|
// Modification time went backwards (logrotate behavior)
|
||||||
rotated = true
|
rotated = true
|
||||||
rotationReason = "modification time reset"
|
rotationReason = "modification time reset"
|
||||||
} else if oldPos > currentSize+1024 {
|
} else if oldPos > currentSize+1024 {
|
||||||
|
// Our position is way beyond file size
|
||||||
rotated = true
|
rotated = true
|
||||||
rotationReason = "position beyond file size"
|
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 {
|
if rotated {
|
||||||
startPos = 0
|
startPos = 0
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
|
|||||||
Reference in New Issue
Block a user