v0.12.2 config validation updated, distributed to their own pacakges and plugins
This commit is contained in:
@ -113,7 +113,18 @@ func defaults() *Config {
|
|||||||
Pipelines: []PipelineConfig{
|
Pipelines: []PipelineConfig{
|
||||||
{
|
{
|
||||||
Name: "default_pipeline",
|
Name: "default_pipeline",
|
||||||
Flow: &FlowConfig{},
|
Flow: &FlowConfig{
|
||||||
|
RateLimit: &RateLimitConfig{
|
||||||
|
Rate: 5,
|
||||||
|
Burst: 10,
|
||||||
|
Policy: "drop",
|
||||||
|
MaxEntrySizeBytes: 65536,
|
||||||
|
},
|
||||||
|
Format: &FormatConfig{
|
||||||
|
Type: "json",
|
||||||
|
SanitizerPolicy: "json",
|
||||||
|
},
|
||||||
|
},
|
||||||
PluginSources: []PluginSourceConfig{
|
PluginSources: []PluginSourceConfig{
|
||||||
{
|
{
|
||||||
ID: "default_source",
|
ID: "default_source",
|
||||||
|
|||||||
63
src/internal/config/validate.go
Normal file
63
src/internal/config/validate.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
lconfig "github.com/lixenwraith/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateConfig validates top-level structure only
|
||||||
|
// Value range validation is delegated to component constructors
|
||||||
|
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: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range cfg.Pipelines {
|
||||||
|
if err := lconfig.NonEmpty(p.Name); err != nil {
|
||||||
|
return fmt.Errorf("pipeline[%d].name: %w", i, err)
|
||||||
|
}
|
||||||
|
if len(p.PluginSources) == 0 {
|
||||||
|
return fmt.Errorf("pipeline[%d]: no sources defined", i)
|
||||||
|
}
|
||||||
|
if len(p.PluginSinks) == 0 {
|
||||||
|
return fmt.Errorf("pipeline[%d]: no sinks defined", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateLogConfig validates application logging settings
|
||||||
|
func validateLogConfig(cfg *LogConfig) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
validateOutput := lconfig.OneOf("file", "stdout", "stderr", "split", "all", "none")
|
||||||
|
if err := validateOutput(cfg.Output); err != nil {
|
||||||
|
return fmt.Errorf("output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
validateLevel := lconfig.OneOf("debug", "info", "warn", "error")
|
||||||
|
if err := validateLevel(cfg.Level); err != nil {
|
||||||
|
return fmt.Errorf("level: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Console != nil {
|
||||||
|
validateTarget := lconfig.OneOf("stdout", "stderr", "split")
|
||||||
|
if err := validateTarget(cfg.Console.Target); err != nil {
|
||||||
|
return fmt.Errorf("console.target: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -1,234 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateFileSource validates the settings for a directory source
|
|
||||||
func validateFileSource(pipelineName string, index int, opts *FileSourceOptions) error {
|
|
||||||
if err := lconfig.NonEmpty(opts.Directory); err != nil {
|
|
||||||
return fmt.Errorf("pipeline '%s' source[%d]: directory requires 'path'", pipelineName, index)
|
|
||||||
} else {
|
|
||||||
absPath, err := filepath.Abs(opts.Directory)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid path %s: %w", opts.Directory, err)
|
|
||||||
}
|
|
||||||
opts.Directory = absPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for directory traversal
|
|
||||||
if strings.Contains(opts.Directory, "..") {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateConsoleSource validates the settings for a console source
|
|
||||||
func validateConsoleSource(pipelineName string, index int, opts *ConsoleSourceOptions) 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"logwisp/src/internal/config"
|
"logwisp/src/internal/config"
|
||||||
"logwisp/src/internal/core"
|
"logwisp/src/internal/core"
|
||||||
|
|
||||||
|
lconfig "github.com/lixenwraith/config"
|
||||||
"github.com/lixenwraith/log"
|
"github.com/lixenwraith/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,6 +28,20 @@ type Filter struct {
|
|||||||
|
|
||||||
// NewFilter creates a new filter from a configuration
|
// NewFilter creates a new filter from a configuration
|
||||||
func NewFilter(cfg config.FilterConfig, logger *log.Logger) (*Filter, error) {
|
func NewFilter(cfg config.FilterConfig, logger *log.Logger) (*Filter, error) {
|
||||||
|
// Validate enums before setting defaults
|
||||||
|
if cfg.Type != "" {
|
||||||
|
validateType := lconfig.OneOf(config.FilterTypeInclude, config.FilterTypeExclude)
|
||||||
|
if err := validateType(cfg.Type); err != nil {
|
||||||
|
return nil, fmt.Errorf("type: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.Logic != "" {
|
||||||
|
validateLogic := lconfig.OneOf(config.FilterLogicOr, config.FilterLogicAnd)
|
||||||
|
if err := validateLogic(cfg.Logic); err != nil {
|
||||||
|
return nil, fmt.Errorf("logic: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set defaults
|
// Set defaults
|
||||||
if cfg.Type == "" {
|
if cfg.Type == "" {
|
||||||
cfg.Type = config.FilterTypeInclude
|
cfg.Type = config.FilterTypeInclude
|
||||||
@ -45,7 +60,7 @@ func NewFilter(cfg config.FilterConfig, logger *log.Logger) (*Filter, error) {
|
|||||||
for i, pattern := range cfg.Patterns {
|
for i, pattern := range cfg.Patterns {
|
||||||
re, err := regexp.Compile(pattern)
|
re, err := regexp.Compile(pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid regex pattern[%d] '%s': %w", i, pattern, err)
|
return nil, fmt.Errorf("pattern[%d] '%s': %w", i, pattern, err)
|
||||||
}
|
}
|
||||||
f.patterns = append(f.patterns, re)
|
f.patterns = append(f.patterns, re)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,9 +63,13 @@ func NewFlow(cfg *config.FlowConfig, logger *log.Logger) (*Flow, error) {
|
|||||||
}
|
}
|
||||||
f.formatter = formatter
|
f.formatter = formatter
|
||||||
|
|
||||||
// Create heartbeat generator with the same formatter
|
// Create heartbeat generator with the same formatter if configured
|
||||||
if cfg.Heartbeat != nil && cfg.Heartbeat.Enabled {
|
if cfg.Heartbeat != nil {
|
||||||
f.heartbeat = NewHeartbeatGenerator(cfg.Heartbeat, formatter, logger)
|
hb, err := NewHeartbeatGenerator(cfg.Heartbeat, formatter, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("heartbeat: %w", err)
|
||||||
|
}
|
||||||
|
f.heartbeat = hb
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("msg", "Flow processor created",
|
logger.Info("msg", "Flow processor created",
|
||||||
|
|||||||
@ -3,17 +3,25 @@ package flow
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"logwisp/src/internal/format"
|
"fmt"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"logwisp/src/internal/config"
|
"logwisp/src/internal/config"
|
||||||
"logwisp/src/internal/core"
|
"logwisp/src/internal/core"
|
||||||
|
"logwisp/src/internal/format"
|
||||||
|
|
||||||
|
lconfig "github.com/lixenwraith/config"
|
||||||
"github.com/lixenwraith/log"
|
"github.com/lixenwraith/log"
|
||||||
"github.com/lixenwraith/log/formatter"
|
"github.com/lixenwraith/log/formatter"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MinHeartbeatIntervalMS = 100
|
||||||
|
DefaultHeartbeatIntervalMS = 1000
|
||||||
|
DefaultHeartbeatFormat = "txt"
|
||||||
|
)
|
||||||
|
|
||||||
// HeartbeatGenerator produces periodic heartbeat events
|
// HeartbeatGenerator produces periodic heartbeat events
|
||||||
type HeartbeatGenerator struct {
|
type HeartbeatGenerator struct {
|
||||||
config *config.HeartbeatConfig
|
config *config.HeartbeatConfig
|
||||||
@ -24,14 +32,35 @@ type HeartbeatGenerator struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewHeartbeatGenerator creates a new heartbeat generator
|
// NewHeartbeatGenerator creates a new heartbeat generator
|
||||||
func NewHeartbeatGenerator(cfg *config.HeartbeatConfig, formatter format.Formatter, logger *log.Logger) *HeartbeatGenerator {
|
func NewHeartbeatGenerator(cfg *config.HeartbeatConfig, formatter format.Formatter, logger *log.Logger) (*HeartbeatGenerator, error) {
|
||||||
|
if cfg == nil || !cfg.Enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if cfg.IntervalMS == 0 {
|
||||||
|
cfg.IntervalMS = DefaultHeartbeatIntervalMS
|
||||||
|
} else if cfg.IntervalMS < MinHeartbeatIntervalMS {
|
||||||
|
return nil, fmt.Errorf("interval_ms: must be >= %d, got %d", MinHeartbeatIntervalMS, cfg.IntervalMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
validateFormat := lconfig.OneOf("txt", "json", "raw", "")
|
||||||
|
if err := validateFormat(cfg.Format); err != nil {
|
||||||
|
return nil, fmt.Errorf("format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
if cfg.Format == "" {
|
||||||
|
cfg.Format = DefaultHeartbeatFormat
|
||||||
|
}
|
||||||
|
|
||||||
hg := &HeartbeatGenerator{
|
hg := &HeartbeatGenerator{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
formatter: formatter,
|
formatter: formatter,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
hg.lastBeat.Store(time.Time{})
|
hg.lastBeat.Store(time.Time{})
|
||||||
return hg
|
return hg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start begins generating heartbeat events
|
// Start begins generating heartbeat events
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package flow
|
package flow
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ import (
|
|||||||
"logwisp/src/internal/core"
|
"logwisp/src/internal/core"
|
||||||
"logwisp/src/internal/tokenbucket"
|
"logwisp/src/internal/tokenbucket"
|
||||||
|
|
||||||
|
lconfig "github.com/lixenwraith/config"
|
||||||
"github.com/lixenwraith/log"
|
"github.com/lixenwraith/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -25,21 +27,36 @@ type RateLimiter struct {
|
|||||||
|
|
||||||
// NewRateLimiter creates a new pipeline-level rate limiter from configuration
|
// NewRateLimiter creates a new pipeline-level rate limiter from configuration
|
||||||
func NewRateLimiter(cfg config.RateLimitConfig, logger *log.Logger) (*RateLimiter, error) {
|
func NewRateLimiter(cfg config.RateLimitConfig, logger *log.Logger) (*RateLimiter, error) {
|
||||||
|
// Rate <= 0 means disabled
|
||||||
if cfg.Rate <= 0 {
|
if cfg.Rate <= 0 {
|
||||||
return nil, nil // No rate limit
|
return nil, nil // No rate limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if err := lconfig.NonNegative(cfg.Rate); err != nil {
|
||||||
|
return nil, fmt.Errorf("rate: %w", err)
|
||||||
|
}
|
||||||
|
if err := lconfig.NonNegative(cfg.Burst); err != nil {
|
||||||
|
return nil, fmt.Errorf("burst: %w", err)
|
||||||
|
}
|
||||||
|
if err := lconfig.NonNegative(cfg.MaxEntrySizeBytes); err != nil {
|
||||||
|
return nil, fmt.Errorf("max_entry_size_bytes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
burst := cfg.Burst
|
burst := cfg.Burst
|
||||||
if burst <= 0 {
|
if burst <= 0 {
|
||||||
burst = cfg.Rate // Default burst to rate
|
burst = cfg.Rate
|
||||||
}
|
}
|
||||||
|
|
||||||
var policy config.RateLimitPolicy
|
var policy config.RateLimitPolicy
|
||||||
switch strings.ToLower(cfg.Policy) {
|
switch strings.ToLower(cfg.Policy) {
|
||||||
case "drop":
|
case "drop":
|
||||||
policy = config.PolicyDrop
|
policy = config.PolicyDrop
|
||||||
default:
|
case "pass", "":
|
||||||
policy = config.PolicyPass
|
policy = config.PolicyPass
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("policy: must be one of [drop, pass], got %s", cfg.Policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
l := &RateLimiter{
|
l := &RateLimiter{
|
||||||
|
|||||||
@ -2,13 +2,20 @@ package format
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"logwisp/src/internal/config"
|
"logwisp/src/internal/config"
|
||||||
"logwisp/src/internal/core"
|
"logwisp/src/internal/core"
|
||||||
|
|
||||||
|
lconfig "github.com/lixenwraith/config"
|
||||||
"github.com/lixenwraith/log/formatter"
|
"github.com/lixenwraith/log/formatter"
|
||||||
"github.com/lixenwraith/log/sanitizer"
|
"github.com/lixenwraith/log/sanitizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultFormatType = "raw"
|
||||||
|
)
|
||||||
|
|
||||||
// FormatterAdapter wraps log/formatter for logwisp compatibility
|
// FormatterAdapter wraps log/formatter for logwisp compatibility
|
||||||
type FormatterAdapter struct {
|
type FormatterAdapter struct {
|
||||||
formatter *formatter.Formatter
|
formatter *formatter.Formatter
|
||||||
@ -18,6 +25,26 @@ type FormatterAdapter struct {
|
|||||||
|
|
||||||
// NewFormatterAdapter creates adapter from config
|
// NewFormatterAdapter creates adapter from config
|
||||||
func NewFormatterAdapter(cfg *config.FormatConfig) (*FormatterAdapter, error) {
|
func NewFormatterAdapter(cfg *config.FormatConfig) (*FormatterAdapter, error) {
|
||||||
|
// Validate
|
||||||
|
if cfg.Type != "" {
|
||||||
|
validateType := lconfig.OneOf("json", "txt", "text", "raw")
|
||||||
|
if err := validateType(cfg.Type); err != nil {
|
||||||
|
return nil, fmt.Errorf("type: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.SanitizerPolicy != "" {
|
||||||
|
validatePolicy := lconfig.OneOf("raw", "json", "txt", "shell")
|
||||||
|
if err := validatePolicy(cfg.SanitizerPolicy); err != nil {
|
||||||
|
return nil, fmt.Errorf("sanitizer_policy: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
if cfg.Type == "" {
|
||||||
|
cfg.Type = DefaultFormatType
|
||||||
|
}
|
||||||
|
|
||||||
// Create sanitizer based on policy
|
// Create sanitizer based on policy
|
||||||
var s *sanitizer.Sanitizer
|
var s *sanitizer.Sanitizer
|
||||||
if cfg.SanitizerPolicy != "" {
|
if cfg.SanitizerPolicy != "" {
|
||||||
@ -44,7 +71,6 @@ func NewFormatterAdapter(cfg *config.FormatConfig) (*FormatterAdapter, error) {
|
|||||||
// Build flags from config
|
// Build flags from config
|
||||||
flags := cfg.Flags
|
flags := cfg.Flags
|
||||||
if flags == 0 {
|
if flags == 0 {
|
||||||
// Set default flags based on format type
|
|
||||||
if cfg.Type == "raw" {
|
if cfg.Type == "raw" {
|
||||||
flags = formatter.FlagRaw
|
flags = formatter.FlagRaw
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -14,17 +14,15 @@ type Formatter interface {
|
|||||||
Name() string
|
Name() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFormatter creates a Formatter using the new formatter/sanitizer packages
|
// NewFormatter creates a Formatter using formatter/sanitizer packages
|
||||||
func NewFormatter(cfg *config.FormatConfig) (Formatter, error) {
|
func NewFormatter(cfg *config.FormatConfig) (Formatter, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
// Default config
|
|
||||||
cfg = &config.FormatConfig{
|
cfg = &config.FormatConfig{
|
||||||
Type: "raw",
|
Type: DefaultFormatType,
|
||||||
Flags: 0,
|
Flags: 0,
|
||||||
SanitizerPolicy: "raw",
|
SanitizerPolicy: "raw",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the new FormatterAdapter that integrates formatter and sanitizer
|
|
||||||
return NewFormatterAdapter(cfg)
|
return NewFormatterAdapter(cfg)
|
||||||
}
|
}
|
||||||
@ -49,6 +49,12 @@ type ConsoleSink struct {
|
|||||||
lastProcessed atomic.Value // time.Time
|
lastProcessed atomic.Value // time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Defaults
|
||||||
|
DefaultConsoleTarget = "stdout"
|
||||||
|
DefaultConsoleBufferSize = 1000
|
||||||
|
)
|
||||||
|
|
||||||
// NewConsoleSinkPlugin creates a console sink through plugin factory
|
// NewConsoleSinkPlugin creates a console sink through plugin factory
|
||||||
func NewConsoleSinkPlugin(
|
func NewConsoleSinkPlugin(
|
||||||
id string,
|
id string,
|
||||||
@ -56,10 +62,7 @@ func NewConsoleSinkPlugin(
|
|||||||
logger *log.Logger,
|
logger *log.Logger,
|
||||||
proxy *session.Proxy,
|
proxy *session.Proxy,
|
||||||
) (sink.Sink, error) {
|
) (sink.Sink, error) {
|
||||||
// Create empty config struct with defaults
|
opts := &config.ConsoleSinkOptions{}
|
||||||
opts := &config.ConsoleSinkOptions{
|
|
||||||
Target: DefaultConsoleTarget,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan config map into struct
|
// Scan config map into struct
|
||||||
if err := lconfig.ScanMap(configMap, opts); err != nil {
|
if err := lconfig.ScanMap(configMap, opts); err != nil {
|
||||||
@ -67,21 +70,28 @@ func NewConsoleSinkPlugin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate and apply defaults
|
// Validate and apply defaults
|
||||||
|
if opts.Target == "" {
|
||||||
|
opts.Target = DefaultConsoleTarget
|
||||||
|
} else {
|
||||||
|
validateTarget := lconfig.OneOf("stdout", "stderr")
|
||||||
|
if err := validateTarget(opts.Target); err != nil {
|
||||||
|
return nil, fmt.Errorf("target: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var output io.Writer
|
var output io.Writer
|
||||||
switch opts.Target {
|
switch opts.Target {
|
||||||
case "stdout":
|
case "stdout":
|
||||||
output = os.Stdout
|
output = os.Stdout
|
||||||
case "stderr":
|
case "stderr":
|
||||||
output = os.Stderr
|
output = os.Stderr
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid console target: %s (must be 'stdout' or 'stderr')", opts.Target)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.BufferSize <= 0 {
|
if opts.BufferSize <= 0 {
|
||||||
opts.BufferSize = DefaultConsoleBufferSize
|
opts.BufferSize = DefaultConsoleBufferSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Create and return plugin instance
|
// Create and return plugin instance
|
||||||
cs := &ConsoleSink{
|
cs := &ConsoleSink{
|
||||||
id: id,
|
id: id,
|
||||||
proxy: proxy,
|
proxy: proxy,
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
package console
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Defaults
|
|
||||||
DefaultConsoleTarget = "stdout"
|
|
||||||
DefaultConsoleBufferSize = 1000
|
|
||||||
)
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package file
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Defaults
|
|
||||||
DefaultFileMaxSizeMB = 100
|
|
||||||
DefaultFileMaxTotalSizeMB = 1000
|
|
||||||
DefaultFileMinDiskFreeMB = 100
|
|
||||||
DefaultFileRetentionHours = 168 // 7 days
|
|
||||||
DefaultFileBufferSize = 1000
|
|
||||||
DefaultFileFlushIntervalMs = 100
|
|
||||||
)
|
|
||||||
@ -47,6 +47,16 @@ type FileSink struct {
|
|||||||
lastProcessed atomic.Value // time.Time
|
lastProcessed atomic.Value // time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Defaults
|
||||||
|
DefaultFileMaxSizeMB = 100
|
||||||
|
DefaultFileMaxTotalSizeMB = 1000
|
||||||
|
DefaultFileMinDiskFreeMB = 100
|
||||||
|
DefaultFileRetentionHours = 168 // 7 days
|
||||||
|
DefaultFileBufferSize = 1000
|
||||||
|
DefaultFileFlushIntervalMs = 100
|
||||||
|
)
|
||||||
|
|
||||||
// NewFileSinkPlugin creates a file sink through plugin factory
|
// NewFileSinkPlugin creates a file sink through plugin factory
|
||||||
func NewFileSinkPlugin(
|
func NewFileSinkPlugin(
|
||||||
id string,
|
id string,
|
||||||
@ -62,13 +72,15 @@ func NewFileSinkPlugin(
|
|||||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields and apply defaults
|
// Validate
|
||||||
if opts.Directory == "" {
|
if err := lconfig.NonEmpty(opts.Directory); err != nil {
|
||||||
return nil, fmt.Errorf("directory is required")
|
return nil, fmt.Errorf("directory: %w", err)
|
||||||
}
|
}
|
||||||
if opts.Name == "" {
|
if err := lconfig.NonEmpty(opts.Name); err != nil {
|
||||||
return nil, fmt.Errorf("name is required")
|
return nil, fmt.Errorf("name: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
if opts.MaxSizeMB <= 0 {
|
if opts.MaxSizeMB <= 0 {
|
||||||
opts.MaxSizeMB = DefaultFileMaxSizeMB
|
opts.MaxSizeMB = DefaultFileMaxSizeMB
|
||||||
}
|
}
|
||||||
@ -88,7 +100,6 @@ func NewFileSinkPlugin(
|
|||||||
opts.FlushIntervalMs = DefaultFileFlushIntervalMs
|
opts.FlushIntervalMs = DefaultFileFlushIntervalMs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Create and return plugin instance
|
|
||||||
// Create configuration for the internal log writer
|
// Create configuration for the internal log writer
|
||||||
writerConfig := log.DefaultConfig()
|
writerConfig := log.DefaultConfig()
|
||||||
writerConfig.Directory = opts.Directory
|
writerConfig.Directory = opts.Directory
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Server lifecycle
|
|
||||||
HttpServerStartTimeout = 100 * time.Millisecond
|
|
||||||
HttpServerShutdownTimeout = 2 * time.Second
|
|
||||||
|
|
||||||
// Defaults
|
|
||||||
DefaultHTTPHost = "0.0.0.0"
|
|
||||||
DefaultHTTPBufferSize = 1000
|
|
||||||
DefaultHTTPStreamPath = "/stream"
|
|
||||||
DefaultHTTPStatusPath = "/status"
|
|
||||||
HTTPMaxPort = 65535
|
|
||||||
)
|
|
||||||
@ -66,6 +66,19 @@ type HTTPSink struct {
|
|||||||
lastProcessed atomic.Value // time.Time
|
lastProcessed atomic.Value // time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Server lifecycle
|
||||||
|
HttpServerStartTimeout = 100 * time.Millisecond
|
||||||
|
HttpServerShutdownTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
DefaultHTTPHost = "0.0.0.0"
|
||||||
|
DefaultHTTPBufferSize = 1000
|
||||||
|
DefaultHTTPStreamPath = "/stream"
|
||||||
|
DefaultHTTPStatusPath = "/status"
|
||||||
|
HTTPMaxPort = 65535
|
||||||
|
)
|
||||||
|
|
||||||
// NewHTTPSinkPlugin creates an HTTP sink through plugin factory
|
// NewHTTPSinkPlugin creates an HTTP sink through plugin factory
|
||||||
func NewHTTPSinkPlugin(
|
func NewHTTPSinkPlugin(
|
||||||
id string,
|
id string,
|
||||||
@ -83,10 +96,12 @@ func NewHTTPSinkPlugin(
|
|||||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and apply defaults
|
// Validate
|
||||||
if opts.Port <= 0 || opts.Port > HTTPMaxPort {
|
if opts.Port <= 0 || opts.Port > HTTPMaxPort {
|
||||||
return nil, fmt.Errorf("port must be between 1 and %d", HTTPMaxPort)
|
return nil, fmt.Errorf("port must be between 1 and %d", HTTPMaxPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
if opts.BufferSize <= 0 {
|
if opts.BufferSize <= 0 {
|
||||||
opts.BufferSize = DefaultHTTPBufferSize
|
opts.BufferSize = DefaultHTTPBufferSize
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
package tcp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Server lifecycle
|
|
||||||
TCPServerStartTimeout = 100 * time.Millisecond
|
|
||||||
TCPServerShutdownTimeout = 2 * time.Second
|
|
||||||
|
|
||||||
// Connection management
|
|
||||||
TCPMaxConsecutiveWriteErrors = 3
|
|
||||||
TCPMaxPort = 65535
|
|
||||||
|
|
||||||
// Defaults
|
|
||||||
DefaultTCPHost = "0.0.0.0"
|
|
||||||
DefaultTCPBufferSize = 1000
|
|
||||||
DefaultTCPWriteTimeoutMS = 5000
|
|
||||||
DefaultTCPKeepAlivePeriod = 30000
|
|
||||||
)
|
|
||||||
@ -61,6 +61,22 @@ type TCPSink struct {
|
|||||||
errorMu sync.Mutex
|
errorMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Server lifecycle
|
||||||
|
TCPServerStartTimeout = 100 * time.Millisecond
|
||||||
|
TCPServerShutdownTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
// Connection management
|
||||||
|
TCPMaxConsecutiveWriteErrors = 3
|
||||||
|
TCPMaxPort = 65535
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
DefaultTCPHost = "0.0.0.0"
|
||||||
|
DefaultTCPBufferSize = 1000
|
||||||
|
DefaultTCPWriteTimeoutMS = 5000
|
||||||
|
DefaultTCPKeepAlivePeriod = 30000
|
||||||
|
)
|
||||||
|
|
||||||
// NewTCPSinkPlugin creates a TCP sink through plugin factory
|
// NewTCPSinkPlugin creates a TCP sink through plugin factory
|
||||||
func NewTCPSinkPlugin(
|
func NewTCPSinkPlugin(
|
||||||
id string,
|
id string,
|
||||||
@ -80,11 +96,12 @@ func NewTCPSinkPlugin(
|
|||||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
// Validate
|
||||||
// Validate and apply defaults
|
if err := lconfig.Port(opts.Port); err != nil {
|
||||||
if opts.Port <= 0 || opts.Port > TCPMaxPort {
|
return nil, fmt.Errorf("port: %w", err)
|
||||||
return nil, fmt.Errorf("port must be between 1 and %d", TCPMaxPort)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
if opts.BufferSize <= 0 {
|
if opts.BufferSize <= 0 {
|
||||||
opts.BufferSize = DefaultTCPBufferSize
|
opts.BufferSize = DefaultTCPBufferSize
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,6 +56,10 @@ type ConsoleSource struct {
|
|||||||
lastEntryTime atomic.Value // time.Time
|
lastEntryTime atomic.Value // time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultConsoleSourceBufferSize = 1000
|
||||||
|
)
|
||||||
|
|
||||||
// NewConsoleSourcePlugin creates a console source through plugin factory
|
// NewConsoleSourcePlugin creates a console source through plugin factory
|
||||||
func NewConsoleSourcePlugin(
|
func NewConsoleSourcePlugin(
|
||||||
id string,
|
id string,
|
||||||
@ -63,22 +67,19 @@ func NewConsoleSourcePlugin(
|
|||||||
logger *log.Logger,
|
logger *log.Logger,
|
||||||
proxy *session.Proxy,
|
proxy *session.Proxy,
|
||||||
) (source.Source, error) {
|
) (source.Source, error) {
|
||||||
// Step 1: Create empty config struct with defaults
|
opts := &config.ConsoleSourceOptions{}
|
||||||
opts := &config.ConsoleSourceOptions{
|
|
||||||
BufferSize: 1000, // Default buffer size
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Use lconfig to scan map into struct (overriding defaults)
|
// Scan config map
|
||||||
if err := lconfig.ScanMap(configMap, opts); err != nil {
|
if err := lconfig.ScanMap(configMap, opts); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Validate required fields
|
// Validate and apply defaults
|
||||||
if opts.BufferSize <= 0 {
|
if opts.BufferSize <= 0 {
|
||||||
opts.BufferSize = 1000
|
opts.BufferSize = DefaultConsoleSourceBufferSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Create and return plugin instance
|
// Create and return plugin instance
|
||||||
cs := &ConsoleSource{
|
cs := &ConsoleSource{
|
||||||
id: id,
|
id: id,
|
||||||
proxy: proxy,
|
proxy: proxy,
|
||||||
@ -89,7 +90,7 @@ func NewConsoleSourcePlugin(
|
|||||||
}
|
}
|
||||||
cs.lastEntryTime.Store(time.Time{})
|
cs.lastEntryTime.Store(time.Time{})
|
||||||
|
|
||||||
// Create session for console
|
// Create session
|
||||||
cs.session = proxy.CreateSession(
|
cs.session = proxy.CreateSession(
|
||||||
"console_stdin",
|
"console_stdin",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
package console
|
|
||||||
@ -57,6 +57,12 @@ type FileSource struct {
|
|||||||
lastEntryTime atomic.Value // time.Time
|
lastEntryTime atomic.Value // time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultFileSourcePattern = "*"
|
||||||
|
DefaultFileSourceCheckIntervalMS = 100
|
||||||
|
MinFileSourceCheckIntervalMS = 10
|
||||||
|
)
|
||||||
|
|
||||||
// NewFileSourcePlugin creates a file source through plugin factory
|
// NewFileSourcePlugin creates a file source through plugin factory
|
||||||
func NewFileSourcePlugin(
|
func NewFileSourcePlugin(
|
||||||
id string,
|
id string,
|
||||||
@ -64,28 +70,28 @@ func NewFileSourcePlugin(
|
|||||||
logger *log.Logger,
|
logger *log.Logger,
|
||||||
proxy *session.Proxy,
|
proxy *session.Proxy,
|
||||||
) (source.Source, error) {
|
) (source.Source, error) {
|
||||||
// Step 1: Create empty config struct with defaults
|
opts := &config.FileSourceOptions{}
|
||||||
opts := &config.FileSourceOptions{
|
|
||||||
Directory: "", // Required field - no default
|
|
||||||
Pattern: "*", // Default pattern
|
|
||||||
CheckIntervalMS: 100, // Default check interval
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Use lconfig to scan map into struct (overriding defaults)
|
// Use lconfig to scan map into struct (overriding defaults)
|
||||||
if err := lconfig.ScanMap(configMap, opts); err != nil {
|
if err := lconfig.ScanMap(configMap, opts); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Validate required fields
|
// Validate and apply defaults
|
||||||
if opts.Directory == "" {
|
if err := lconfig.NonEmpty(opts.Directory); err != nil {
|
||||||
return nil, fmt.Errorf("directory is mandatory")
|
return nil, fmt.Errorf("directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.CheckIntervalMS < 10 {
|
if opts.Pattern == "" {
|
||||||
return nil, fmt.Errorf("check_interval_ms must be at least 10ms")
|
opts.Pattern = DefaultFileSourcePattern
|
||||||
|
}
|
||||||
|
if opts.CheckIntervalMS <= 0 {
|
||||||
|
opts.CheckIntervalMS = DefaultFileSourceCheckIntervalMS
|
||||||
|
} else if opts.CheckIntervalMS < MinFileSourceCheckIntervalMS {
|
||||||
|
return nil, fmt.Errorf("check_interval_ms: must be >= %d", MinFileSourceCheckIntervalMS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Create and return plugin instance
|
// Create and return plugin instance
|
||||||
fs := &FileSource{
|
fs := &FileSource{
|
||||||
id: id,
|
id: id,
|
||||||
proxy: proxy,
|
proxy: proxy,
|
||||||
|
|||||||
@ -53,6 +53,12 @@ type RandomSource struct {
|
|||||||
lastEntryTime atomic.Value // time.Time
|
lastEntryTime atomic.Value // time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultRandomSourceIntervalMS = 500
|
||||||
|
DefaultRandomSourceFormat = "txt"
|
||||||
|
DefaultRandomSourceLength = 20
|
||||||
|
)
|
||||||
|
|
||||||
// NewRandomSourcePlugin creates a random source through plugin factory
|
// NewRandomSourcePlugin creates a random source through plugin factory
|
||||||
func NewRandomSourcePlugin(
|
func NewRandomSourcePlugin(
|
||||||
id string,
|
id string,
|
||||||
@ -69,26 +75,33 @@ func NewRandomSourcePlugin(
|
|||||||
Special: false,
|
Special: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Use lconfig to scan map into struct (overriding defaults)
|
// Scan config map
|
||||||
if err := lconfig.ScanMap(configMap, opts); err != nil {
|
if err := lconfig.ScanMap(configMap, opts); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Validate
|
// Defaults
|
||||||
if opts.IntervalMS <= 0 {
|
if opts.IntervalMS <= 0 {
|
||||||
return nil, fmt.Errorf("interval_ms must be positive")
|
opts.IntervalMS = DefaultRandomSourceIntervalMS
|
||||||
}
|
}
|
||||||
|
if opts.Format == "" {
|
||||||
|
opts.Format = DefaultRandomSourceFormat
|
||||||
|
}
|
||||||
|
if opts.Length <= 0 {
|
||||||
|
opts.Length = DefaultRandomSourceLength
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate
|
||||||
if opts.JitterMS < 0 {
|
if opts.JitterMS < 0 {
|
||||||
return nil, fmt.Errorf("jitter_ms cannot be negative")
|
return nil, fmt.Errorf("jitter_ms cannot be negative")
|
||||||
}
|
}
|
||||||
if opts.JitterMS > opts.IntervalMS {
|
if opts.JitterMS > opts.IntervalMS {
|
||||||
opts.JitterMS = opts.IntervalMS
|
opts.JitterMS = opts.IntervalMS
|
||||||
}
|
}
|
||||||
if opts.Length <= 0 {
|
|
||||||
return nil, fmt.Errorf("length must be positive")
|
validateFormat := lconfig.OneOf("raw", "txt", "json")
|
||||||
}
|
if err := validateFormat(opts.Format); err != nil {
|
||||||
if opts.Format != "raw" && opts.Format != "txt" && opts.Format != "json" {
|
return nil, fmt.Errorf("format: %w", err)
|
||||||
return nil, fmt.Errorf("format must be 'raw', 'txt', or 'json'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rs := &RandomSource{
|
rs := &RandomSource{
|
||||||
|
|||||||
Reference in New Issue
Block a user