358 lines
8.9 KiB
Go
358 lines
8.9 KiB
Go
package random
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"logwisp/internal/config"
|
|
"logwisp/internal/core"
|
|
"logwisp/internal/plugin"
|
|
"logwisp/internal/session"
|
|
"logwisp/internal/source"
|
|
|
|
lconfig "github.com/lixenwraith/config"
|
|
"github.com/lixenwraith/log"
|
|
)
|
|
|
|
// init registers the component in plugin factory
|
|
func init() {
|
|
if err := plugin.RegisterSource("random", NewRandomSourcePlugin); err != nil {
|
|
panic(fmt.Sprintf("failed to register random source: %v", err))
|
|
}
|
|
}
|
|
|
|
// RandomSource generates random log entries for testing
|
|
type RandomSource struct {
|
|
// Plugin identity and session management
|
|
id string
|
|
proxy *session.Proxy
|
|
session *session.Session
|
|
|
|
// Configuration
|
|
config *config.RandomSourceOptions
|
|
|
|
// Application
|
|
subscribers []chan core.LogEntry
|
|
logger *log.Logger
|
|
rng *rand.Rand
|
|
mu sync.RWMutex
|
|
|
|
// Runtime
|
|
done chan struct{}
|
|
wg sync.WaitGroup
|
|
cancel chan struct{}
|
|
|
|
// Statistics
|
|
totalEntries atomic.Uint64
|
|
droppedEntries atomic.Uint64
|
|
startTime time.Time
|
|
lastEntryTime atomic.Value // time.Time
|
|
}
|
|
|
|
const (
|
|
DefaultRandomSourceIntervalMS = 500
|
|
DefaultRandomSourceFormat = "txt"
|
|
DefaultRandomSourceLength = 20
|
|
)
|
|
|
|
// NewRandomSourcePlugin creates a random source through plugin factory
|
|
func NewRandomSourcePlugin(
|
|
id string,
|
|
configMap map[string]any,
|
|
logger *log.Logger,
|
|
proxy *session.Proxy,
|
|
) (source.Source, error) {
|
|
// Step 1: Create empty config struct with defaults
|
|
opts := &config.RandomSourceOptions{
|
|
IntervalMS: 500,
|
|
JitterMS: 0,
|
|
Format: "txt",
|
|
Length: 20,
|
|
Special: false,
|
|
}
|
|
|
|
// Scan config map
|
|
if err := lconfig.ScanMap(configMap, opts); err != nil {
|
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
|
}
|
|
|
|
// Defaults
|
|
if opts.IntervalMS <= 0 {
|
|
opts.IntervalMS = DefaultRandomSourceIntervalMS
|
|
}
|
|
if opts.Format == "" {
|
|
opts.Format = DefaultRandomSourceFormat
|
|
}
|
|
if opts.Length <= 0 {
|
|
opts.Length = DefaultRandomSourceLength
|
|
}
|
|
|
|
// Validate
|
|
if opts.JitterMS < 0 {
|
|
return nil, fmt.Errorf("jitter_ms cannot be negative")
|
|
}
|
|
if opts.JitterMS > opts.IntervalMS {
|
|
opts.JitterMS = opts.IntervalMS
|
|
}
|
|
|
|
validateFormat := lconfig.OneOf("raw", "txt", "json")
|
|
if err := validateFormat(opts.Format); err != nil {
|
|
return nil, fmt.Errorf("format: %w", err)
|
|
}
|
|
|
|
rs := &RandomSource{
|
|
id: id,
|
|
proxy: proxy,
|
|
config: opts,
|
|
subscribers: make([]chan core.LogEntry, 0),
|
|
done: make(chan struct{}),
|
|
cancel: make(chan struct{}),
|
|
logger: logger,
|
|
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
|
|
}
|
|
rs.lastEntryTime.Store(time.Time{})
|
|
|
|
// Create session for random source
|
|
rs.session = proxy.CreateSession(
|
|
fmt.Sprintf("random://%s", id),
|
|
map[string]any{
|
|
"instance_id": id,
|
|
"type": "random",
|
|
"format": opts.Format,
|
|
"interval_ms": opts.IntervalMS,
|
|
},
|
|
)
|
|
|
|
logger.Debug("msg", "Random source initialized",
|
|
"component", "random_source",
|
|
"instance_id", id,
|
|
"format", opts.Format,
|
|
"interval_ms", opts.IntervalMS,
|
|
"jitter_ms", opts.JitterMS)
|
|
|
|
return rs, nil
|
|
}
|
|
|
|
// Capabilities returns supported capabilities
|
|
func (rs *RandomSource) Capabilities() []core.Capability {
|
|
return []core.Capability{
|
|
core.CapSessionAware,
|
|
}
|
|
}
|
|
|
|
// Subscribe returns a channel for receiving log entries
|
|
func (rs *RandomSource) Subscribe() <-chan core.LogEntry {
|
|
rs.mu.Lock()
|
|
defer rs.mu.Unlock()
|
|
ch := make(chan core.LogEntry, 1000)
|
|
rs.subscribers = append(rs.subscribers, ch)
|
|
return ch
|
|
}
|
|
|
|
// Start begins generating random log entries
|
|
func (rs *RandomSource) Start() error {
|
|
rs.startTime = time.Now()
|
|
rs.wg.Add(1)
|
|
go rs.generateLoop()
|
|
|
|
rs.proxy.UpdateActivity(rs.session.ID)
|
|
rs.logger.Debug("msg", "Random source started",
|
|
"component", "random_source",
|
|
"instance_id", rs.id)
|
|
return nil
|
|
}
|
|
|
|
// Stop signals the source to stop generating
|
|
func (rs *RandomSource) Stop() {
|
|
close(rs.cancel)
|
|
rs.wg.Wait()
|
|
|
|
if rs.session != nil {
|
|
rs.proxy.RemoveSession(rs.session.ID)
|
|
}
|
|
|
|
rs.mu.Lock()
|
|
for _, ch := range rs.subscribers {
|
|
close(ch)
|
|
}
|
|
rs.mu.Unlock()
|
|
|
|
rs.logger.Debug("msg", "Random source stopped",
|
|
"component", "random_source",
|
|
"instance_id", rs.id,
|
|
"total_entries", rs.totalEntries.Load())
|
|
}
|
|
|
|
// GetStats returns the source's statistics
|
|
func (rs *RandomSource) GetStats() source.SourceStats {
|
|
lastEntry, _ := rs.lastEntryTime.Load().(time.Time)
|
|
|
|
return source.SourceStats{
|
|
ID: rs.id,
|
|
Type: "random",
|
|
TotalEntries: rs.totalEntries.Load(),
|
|
DroppedEntries: rs.droppedEntries.Load(),
|
|
StartTime: rs.startTime,
|
|
LastEntryTime: lastEntry,
|
|
Details: map[string]any{
|
|
"format": rs.config.Format,
|
|
"interval_ms": rs.config.IntervalMS,
|
|
"jitter_ms": rs.config.JitterMS,
|
|
"length": rs.config.Length,
|
|
"special": rs.config.Special,
|
|
},
|
|
}
|
|
}
|
|
|
|
// generateLoop continuously generates random log entries at configured intervals
|
|
func (rs *RandomSource) generateLoop() {
|
|
defer rs.wg.Done()
|
|
|
|
for {
|
|
// Calculate next interval with jitter
|
|
interval := time.Duration(rs.config.IntervalMS) * time.Millisecond
|
|
if rs.config.JitterMS > 0 {
|
|
jitter := time.Duration(rs.rng.Intn(int(rs.config.JitterMS))) * time.Millisecond
|
|
interval = interval - time.Duration(rs.config.JitterMS/2)*time.Millisecond + jitter
|
|
}
|
|
|
|
select {
|
|
case <-time.After(interval):
|
|
entry := rs.generateEntry()
|
|
rs.publish(entry)
|
|
rs.proxy.UpdateActivity(rs.session.ID)
|
|
case <-rs.cancel:
|
|
return
|
|
case <-rs.done:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// generateEntry creates a random log entry based on configured format
|
|
func (rs *RandomSource) generateEntry() core.LogEntry {
|
|
now := time.Now()
|
|
|
|
switch rs.config.Format {
|
|
case "raw":
|
|
message := rs.generateRandomString(int(rs.config.Length))
|
|
return core.LogEntry{
|
|
Time: now,
|
|
Source: fmt.Sprintf("random_%s", rs.id),
|
|
Message: message,
|
|
RawSize: int64(len(message) + 1), // +1 for newline
|
|
}
|
|
|
|
case "txt":
|
|
level := rs.randomLogLevel()
|
|
message := rs.generateRandomString(int(rs.config.Length))
|
|
formatted := fmt.Sprintf("[%s] [%s] random_%s - %s",
|
|
now.Format(time.RFC3339),
|
|
level,
|
|
rs.id,
|
|
message)
|
|
return core.LogEntry{
|
|
Time: now,
|
|
Source: fmt.Sprintf("random_%s", rs.id),
|
|
Level: level,
|
|
Message: formatted,
|
|
RawSize: int64(len(formatted) + 1),
|
|
}
|
|
|
|
case "json":
|
|
level := rs.randomLogLevel()
|
|
message := rs.generateRandomString(int(rs.config.Length))
|
|
data := map[string]any{
|
|
"time": now.Format(time.RFC3339Nano),
|
|
"level": level,
|
|
"source": fmt.Sprintf("random_%s", rs.id),
|
|
"message": message,
|
|
}
|
|
jsonBytes, _ := json.Marshal(data)
|
|
return core.LogEntry{
|
|
Time: now,
|
|
Source: fmt.Sprintf("random_%s", rs.id),
|
|
Level: level,
|
|
Message: string(jsonBytes),
|
|
RawSize: int64(len(jsonBytes) + 1),
|
|
}
|
|
|
|
default:
|
|
return core.LogEntry{}
|
|
}
|
|
}
|
|
|
|
// generateRandomString creates a random string of specified length
|
|
func (rs *RandomSource) generateRandomString(length int) string {
|
|
const normalChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "
|
|
const specialChars = "\t\n\r\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F"
|
|
const unicodeChars = "™€¢£¥§©®°±µ¶·ÀÉÑÖÜßäëïöü←↑→↓∀∃∅∇∈∉∪∩≈≠≤≥"
|
|
|
|
result := make([]byte, 0, length)
|
|
|
|
if rs.config.Special && length >= 3 {
|
|
// Reserve space for at least one special and one unicode char
|
|
normalLength := length - 2
|
|
|
|
// Generate normal characters
|
|
for i := 0; i < normalLength; i++ {
|
|
result = append(result, normalChars[rs.rng.Intn(len(normalChars))])
|
|
}
|
|
|
|
// Insert special character at random position
|
|
specialPos := rs.rng.Intn(len(result) + 1)
|
|
specialChar := specialChars[rs.rng.Intn(len(specialChars))]
|
|
result = append(result[:specialPos], append([]byte{specialChar}, result[specialPos:]...)...)
|
|
|
|
// Insert unicode character at random position
|
|
unicodePos := rs.rng.Intn(len(result) + 1)
|
|
unicodeChar := unicodeChars[rs.rng.Intn(len(unicodeChars)/3)*3:]
|
|
if len(unicodeChar) >= 3 {
|
|
unicodeBytes := []byte(unicodeChar[:3])
|
|
if unicodePos == len(result) {
|
|
result = append(result, unicodeBytes...)
|
|
} else {
|
|
result = append(result[:unicodePos], append(unicodeBytes, result[unicodePos:]...)...)
|
|
}
|
|
}
|
|
|
|
// Trim to exact length if needed
|
|
if len(result) > length {
|
|
result = result[:length]
|
|
}
|
|
} else {
|
|
// Normal generation without special characters
|
|
for i := 0; i < length; i++ {
|
|
result = append(result, normalChars[rs.rng.Intn(len(normalChars))])
|
|
}
|
|
}
|
|
|
|
return string(result)
|
|
}
|
|
|
|
// randomLogLevel returns a random log level
|
|
func (rs *RandomSource) randomLogLevel() string {
|
|
levels := []string{"DEBUG", "INFO", "WARN", "ERROR"}
|
|
return levels[rs.rng.Intn(len(levels))]
|
|
}
|
|
|
|
// publish sends a log entry to all subscribers
|
|
func (rs *RandomSource) publish(entry core.LogEntry) {
|
|
rs.mu.RLock()
|
|
defer rs.mu.RUnlock()
|
|
|
|
rs.totalEntries.Add(1)
|
|
rs.lastEntryTime.Store(entry.Time)
|
|
|
|
for _, ch := range rs.subscribers {
|
|
select {
|
|
case ch <- entry:
|
|
default:
|
|
rs.droppedEntries.Add(1)
|
|
}
|
|
}
|
|
} |