mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-25 04:16:22 +02:00
Release prep: 54 engines, self-hosted signatures, i18n, dashboard updates
This commit is contained in:
parent
694e32be26
commit
41cbfd6e0a
178 changed files with 36008 additions and 399 deletions
153
internal/application/soc/load_test.go
Normal file
153
internal/application/soc/load_test.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package soc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
domsoc "github.com/syntrex/gomcp/internal/domain/soc"
|
||||
"github.com/syntrex/gomcp/internal/infrastructure/audit"
|
||||
"github.com/syntrex/gomcp/internal/infrastructure/sqlite"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestLoadTest_SustainedThroughput measures SOC pipeline throughput and latency
|
||||
// under sustained concurrent load. Reports p50/p95/p99 latencies and events/sec.
|
||||
func TestLoadTest_SustainedThroughput(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping load test in short mode")
|
||||
}
|
||||
|
||||
// Setup service with file-based SQLite for concurrency safety.
|
||||
tmpDir := t.TempDir()
|
||||
db, err := sqlite.Open(tmpDir + "/loadtest.db")
|
||||
require.NoError(t, err)
|
||||
|
||||
repo, err := sqlite.NewSOCRepo(db)
|
||||
require.NoError(t, err)
|
||||
|
||||
logger, err := audit.NewDecisionLogger(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
logger.Close()
|
||||
db.Close()
|
||||
})
|
||||
|
||||
svc := NewService(repo, logger)
|
||||
svc.DisableRateLimit() // bypass rate limiter for raw throughput
|
||||
|
||||
// Load test parameters.
|
||||
const (
|
||||
numWorkers = 16
|
||||
eventsPerWkr = 200
|
||||
totalEvents = numWorkers * eventsPerWkr
|
||||
)
|
||||
|
||||
categories := []string{"jailbreak", "injection", "exfiltration", "auth_bypass", "tool_abuse"}
|
||||
sources := []domsoc.EventSource{domsoc.SourceSentinelCore, domsoc.SourceShield, domsoc.SourceGoMCP}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
latencies = make([]time.Duration, totalEvents)
|
||||
errors int64
|
||||
incidents int64
|
||||
)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
for w := 0; w < numWorkers; w++ {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < eventsPerWkr; i++ {
|
||||
idx := workerID*eventsPerWkr + i
|
||||
evt := domsoc.NewSOCEvent(
|
||||
sources[idx%len(sources)],
|
||||
domsoc.SeverityHigh,
|
||||
categories[idx%len(categories)],
|
||||
fmt.Sprintf("load-test w%d-e%d", workerID, i),
|
||||
)
|
||||
evt.SensorID = fmt.Sprintf("load-sensor-%d", workerID)
|
||||
|
||||
t0 := time.Now()
|
||||
_, inc, err := svc.IngestEvent(evt)
|
||||
latencies[idx] = time.Since(t0)
|
||||
|
||||
if err != nil {
|
||||
atomic.AddInt64(&errors, 1)
|
||||
}
|
||||
if inc != nil {
|
||||
atomic.AddInt64(&incidents, 1)
|
||||
}
|
||||
}
|
||||
}(w)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
totalDuration := time.Since(start)
|
||||
|
||||
// Compute latency percentiles.
|
||||
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
|
||||
|
||||
p50 := percentile(latencies, 50)
|
||||
p95 := percentile(latencies, 95)
|
||||
p99 := percentile(latencies, 99)
|
||||
mean := meanDuration(latencies)
|
||||
eventsPerSec := float64(totalEvents) / totalDuration.Seconds()
|
||||
|
||||
// Report results.
|
||||
t.Logf("═══════════════════════════════════════════════")
|
||||
t.Logf(" SENTINEL SOC Load Test Results")
|
||||
t.Logf("═══════════════════════════════════════════════")
|
||||
t.Logf(" Workers: %d", numWorkers)
|
||||
t.Logf(" Events/worker: %d", eventsPerWkr)
|
||||
t.Logf(" Total events: %d", totalEvents)
|
||||
t.Logf(" Duration: %s", totalDuration.Round(time.Millisecond))
|
||||
t.Logf(" Throughput: %.0f events/sec", eventsPerSec)
|
||||
t.Logf("───────────────────────────────────────────────")
|
||||
t.Logf(" Mean: %s", mean.Round(time.Microsecond))
|
||||
t.Logf(" P50: %s", p50.Round(time.Microsecond))
|
||||
t.Logf(" P95: %s", p95.Round(time.Microsecond))
|
||||
t.Logf(" P99: %s", p99.Round(time.Microsecond))
|
||||
t.Logf(" Min: %s", latencies[0].Round(time.Microsecond))
|
||||
t.Logf(" Max: %s", latencies[len(latencies)-1].Round(time.Microsecond))
|
||||
t.Logf("───────────────────────────────────────────────")
|
||||
t.Logf(" Errors: %d (%.1f%%)", errors, float64(errors)/float64(totalEvents)*100)
|
||||
t.Logf(" Incidents: %d", incidents)
|
||||
t.Logf("═══════════════════════════════════════════════")
|
||||
|
||||
// Assertions: basic sanity checks.
|
||||
require.Less(t, float64(errors)/float64(totalEvents), 0.05, "error rate should be < 5%%")
|
||||
require.Greater(t, eventsPerSec, float64(100), "should sustain > 100 events/sec")
|
||||
}
|
||||
|
||||
func percentile(sorted []time.Duration, p int) time.Duration {
|
||||
if len(sorted) == 0 {
|
||||
return 0
|
||||
}
|
||||
idx := int(math.Ceil(float64(p)/100.0*float64(len(sorted)))) - 1
|
||||
if idx < 0 {
|
||||
idx = 0
|
||||
}
|
||||
if idx >= len(sorted) {
|
||||
idx = len(sorted) - 1
|
||||
}
|
||||
return sorted[idx]
|
||||
}
|
||||
|
||||
func meanDuration(ds []time.Duration) time.Duration {
|
||||
if len(ds) == 0 {
|
||||
return 0
|
||||
}
|
||||
var total time.Duration
|
||||
for _, d := range ds {
|
||||
total += d
|
||||
}
|
||||
return total / time.Duration(len(ds))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue