206 lines
5.8 KiB
Go
206 lines
5.8 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"logwisp/internal/core"
|
|
|
|
lconfig "github.com/lixenwraith/config"
|
|
)
|
|
|
|
// configManager holds the global instance of the configuration manager
|
|
var configManager *lconfig.Config
|
|
|
|
// Load is the single entry point for loading all application configuration
|
|
func Load(args []string) (*Config, error) {
|
|
configPath, isExplicit := resolveConfigPath(args)
|
|
// Build configuration with all sources
|
|
|
|
// Create target config instance that will be populated
|
|
finalConfig := &Config{}
|
|
|
|
// Builder handles loading, populating the target struct, and validation
|
|
cfg, err := lconfig.NewBuilder().
|
|
WithTarget(finalConfig). // Typed target struct
|
|
WithDefaults(defaults()). // Default values
|
|
WithSources(
|
|
lconfig.SourceCLI,
|
|
lconfig.SourceEnv,
|
|
lconfig.SourceFile,
|
|
lconfig.SourceDefault,
|
|
).
|
|
WithEnvTransform(customEnvTransform). // Convert '.' to '_' in env separation
|
|
WithEnvPrefix("LOGWISP_"). // Environment variable prefix
|
|
WithArgs(args). // Command-line arguments
|
|
WithFile(configPath). // TOML config file
|
|
WithFileFormat("toml"). // Explicit format
|
|
WithTypedValidator(ValidateConfig). // Centralized validation
|
|
WithSecurityOptions(lconfig.SecurityOptions{
|
|
PreventPathTraversal: true,
|
|
MaxFileSize: 10 * 1024 * 1024, // 10MB max config
|
|
}).
|
|
Build()
|
|
|
|
if err != nil {
|
|
// Handle file not found errors - maintain existing behavior
|
|
if errors.Is(err, lconfig.ErrConfigNotFound) {
|
|
if isExplicit {
|
|
// Return empty config with file path
|
|
finalConfig.ConfigFile = configPath
|
|
return finalConfig, fmt.Errorf("config file not found: %s", configPath)
|
|
}
|
|
// If the default config file is not found, it's not an error, default/cli/env will be used
|
|
} else {
|
|
return nil, fmt.Errorf("failed to load or validate config: %w", err)
|
|
}
|
|
}
|
|
|
|
// Store the config file path for hot reload
|
|
finalConfig.ConfigFile = configPath
|
|
|
|
// Store the manager for hot reload
|
|
configManager = cfg
|
|
|
|
// Surface typo'd flags (e.g. --status-reporter vs --status_reporter);
|
|
// pre-logger phase, stderr only, suppressed in quiet mode
|
|
if unknown := cfg.UnknownCLIKeys(); len(unknown) > 0 && !finalConfig.Quiet {
|
|
fmt.Fprintf(os.Stderr, "Warning: unrecognized flags ignored: %v\n", unknown)
|
|
}
|
|
|
|
// Start watcher if auto-reload is enabled
|
|
if finalConfig.ConfigAutoReload {
|
|
watchOpts := lconfig.WatchOptions{
|
|
PollInterval: core.ReloadWatchPollInterval,
|
|
Debounce: core.ReloadWatchDebounce,
|
|
ReloadTimeout: core.ReloadWatchTimeout,
|
|
VerifyPermissions: true,
|
|
}
|
|
cfg.AutoUpdateWithOptions(watchOpts)
|
|
}
|
|
|
|
return finalConfig, nil
|
|
}
|
|
|
|
// GetConfigManager returns the global configuration manager instance for hot-reloading
|
|
func GetConfigManager() *lconfig.Config {
|
|
return configManager
|
|
}
|
|
|
|
// defaults provides the default configuration values for the application
|
|
func defaults() *Config {
|
|
return &Config{
|
|
// Top-level flag defaults
|
|
ShowVersion: false,
|
|
Quiet: false,
|
|
|
|
// Runtime behavior defaults
|
|
StatusReporter: true,
|
|
ConfigAutoReload: false,
|
|
|
|
// Existing defaults
|
|
Logging: &LogConfig{
|
|
Output: "stdout",
|
|
Level: "info",
|
|
Format: "txt",
|
|
File: &LogFileConfig{
|
|
Directory: "./log",
|
|
Name: "logwisp",
|
|
MaxSizeMB: 100,
|
|
MaxTotalSizeMB: 1000,
|
|
RetentionHours: 168, // 7 days
|
|
},
|
|
Console: &LogConsoleConfig{
|
|
Target: "stdout",
|
|
},
|
|
},
|
|
Pipelines: []PipelineConfig{
|
|
{
|
|
Name: "default_pipeline",
|
|
Flow: &FlowConfig{
|
|
RateLimit: &RateLimitConfig{
|
|
Rate: 5,
|
|
Burst: 10,
|
|
Policy: "drop",
|
|
MaxEntrySizeBytes: 65536,
|
|
},
|
|
Format: &FormatConfig{
|
|
Type: "json",
|
|
SanitizerPolicy: "json",
|
|
},
|
|
},
|
|
PluginSources: []PluginSourceConfig{
|
|
{
|
|
ID: "default_source",
|
|
Type: "random",
|
|
Config: map[string]any{
|
|
"special": true,
|
|
},
|
|
// Config: &FileSourceOptions{
|
|
// Directory: "./",
|
|
// Pattern: "*.log",
|
|
// CheckIntervalMS: int64(100),
|
|
},
|
|
},
|
|
PluginSinks: []PluginSinkConfig{
|
|
{
|
|
ID: "default_sink",
|
|
Type: "console",
|
|
Config: map[string]any{
|
|
"target": "stdout",
|
|
"buffer_size": 100,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// resolveConfigPath determines the configuration file path based on CLI args, env vars, and default locations
|
|
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 == "-c" {
|
|
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
|
|
}
|
|
|
|
// customEnvTransform converts TOML-style config paths (e.g., logging.level) to environment variable format (LOGGING_LEVEL)
|
|
func customEnvTransform(path string) string {
|
|
env := strings.ReplaceAll(path, ".", "_")
|
|
env = strings.ToUpper(env)
|
|
// env = "LOGWISP_" + env // already added by WithEnvPrefix
|
|
return env
|
|
}
|