814 lines
23 KiB
Go
814 lines
23 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
lconfig "github.com/lixenwraith/config"
|
|
)
|
|
|
|
// ValidateConfig is the centralized validator for the entire configuration structure.
|
|
func ValidateConfig(cfg *Config) error {
|
|
if cfg == nil {
|
|
return fmt.Errorf("config is nil")
|
|
}
|
|
|
|
if len(cfg.Pipelines) == 0 {
|
|
return fmt.Errorf("no pipelines configured")
|
|
}
|
|
|
|
if err := validateLogConfig(cfg.Logging); err != nil {
|
|
return fmt.Errorf("logging config: %w", err)
|
|
}
|
|
|
|
// Track used ports across all pipelines
|
|
allPorts := make(map[int64]string)
|
|
pipelineNames := make(map[string]bool)
|
|
|
|
for i, pipeline := range cfg.Pipelines {
|
|
if err := validatePipeline(i, &pipeline, pipelineNames, allPorts); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateLogConfig validates the application's own logging settings.
|
|
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
|
|
}
|
|
|
|
// validatePipeline validates a single pipeline's configuration.
|
|
func validatePipeline(index int, p *PipelineConfig, pipelineNames map[string]bool, allPorts map[int64]string) error {
|
|
// Validate pipeline name
|
|
if err := lconfig.NonEmpty(p.Name); err != nil {
|
|
return fmt.Errorf("pipeline %d: missing name", index)
|
|
}
|
|
|
|
if pipelineNames[p.Name] {
|
|
return fmt.Errorf("pipeline %d: duplicate name '%s'", index, p.Name)
|
|
}
|
|
pipelineNames[p.Name] = true
|
|
|
|
// Must have at least one source
|
|
if len(p.Sources) == 0 {
|
|
return fmt.Errorf("pipeline '%s': no sources specified", p.Name)
|
|
}
|
|
|
|
// Validate each source
|
|
for j, source := range p.Sources {
|
|
if err := validateSourceConfig(p.Name, j, &source); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Validate rate limit if present
|
|
if p.RateLimit != nil {
|
|
if err := validateRateLimit(p.Name, p.RateLimit); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Validate filters
|
|
for j, filter := range p.Filters {
|
|
if err := validateFilter(p.Name, j, &filter); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Validate formatter configuration
|
|
if err := validateFormatterConfig(p); err != nil {
|
|
return fmt.Errorf("pipeline '%s': %w", p.Name, err)
|
|
}
|
|
|
|
// Must have at least one sink
|
|
if len(p.Sinks) == 0 {
|
|
return fmt.Errorf("pipeline '%s': no sinks specified", p.Name)
|
|
}
|
|
|
|
// Validate each sink
|
|
for j, sink := range p.Sinks {
|
|
if err := validateSinkConfig(p.Name, j, &sink, allPorts); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateSourceConfig validates a polymorphic source configuration.
|
|
func validateSourceConfig(pipelineName string, index int, s *SourceConfig) error {
|
|
if err := lconfig.NonEmpty(s.Type); err != nil {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: missing type", pipelineName, index)
|
|
}
|
|
|
|
// Count how many source configs are populated
|
|
populated := 0
|
|
var populatedType string
|
|
|
|
if s.Directory != nil {
|
|
populated++
|
|
populatedType = "directory"
|
|
}
|
|
if s.Stdin != nil {
|
|
populated++
|
|
populatedType = "stdin"
|
|
}
|
|
if s.HTTP != nil {
|
|
populated++
|
|
populatedType = "http"
|
|
}
|
|
if s.TCP != nil {
|
|
populated++
|
|
populatedType = "tcp"
|
|
}
|
|
|
|
if populated == 0 {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: no configuration provided for type '%s'",
|
|
pipelineName, index, s.Type)
|
|
}
|
|
if populated > 1 {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: multiple configurations provided, only one allowed",
|
|
pipelineName, index)
|
|
}
|
|
if populatedType != s.Type {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: type mismatch - type is '%s' but config is for '%s'",
|
|
pipelineName, index, s.Type, populatedType)
|
|
}
|
|
|
|
// Validate specific source type
|
|
switch s.Type {
|
|
case "directory":
|
|
return validateDirectorySource(pipelineName, index, s.Directory)
|
|
case "stdin":
|
|
return validateStdinSource(pipelineName, index, s.Stdin)
|
|
case "http":
|
|
return validateHTTPSource(pipelineName, index, s.HTTP)
|
|
case "tcp":
|
|
return validateTCPSource(pipelineName, index, s.TCP)
|
|
default:
|
|
return fmt.Errorf("pipeline '%s' source[%d]: unknown type '%s'", pipelineName, index, s.Type)
|
|
}
|
|
}
|
|
|
|
// validateSinkConfig validates a polymorphic sink configuration.
|
|
func validateSinkConfig(pipelineName string, index int, s *SinkConfig, allPorts map[int64]string) error {
|
|
if err := lconfig.NonEmpty(s.Type); err != nil {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: missing type", pipelineName, index)
|
|
}
|
|
|
|
// Count populated sink configs
|
|
populated := 0
|
|
var populatedType string
|
|
|
|
if s.Console != nil {
|
|
populated++
|
|
populatedType = "console"
|
|
}
|
|
if s.File != nil {
|
|
populated++
|
|
populatedType = "file"
|
|
}
|
|
if s.HTTP != nil {
|
|
populated++
|
|
populatedType = "http"
|
|
}
|
|
if s.TCP != nil {
|
|
populated++
|
|
populatedType = "tcp"
|
|
}
|
|
if s.HTTPClient != nil {
|
|
populated++
|
|
populatedType = "http_client"
|
|
}
|
|
if s.TCPClient != nil {
|
|
populated++
|
|
populatedType = "tcp_client"
|
|
}
|
|
|
|
if populated == 0 {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: no configuration provided for type '%s'",
|
|
pipelineName, index, s.Type)
|
|
}
|
|
if populated > 1 {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: multiple configurations provided, only one allowed",
|
|
pipelineName, index)
|
|
}
|
|
if populatedType != s.Type {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: type mismatch - type is '%s' but config is for '%s'",
|
|
pipelineName, index, s.Type, populatedType)
|
|
}
|
|
|
|
// Validate specific sink type
|
|
switch s.Type {
|
|
case "console":
|
|
return validateConsoleSink(pipelineName, index, s.Console)
|
|
case "file":
|
|
return validateFileSink(pipelineName, index, s.File)
|
|
case "http":
|
|
return validateHTTPSink(pipelineName, index, s.HTTP, allPorts)
|
|
case "tcp":
|
|
return validateTCPSink(pipelineName, index, s.TCP, allPorts)
|
|
case "http_client":
|
|
return validateHTTPClientSink(pipelineName, index, s.HTTPClient)
|
|
case "tcp_client":
|
|
return validateTCPClientSink(pipelineName, index, s.TCPClient)
|
|
default:
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: unknown type '%s'", pipelineName, index, s.Type)
|
|
}
|
|
}
|
|
|
|
// validateFormatterConfig validates formatter configuration
|
|
func validateFormatterConfig(p *PipelineConfig) error {
|
|
if p.Format == nil {
|
|
p.Format = &FormatConfig{
|
|
Type: "raw",
|
|
}
|
|
} else if p.Format.Type == "" {
|
|
p.Format.Type = "raw" // Default
|
|
}
|
|
|
|
switch p.Format.Type {
|
|
|
|
case "raw":
|
|
if p.Format.RawFormatOptions == nil {
|
|
p.Format.RawFormatOptions = &RawFormatterOptions{}
|
|
}
|
|
|
|
case "txt":
|
|
if p.Format.TxtFormatOptions == nil {
|
|
p.Format.TxtFormatOptions = &TxtFormatterOptions{}
|
|
}
|
|
|
|
// Default template format
|
|
templateStr := "[{{.Timestamp | FmtTime}}] [{{.Level | ToUpper}}] {{.Source}} - {{.Message}}{{ if .Fields }} {{.Fields}}{{ end }}"
|
|
if p.Format.TxtFormatOptions.Template != "" {
|
|
p.Format.TxtFormatOptions.Template = templateStr
|
|
}
|
|
|
|
// Default timestamp format
|
|
timestampFormat := time.RFC3339
|
|
if p.Format.TxtFormatOptions.TimestampFormat != "" {
|
|
p.Format.TxtFormatOptions.TimestampFormat = timestampFormat
|
|
}
|
|
|
|
case "json":
|
|
if p.Format.JSONFormatOptions == nil {
|
|
p.Format.JSONFormatOptions = &JSONFormatterOptions{}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateRateLimit validates the pipeline-level rate limit settings.
|
|
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
|
|
}
|
|
|
|
// validateFilter validates a single filter's configuration.
|
|
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
|
|
}
|
|
|
|
// validateDirectorySource validates the settings for a directory source.
|
|
func validateDirectorySource(pipelineName string, index int, opts *DirectorySourceOptions) error {
|
|
if err := lconfig.NonEmpty(opts.Path); err != nil {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: directory requires 'path'", pipelineName, index)
|
|
} else {
|
|
absPath, err := filepath.Abs(opts.Path)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid path %s: %w", opts.Path, err)
|
|
}
|
|
opts.Path = absPath
|
|
}
|
|
|
|
// Check for directory traversal
|
|
// TODO: traversal check only if optional security settings from cli/env set
|
|
if strings.Contains(opts.Path, "..") {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: path contains directory traversal", pipelineName, index)
|
|
}
|
|
|
|
// Validate pattern if provided
|
|
if opts.Pattern != "" {
|
|
if strings.Count(opts.Pattern, "*") == 0 && strings.Count(opts.Pattern, "?") == 0 {
|
|
// If no wildcards, ensure valid filename
|
|
if filepath.Base(opts.Pattern) != opts.Pattern {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: pattern contains path separators", pipelineName, index)
|
|
}
|
|
}
|
|
} else {
|
|
opts.Pattern = "*"
|
|
}
|
|
|
|
// Validate check interval
|
|
if opts.CheckIntervalMS < 10 {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: check_interval_ms must be at least 10ms", pipelineName, index)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateStdinSource validates the settings for a stdin source.
|
|
func validateStdinSource(pipelineName string, index int, opts *StdinSourceOptions) error {
|
|
if opts.BufferSize < 0 {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: buffer_size must be positive", pipelineName, index)
|
|
} else if opts.BufferSize == 0 {
|
|
opts.BufferSize = 1000
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateHTTPSource validates the settings for an HTTP source.
|
|
func validateHTTPSource(pipelineName string, index int, opts *HTTPSourceOptions) error {
|
|
// Validate port
|
|
if err := lconfig.Port(opts.Port); err != nil {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: %w", pipelineName, index, err)
|
|
}
|
|
|
|
// Set defaults
|
|
if opts.Host == "" {
|
|
opts.Host = "0.0.0.0"
|
|
}
|
|
if opts.IngestPath == "" {
|
|
opts.IngestPath = "/ingest"
|
|
}
|
|
if opts.MaxRequestBodySize <= 0 {
|
|
opts.MaxRequestBodySize = 10 * 1024 * 1024 // 10MB default
|
|
}
|
|
if opts.ReadTimeout <= 0 {
|
|
opts.ReadTimeout = 5000 // 5 seconds
|
|
}
|
|
if opts.WriteTimeout <= 0 {
|
|
opts.WriteTimeout = 5000 // 5 seconds
|
|
}
|
|
|
|
// Validate host if specified
|
|
if opts.Host != "" && opts.Host != "0.0.0.0" {
|
|
if err := lconfig.IPAddress(opts.Host); err != nil {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: %w", pipelineName, index, err)
|
|
}
|
|
}
|
|
|
|
// Validate paths
|
|
if !strings.HasPrefix(opts.IngestPath, "/") {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: ingest_path must start with /", pipelineName, index)
|
|
}
|
|
|
|
// Validate auth configuration
|
|
validHTTPSourceAuthTypes := map[string]bool{"basic": true, "token": true, "mtls": true}
|
|
if opts.Auth != nil && opts.Auth.Type != "none" && opts.Auth.Type != "" {
|
|
if !validHTTPSourceAuthTypes[opts.Auth.Type] {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: %s is not a valid auth type",
|
|
pipelineName, index, opts.Auth.Type)
|
|
}
|
|
// All non-none auth types require TLS for HTTP
|
|
if opts.TLS == nil || !opts.TLS.Enabled {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: %s auth requires TLS to be enabled",
|
|
pipelineName, index, opts.Auth.Type)
|
|
}
|
|
}
|
|
|
|
// Validate nested configs
|
|
if opts.NetLimit != nil {
|
|
if err := validateNetLimit(pipelineName, fmt.Sprintf("source[%d]", index), opts.NetLimit); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if opts.TLS != nil {
|
|
if err := validateTLSServer(pipelineName, fmt.Sprintf("source[%d]", index), opts.TLS); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateTCPSource validates the settings for a TCP source.
|
|
func validateTCPSource(pipelineName string, index int, opts *TCPSourceOptions) error {
|
|
// Validate port
|
|
if err := lconfig.Port(opts.Port); err != nil {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: %w", pipelineName, index, err)
|
|
}
|
|
|
|
// Set defaults
|
|
if opts.Host == "" {
|
|
opts.Host = "0.0.0.0"
|
|
}
|
|
if opts.ReadTimeout <= 0 {
|
|
opts.ReadTimeout = 5000 // 5 seconds
|
|
}
|
|
if !opts.KeepAlive {
|
|
opts.KeepAlive = true // Default enabled
|
|
}
|
|
if opts.KeepAlivePeriod <= 0 {
|
|
opts.KeepAlivePeriod = 30000 // 30 seconds
|
|
}
|
|
|
|
// Validate host if specified
|
|
if opts.Host != "" && opts.Host != "0.0.0.0" {
|
|
if err := lconfig.IPAddress(opts.Host); err != nil {
|
|
return fmt.Errorf("pipeline '%s' source[%d]: %w", pipelineName, index, err)
|
|
}
|
|
}
|
|
|
|
// Validate NetLimit if present
|
|
if opts.NetLimit != nil {
|
|
if err := validateNetLimit(pipelineName, fmt.Sprintf("source[%d]", index), opts.NetLimit); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateConsoleSink validates the settings for a console sink.
|
|
func validateConsoleSink(pipelineName string, index int, opts *ConsoleSinkOptions) error {
|
|
if opts.BufferSize < 1 {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: buffer_size must be positive", pipelineName, index)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateFileSink validates the settings for a file sink.
|
|
func validateFileSink(pipelineName string, index int, opts *FileSinkOptions) error {
|
|
if err := lconfig.NonEmpty(opts.Directory); err != nil {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: file requires 'directory'", pipelineName, index)
|
|
}
|
|
|
|
if err := lconfig.NonEmpty(opts.Name); err != nil {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: file requires 'name'", pipelineName, index)
|
|
}
|
|
|
|
if opts.BufferSize <= 0 {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: max_size_mb must be positive", pipelineName, index)
|
|
}
|
|
|
|
// Validate sizes
|
|
if opts.MaxSizeMB < 0 {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: max_size_mb must be positive", pipelineName, index)
|
|
}
|
|
|
|
if opts.MaxTotalSizeMB <= 0 {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: max_total_size_mb cannot be negative", pipelineName, index)
|
|
}
|
|
|
|
if opts.MinDiskFreeMB < 0 {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: min_disk_free_mb must be positive", pipelineName, index)
|
|
}
|
|
|
|
if opts.RetentionHours <= 0 {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: retention_hours cannot be negative", pipelineName, index)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateHTTPSink validates the settings for an HTTP sink.
|
|
func validateHTTPSink(pipelineName string, index int, opts *HTTPSinkOptions, allPorts map[int64]string) error {
|
|
// Validate port
|
|
if err := lconfig.Port(opts.Port); err != nil {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: %w", pipelineName, index, err)
|
|
}
|
|
|
|
// Check port conflicts
|
|
if existing, exists := allPorts[opts.Port]; exists {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: port %d already used by %s",
|
|
pipelineName, index, opts.Port, existing)
|
|
}
|
|
allPorts[opts.Port] = fmt.Sprintf("%s-http[%d]", pipelineName, index)
|
|
|
|
// Validate host if specified
|
|
if opts.Host != "" {
|
|
if err := lconfig.IPAddress(opts.Host); err != nil {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: %w", pipelineName, index, err)
|
|
}
|
|
}
|
|
|
|
// Validate paths
|
|
if !strings.HasPrefix(opts.StreamPath, "/") {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: stream_path must start with /", pipelineName, index)
|
|
}
|
|
|
|
if !strings.HasPrefix(opts.StatusPath, "/") {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: status_path must start with /", pipelineName, index)
|
|
}
|
|
|
|
// Validate buffer
|
|
if opts.BufferSize < 1 {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: buffer_size must be positive", pipelineName, index)
|
|
}
|
|
|
|
// Validate nested configs
|
|
if opts.Heartbeat != nil {
|
|
if err := validateHeartbeat(pipelineName, fmt.Sprintf("sink[%d]", index), opts.Heartbeat); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if opts.NetLimit != nil {
|
|
if err := validateNetLimit(pipelineName, fmt.Sprintf("sink[%d]", index), opts.NetLimit); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if opts.TLS != nil {
|
|
if err := validateTLSServer(pipelineName, fmt.Sprintf("sink[%d]", index), opts.TLS); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateTCPSink validates the settings for a TCP sink.
|
|
func validateTCPSink(pipelineName string, index int, opts *TCPSinkOptions, allPorts map[int64]string) error {
|
|
// Validate port
|
|
if err := lconfig.Port(opts.Port); err != nil {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: %w", pipelineName, index, err)
|
|
}
|
|
|
|
// Check port conflicts
|
|
if existing, exists := allPorts[opts.Port]; exists {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: port %d already used by %s",
|
|
pipelineName, index, opts.Port, existing)
|
|
}
|
|
allPorts[opts.Port] = fmt.Sprintf("%s-tcp[%d]", pipelineName, index)
|
|
|
|
// Validate host if specified
|
|
if opts.Host != "" {
|
|
if err := lconfig.IPAddress(opts.Host); err != nil {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: %w", pipelineName, index, err)
|
|
}
|
|
}
|
|
|
|
// Validate buffer
|
|
if opts.BufferSize < 1 {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: buffer_size must be positive", pipelineName, index)
|
|
}
|
|
|
|
// Validate nested configs
|
|
if opts.Heartbeat != nil {
|
|
if err := validateHeartbeat(pipelineName, fmt.Sprintf("sink[%d]", index), opts.Heartbeat); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if opts.NetLimit != nil {
|
|
if err := validateNetLimit(pipelineName, fmt.Sprintf("sink[%d]", index), opts.NetLimit); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateHTTPClientSink validates the settings for an HTTP client sink.
|
|
func validateHTTPClientSink(pipelineName string, index int, opts *HTTPClientSinkOptions) error {
|
|
// Validate URL
|
|
if err := lconfig.NonEmpty(opts.URL); err != nil {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: http_client requires 'url'", pipelineName, index)
|
|
}
|
|
|
|
parsedURL, err := url.Parse(opts.URL)
|
|
if err != nil {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: invalid URL: %w", pipelineName, index, err)
|
|
}
|
|
|
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: URL must use http or https scheme", pipelineName, index)
|
|
}
|
|
|
|
// Set defaults for unspecified fields
|
|
if opts.BufferSize <= 0 {
|
|
opts.BufferSize = 1000
|
|
}
|
|
if opts.BatchSize <= 0 {
|
|
opts.BatchSize = 100
|
|
}
|
|
if opts.BatchDelayMS <= 0 {
|
|
opts.BatchDelayMS = 1000 // 1 second in ms
|
|
}
|
|
if opts.Timeout <= 0 {
|
|
opts.Timeout = 30 // 30 seconds
|
|
}
|
|
if opts.MaxRetries < 0 {
|
|
opts.MaxRetries = 3
|
|
}
|
|
if opts.RetryDelayMS <= 0 {
|
|
opts.RetryDelayMS = 1000 // 1 second in ms
|
|
}
|
|
if opts.RetryBackoff < 1.0 {
|
|
opts.RetryBackoff = 2.0
|
|
}
|
|
|
|
// Validate TLS config if present
|
|
if opts.TLS != nil {
|
|
if err := validateTLSClient(pipelineName, fmt.Sprintf("sink[%d]", index), opts.TLS); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateTCPClientSink validates the settings for a TCP client sink.
|
|
func validateTCPClientSink(pipelineName string, index int, opts *TCPClientSinkOptions) error {
|
|
// Validate host and port
|
|
if err := lconfig.NonEmpty(opts.Host); err != nil {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: tcp_client requires 'host'", pipelineName, index)
|
|
}
|
|
|
|
if err := lconfig.Port(opts.Port); err != nil {
|
|
return fmt.Errorf("pipeline '%s' sink[%d]: %w", pipelineName, index, err)
|
|
}
|
|
|
|
// Set defaults
|
|
if opts.BufferSize <= 0 {
|
|
opts.BufferSize = 1000
|
|
}
|
|
if opts.DialTimeout <= 0 {
|
|
opts.DialTimeout = 10 // 10 seconds
|
|
}
|
|
if opts.WriteTimeout <= 0 {
|
|
opts.WriteTimeout = 30 // 30 seconds
|
|
}
|
|
if opts.ReadTimeout <= 0 {
|
|
opts.ReadTimeout = 10 // 10 seconds
|
|
}
|
|
if opts.KeepAlive <= 0 {
|
|
opts.KeepAlive = 30 // 30 seconds
|
|
}
|
|
if opts.ReconnectDelayMS <= 0 {
|
|
opts.ReconnectDelayMS = 1000 // 1 second in ms
|
|
}
|
|
if opts.MaxReconnectDelayMS <= 0 {
|
|
opts.MaxReconnectDelayMS = 30000 // 30 seconds in ms
|
|
}
|
|
if opts.ReconnectBackoff < 1.0 {
|
|
opts.ReconnectBackoff = 1.5
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateNetLimit validates nested NetLimitConfig settings.
|
|
func validateNetLimit(pipelineName, location string, nl *NetLimitConfig) error {
|
|
if !nl.Enabled {
|
|
return nil // Skip validation if disabled
|
|
}
|
|
|
|
if nl.MaxConnections < 0 {
|
|
return fmt.Errorf("pipeline '%s' %s: max_connections cannot be negative", pipelineName, location)
|
|
}
|
|
|
|
if nl.BurstSize < 0 {
|
|
return fmt.Errorf("pipeline '%s' %s: burst_size cannot be negative", pipelineName, location)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateTLSServer validates the new TLSServerConfig struct.
|
|
func validateTLSServer(pipelineName, location string, tls *TLSServerConfig) error {
|
|
if !tls.Enabled {
|
|
return nil // Skip validation if disabled
|
|
}
|
|
|
|
// If TLS is enabled for a server, cert and key files are mandatory.
|
|
if tls.CertFile == "" || tls.KeyFile == "" {
|
|
return fmt.Errorf("pipeline '%s' %s: TLS enabled requires both cert_file and key_file", pipelineName, location)
|
|
}
|
|
|
|
// If mTLS (ClientAuth) is enabled, a client CA file is mandatory.
|
|
if tls.ClientAuth && tls.ClientCAFile == "" {
|
|
return fmt.Errorf("pipeline '%s' %s: client_auth is enabled, which requires a client_ca_file", pipelineName, location)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateTLSClient validates the new TLSClientConfig struct.
|
|
func validateTLSClient(pipelineName, location string, tls *TLSClientConfig) error {
|
|
if !tls.Enabled {
|
|
return nil // Skip validation if disabled
|
|
}
|
|
|
|
// If verification is not skipped, a server CA file must be provided.
|
|
if !tls.InsecureSkipVerify && tls.ServerCAFile == "" {
|
|
return fmt.Errorf("pipeline '%s' %s: TLS verification is enabled (insecure_skip_verify=false) but server_ca_file is not provided", pipelineName, location)
|
|
}
|
|
|
|
// For client mTLS, both the cert and key must be provided together.
|
|
if (tls.ClientCertFile != "" && tls.ClientKeyFile == "") || (tls.ClientCertFile == "" && tls.ClientKeyFile != "") {
|
|
return fmt.Errorf("pipeline '%s' %s: for client mTLS, both client_cert_file and client_key_file must be provided", pipelineName, location)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateHeartbeat validates nested HeartbeatConfig settings.
|
|
func validateHeartbeat(pipelineName, location string, hb *HeartbeatConfig) error {
|
|
if !hb.Enabled {
|
|
return nil // Skip validation if disabled
|
|
}
|
|
|
|
if hb.IntervalMS < 1000 { // At least 1 second
|
|
return fmt.Errorf("pipeline '%s' %s: heartbeat interval must be at least 1000ms", pipelineName, location)
|
|
}
|
|
|
|
return nil
|
|
} |