mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-22 02:06:39 +00:00
add CLI
This commit is contained in:
@@ -17,6 +17,56 @@ Load testing tool for the NetBird signal server.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Standalone Binary
|
||||||
|
|
||||||
|
Build and run the load test as a standalone binary:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the binary
|
||||||
|
cd signal/loadtest/cmd/signal-loadtest
|
||||||
|
go build -o signal-loadtest
|
||||||
|
|
||||||
|
# Single message exchange
|
||||||
|
./signal-loadtest \
|
||||||
|
-server http://localhost:10000 \
|
||||||
|
-pairs-per-sec 10 \
|
||||||
|
-total-pairs 100 \
|
||||||
|
-message-size 100
|
||||||
|
|
||||||
|
# Continuous exchange for 30 seconds
|
||||||
|
./signal-loadtest \
|
||||||
|
-server http://localhost:10000 \
|
||||||
|
-pairs-per-sec 10 \
|
||||||
|
-total-pairs 20 \
|
||||||
|
-message-size 200 \
|
||||||
|
-exchange-duration 30s \
|
||||||
|
-message-interval 200ms
|
||||||
|
|
||||||
|
# Long-running test (10 minutes)
|
||||||
|
./signal-loadtest \
|
||||||
|
-server http://localhost:10000 \
|
||||||
|
-pairs-per-sec 20 \
|
||||||
|
-total-pairs 50 \
|
||||||
|
-message-size 500 \
|
||||||
|
-exchange-duration 10m \
|
||||||
|
-message-interval 100ms \
|
||||||
|
-test-duration 15m \
|
||||||
|
-log-level debug
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
./signal-loadtest -h
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available Flags:**
|
||||||
|
- `-server`: Signal server URL (default: `http://localhost:10000`)
|
||||||
|
- `-pairs-per-sec`: Peer pairs created per second (default: 10)
|
||||||
|
- `-total-pairs`: Total number of peer pairs (default: 100)
|
||||||
|
- `-message-size`: Message size in bytes (default: 100)
|
||||||
|
- `-test-duration`: Maximum test duration, 0 = unlimited (default: 0)
|
||||||
|
- `-exchange-duration`: Continuous exchange duration per pair, 0 = single message (default: 0)
|
||||||
|
- `-message-interval`: Interval between messages in continuous mode (default: 100ms)
|
||||||
|
- `-log-level`: Log level: trace, debug, info, warn, error (default: info)
|
||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
128
signal/loadtest/cmd/signal-loadtest/integration_test.go
Normal file
128
signal/loadtest/cmd/signal-loadtest/integration_test.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/shared/signal/proto"
|
||||||
|
"github.com/netbirdio/netbird/signal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCLI_SingleMessage(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
grpcServer, serverAddr := startTestSignalServer(t, ctx)
|
||||||
|
defer grpcServer.Stop()
|
||||||
|
|
||||||
|
cmd := exec.Command("go", "run", "main.go",
|
||||||
|
"-server", serverAddr,
|
||||||
|
"-pairs-per-sec", "3",
|
||||||
|
"-total-pairs", "5",
|
||||||
|
"-message-size", "50",
|
||||||
|
"-log-level", "warn")
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
require.NoError(t, err, "CLI should execute successfully")
|
||||||
|
|
||||||
|
outputStr := string(output)
|
||||||
|
require.Contains(t, outputStr, "Load Test Report")
|
||||||
|
require.Contains(t, outputStr, "Total Pairs Sent: 5")
|
||||||
|
require.Contains(t, outputStr, "Successful Exchanges: 5")
|
||||||
|
t.Logf("Output:\n%s", outputStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_ContinuousExchange(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping continuous exchange CLI test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
grpcServer, serverAddr := startTestSignalServer(t, ctx)
|
||||||
|
defer grpcServer.Stop()
|
||||||
|
|
||||||
|
cmd := exec.Command("go", "run", "main.go",
|
||||||
|
"-server", serverAddr,
|
||||||
|
"-pairs-per-sec", "2",
|
||||||
|
"-total-pairs", "3",
|
||||||
|
"-message-size", "100",
|
||||||
|
"-exchange-duration", "3s",
|
||||||
|
"-message-interval", "100ms",
|
||||||
|
"-log-level", "warn")
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
require.NoError(t, err, "CLI should execute successfully")
|
||||||
|
|
||||||
|
outputStr := string(output)
|
||||||
|
require.Contains(t, outputStr, "Load Test Report")
|
||||||
|
require.Contains(t, outputStr, "Total Pairs Sent: 3")
|
||||||
|
require.Contains(t, outputStr, "Successful Exchanges: 3")
|
||||||
|
t.Logf("Output:\n%s", outputStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_InvalidConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "negative pairs",
|
||||||
|
args: []string{"-pairs-per-sec", "-1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero total pairs",
|
||||||
|
args: []string{"-total-pairs", "0"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative message size",
|
||||||
|
args: []string{"-message-size", "-100"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
args := append([]string{"run", "main.go"}, tt.args...)
|
||||||
|
cmd := exec.Command("go", args...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
require.Error(t, err, "Should fail with invalid config")
|
||||||
|
require.Contains(t, string(output), "Configuration error")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startTestSignalServer(t *testing.T, ctx context.Context) (*grpc.Server, string) {
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
grpcServer := grpc.NewServer()
|
||||||
|
|
||||||
|
signalServer, err := server.NewServer(ctx, otel.Meter("cli-test"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
proto.RegisterSignalExchangeServer(grpcServer, signalServer)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := grpcServer.Serve(listener); err != nil {
|
||||||
|
t.Logf("Server stopped: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
return grpcServer, fmt.Sprintf("http://%s", listener.Addr().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
105
signal/loadtest/cmd/signal-loadtest/main.go
Normal file
105
signal/loadtest/cmd/signal-loadtest/main.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/signal/loadtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
serverURL string
|
||||||
|
pairsPerSecond int
|
||||||
|
totalPairs int
|
||||||
|
messageSize int
|
||||||
|
testDuration time.Duration
|
||||||
|
exchangeDuration time.Duration
|
||||||
|
messageInterval time.Duration
|
||||||
|
logLevel string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&serverURL, "server", "http://localhost:10000", "Signal server URL")
|
||||||
|
flag.IntVar(&pairsPerSecond, "pairs-per-sec", 10, "Number of peer pairs to create per second")
|
||||||
|
flag.IntVar(&totalPairs, "total-pairs", 100, "Total number of peer pairs to create")
|
||||||
|
flag.IntVar(&messageSize, "message-size", 100, "Size of test message in bytes")
|
||||||
|
flag.DurationVar(&testDuration, "test-duration", 0, "Maximum test duration (0 = unlimited)")
|
||||||
|
flag.DurationVar(&exchangeDuration, "exchange-duration", 0, "Duration for continuous message exchange per pair (0 = single message)")
|
||||||
|
flag.DurationVar(&messageInterval, "message-interval", 100*time.Millisecond, "Interval between messages in continuous mode")
|
||||||
|
flag.StringVar(&logLevel, "log-level", "info", "Log level (trace, debug, info, warn, error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
level, err := log.ParseLevel(logLevel)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Invalid log level: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
log.SetLevel(level)
|
||||||
|
|
||||||
|
config := loadtest.LoadTestConfig{
|
||||||
|
ServerURL: serverURL,
|
||||||
|
PairsPerSecond: pairsPerSecond,
|
||||||
|
TotalPairs: totalPairs,
|
||||||
|
MessageSize: messageSize,
|
||||||
|
TestDuration: testDuration,
|
||||||
|
ExchangeDuration: exchangeDuration,
|
||||||
|
MessageInterval: messageInterval,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateConfig(config); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err)
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Signal Load Test Configuration:")
|
||||||
|
log.Infof(" Server URL: %s", config.ServerURL)
|
||||||
|
log.Infof(" Pairs per second: %d", config.PairsPerSecond)
|
||||||
|
log.Infof(" Total pairs: %d", config.TotalPairs)
|
||||||
|
log.Infof(" Message size: %d bytes", config.MessageSize)
|
||||||
|
if config.TestDuration > 0 {
|
||||||
|
log.Infof(" Test duration: %v", config.TestDuration)
|
||||||
|
}
|
||||||
|
if config.ExchangeDuration > 0 {
|
||||||
|
log.Infof(" Exchange duration: %v", config.ExchangeDuration)
|
||||||
|
log.Infof(" Message interval: %v", config.MessageInterval)
|
||||||
|
} else {
|
||||||
|
log.Infof(" Mode: Single message exchange")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
lt := loadtest.NewLoadTest(config)
|
||||||
|
if err := lt.Run(); err != nil {
|
||||||
|
log.Errorf("Load test failed: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics := lt.GetMetrics()
|
||||||
|
metrics.PrintReport()
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfig(config loadtest.LoadTestConfig) error {
|
||||||
|
if config.ServerURL == "" {
|
||||||
|
return fmt.Errorf("server URL is required")
|
||||||
|
}
|
||||||
|
if config.PairsPerSecond <= 0 {
|
||||||
|
return fmt.Errorf("pairs-per-sec must be greater than 0")
|
||||||
|
}
|
||||||
|
if config.TotalPairs <= 0 {
|
||||||
|
return fmt.Errorf("total-pairs must be greater than 0")
|
||||||
|
}
|
||||||
|
if config.MessageSize <= 0 {
|
||||||
|
return fmt.Errorf("message-size must be greater than 0")
|
||||||
|
}
|
||||||
|
if config.MessageInterval <= 0 {
|
||||||
|
return fmt.Errorf("message-interval must be greater than 0")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
signal/loadtest/cmd/signal-loadtest/test.sh
Normal file
28
signal/loadtest/cmd/signal-loadtest/test.sh
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Building signal-loadtest binary..."
|
||||||
|
go build -o signal-loadtest
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Test 1: Single message exchange (5 pairs) ==="
|
||||||
|
./signal-loadtest \
|
||||||
|
-server http://localhost:10000 \
|
||||||
|
-pairs-per-sec 5 \
|
||||||
|
-total-pairs 5 \
|
||||||
|
-message-size 50 \
|
||||||
|
-log-level info
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Test 2: Continuous exchange (3 pairs, 5 seconds) ==="
|
||||||
|
./signal-loadtest \
|
||||||
|
-server http://localhost:10000 \
|
||||||
|
-pairs-per-sec 3 \
|
||||||
|
-total-pairs 3 \
|
||||||
|
-message-size 100 \
|
||||||
|
-exchange-duration 5s \
|
||||||
|
-message-interval 200ms \
|
||||||
|
-log-level info
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "All tests completed!"
|
||||||
Reference in New Issue
Block a user