v0.3.1 background mode fixed, bug fixes and refactoring
This commit is contained in:
@ -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
|
||||
return nil, fmt.Errorf("config file not found: %s", configPath)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "not found") {
|
||||
// Config file load errors
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
if isExplicit {
|
||||
return nil, fmt.Errorf("config file not found: %s", configPath)
|
||||
}
|
||||
// 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
|
||||
@ -149,41 +201,4 @@ 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