v0.7.0 major configuration and sub-command restructuring, not tested, docs and default config outdated
This commit is contained in:
@@ -1,81 +0,0 @@
|
||||
// FILE: logwisp/src/internal/config/auth.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type AuthConfig struct {
|
||||
// Authentication type: "none", "basic", "scram", "bearer", "mtls"
|
||||
Type string `toml:"type"`
|
||||
|
||||
BasicAuth *BasicAuthConfig `toml:"basic_auth"`
|
||||
ScramAuth *ScramAuthConfig `toml:"scram_auth"`
|
||||
BearerAuth *BearerAuthConfig `toml:"bearer_auth"`
|
||||
}
|
||||
|
||||
type BasicAuthConfig struct {
|
||||
Users []BasicAuthUser `toml:"users"`
|
||||
Realm string `toml:"realm"`
|
||||
}
|
||||
|
||||
type BasicAuthUser struct {
|
||||
Username string `toml:"username"`
|
||||
PasswordHash string `toml:"password_hash"` // Argon2
|
||||
}
|
||||
|
||||
type ScramAuthConfig struct {
|
||||
Users []ScramUser `toml:"users"`
|
||||
}
|
||||
|
||||
type ScramUser struct {
|
||||
Username string `toml:"username"`
|
||||
StoredKey string `toml:"stored_key"` // base64
|
||||
ServerKey string `toml:"server_key"` // base64
|
||||
Salt string `toml:"salt"` // base64
|
||||
ArgonTime uint32 `toml:"argon_time"`
|
||||
ArgonMemory uint32 `toml:"argon_memory"`
|
||||
ArgonThreads uint8 `toml:"argon_threads"`
|
||||
}
|
||||
|
||||
type BearerAuthConfig struct {
|
||||
// Static tokens
|
||||
Tokens []string `toml:"tokens"`
|
||||
|
||||
// TODO: Maybe future development
|
||||
// // JWT validation
|
||||
// JWT *JWTConfig `toml:"jwt"`
|
||||
}
|
||||
|
||||
// TODO: Maybe future development
|
||||
// type JWTConfig struct {
|
||||
// JWKSURL string `toml:"jwks_url"`
|
||||
// SigningKey string `toml:"signing_key"`
|
||||
// Issuer string `toml:"issuer"`
|
||||
// Audience string `toml:"audience"`
|
||||
// }
|
||||
|
||||
func validateAuth(pipelineName string, auth *AuthConfig) error {
|
||||
if auth == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
validTypes := map[string]bool{"none": true, "basic": true, "scram": true, "bearer": true, "mtls": true}
|
||||
if !validTypes[auth.Type] {
|
||||
return fmt.Errorf("pipeline '%s': invalid auth type: %s", pipelineName, auth.Type)
|
||||
}
|
||||
|
||||
if auth.Type == "basic" && auth.BasicAuth == nil {
|
||||
return fmt.Errorf("pipeline '%s': basic auth type specified but config missing", pipelineName)
|
||||
}
|
||||
|
||||
if auth.Type == "scram" && auth.ScramAuth == nil {
|
||||
return fmt.Errorf("pipeline '%s': scram auth type specified but config missing", pipelineName)
|
||||
}
|
||||
|
||||
if auth.Type == "bearer" && auth.BearerAuth == nil {
|
||||
return fmt.Errorf("pipeline '%s': bearer auth type specified but config missing", pipelineName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// FILE: logwisp/src/internal/config/config.go
|
||||
package config
|
||||
|
||||
// --- LogWisp Configuration Options ---
|
||||
|
||||
type Config struct {
|
||||
// Top-level flags for application control
|
||||
Background bool `toml:"background"`
|
||||
@@ -10,7 +12,6 @@ type Config struct {
|
||||
// Runtime behavior flags
|
||||
DisableStatusReporter bool `toml:"disable_status_reporter"`
|
||||
ConfigAutoReload bool `toml:"config_auto_reload"`
|
||||
ConfigSaveOnExit bool `toml:"config_save_on_exit"`
|
||||
|
||||
// Internal flag indicating demonized child process
|
||||
BackgroundDaemon bool `toml:"background-daemon"`
|
||||
@@ -21,4 +22,365 @@ type Config struct {
|
||||
// Existing fields
|
||||
Logging *LogConfig `toml:"logging"`
|
||||
Pipelines []PipelineConfig `toml:"pipelines"`
|
||||
}
|
||||
|
||||
// --- Logging Options ---
|
||||
|
||||
// Represents logging configuration for LogWisp
|
||||
type LogConfig struct {
|
||||
// Output mode: "file", "stdout", "stderr", "split", "all", "none"
|
||||
Output string `toml:"output"`
|
||||
|
||||
// Log level: "debug", "info", "warn", "error"
|
||||
Level string `toml:"level"`
|
||||
|
||||
// File output settings (when Output includes "file" or "all")
|
||||
File *LogFileConfig `toml:"file"`
|
||||
|
||||
// Console output settings
|
||||
Console *LogConsoleConfig `toml:"console"`
|
||||
}
|
||||
|
||||
type LogFileConfig struct {
|
||||
// Directory for log files
|
||||
Directory string `toml:"directory"`
|
||||
|
||||
// Base name for log files
|
||||
Name string `toml:"name"`
|
||||
|
||||
// Maximum size per log file in MB
|
||||
MaxSizeMB int64 `toml:"max_size_mb"`
|
||||
|
||||
// Maximum total size of all logs in MB
|
||||
MaxTotalSizeMB int64 `toml:"max_total_size_mb"`
|
||||
|
||||
// Log retention in hours (0 = disabled)
|
||||
RetentionHours float64 `toml:"retention_hours"`
|
||||
}
|
||||
|
||||
type LogConsoleConfig struct {
|
||||
// Target for console output: "stdout", "stderr", "split"
|
||||
// "split": info/debug to stdout, warn/error to stderr
|
||||
Target string `toml:"target"`
|
||||
|
||||
// Format: "txt" or "json"
|
||||
Format string `toml:"format"`
|
||||
}
|
||||
|
||||
// --- Pipeline Options ---
|
||||
|
||||
type PipelineConfig struct {
|
||||
Name string `toml:"name"`
|
||||
Sources []SourceConfig `toml:"sources"`
|
||||
RateLimit *RateLimitConfig `toml:"rate_limit"`
|
||||
Filters []FilterConfig `toml:"filters"`
|
||||
Format *FormatConfig `toml:"format"`
|
||||
|
||||
Sinks []SinkConfig `toml:"sinks"`
|
||||
// Auth *ServerAuthConfig `toml:"auth"` // Global auth for pipeline
|
||||
}
|
||||
|
||||
// Common configuration structs used across components
|
||||
|
||||
type NetLimitConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
MaxConnections int64 `toml:"max_connections"`
|
||||
RequestsPerSecond float64 `toml:"requests_per_second"`
|
||||
BurstSize int64 `toml:"burst_size"`
|
||||
ResponseMessage string `toml:"response_message"`
|
||||
ResponseCode int64 `toml:"response_code"` // Default: 429
|
||||
MaxConnectionsPerIP int64 `toml:"max_connections_per_ip"`
|
||||
MaxConnectionsPerUser int64 `toml:"max_connections_per_user"`
|
||||
MaxConnectionsPerToken int64 `toml:"max_connections_per_token"`
|
||||
MaxConnectionsTotal int64 `toml:"max_connections_total"`
|
||||
IPWhitelist []string `toml:"ip_whitelist"`
|
||||
IPBlacklist []string `toml:"ip_blacklist"`
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
CertFile string `toml:"cert_file"`
|
||||
KeyFile string `toml:"key_file"`
|
||||
CAFile string `toml:"ca_file"`
|
||||
ServerName string `toml:"server_name"` // for client verification
|
||||
SkipVerify bool `toml:"skip_verify"`
|
||||
|
||||
// Client certificate authentication
|
||||
ClientAuth bool `toml:"client_auth"`
|
||||
ClientCAFile string `toml:"client_ca_file"`
|
||||
VerifyClientCert bool `toml:"verify_client_cert"`
|
||||
|
||||
// TLS version constraints
|
||||
MinVersion string `toml:"min_version"` // "TLS1.2", "TLS1.3"
|
||||
MaxVersion string `toml:"max_version"`
|
||||
|
||||
// Cipher suites (comma-separated list)
|
||||
CipherSuites string `toml:"cipher_suites"`
|
||||
}
|
||||
|
||||
type HeartbeatConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Interval int64 `toml:"interval_ms"`
|
||||
IncludeTimestamp bool `toml:"include_timestamp"`
|
||||
IncludeStats bool `toml:"include_stats"`
|
||||
Format string `toml:"format"`
|
||||
}
|
||||
|
||||
type ClientAuthConfig struct {
|
||||
Type string `toml:"type"` // "none", "basic", "token", "scram"
|
||||
Username string `toml:"username"`
|
||||
Password string `toml:"password"`
|
||||
Token string `toml:"token"`
|
||||
}
|
||||
|
||||
// --- Source Options ---
|
||||
|
||||
type SourceConfig struct {
|
||||
Type string `toml:"type"`
|
||||
|
||||
// Polymorphic - only one populated based on type
|
||||
Directory *DirectorySourceOptions `toml:"directory,omitempty"`
|
||||
Stdin *StdinSourceOptions `toml:"stdin,omitempty"`
|
||||
HTTP *HTTPSourceOptions `toml:"http,omitempty"`
|
||||
TCP *TCPSourceOptions `toml:"tcp,omitempty"`
|
||||
}
|
||||
|
||||
type DirectorySourceOptions struct {
|
||||
Path string `toml:"path"`
|
||||
Pattern string `toml:"pattern"` // glob pattern
|
||||
CheckIntervalMS int64 `toml:"check_interval_ms"`
|
||||
Recursive bool `toml:"recursive"`
|
||||
FollowSymlinks bool `toml:"follow_symlinks"`
|
||||
DeleteAfterRead bool `toml:"delete_after_read"`
|
||||
MoveToDirectory string `toml:"move_to_directory"` // move after processing
|
||||
}
|
||||
|
||||
type StdinSourceOptions struct {
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
}
|
||||
|
||||
type HTTPSourceOptions struct {
|
||||
Host string `toml:"host"`
|
||||
Port int64 `toml:"port"`
|
||||
IngestPath string `toml:"ingest_path"`
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
MaxRequestBodySize int64 `toml:"max_body_size"`
|
||||
ReadTimeout int64 `toml:"read_timeout_ms"`
|
||||
WriteTimeout int64 `toml:"write_timeout_ms"`
|
||||
NetLimit *NetLimitConfig `toml:"net_limit"`
|
||||
TLS *TLSConfig `toml:"tls"`
|
||||
Auth *ServerAuthConfig `toml:"auth"`
|
||||
}
|
||||
|
||||
type TCPSourceOptions struct {
|
||||
Host string `toml:"host"`
|
||||
Port int64 `toml:"port"`
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
ReadTimeout int64 `toml:"read_timeout_ms"`
|
||||
KeepAlive bool `toml:"keep_alive"`
|
||||
KeepAlivePeriod int64 `toml:"keep_alive_period_ms"`
|
||||
NetLimit *NetLimitConfig `toml:"net_limit"`
|
||||
Auth *ServerAuthConfig `toml:"auth"`
|
||||
}
|
||||
|
||||
// --- Sink Options ---
|
||||
|
||||
type SinkConfig struct {
|
||||
Type string `toml:"type"`
|
||||
|
||||
// Polymorphic - only one populated based on type
|
||||
Console *ConsoleSinkOptions `toml:"console,omitempty"`
|
||||
File *FileSinkOptions `toml:"file,omitempty"`
|
||||
HTTP *HTTPSinkOptions `toml:"http,omitempty"`
|
||||
TCP *TCPSinkOptions `toml:"tcp,omitempty"`
|
||||
HTTPClient *HTTPClientSinkOptions `toml:"http_client,omitempty"`
|
||||
TCPClient *TCPClientSinkOptions `toml:"tcp_client,omitempty"`
|
||||
}
|
||||
|
||||
type ConsoleSinkOptions struct {
|
||||
Target string `toml:"target"` // "stdout", "stderr", "split"
|
||||
Colorize bool `toml:"colorize"`
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
}
|
||||
|
||||
type FileSinkOptions struct {
|
||||
Directory string `toml:"directory"`
|
||||
Name string `toml:"name"`
|
||||
// Extension string `toml:"extension"`
|
||||
MaxSizeMB int64 `toml:"max_size_mb"`
|
||||
MaxTotalSizeMB int64 `toml:"max_total_size_mb"`
|
||||
MinDiskFreeMB int64 `toml:"min_disk_free_mb"`
|
||||
RetentionHours float64 `toml:"retention_hours"`
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
FlushInterval int64 `toml:"flush_interval_ms"`
|
||||
}
|
||||
|
||||
type HTTPSinkOptions struct {
|
||||
Host string `toml:"host"`
|
||||
Port int64 `toml:"port"`
|
||||
StreamPath string `toml:"stream_path"`
|
||||
StatusPath string `toml:"status_path"`
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
WriteTimeout int64 `toml:"write_timeout_ms"`
|
||||
Heartbeat *HeartbeatConfig `toml:"heartbeat"`
|
||||
NetLimit *NetLimitConfig `toml:"net_limit"`
|
||||
TLS *TLSConfig `toml:"tls"`
|
||||
Auth *ServerAuthConfig `toml:"auth"`
|
||||
}
|
||||
|
||||
type TCPSinkOptions struct {
|
||||
Host string `toml:"host"`
|
||||
Port int64 `toml:"port"`
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
WriteTimeout int64 `toml:"write_timeout_ms"`
|
||||
KeepAlive bool `toml:"keep_alive"`
|
||||
KeepAlivePeriod int64 `toml:"keep_alive_period_ms"`
|
||||
Heartbeat *HeartbeatConfig `toml:"heartbeat"`
|
||||
NetLimit *NetLimitConfig `toml:"net_limit"`
|
||||
Auth *ServerAuthConfig `toml:"auth"`
|
||||
}
|
||||
|
||||
type HTTPClientSinkOptions struct {
|
||||
URL string `toml:"url"`
|
||||
Headers map[string]string `toml:"headers"`
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
BatchSize int64 `toml:"batch_size"`
|
||||
BatchDelayMS int64 `toml:"batch_delay_ms"`
|
||||
Timeout int64 `toml:"timeout_seconds"`
|
||||
MaxRetries int64 `toml:"max_retries"`
|
||||
RetryDelayMS int64 `toml:"retry_delay_ms"`
|
||||
RetryBackoff float64 `toml:"retry_backoff"`
|
||||
InsecureSkipVerify bool `toml:"insecure_skip_verify"`
|
||||
TLS *TLSConfig `toml:"tls"`
|
||||
Auth *ClientAuthConfig `toml:"auth"`
|
||||
}
|
||||
|
||||
type TCPClientSinkOptions struct {
|
||||
Host string `toml:"host"`
|
||||
Port int64 `toml:"port"`
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
DialTimeout int64 `toml:"dial_timeout_seconds"`
|
||||
WriteTimeout int64 `toml:"write_timeout_seconds"`
|
||||
ReadTimeout int64 `toml:"read_timeout_seconds"`
|
||||
KeepAlive int64 `toml:"keep_alive_seconds"`
|
||||
ReconnectDelayMS int64 `toml:"reconnect_delay_ms"`
|
||||
MaxReconnectDelayMS int64 `toml:"max_reconnect_delay_ms"`
|
||||
ReconnectBackoff float64 `toml:"reconnect_backoff"`
|
||||
Auth *ClientAuthConfig `toml:"auth"`
|
||||
}
|
||||
|
||||
// --- Rate Limit Options ---
|
||||
|
||||
// Defines the action to take when a rate limit is exceeded.
|
||||
type RateLimitPolicy int
|
||||
|
||||
const (
|
||||
// PolicyPass allows all logs through, effectively disabling the limiter.
|
||||
PolicyPass RateLimitPolicy = iota
|
||||
// PolicyDrop drops logs that exceed the rate limit.
|
||||
PolicyDrop
|
||||
)
|
||||
|
||||
// Defines the configuration for pipeline-level rate limiting.
|
||||
type RateLimitConfig struct {
|
||||
// Rate is the number of log entries allowed per second. Default: 0 (disabled).
|
||||
Rate float64 `toml:"rate"`
|
||||
// Burst is the maximum number of log entries that can be sent in a short burst. Defaults to the Rate.
|
||||
Burst float64 `toml:"burst"`
|
||||
// Policy defines the action to take when the limit is exceeded. "pass" or "drop".
|
||||
Policy string `toml:"policy"`
|
||||
// MaxEntrySizeBytes is the maximum allowed size for a single log entry. 0 = no limit.
|
||||
MaxEntrySizeBytes int64 `toml:"max_entry_size_bytes"`
|
||||
}
|
||||
|
||||
// --- Filter Options ---
|
||||
|
||||
// Represents the filter type
|
||||
type FilterType string
|
||||
|
||||
const (
|
||||
FilterTypeInclude FilterType = "include" // Whitelist - only matching logs pass
|
||||
FilterTypeExclude FilterType = "exclude" // Blacklist - matching logs are dropped
|
||||
)
|
||||
|
||||
// Represents how multiple patterns are combined
|
||||
type FilterLogic string
|
||||
|
||||
const (
|
||||
FilterLogicOr FilterLogic = "or" // Match any pattern
|
||||
FilterLogicAnd FilterLogic = "and" // Match all patterns
|
||||
)
|
||||
|
||||
// Represents filter configuration
|
||||
type FilterConfig struct {
|
||||
Type FilterType `toml:"type"`
|
||||
Logic FilterLogic `toml:"logic"`
|
||||
Patterns []string `toml:"patterns"`
|
||||
}
|
||||
|
||||
// --- Formatter Options ---
|
||||
|
||||
type FormatConfig struct {
|
||||
// Format configuration - polymorphic like sources/sinks
|
||||
Type string `toml:"type"` // "json", "text", "raw"
|
||||
|
||||
// Only one will be populated based on format type
|
||||
JSONFormatOptions *JSONFormatterOptions `toml:"json_format,omitempty"`
|
||||
TextFormatOptions *TextFormatterOptions `toml:"text_format,omitempty"`
|
||||
RawFormatOptions *RawFormatterOptions `toml:"raw_format,omitempty"`
|
||||
}
|
||||
|
||||
type JSONFormatterOptions struct {
|
||||
Pretty bool `toml:"pretty"`
|
||||
TimestampField string `toml:"timestamp_field"`
|
||||
LevelField string `toml:"level_field"`
|
||||
MessageField string `toml:"message_field"`
|
||||
SourceField string `toml:"source_field"`
|
||||
}
|
||||
|
||||
type TextFormatterOptions struct {
|
||||
Template string `toml:"template"`
|
||||
TimestampFormat string `toml:"timestamp_format"`
|
||||
}
|
||||
|
||||
type RawFormatterOptions struct {
|
||||
AddNewLine bool `toml:"add_new_line"`
|
||||
}
|
||||
|
||||
// --- Server-side Auth (for sources) ---
|
||||
|
||||
type BasicAuthConfig struct {
|
||||
Users []BasicAuthUser `toml:"users"`
|
||||
Realm string `toml:"realm"`
|
||||
}
|
||||
|
||||
type BasicAuthUser struct {
|
||||
Username string `toml:"username"`
|
||||
PasswordHash string `toml:"password_hash"` // Argon2
|
||||
}
|
||||
|
||||
type ScramAuthConfig struct {
|
||||
Users []ScramUser `toml:"users"`
|
||||
}
|
||||
|
||||
type ScramUser struct {
|
||||
Username string `toml:"username"`
|
||||
StoredKey string `toml:"stored_key"` // base64
|
||||
ServerKey string `toml:"server_key"` // base64
|
||||
Salt string `toml:"salt"` // base64
|
||||
ArgonTime uint32 `toml:"argon_time"`
|
||||
ArgonMemory uint32 `toml:"argon_memory"`
|
||||
ArgonThreads uint8 `toml:"argon_threads"`
|
||||
}
|
||||
|
||||
type TokenAuthConfig struct {
|
||||
Tokens []string `toml:"tokens"`
|
||||
}
|
||||
|
||||
// Server auth wrapper (for sources accepting connections)
|
||||
type ServerAuthConfig struct {
|
||||
Type string `toml:"type"` // "none", "basic", "token", "scram"
|
||||
Basic *BasicAuthConfig `toml:"basic,omitempty"`
|
||||
Token *TokenAuthConfig `toml:"token,omitempty"`
|
||||
Scram *ScramAuthConfig `toml:"scram,omitempty"`
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
// FILE: logwisp/src/internal/config/filter.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Represents the filter type
|
||||
type FilterType string
|
||||
|
||||
const (
|
||||
FilterTypeInclude FilterType = "include" // Whitelist - only matching logs pass
|
||||
FilterTypeExclude FilterType = "exclude" // Blacklist - matching logs are dropped
|
||||
)
|
||||
|
||||
// Represents how multiple patterns are combined
|
||||
type FilterLogic string
|
||||
|
||||
const (
|
||||
FilterLogicOr FilterLogic = "or" // Match any pattern
|
||||
FilterLogicAnd FilterLogic = "and" // Match all patterns
|
||||
)
|
||||
|
||||
// Represents filter configuration
|
||||
type FilterConfig struct {
|
||||
Type FilterType `toml:"type"`
|
||||
Logic FilterLogic `toml:"logic"`
|
||||
Patterns []string `toml:"patterns"`
|
||||
}
|
||||
|
||||
func validateFilter(pipelineName string, filterIndex int, cfg *FilterConfig) error {
|
||||
// Validate filter type
|
||||
switch cfg.Type {
|
||||
case FilterTypeInclude, FilterTypeExclude, "":
|
||||
// Valid types
|
||||
default:
|
||||
return fmt.Errorf("pipeline '%s' filter[%d]: invalid type '%s' (must be 'include' or 'exclude')",
|
||||
pipelineName, filterIndex, cfg.Type)
|
||||
}
|
||||
|
||||
// Validate filter logic
|
||||
switch cfg.Logic {
|
||||
case FilterLogicOr, FilterLogicAnd, "":
|
||||
// Valid logic
|
||||
default:
|
||||
return fmt.Errorf("pipeline '%s' filter[%d]: invalid logic '%s' (must be 'or' or 'and')",
|
||||
pipelineName, filterIndex, cfg.Logic)
|
||||
}
|
||||
|
||||
// Empty patterns is valid - passes everything
|
||||
if len(cfg.Patterns) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate regex patterns
|
||||
for i, pattern := range cfg.Patterns {
|
||||
if _, err := regexp.Compile(pattern); err != nil {
|
||||
return fmt.Errorf("pipeline '%s' filter[%d] pattern[%d] '%s': invalid regex: %w",
|
||||
pipelineName, filterIndex, i, pattern, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// FILE: logwisp/src/internal/config/ratelimit.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Defines the action to take when a rate limit is exceeded.
|
||||
type RateLimitPolicy int
|
||||
|
||||
const (
|
||||
// PolicyPass allows all logs through, effectively disabling the limiter.
|
||||
PolicyPass RateLimitPolicy = iota
|
||||
// PolicyDrop drops logs that exceed the rate limit.
|
||||
PolicyDrop
|
||||
)
|
||||
|
||||
// Defines the configuration for pipeline-level rate limiting.
|
||||
type RateLimitConfig struct {
|
||||
// Rate is the number of log entries allowed per second. Default: 0 (disabled).
|
||||
Rate float64 `toml:"rate"`
|
||||
// Burst is the maximum number of log entries that can be sent in a short burst. Defaults to the Rate.
|
||||
Burst float64 `toml:"burst"`
|
||||
// Policy defines the action to take when the limit is exceeded. "pass" or "drop".
|
||||
Policy string `toml:"policy"`
|
||||
// MaxEntrySizeBytes is the maximum allowed size for a single log entry. 0 = no limit.
|
||||
MaxEntrySizeBytes int64 `toml:"max_entry_size_bytes"`
|
||||
}
|
||||
|
||||
func validateRateLimit(pipelineName string, cfg *RateLimitConfig) error {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cfg.Rate < 0 {
|
||||
return fmt.Errorf("pipeline '%s': rate limit rate cannot be negative", pipelineName)
|
||||
}
|
||||
|
||||
if cfg.Burst < 0 {
|
||||
return fmt.Errorf("pipeline '%s': rate limit burst cannot be negative", pipelineName)
|
||||
}
|
||||
|
||||
if cfg.MaxEntrySizeBytes < 0 {
|
||||
return fmt.Errorf("pipeline '%s': max entry size bytes cannot be negative", pipelineName)
|
||||
}
|
||||
|
||||
// Validate policy
|
||||
switch strings.ToLower(cfg.Policy) {
|
||||
case "", "pass", "drop":
|
||||
// Valid policies
|
||||
default:
|
||||
return fmt.Errorf("pipeline '%s': invalid rate limit policy '%s' (must be 'pass' or 'drop')",
|
||||
pipelineName, cfg.Policy)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
lconfig "github.com/lixenwraith/config"
|
||||
)
|
||||
|
||||
var configManager *lconfig.Config
|
||||
|
||||
func defaults() *Config {
|
||||
return &Config{
|
||||
// Top-level flag defaults
|
||||
@@ -21,41 +23,46 @@ func defaults() *Config {
|
||||
// Runtime behavior defaults
|
||||
DisableStatusReporter: false,
|
||||
ConfigAutoReload: false,
|
||||
ConfigSaveOnExit: false,
|
||||
|
||||
// Child process indicator
|
||||
BackgroundDaemon: false,
|
||||
|
||||
// Existing defaults
|
||||
Logging: DefaultLogConfig(),
|
||||
Logging: &LogConfig{
|
||||
Output: "stdout",
|
||||
Level: "info",
|
||||
File: &LogFileConfig{
|
||||
Directory: "./log",
|
||||
Name: "logwisp",
|
||||
MaxSizeMB: 100,
|
||||
MaxTotalSizeMB: 1000,
|
||||
RetentionHours: 168, // 7 days
|
||||
},
|
||||
Console: &LogConsoleConfig{
|
||||
Target: "stdout",
|
||||
Format: "txt",
|
||||
},
|
||||
},
|
||||
Pipelines: []PipelineConfig{
|
||||
{
|
||||
Name: "default",
|
||||
Sources: []SourceConfig{
|
||||
{
|
||||
Type: "directory",
|
||||
Options: map[string]any{
|
||||
"path": "./",
|
||||
"pattern": "*.log",
|
||||
"check_interval_ms": int64(100),
|
||||
Directory: &DirectorySourceOptions{
|
||||
Path: "./",
|
||||
Pattern: "*.log",
|
||||
CheckIntervalMS: int64(100),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sinks: []SinkConfig{
|
||||
{
|
||||
Type: "http",
|
||||
Options: map[string]any{
|
||||
"port": int64(8080),
|
||||
"buffer_size": int64(1000),
|
||||
"stream_path": "/stream",
|
||||
"status_path": "/status",
|
||||
"heartbeat": map[string]any{
|
||||
"enabled": true,
|
||||
"interval_seconds": int64(30),
|
||||
"include_timestamp": true,
|
||||
"include_stats": false,
|
||||
"format": "comment",
|
||||
},
|
||||
Type: "console",
|
||||
Console: &ConsoleSinkOptions{
|
||||
Target: "stdout",
|
||||
Colorize: false,
|
||||
BufferSize: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -68,18 +75,30 @@ func defaults() *Config {
|
||||
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{}
|
||||
|
||||
// The builder now handles loading, populating the target struct, and validation
|
||||
cfg, err := lconfig.NewBuilder().
|
||||
WithDefaults(defaults()).
|
||||
WithEnvPrefix("LOGWISP_").
|
||||
WithEnvTransform(customEnvTransform).
|
||||
WithArgs(args).
|
||||
WithFile(configPath).
|
||||
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 {
|
||||
@@ -88,42 +107,28 @@ func Load(args []string) (*Config, error) {
|
||||
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
|
||||
// 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 config: %w", err)
|
||||
return nil, fmt.Errorf("failed to load or validate config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Scan into final config struct - using new interface
|
||||
finalConfig := &Config{}
|
||||
if err := cfg.Scan(finalConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan config: %w", err)
|
||||
// Store the config file path for hot reload
|
||||
finalConfig.ConfigFile = configPath
|
||||
|
||||
// Store the manager for hot reload
|
||||
if cfg != nil {
|
||||
configManager = cfg
|
||||
}
|
||||
|
||||
// Set config file path if it exists
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
finalConfig.ConfigFile = configPath
|
||||
}
|
||||
|
||||
// Ensure critical fields are not nil
|
||||
if finalConfig.Logging == nil {
|
||||
finalConfig.Logging = DefaultLogConfig()
|
||||
}
|
||||
|
||||
// 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()
|
||||
return finalConfig, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if arg == "-c" {
|
||||
return args[i+1], true
|
||||
}
|
||||
if strings.HasPrefix(arg, "--config=") {
|
||||
@@ -160,38 +165,4 @@ func customEnvTransform(path string) string {
|
||||
env = strings.ToUpper(env)
|
||||
// env = "LOGWISP_" + env // already added by WithEnvPrefix
|
||||
return env
|
||||
}
|
||||
|
||||
// Centralizes console target configuration
|
||||
func applyConsoleTargetOverrides(cfg *Config) error {
|
||||
// Check environment variable for console target override
|
||||
consoleTarget := os.Getenv("LOGWISP_CONSOLE_TARGET")
|
||||
if consoleTarget == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate console target value
|
||||
validTargets := map[string]bool{
|
||||
"stdout": true,
|
||||
"stderr": true,
|
||||
"split": true,
|
||||
}
|
||||
if !validTargets[consoleTarget] {
|
||||
return fmt.Errorf("invalid LOGWISP_CONSOLE_TARGET value: %s", consoleTarget)
|
||||
}
|
||||
|
||||
// Apply to console sinks
|
||||
for i, pipeline := range cfg.Pipelines {
|
||||
for j, sink := range pipeline.Sinks {
|
||||
if sink.Type == "console" {
|
||||
if sink.Options == nil {
|
||||
cfg.Pipelines[i].Sinks[j].Options = make(map[string]any)
|
||||
}
|
||||
// Set target for split mode handling
|
||||
cfg.Pipelines[i].Sinks[j].Options["target"] = consoleTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// FILE: logwisp/src/internal/config/logging.go
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Represents logging configuration for LogWisp
|
||||
type LogConfig struct {
|
||||
// Output mode: "file", "stdout", "stderr", "split", "all", "none"
|
||||
Output string `toml:"output"`
|
||||
|
||||
// Log level: "debug", "info", "warn", "error"
|
||||
Level string `toml:"level"`
|
||||
|
||||
// File output settings (when Output includes "file" or "all")
|
||||
File *LogFileConfig `toml:"file"`
|
||||
|
||||
// Console output settings
|
||||
Console *LogConsoleConfig `toml:"console"`
|
||||
}
|
||||
|
||||
type LogFileConfig struct {
|
||||
// Directory for log files
|
||||
Directory string `toml:"directory"`
|
||||
|
||||
// Base name for log files
|
||||
Name string `toml:"name"`
|
||||
|
||||
// Maximum size per log file in MB
|
||||
MaxSizeMB int64 `toml:"max_size_mb"`
|
||||
|
||||
// Maximum total size of all logs in MB
|
||||
MaxTotalSizeMB int64 `toml:"max_total_size_mb"`
|
||||
|
||||
// Log retention in hours (0 = disabled)
|
||||
RetentionHours float64 `toml:"retention_hours"`
|
||||
}
|
||||
|
||||
type LogConsoleConfig struct {
|
||||
// Target for console output: "stdout", "stderr", "split"
|
||||
// "split": info/debug to stdout, warn/error to stderr
|
||||
Target string `toml:"target"`
|
||||
|
||||
// Format: "txt" or "json"
|
||||
Format string `toml:"format"`
|
||||
}
|
||||
|
||||
// Returns sensible logging defaults
|
||||
func DefaultLogConfig() *LogConfig {
|
||||
return &LogConfig{
|
||||
Output: "stdout",
|
||||
Level: "info",
|
||||
File: &LogFileConfig{
|
||||
Directory: "./log",
|
||||
Name: "logwisp",
|
||||
MaxSizeMB: 100,
|
||||
MaxTotalSizeMB: 1000,
|
||||
RetentionHours: 168, // 7 days
|
||||
},
|
||||
Console: &LogConsoleConfig{
|
||||
Target: "stdout",
|
||||
Format: "txt",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func validateLogConfig(cfg *LogConfig) error {
|
||||
validOutputs := map[string]bool{
|
||||
"file": true, "stdout": true, "stderr": true,
|
||||
"split": true, "all": true, "none": true,
|
||||
}
|
||||
if !validOutputs[cfg.Output] {
|
||||
return fmt.Errorf("invalid log output mode: %s", cfg.Output)
|
||||
}
|
||||
|
||||
validLevels := map[string]bool{
|
||||
"debug": true, "info": true, "warn": true, "error": true,
|
||||
}
|
||||
if !validLevels[cfg.Level] {
|
||||
return fmt.Errorf("invalid log level: %s", cfg.Level)
|
||||
}
|
||||
|
||||
if cfg.Console != nil {
|
||||
validTargets := map[string]bool{
|
||||
"stdout": true, "stderr": true, "split": true,
|
||||
}
|
||||
if !validTargets[cfg.Console.Target] {
|
||||
return fmt.Errorf("invalid console target: %s", cfg.Console.Target)
|
||||
}
|
||||
|
||||
validFormats := map[string]bool{
|
||||
"txt": true, "json": true, "": true,
|
||||
}
|
||||
if !validFormats[cfg.Console.Format] {
|
||||
return fmt.Errorf("invalid console format: %s", cfg.Console.Format)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
// FILE: logwisp/src/internal/config/pipeline.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Represents a data processing pipeline
|
||||
type PipelineConfig struct {
|
||||
// Pipeline identifier (used in logs and metrics)
|
||||
Name string `toml:"name"`
|
||||
|
||||
// Data sources for this pipeline
|
||||
Sources []SourceConfig `toml:"sources"`
|
||||
|
||||
// Rate limiting
|
||||
RateLimit *RateLimitConfig `toml:"rate_limit"`
|
||||
|
||||
// Filter configuration
|
||||
Filters []FilterConfig `toml:"filters"`
|
||||
|
||||
// Log formatting configuration
|
||||
Format string `toml:"format"`
|
||||
FormatOptions map[string]any `toml:"format_options"`
|
||||
|
||||
// Output sinks for this pipeline
|
||||
Sinks []SinkConfig `toml:"sinks"`
|
||||
|
||||
// Authentication/Authorization (applies to network sinks)
|
||||
Auth *AuthConfig `toml:"auth"`
|
||||
}
|
||||
|
||||
// Represents an input data source
|
||||
type SourceConfig struct {
|
||||
// Source type
|
||||
Type string `toml:"type"`
|
||||
|
||||
// Type-specific configuration options
|
||||
Options map[string]any `toml:"options"`
|
||||
}
|
||||
|
||||
// Represents an output destination
|
||||
type SinkConfig struct {
|
||||
// Sink type
|
||||
Type string `toml:"type"`
|
||||
|
||||
// Type-specific configuration options
|
||||
Options map[string]any `toml:"options"`
|
||||
}
|
||||
|
||||
func validateSource(pipelineName string, sourceIndex int, cfg *SourceConfig) error {
|
||||
if cfg.Type == "" {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: missing type", pipelineName, sourceIndex)
|
||||
}
|
||||
|
||||
switch cfg.Type {
|
||||
case "directory":
|
||||
// Validate path
|
||||
path, ok := cfg.Options["path"].(string)
|
||||
if !ok || path == "" {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: directory source requires 'path' option",
|
||||
pipelineName, sourceIndex)
|
||||
}
|
||||
|
||||
// Check for directory traversal
|
||||
if strings.Contains(path, "..") {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: path contains directory traversal",
|
||||
pipelineName, sourceIndex)
|
||||
}
|
||||
|
||||
// Validate pattern
|
||||
if pattern, ok := cfg.Options["pattern"].(string); ok && pattern != "" {
|
||||
// Try to compile as glob pattern (will be converted to regex internally)
|
||||
if strings.Count(pattern, "*") == 0 && strings.Count(pattern, "?") == 0 {
|
||||
// If no wildcards, ensure it's a valid filename
|
||||
if filepath.Base(pattern) != pattern {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: pattern contains path separators",
|
||||
pipelineName, sourceIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate check interval
|
||||
if interval, ok := cfg.Options["check_interval_ms"]; ok {
|
||||
if intVal, ok := interval.(int64); ok {
|
||||
if intVal < 10 {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: check interval too small: %d ms (min: 10ms)",
|
||||
pipelineName, sourceIndex, intVal)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: invalid check_interval_ms type",
|
||||
pipelineName, sourceIndex)
|
||||
}
|
||||
}
|
||||
|
||||
case "stdin":
|
||||
// Validate buffer size
|
||||
if bufSize, ok := cfg.Options["buffer_size"].(int64); ok {
|
||||
if bufSize < 1 {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: stdin buffer_size must be positive: %d",
|
||||
pipelineName, sourceIndex, bufSize)
|
||||
}
|
||||
}
|
||||
|
||||
case "http":
|
||||
// Validate host
|
||||
if host, ok := cfg.Options["host"].(string); ok && host != "" {
|
||||
if net.ParseIP(host) == nil {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: invalid IP address: %s",
|
||||
pipelineName, sourceIndex, host)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate port
|
||||
port, ok := cfg.Options["port"].(int64)
|
||||
if !ok || port < 1 || port > 65535 {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: invalid or missing HTTP port",
|
||||
pipelineName, sourceIndex)
|
||||
}
|
||||
|
||||
// Validate path
|
||||
if path, ok := cfg.Options["ingest_path"].(string); ok {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: ingest path must start with /: %s",
|
||||
pipelineName, sourceIndex, path)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate net_limit
|
||||
if nl, ok := cfg.Options["net_limit"].(map[string]any); ok {
|
||||
if err := validateNetLimitOptions("HTTP source", pipelineName, sourceIndex, nl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Validate TLS
|
||||
if tls, ok := cfg.Options["tls"].(map[string]any); ok {
|
||||
if err := validateTLSOptions("HTTP source", pipelineName, sourceIndex, tls); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case "tcp":
|
||||
// Validate host
|
||||
if host, ok := cfg.Options["host"].(string); ok && host != "" {
|
||||
if net.ParseIP(host) == nil {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: invalid IP address: %s",
|
||||
pipelineName, sourceIndex, host)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate port
|
||||
port, ok := cfg.Options["port"].(int64)
|
||||
if !ok || port < 1 || port > 65535 {
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: invalid or missing TCP port",
|
||||
pipelineName, sourceIndex)
|
||||
}
|
||||
|
||||
// Validate net_limit
|
||||
if nl, ok := cfg.Options["net_limit"].(map[string]any); ok {
|
||||
if err := validateNetLimitOptions("TCP source", pipelineName, sourceIndex, nl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("pipeline '%s' source[%d]: unknown source type '%s'",
|
||||
pipelineName, sourceIndex, cfg.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSink(pipelineName string, sinkIndex int, cfg *SinkConfig, allPorts map[int64]string) error {
|
||||
if cfg.Type == "" {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: missing type", pipelineName, sinkIndex)
|
||||
}
|
||||
|
||||
switch cfg.Type {
|
||||
case "http":
|
||||
// Extract and validate HTTP configuration
|
||||
port, ok := cfg.Options["port"].(int64)
|
||||
if !ok || port < 1 || port > 65535 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: invalid or missing HTTP port",
|
||||
pipelineName, sinkIndex)
|
||||
}
|
||||
|
||||
// Validate host
|
||||
if host, ok := cfg.Options["host"].(string); ok && host != "" {
|
||||
if net.ParseIP(host) == nil {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: invalid IP address: %s",
|
||||
pipelineName, sinkIndex, host)
|
||||
}
|
||||
}
|
||||
|
||||
// Check port conflicts
|
||||
if existing, exists := allPorts[port]; exists {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: HTTP port %d already used by %s",
|
||||
pipelineName, sinkIndex, port, existing)
|
||||
}
|
||||
allPorts[port] = fmt.Sprintf("%s-http[%d]", pipelineName, sinkIndex)
|
||||
|
||||
// Validate buffer size
|
||||
if bufSize, ok := cfg.Options["buffer_size"].(int64); ok {
|
||||
if bufSize < 1 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: HTTP buffer size must be positive: %d",
|
||||
pipelineName, sinkIndex, bufSize)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate paths
|
||||
if streamPath, ok := cfg.Options["stream_path"].(string); ok {
|
||||
if !strings.HasPrefix(streamPath, "/") {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: stream path must start with /: %s",
|
||||
pipelineName, sinkIndex, streamPath)
|
||||
}
|
||||
}
|
||||
|
||||
if statusPath, ok := cfg.Options["status_path"].(string); ok {
|
||||
if !strings.HasPrefix(statusPath, "/") {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: status path must start with /: %s",
|
||||
pipelineName, sinkIndex, statusPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate heartbeat
|
||||
if hb, ok := cfg.Options["heartbeat"].(map[string]any); ok {
|
||||
if err := validateHeartbeatOptions("HTTP", pipelineName, sinkIndex, hb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Validate TLS if present
|
||||
if tls, ok := cfg.Options["tls"].(map[string]any); ok {
|
||||
if err := validateTLSOptions("HTTP", pipelineName, sinkIndex, tls); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Validate net limit
|
||||
if nl, ok := cfg.Options["net_limit"].(map[string]any); ok {
|
||||
if err := validateNetLimitOptions("HTTP", pipelineName, sinkIndex, nl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case "tcp":
|
||||
// Extract and validate TCP configuration
|
||||
port, ok := cfg.Options["port"].(int64)
|
||||
if !ok || port < 1 || port > 65535 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: invalid or missing TCP port",
|
||||
pipelineName, sinkIndex)
|
||||
}
|
||||
|
||||
// Validate host
|
||||
if host, ok := cfg.Options["host"].(string); ok && host != "" {
|
||||
if net.ParseIP(host) == nil {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: invalid IP address: %s",
|
||||
pipelineName, sinkIndex, host)
|
||||
}
|
||||
}
|
||||
|
||||
// Check port conflicts
|
||||
if existing, exists := allPorts[port]; exists {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: TCP port %d already used by %s",
|
||||
pipelineName, sinkIndex, port, existing)
|
||||
}
|
||||
allPorts[port] = fmt.Sprintf("%s-tcp[%d]", pipelineName, sinkIndex)
|
||||
|
||||
// Validate buffer size
|
||||
if bufSize, ok := cfg.Options["buffer_size"].(int64); ok {
|
||||
if bufSize < 1 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: TCP buffer size must be positive: %d",
|
||||
pipelineName, sinkIndex, bufSize)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate heartbeat
|
||||
if hb, ok := cfg.Options["heartbeat"].(map[string]any); ok {
|
||||
if err := validateHeartbeatOptions("TCP", pipelineName, sinkIndex, hb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Validate net limit
|
||||
if nl, ok := cfg.Options["net_limit"].(map[string]any); ok {
|
||||
if err := validateNetLimitOptions("TCP", pipelineName, sinkIndex, nl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case "http_client":
|
||||
// Validate URL
|
||||
urlStr, ok := cfg.Options["url"].(string)
|
||||
if !ok || urlStr == "" {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: http_client sink requires 'url' option",
|
||||
pipelineName, sinkIndex)
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: invalid URL: %w",
|
||||
pipelineName, sinkIndex, err)
|
||||
}
|
||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: URL must use http or https scheme",
|
||||
pipelineName, sinkIndex)
|
||||
}
|
||||
|
||||
// Validate batch size
|
||||
if batchSize, ok := cfg.Options["batch_size"].(int64); ok {
|
||||
if batchSize < 1 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: batch_size must be positive: %d",
|
||||
pipelineName, sinkIndex, batchSize)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate timeout
|
||||
if timeout, ok := cfg.Options["timeout_seconds"].(int64); ok {
|
||||
if timeout < 1 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: timeout_seconds must be positive: %d",
|
||||
pipelineName, sinkIndex, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
case "tcp_client":
|
||||
// Added validation for TCP client sink
|
||||
// Validate address
|
||||
address, ok := cfg.Options["address"].(string)
|
||||
if !ok || address == "" {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: tcp_client sink requires 'address' option",
|
||||
pipelineName, sinkIndex)
|
||||
}
|
||||
|
||||
// Validate address format
|
||||
_, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: invalid address format (expected host:port): %w",
|
||||
pipelineName, sinkIndex, err)
|
||||
}
|
||||
|
||||
// Validate timeouts
|
||||
if dialTimeout, ok := cfg.Options["dial_timeout_seconds"].(int64); ok {
|
||||
if dialTimeout < 1 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: dial_timeout_seconds must be positive: %d",
|
||||
pipelineName, sinkIndex, dialTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
if writeTimeout, ok := cfg.Options["write_timeout_seconds"].(int64); ok {
|
||||
if writeTimeout < 1 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: write_timeout_seconds must be positive: %d",
|
||||
pipelineName, sinkIndex, writeTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
case "file":
|
||||
// Validate directory
|
||||
directory, ok := cfg.Options["directory"].(string)
|
||||
if !ok || directory == "" {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: file sink requires 'directory' option",
|
||||
pipelineName, sinkIndex)
|
||||
}
|
||||
|
||||
// Validate filename
|
||||
name, ok := cfg.Options["name"].(string)
|
||||
if !ok || name == "" {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: file sink requires 'name' option",
|
||||
pipelineName, sinkIndex)
|
||||
}
|
||||
|
||||
// Validate size options
|
||||
if maxSize, ok := cfg.Options["max_size_mb"].(int64); ok {
|
||||
if maxSize < 1 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: max_size_mb must be positive: %d",
|
||||
pipelineName, sinkIndex, maxSize)
|
||||
}
|
||||
}
|
||||
|
||||
if maxTotalSize, ok := cfg.Options["max_total_size_mb"].(int64); ok {
|
||||
if maxTotalSize < 0 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: max_total_size_mb cannot be negative: %d",
|
||||
pipelineName, sinkIndex, maxTotalSize)
|
||||
}
|
||||
}
|
||||
|
||||
if minDiskFree, ok := cfg.Options["min_disk_free_mb"].(int64); ok {
|
||||
if minDiskFree < 0 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: min_disk_free_mb cannot be negative: %d",
|
||||
pipelineName, sinkIndex, minDiskFree)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate retention period
|
||||
if retention, ok := cfg.Options["retention_hours"].(float64); ok {
|
||||
if retention < 0 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: retention_hours cannot be negative: %f",
|
||||
pipelineName, sinkIndex, retention)
|
||||
}
|
||||
}
|
||||
|
||||
case "console":
|
||||
// No specific validation needed for console sinks
|
||||
|
||||
default:
|
||||
return fmt.Errorf("pipeline '%s' sink[%d]: unknown sink type '%s'",
|
||||
pipelineName, sinkIndex, cfg.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// FILE: logwisp/src/internal/config/saver.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
lconfig "github.com/lixenwraith/config"
|
||||
)
|
||||
|
||||
// Saves the configuration to the specified file path.
|
||||
func (c *Config) SaveToFile(path string) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("cannot save config: path is empty")
|
||||
}
|
||||
|
||||
// Create a temporary lconfig instance just for saving
|
||||
// This avoids the need to track lconfig throughout the application
|
||||
lcfg, err := lconfig.NewBuilder().
|
||||
WithFile(path).
|
||||
WithTarget(c).
|
||||
WithFileFormat("toml").
|
||||
Build()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config builder: %w", err)
|
||||
}
|
||||
|
||||
// Use lconfig's Save method which handles atomic writes
|
||||
if err := lcfg.Save(path); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
// FILE: logwisp/src/internal/config/server.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TCPConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Port int64 `toml:"port"`
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
|
||||
// Net limiting
|
||||
NetLimit *NetLimitConfig `toml:"net_limit"`
|
||||
|
||||
// Heartbeat
|
||||
Heartbeat *HeartbeatConfig `toml:"heartbeat"`
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Port int64 `toml:"port"`
|
||||
BufferSize int64 `toml:"buffer_size"`
|
||||
|
||||
// Endpoint paths
|
||||
StreamPath string `toml:"stream_path"`
|
||||
StatusPath string `toml:"status_path"`
|
||||
|
||||
// TLS Configuration
|
||||
TLS *TLSConfig `toml:"tls"`
|
||||
|
||||
// Nate limiting
|
||||
NetLimit *NetLimitConfig `toml:"net_limit"`
|
||||
|
||||
// Heartbeat
|
||||
Heartbeat *HeartbeatConfig `toml:"heartbeat"`
|
||||
}
|
||||
|
||||
type HeartbeatConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
IntervalSeconds int64 `toml:"interval_seconds"`
|
||||
IncludeTimestamp bool `toml:"include_timestamp"`
|
||||
IncludeStats bool `toml:"include_stats"`
|
||||
Format string `toml:"format"`
|
||||
}
|
||||
|
||||
type NetLimitConfig struct {
|
||||
// Enable net limiting
|
||||
Enabled bool `toml:"enabled"`
|
||||
|
||||
// IP Access Control Lists
|
||||
IPWhitelist []string `toml:"ip_whitelist"`
|
||||
IPBlacklist []string `toml:"ip_blacklist"`
|
||||
|
||||
// Requests per second per client
|
||||
RequestsPerSecond float64 `toml:"requests_per_second"`
|
||||
|
||||
// Burst size (token bucket)
|
||||
BurstSize int64 `toml:"burst_size"`
|
||||
|
||||
// Response when net limited
|
||||
ResponseCode int64 `toml:"response_code"` // Default: 429
|
||||
ResponseMessage string `toml:"response_message"` // Default: "Net limit exceeded"
|
||||
|
||||
// Connection limits
|
||||
MaxConnectionsPerIP int64 `toml:"max_connections_per_ip"`
|
||||
MaxConnectionsPerUser int64 `toml:"max_connections_per_user"`
|
||||
MaxConnectionsPerToken int64 `toml:"max_connections_per_token"`
|
||||
MaxConnectionsTotal int64 `toml:"max_connections_total"`
|
||||
}
|
||||
|
||||
func validateHeartbeatOptions(serverType, pipelineName string, sinkIndex int, hb map[string]any) error {
|
||||
if enabled, ok := hb["enabled"].(bool); ok && enabled {
|
||||
interval, ok := hb["interval_seconds"].(int64)
|
||||
if !ok || interval < 1 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: heartbeat interval must be positive",
|
||||
pipelineName, sinkIndex, serverType)
|
||||
}
|
||||
|
||||
if format, ok := hb["format"].(string); ok {
|
||||
if format != "json" && format != "comment" {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: heartbeat format must be 'json' or 'comment': %s",
|
||||
pipelineName, sinkIndex, serverType, format)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateNetLimitOptions(serverType, pipelineName string, sinkIndex int, nl map[string]any) error {
|
||||
if enabled, ok := nl["enabled"].(bool); !ok || !enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate IP lists if present
|
||||
if ipWhitelist, ok := nl["ip_whitelist"].([]any); ok {
|
||||
for i, entry := range ipWhitelist {
|
||||
entryStr, ok := entry.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := validateIPv4Entry(entryStr); err != nil {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: whitelist[%d] %v",
|
||||
pipelineName, sinkIndex, serverType, i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ipBlacklist, ok := nl["ip_blacklist"].([]any); ok {
|
||||
for i, entry := range ipBlacklist {
|
||||
entryStr, ok := entry.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := validateIPv4Entry(entryStr); err != nil {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: blacklist[%d] %v",
|
||||
pipelineName, sinkIndex, serverType, i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate requests per second
|
||||
rps, ok := nl["requests_per_second"].(float64)
|
||||
if !ok || rps <= 0 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: requests_per_second must be positive",
|
||||
pipelineName, sinkIndex, serverType)
|
||||
}
|
||||
|
||||
// Validate burst size
|
||||
burst, ok := nl["burst_size"].(int64)
|
||||
if !ok || burst < 1 {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: burst_size must be at least 1",
|
||||
pipelineName, sinkIndex, serverType)
|
||||
}
|
||||
|
||||
// Validate response code
|
||||
if respCode, ok := nl["response_code"].(int64); ok {
|
||||
if respCode > 0 && (respCode < 400 || respCode >= 600) {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: response_code must be 4xx or 5xx: %d",
|
||||
pipelineName, sinkIndex, serverType, respCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate connection limits
|
||||
maxPerIP, perIPOk := nl["max_connections_per_ip"].(int64)
|
||||
maxPerUser, perUserOk := nl["max_connections_per_user"].(int64)
|
||||
maxPerToken, perTokenOk := nl["max_connections_per_token"].(int64)
|
||||
maxTotal, totalOk := nl["max_connections_total"].(int64)
|
||||
|
||||
if perIPOk && perUserOk && perTokenOk && totalOk &&
|
||||
maxPerIP > 0 && maxPerUser > 0 && maxPerToken > 0 && maxTotal > 0 {
|
||||
if maxPerIP > maxTotal {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: max_connections_per_ip (%d) cannot exceed max_connections_total (%d)",
|
||||
pipelineName, sinkIndex, serverType, maxPerIP, maxTotal)
|
||||
}
|
||||
if maxPerUser > maxTotal {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: max_connections_per_user (%d) cannot exceed max_connections_total (%d)",
|
||||
pipelineName, sinkIndex, serverType, maxPerUser, maxTotal)
|
||||
}
|
||||
if maxPerToken > maxTotal {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: max_connections_per_token (%d) cannot exceed max_connections_total (%d)",
|
||||
pipelineName, sinkIndex, serverType, maxPerToken, maxTotal)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensures an IP or CIDR is IPv4
|
||||
func validateIPv4Entry(entry string) error {
|
||||
// Handle single IP
|
||||
if !strings.Contains(entry, "/") {
|
||||
ip := net.ParseIP(entry)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("invalid IP address: %s", entry)
|
||||
}
|
||||
if ip.To4() == nil {
|
||||
return fmt.Errorf("IPv6 not supported (IPv4-only): %s", entry)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle CIDR
|
||||
ipAddr, ipNet, err := net.ParseCIDR(entry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid CIDR: %s", entry)
|
||||
}
|
||||
|
||||
// Check if the IP is IPv4
|
||||
if ipAddr.To4() == nil {
|
||||
return fmt.Errorf("IPv6 CIDR not supported (IPv4-only): %s", entry)
|
||||
}
|
||||
|
||||
// Verify the network mask is appropriate for IPv4
|
||||
_, bits := ipNet.Mask.Size()
|
||||
if bits != 32 {
|
||||
return fmt.Errorf("invalid IPv4 CIDR mask (got %d bits, expected 32): %s", bits, entry)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// FILE: logwisp/src/internal/config/tls.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type TLSConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
CertFile string `toml:"cert_file"`
|
||||
KeyFile string `toml:"key_file"`
|
||||
|
||||
// Client certificate authentication
|
||||
ClientAuth bool `toml:"client_auth"`
|
||||
ClientCAFile string `toml:"client_ca_file"`
|
||||
VerifyClientCert bool `toml:"verify_client_cert"`
|
||||
|
||||
// Option to skip verification for clients
|
||||
InsecureSkipVerify bool `toml:"insecure_skip_verify"`
|
||||
|
||||
// CA file for client to trust specific server certificates
|
||||
CAFile string `toml:"ca_file"`
|
||||
|
||||
// TLS version constraints
|
||||
MinVersion string `toml:"min_version"` // "TLS1.2", "TLS1.3"
|
||||
MaxVersion string `toml:"max_version"`
|
||||
|
||||
// Cipher suites (comma-separated list)
|
||||
CipherSuites string `toml:"cipher_suites"`
|
||||
}
|
||||
|
||||
func validateTLSOptions(serverType, pipelineName string, sinkIndex int, tls map[string]any) error {
|
||||
if enabled, ok := tls["enabled"].(bool); ok && enabled {
|
||||
certFile, certOk := tls["cert_file"].(string)
|
||||
keyFile, keyOk := tls["key_file"].(string)
|
||||
|
||||
if !certOk || certFile == "" || !keyOk || keyFile == "" {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: TLS enabled but cert/key files not specified",
|
||||
pipelineName, sinkIndex, serverType)
|
||||
}
|
||||
|
||||
// Validate that certificate files exist and are readable
|
||||
if _, err := os.Stat(certFile); err != nil {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: cert_file is not accessible: %w",
|
||||
pipelineName, sinkIndex, serverType, err)
|
||||
}
|
||||
if _, err := os.Stat(keyFile); err != nil {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: key_file is not accessible: %w",
|
||||
pipelineName, sinkIndex, serverType, err)
|
||||
}
|
||||
|
||||
if clientAuth, ok := tls["client_auth"].(bool); ok && clientAuth {
|
||||
caFile, caOk := tls["client_ca_file"].(string)
|
||||
if !caOk || caFile == "" {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: client auth enabled but CA file not specified",
|
||||
pipelineName, sinkIndex, serverType)
|
||||
}
|
||||
// Validate that the client CA file exists and is readable
|
||||
if _, err := os.Stat(caFile); err != nil {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: client_ca_file is not accessible: %w",
|
||||
pipelineName, sinkIndex, serverType, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate TLS versions
|
||||
validVersions := map[string]bool{"TLS1.0": true, "TLS1.1": true, "TLS1.2": true, "TLS1.3": true}
|
||||
if minVer, ok := tls["min_version"].(string); ok && minVer != "" {
|
||||
if !validVersions[minVer] {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: invalid min TLS version: %s",
|
||||
pipelineName, sinkIndex, serverType, minVer)
|
||||
}
|
||||
}
|
||||
if maxVer, ok := tls["max_version"].(string); ok && maxVer != "" {
|
||||
if !validVersions[maxVer] {
|
||||
return fmt.Errorf("pipeline '%s' sink[%d] %s: invalid max TLS version: %s",
|
||||
pipelineName, sinkIndex, serverType, maxVer)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user