381 lines
8.4 KiB
Go
381 lines
8.4 KiB
Go
// FILE: src/internal/source/tcp.go
|
|
package source
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"logwisp/src/internal/config"
|
|
"logwisp/src/internal/netlimit"
|
|
|
|
"github.com/lixenwraith/log"
|
|
"github.com/panjf2000/gnet/v2"
|
|
)
|
|
|
|
// TCPSource receives log entries via TCP connections
|
|
type TCPSource struct {
|
|
port int
|
|
bufferSize int
|
|
server *tcpSourceServer
|
|
subscribers []chan LogEntry
|
|
mu sync.RWMutex
|
|
done chan struct{}
|
|
engine *gnet.Engine
|
|
engineMu sync.Mutex
|
|
wg sync.WaitGroup
|
|
netLimiter *netlimit.Limiter
|
|
logger *log.Logger
|
|
|
|
// Statistics
|
|
totalEntries atomic.Uint64
|
|
droppedEntries atomic.Uint64
|
|
invalidEntries atomic.Uint64
|
|
activeConns atomic.Int32
|
|
startTime time.Time
|
|
lastEntryTime atomic.Value // time.Time
|
|
}
|
|
|
|
// NewTCPSource creates a new TCP server source
|
|
func NewTCPSource(options map[string]any, logger *log.Logger) (*TCPSource, error) {
|
|
port, ok := toInt(options["port"])
|
|
if !ok || port < 1 || port > 65535 {
|
|
return nil, fmt.Errorf("tcp source requires valid 'port' option")
|
|
}
|
|
|
|
bufferSize := 1000
|
|
if bufSize, ok := toInt(options["buffer_size"]); ok && bufSize > 0 {
|
|
bufferSize = bufSize
|
|
}
|
|
|
|
t := &TCPSource{
|
|
port: port,
|
|
bufferSize: bufferSize,
|
|
done: make(chan struct{}),
|
|
startTime: time.Now(),
|
|
logger: logger,
|
|
}
|
|
t.lastEntryTime.Store(time.Time{})
|
|
|
|
// Initialize net limiter if configured
|
|
if rl, ok := options["net_limit"].(map[string]any); ok {
|
|
if enabled, _ := rl["enabled"].(bool); enabled {
|
|
cfg := config.NetLimitConfig{
|
|
Enabled: true,
|
|
}
|
|
|
|
if rps, ok := toFloat(rl["requests_per_second"]); ok {
|
|
cfg.RequestsPerSecond = rps
|
|
}
|
|
if burst, ok := toInt(rl["burst_size"]); ok {
|
|
cfg.BurstSize = burst
|
|
}
|
|
if limitBy, ok := rl["limit_by"].(string); ok {
|
|
cfg.LimitBy = limitBy
|
|
}
|
|
if maxPerIP, ok := toInt(rl["max_connections_per_ip"]); ok {
|
|
cfg.MaxConnectionsPerIP = maxPerIP
|
|
}
|
|
if maxTotal, ok := toInt(rl["max_total_connections"]); ok {
|
|
cfg.MaxTotalConnections = maxTotal
|
|
}
|
|
|
|
t.netLimiter = netlimit.New(cfg, logger)
|
|
}
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
func (t *TCPSource) Subscribe() <-chan LogEntry {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
ch := make(chan LogEntry, t.bufferSize)
|
|
t.subscribers = append(t.subscribers, ch)
|
|
return ch
|
|
}
|
|
|
|
func (t *TCPSource) Start() error {
|
|
t.server = &tcpSourceServer{
|
|
source: t,
|
|
clients: make(map[gnet.Conn]*tcpClient),
|
|
}
|
|
|
|
addr := fmt.Sprintf("tcp://:%d", t.port)
|
|
|
|
// Start gnet server in background
|
|
t.wg.Add(1)
|
|
go func() {
|
|
defer t.wg.Done()
|
|
t.logger.Info("msg", "TCP source server starting",
|
|
"component", "tcp_source",
|
|
"port", t.port)
|
|
|
|
err := gnet.Run(t.server, addr,
|
|
gnet.WithLogger(noopLogger{}),
|
|
gnet.WithMulticore(true),
|
|
gnet.WithReusePort(true),
|
|
)
|
|
if err != nil {
|
|
t.logger.Error("msg", "TCP source server failed",
|
|
"component", "tcp_source",
|
|
"port", t.port,
|
|
"error", err)
|
|
}
|
|
}()
|
|
|
|
// Give server time to start
|
|
time.Sleep(100 * time.Millisecond)
|
|
return nil
|
|
}
|
|
|
|
func (t *TCPSource) Stop() {
|
|
t.logger.Info("msg", "Stopping TCP source")
|
|
close(t.done)
|
|
|
|
// Stop gnet engine if running
|
|
t.engineMu.Lock()
|
|
engine := t.engine
|
|
t.engineMu.Unlock()
|
|
|
|
if engine != nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
(*engine).Stop(ctx)
|
|
}
|
|
|
|
// Shutdown net limiter
|
|
if t.netLimiter != nil {
|
|
t.netLimiter.Shutdown()
|
|
}
|
|
|
|
t.wg.Wait()
|
|
|
|
// Close subscriber channels
|
|
t.mu.Lock()
|
|
for _, ch := range t.subscribers {
|
|
close(ch)
|
|
}
|
|
t.mu.Unlock()
|
|
|
|
t.logger.Info("msg", "TCP source stopped")
|
|
}
|
|
|
|
func (t *TCPSource) GetStats() SourceStats {
|
|
lastEntry, _ := t.lastEntryTime.Load().(time.Time)
|
|
|
|
var netLimitStats map[string]any
|
|
if t.netLimiter != nil {
|
|
netLimitStats = t.netLimiter.GetStats()
|
|
}
|
|
|
|
return SourceStats{
|
|
Type: "tcp",
|
|
TotalEntries: t.totalEntries.Load(),
|
|
DroppedEntries: t.droppedEntries.Load(),
|
|
StartTime: t.startTime,
|
|
LastEntryTime: lastEntry,
|
|
Details: map[string]any{
|
|
"port": t.port,
|
|
"active_connections": t.activeConns.Load(),
|
|
"invalid_entries": t.invalidEntries.Load(),
|
|
"net_limit": netLimitStats,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (t *TCPSource) publish(entry LogEntry) bool {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
t.totalEntries.Add(1)
|
|
t.lastEntryTime.Store(entry.Time)
|
|
|
|
dropped := false
|
|
for _, ch := range t.subscribers {
|
|
select {
|
|
case ch <- entry:
|
|
default:
|
|
dropped = true
|
|
t.droppedEntries.Add(1)
|
|
}
|
|
}
|
|
|
|
if dropped {
|
|
t.logger.Debug("msg", "Dropped log entry - subscriber buffer full",
|
|
"component", "tcp_source")
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// tcpClient represents a connected TCP client
|
|
type tcpClient struct {
|
|
conn gnet.Conn
|
|
buffer bytes.Buffer
|
|
}
|
|
|
|
// tcpSourceServer handles gnet events
|
|
type tcpSourceServer struct {
|
|
gnet.BuiltinEventEngine
|
|
source *TCPSource
|
|
clients map[gnet.Conn]*tcpClient
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func (s *tcpSourceServer) OnBoot(eng gnet.Engine) gnet.Action {
|
|
// Store engine reference for shutdown
|
|
s.source.engineMu.Lock()
|
|
s.source.engine = &eng
|
|
s.source.engineMu.Unlock()
|
|
|
|
s.source.logger.Debug("msg", "TCP source server booted",
|
|
"component", "tcp_source",
|
|
"port", s.source.port)
|
|
return gnet.None
|
|
}
|
|
|
|
func (s *tcpSourceServer) OnOpen(c gnet.Conn) (out []byte, action gnet.Action) {
|
|
remoteAddr := c.RemoteAddr().String()
|
|
s.source.logger.Debug("msg", "TCP connection attempt",
|
|
"component", "tcp_source",
|
|
"remote_addr", remoteAddr)
|
|
|
|
// Check net limit
|
|
if s.source.netLimiter != nil {
|
|
remoteStr := c.RemoteAddr().String()
|
|
tcpAddr, err := net.ResolveTCPAddr("tcp", remoteStr)
|
|
if err != nil {
|
|
s.source.logger.Warn("msg", "Failed to parse TCP address",
|
|
"component", "tcp_source",
|
|
"remote_addr", remoteAddr,
|
|
"error", err)
|
|
return nil, gnet.Close
|
|
}
|
|
|
|
if !s.source.netLimiter.CheckTCP(tcpAddr) {
|
|
s.source.logger.Warn("msg", "TCP connection net limited",
|
|
"component", "tcp_source",
|
|
"remote_addr", remoteAddr)
|
|
return nil, gnet.Close
|
|
}
|
|
|
|
// Track connection
|
|
s.source.netLimiter.AddConnection(remoteStr)
|
|
}
|
|
|
|
// Create client state
|
|
s.mu.Lock()
|
|
s.clients[c] = &tcpClient{conn: c}
|
|
s.mu.Unlock()
|
|
|
|
newCount := s.source.activeConns.Add(1)
|
|
s.source.logger.Debug("msg", "TCP connection opened",
|
|
"component", "tcp_source",
|
|
"remote_addr", remoteAddr,
|
|
"active_connections", newCount)
|
|
|
|
return nil, gnet.None
|
|
}
|
|
|
|
func (s *tcpSourceServer) OnClose(c gnet.Conn, err error) gnet.Action {
|
|
remoteAddr := c.RemoteAddr().String()
|
|
|
|
// Remove client state
|
|
s.mu.Lock()
|
|
delete(s.clients, c)
|
|
s.mu.Unlock()
|
|
|
|
// Remove connection tracking
|
|
if s.source.netLimiter != nil {
|
|
s.source.netLimiter.RemoveConnection(remoteAddr)
|
|
}
|
|
|
|
newCount := s.source.activeConns.Add(-1)
|
|
s.source.logger.Debug("msg", "TCP connection closed",
|
|
"component", "tcp_source",
|
|
"remote_addr", remoteAddr,
|
|
"active_connections", newCount,
|
|
"error", err)
|
|
return gnet.None
|
|
}
|
|
|
|
func (s *tcpSourceServer) OnTraffic(c gnet.Conn) gnet.Action {
|
|
s.mu.RLock()
|
|
client, exists := s.clients[c]
|
|
s.mu.RUnlock()
|
|
|
|
if !exists {
|
|
return gnet.Close
|
|
}
|
|
|
|
// Read all available data
|
|
data, err := c.Next(-1)
|
|
if err != nil {
|
|
s.source.logger.Error("msg", "Error reading from connection",
|
|
"component", "tcp_source",
|
|
"error", err)
|
|
return gnet.Close
|
|
}
|
|
|
|
// Append to client buffer
|
|
client.buffer.Write(data)
|
|
|
|
// Process complete lines
|
|
for {
|
|
line, err := client.buffer.ReadBytes('\n')
|
|
if err != nil {
|
|
// No complete line available
|
|
break
|
|
}
|
|
|
|
// Trim newline
|
|
line = bytes.TrimRight(line, "\r\n")
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Parse JSON log entry
|
|
var entry LogEntry
|
|
if err := json.Unmarshal(line, &entry); err != nil {
|
|
s.source.invalidEntries.Add(1)
|
|
s.source.logger.Debug("msg", "Invalid JSON log entry",
|
|
"component", "tcp_source",
|
|
"error", err,
|
|
"data", string(line))
|
|
continue
|
|
}
|
|
|
|
// Validate and set defaults
|
|
if entry.Message == "" {
|
|
s.source.invalidEntries.Add(1)
|
|
continue
|
|
}
|
|
if entry.Time.IsZero() {
|
|
entry.Time = time.Now()
|
|
}
|
|
if entry.Source == "" {
|
|
entry.Source = "tcp"
|
|
}
|
|
|
|
// Publish the entry
|
|
s.source.publish(entry)
|
|
}
|
|
|
|
return gnet.None
|
|
}
|
|
|
|
// noopLogger implements gnet's Logger interface but discards everything
|
|
type noopLogger struct{}
|
|
|
|
func (n noopLogger) Debugf(format string, args ...any) {}
|
|
func (n noopLogger) Infof(format string, args ...any) {}
|
|
func (n noopLogger) Warnf(format string, args ...any) {}
|
|
func (n noopLogger) Errorf(format string, args ...any) {}
|
|
func (n noopLogger) Fatalf(format string, args ...any) {} |