v0.1.3 stream changed from net/http to fasthttp for http and gnet for tcp stream, heartbeat config added
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
// File: logwisp/src/internal/config/config.go
|
||||
// FILE: src/internal/config/config.go
|
||||
package config
|
||||
|
||||
import (
|
||||
@ -10,114 +10,97 @@ import (
|
||||
lconfig "github.com/lixenwraith/config"
|
||||
)
|
||||
|
||||
// Config holds the complete configuration
|
||||
type Config struct {
|
||||
Port int `toml:"port"`
|
||||
Monitor MonitorConfig `toml:"monitor"`
|
||||
Stream StreamConfig `toml:"stream"`
|
||||
Monitor MonitorConfig `toml:"monitor"`
|
||||
TCPServer TCPConfig `toml:"tcpserver"`
|
||||
HTTPServer HTTPConfig `toml:"httpserver"`
|
||||
}
|
||||
|
||||
// MonitorConfig holds monitoring settings
|
||||
type MonitorConfig struct {
|
||||
CheckIntervalMs int `toml:"check_interval_ms"`
|
||||
Targets []MonitorTarget `toml:"targets"`
|
||||
}
|
||||
|
||||
// MonitorTarget represents a path to monitor
|
||||
type MonitorTarget struct {
|
||||
Path string `toml:"path"` // File or directory path
|
||||
Pattern string `toml:"pattern"` // Glob pattern for directories
|
||||
IsFile bool `toml:"is_file"` // True if monitoring specific file
|
||||
Path string `toml:"path"`
|
||||
Pattern string `toml:"pattern"`
|
||||
IsFile bool `toml:"is_file"`
|
||||
}
|
||||
|
||||
// StreamConfig holds streaming settings
|
||||
type StreamConfig struct {
|
||||
BufferSize int `toml:"buffer_size"`
|
||||
RateLimit RateLimitConfig `toml:"rate_limit"`
|
||||
type TCPConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Port int `toml:"port"`
|
||||
BufferSize int `toml:"buffer_size"`
|
||||
SSLEnabled bool `toml:"ssl_enabled"`
|
||||
SSLCertFile string `toml:"ssl_cert_file"`
|
||||
SSLKeyFile string `toml:"ssl_key_file"`
|
||||
Heartbeat HeartbeatConfig `toml:"heartbeat"`
|
||||
}
|
||||
|
||||
// RateLimitConfig holds rate limiting settings
|
||||
type RateLimitConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
RequestsPerSecond int `toml:"requests_per_second"`
|
||||
BurstSize int `toml:"burst_size"`
|
||||
CleanupIntervalS int64 `toml:"cleanup_interval_s"`
|
||||
type HTTPConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Port int `toml:"port"`
|
||||
BufferSize int `toml:"buffer_size"`
|
||||
SSLEnabled bool `toml:"ssl_enabled"`
|
||||
SSLCertFile string `toml:"ssl_cert_file"`
|
||||
SSLKeyFile string `toml:"ssl_key_file"`
|
||||
Heartbeat HeartbeatConfig `toml:"heartbeat"`
|
||||
}
|
||||
|
||||
type HeartbeatConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
IntervalSeconds int `toml:"interval_seconds"`
|
||||
IncludeTimestamp bool `toml:"include_timestamp"`
|
||||
IncludeStats bool `toml:"include_stats"`
|
||||
Format string `toml:"format"` // "comment" or "json"
|
||||
}
|
||||
|
||||
// defaults returns configuration with default values
|
||||
func defaults() *Config {
|
||||
return &Config{
|
||||
Port: 8080,
|
||||
Monitor: MonitorConfig{
|
||||
CheckIntervalMs: 100,
|
||||
Targets: []MonitorTarget{
|
||||
{Path: "./", Pattern: "*.log", IsFile: false},
|
||||
},
|
||||
},
|
||||
Stream: StreamConfig{
|
||||
TCPServer: TCPConfig{
|
||||
Enabled: false,
|
||||
Port: 9090,
|
||||
BufferSize: 1000,
|
||||
RateLimit: RateLimitConfig{
|
||||
Enabled: false,
|
||||
RequestsPerSecond: 10,
|
||||
BurstSize: 20,
|
||||
CleanupIntervalS: 60,
|
||||
Heartbeat: HeartbeatConfig{
|
||||
Enabled: false,
|
||||
IntervalSeconds: 30,
|
||||
IncludeTimestamp: true,
|
||||
IncludeStats: false,
|
||||
Format: "json",
|
||||
},
|
||||
},
|
||||
HTTPServer: HTTPConfig{
|
||||
Enabled: true,
|
||||
Port: 8080,
|
||||
BufferSize: 1000,
|
||||
Heartbeat: HeartbeatConfig{
|
||||
Enabled: true,
|
||||
IntervalSeconds: 30,
|
||||
IncludeTimestamp: true,
|
||||
IncludeStats: false,
|
||||
Format: "comment",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Load reads configuration using lixenwraith/config Builder pattern
|
||||
func Load() (*Config, error) {
|
||||
configPath := GetConfigPath()
|
||||
|
||||
cfg, err := lconfig.NewBuilder().
|
||||
WithDefaults(defaults()).
|
||||
WithEnvPrefix("LOGWISP_").
|
||||
WithFile(configPath).
|
||||
WithEnvTransform(customEnvTransform).
|
||||
WithSources(
|
||||
lconfig.SourceEnv,
|
||||
lconfig.SourceFile,
|
||||
lconfig.SourceDefault,
|
||||
).
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
// Only fail on actual errors, not missing config file
|
||||
if !strings.Contains(err.Error(), "not found") {
|
||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for LOGWISP_MONITOR_TARGETS env var
|
||||
if err := handleMonitorTargetsEnv(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Scan into final config
|
||||
finalConfig := &Config{}
|
||||
if err := cfg.Scan("", finalConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan config: %w", err)
|
||||
}
|
||||
|
||||
return finalConfig, finalConfig.validate()
|
||||
}
|
||||
|
||||
// LoadWithCLI loads configuration and applies CLI arguments
|
||||
func LoadWithCLI(cliArgs []string) (*Config, error) {
|
||||
configPath := GetConfigPath()
|
||||
|
||||
// Convert CLI args to config format
|
||||
convertedArgs := convertCLIArgs(cliArgs)
|
||||
|
||||
cfg, err := lconfig.NewBuilder().
|
||||
WithDefaults(defaults()).
|
||||
WithEnvPrefix("LOGWISP_").
|
||||
WithFile(configPath).
|
||||
WithArgs(convertedArgs).
|
||||
WithArgs(cliArgs).
|
||||
WithEnvTransform(customEnvTransform).
|
||||
WithSources(
|
||||
lconfig.SourceCLI, // CLI highest priority
|
||||
lconfig.SourceCLI,
|
||||
lconfig.SourceEnv,
|
||||
lconfig.SourceFile,
|
||||
lconfig.SourceDefault,
|
||||
@ -130,12 +113,10 @@ func LoadWithCLI(cliArgs []string) (*Config, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle special env var
|
||||
if err := handleMonitorTargetsEnv(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Scan into final config
|
||||
finalConfig := &Config{}
|
||||
if err := cfg.Scan("", finalConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan config: %w", err)
|
||||
@ -144,52 +125,14 @@ func LoadWithCLI(cliArgs []string) (*Config, error) {
|
||||
return finalConfig, finalConfig.validate()
|
||||
}
|
||||
|
||||
// customEnvTransform handles LOGWISP_ prefix environment variables
|
||||
func customEnvTransform(path string) string {
|
||||
// Standard transform
|
||||
env := strings.ReplaceAll(path, ".", "_")
|
||||
env = strings.ToUpper(env)
|
||||
env = "LOGWISP_" + env
|
||||
|
||||
// Handle common variations
|
||||
switch env {
|
||||
case "LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SECOND":
|
||||
if _, exists := os.LookupEnv("LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC"); exists {
|
||||
return "LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC"
|
||||
}
|
||||
case "LOGWISP_STREAM_RATE_LIMIT_CLEANUP_INTERVAL_S":
|
||||
if _, exists := os.LookupEnv("LOGWISP_STREAM_RATE_LIMIT_CLEANUP_INTERVAL"); exists {
|
||||
return "LOGWISP_STREAM_RATE_LIMIT_CLEANUP_INTERVAL"
|
||||
}
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// convertCLIArgs converts CLI args to config package format
|
||||
func convertCLIArgs(args []string) []string {
|
||||
var converted []string
|
||||
|
||||
for _, arg := range args {
|
||||
switch {
|
||||
case arg == "-c" || arg == "--color":
|
||||
// Color mode is handled separately by main.go
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--config="):
|
||||
// Config file path handled separately
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--"):
|
||||
// Pass through other long flags
|
||||
converted = append(converted, arg)
|
||||
}
|
||||
}
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
// GetConfigPath returns the configuration file path
|
||||
func GetConfigPath() string {
|
||||
// Check explicit config file paths
|
||||
if configFile := os.Getenv("LOGWISP_CONFIG_FILE"); configFile != "" {
|
||||
if filepath.IsAbs(configFile) {
|
||||
return configFile
|
||||
@ -204,7 +147,6 @@ func GetConfigPath() string {
|
||||
return filepath.Join(configDir, "logwisp.toml")
|
||||
}
|
||||
|
||||
// Default location
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(homeDir, ".config", "logwisp.toml")
|
||||
}
|
||||
@ -212,13 +154,10 @@ func GetConfigPath() string {
|
||||
return "logwisp.toml"
|
||||
}
|
||||
|
||||
// handleMonitorTargetsEnv handles comma-separated monitor targets env var
|
||||
func handleMonitorTargetsEnv(cfg *lconfig.Config) error {
|
||||
if targetsStr := os.Getenv("LOGWISP_MONITOR_TARGETS"); targetsStr != "" {
|
||||
// Clear any existing targets from file/defaults
|
||||
cfg.Set("monitor.targets", []MonitorTarget{})
|
||||
|
||||
// Parse comma-separated format: path:pattern:isfile,path2:pattern2:isfile
|
||||
parts := strings.Split(targetsStr, ",")
|
||||
for i, part := range parts {
|
||||
targetParts := strings.Split(part, ":")
|
||||
@ -248,12 +187,7 @@ func handleMonitorTargetsEnv(cfg *lconfig.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate ensures configuration is valid
|
||||
func (c *Config) validate() error {
|
||||
if c.Port < 1 || c.Port > 65535 {
|
||||
return fmt.Errorf("invalid port: %d", c.Port)
|
||||
}
|
||||
|
||||
if c.Monitor.CheckIntervalMs < 10 {
|
||||
return fmt.Errorf("check interval too small: %d ms", c.Monitor.CheckIntervalMs)
|
||||
}
|
||||
@ -266,33 +200,44 @@ func (c *Config) validate() error {
|
||||
if target.Path == "" {
|
||||
return fmt.Errorf("target %d: empty path", i)
|
||||
}
|
||||
|
||||
if !target.IsFile && target.Pattern == "" {
|
||||
return fmt.Errorf("target %d: pattern required for directory monitoring", i)
|
||||
}
|
||||
|
||||
// SECURITY: Validate paths don't contain directory traversal
|
||||
if strings.Contains(target.Path, "..") {
|
||||
return fmt.Errorf("target %d: path contains directory traversal", i)
|
||||
}
|
||||
}
|
||||
|
||||
if c.Stream.BufferSize < 1 {
|
||||
return fmt.Errorf("buffer size must be positive: %d", c.Stream.BufferSize)
|
||||
if c.TCPServer.Enabled {
|
||||
if c.TCPServer.Port < 1 || c.TCPServer.Port > 65535 {
|
||||
return fmt.Errorf("invalid TCP port: %d", c.TCPServer.Port)
|
||||
}
|
||||
if c.TCPServer.BufferSize < 1 {
|
||||
return fmt.Errorf("TCP buffer size must be positive: %d", c.TCPServer.BufferSize)
|
||||
}
|
||||
}
|
||||
|
||||
if c.Stream.RateLimit.Enabled {
|
||||
if c.Stream.RateLimit.RequestsPerSecond < 1 {
|
||||
return fmt.Errorf("rate limit requests per second must be positive: %d",
|
||||
c.Stream.RateLimit.RequestsPerSecond)
|
||||
if c.HTTPServer.Enabled {
|
||||
if c.HTTPServer.Port < 1 || c.HTTPServer.Port > 65535 {
|
||||
return fmt.Errorf("invalid HTTP port: %d", c.HTTPServer.Port)
|
||||
}
|
||||
if c.Stream.RateLimit.BurstSize < 1 {
|
||||
return fmt.Errorf("rate limit burst size must be positive: %d",
|
||||
c.Stream.RateLimit.BurstSize)
|
||||
if c.HTTPServer.BufferSize < 1 {
|
||||
return fmt.Errorf("HTTP buffer size must be positive: %d", c.HTTPServer.BufferSize)
|
||||
}
|
||||
if c.Stream.RateLimit.CleanupIntervalS < 1 {
|
||||
return fmt.Errorf("rate limit cleanup interval must be positive: %d",
|
||||
c.Stream.RateLimit.CleanupIntervalS)
|
||||
}
|
||||
|
||||
if c.TCPServer.Enabled && c.TCPServer.Heartbeat.Enabled {
|
||||
if c.TCPServer.Heartbeat.IntervalSeconds < 1 {
|
||||
return fmt.Errorf("TCP heartbeat interval must be positive: %d", c.TCPServer.Heartbeat.IntervalSeconds)
|
||||
}
|
||||
if c.TCPServer.Heartbeat.Format != "json" && c.TCPServer.Heartbeat.Format != "comment" {
|
||||
return fmt.Errorf("TCP heartbeat format must be 'json' or 'comment': %s", c.TCPServer.Heartbeat.Format)
|
||||
}
|
||||
}
|
||||
|
||||
if c.HTTPServer.Enabled && c.HTTPServer.Heartbeat.Enabled {
|
||||
if c.HTTPServer.Heartbeat.IntervalSeconds < 1 {
|
||||
return fmt.Errorf("HTTP heartbeat interval must be positive: %d", c.HTTPServer.Heartbeat.IntervalSeconds)
|
||||
}
|
||||
if c.HTTPServer.Heartbeat.Format != "json" && c.HTTPServer.Heartbeat.Format != "comment" {
|
||||
return fmt.Errorf("HTTP heartbeat format must be 'json' or 'comment': %s", c.HTTPServer.Heartbeat.Format)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user