v0.3.1 background mode fixed, bug fixes and refactoring

This commit is contained in:
2025-07-12 20:30:47 -04:00
parent 58d33d7872
commit e31591ac8d
9 changed files with 193 additions and 353 deletions

4
go.mod
View File

@ -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
View File

@ -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=

View File

@ -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":

View File

@ -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")
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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()