Skip to content

Commit 76380bc

Browse files
authored
feat: add proxy-stress-test sample app for e2e regression testing (#217)
* feat: add proxy-stress-test sample app for e2e regression testing Go app that exercises Keploy proxy under stress conditions: - 10+ concurrent HTTPS connections through a forward proxy (tinyproxy) via CONNECT tunnels, triggering TLS MITM cert generation - PostgreSQL queries returning 50+ rows with 100KB+ payloads, testing wire protocol handling for large DataRow responses - HTTP POST through CONNECT tunnel for MatchType validation Includes Docker Compose with postgres + tinyproxy + the app. Used by the proxy-stress-test CI pipeline in keploy/keploy. Signed-off-by: Shubham Jain <shubham@keploy.io> Signed-off-by: slayerjain <shubhamkjain@outlook.com> * fix: address all Copilot review comments on sample app - Fix module path to match repo layout (samples-go/proxy-stress-test) - Remove dependency on external test-iid.sh in test.sh - Remove sudo usage in test.sh for portability - Pin tinyproxy image tag, bind proxy to localhost, restrict Allow - Replace WARN log with normal log level - Add actionable hint to database connection fatal - Handle http.NewRequest error in background noise goroutine - Redact database credentials in log output - Check rows.Err() after iteration for mid-stream error detection - Handle wideRows.Scan error instead of discarding - Add nolint:gosec comment for intentional InsecureSkipVerify - Update go.mod after module path change Signed-off-by: Shubham Jain <shubham@keploy.io> Signed-off-by: slayerjain <shubhamkjain@outlook.com> * fix: consistent CLI flags, health timeout, gofmt formatting - Use --generate-github-actions=false and --container-name consistently (matching other samples in the repo). - Add max_attempts=30 timeout to health wait loop to fail fast on broken builds instead of looping indefinitely. - Run gofmt on main.go to fix indentation around scan error check. Signed-off-by: Shubham Jain <shubham@keploy.io> Signed-off-by: slayerjain <shubhamkjain@outlook.com> * fix: address Copilot review round 3 — naming, cleanup, timeouts - Rename binary from repro-app to proxy-stress-test in Dockerfile. - Update OTel service name to match sample directory name. - Add cleanup trap (docker compose down) on script exit. - Add curl --max-time to all requests to prevent CI hangs. - Add docker compose down after replay phase. - Add 5s timeout to /health db ping. - Fix misleading "concurrently" comment (queries are sequential). Signed-off-by: Shubham Jain <shubham@keploy.io> Signed-off-by: slayerjain <shubhamkjain@outlook.com> * fix: cd to script directory before running docker compose Ensures the script works when invoked from any working directory. Signed-off-by: Shubham Jain <shubham@keploy.io> Signed-off-by: slayerjain <shubhamkjain@outlook.com> * ci: add CPU/memory limits and stress parameters to docker-compose - Set CONCURRENT_CONNS=42 and BATCH_SIZE=42 matching production traffic pattern from Agoda travel-card-api. - Enable OTel with no collector to generate mock-not-found errors that stress the error channel during replay. - Add BG_NOISE_CONNS=2 for background connection noise. - Set deploy.resources.limits: cpus=0.50, memory=512M to simulate resource-constrained K8s pods where the bugs are observable. Signed-off-by: Shubham Jain <shubham@keploy.io> Signed-off-by: slayerjain <shubhamkjain@outlook.com> --------- Signed-off-by: Shubham Jain <shubham@keploy.io> Signed-off-by: slayerjain <shubhamkjain@outlook.com>
1 parent 7a978e1 commit 76380bc

File tree

7 files changed

+867
-0
lines changed

7 files changed

+867
-0
lines changed

proxy-stress-test/Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM golang:1.22-alpine AS builder
2+
3+
WORKDIR /app
4+
COPY go.mod go.sum ./
5+
RUN go mod download
6+
COPY main.go ./
7+
RUN CGO_ENABLED=0 go build -o proxy-stress-test .
8+
9+
FROM alpine:3.19
10+
RUN apk add --no-cache ca-certificates curl
11+
COPY --from=builder /app/proxy-stress-test /usr/local/bin/proxy-stress-test
12+
ENTRYPOINT ["proxy-stress-test"]
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
services:
2+
db:
3+
image: postgres:16-alpine
4+
environment:
5+
POSTGRES_USER: repro
6+
POSTGRES_PASSWORD: repro
7+
POSTGRES_DB: reprodb
8+
ports:
9+
- "5432:5432"
10+
volumes:
11+
- ./init.sql:/docker-entrypoint-initdb.d/01-init.sql
12+
healthcheck:
13+
test: ["CMD-SHELL", "pg_isready -U repro -d reprodb"]
14+
interval: 2s
15+
timeout: 5s
16+
retries: 10
17+
18+
proxy:
19+
image: vimagick/tinyproxy:latest
20+
ports:
21+
- "127.0.0.1:3128:3128"
22+
command: >
23+
sh -c "echo 'Port 3128' > /tmp/tp.conf &&
24+
echo 'Listen 0.0.0.0' >> /tmp/tp.conf &&
25+
echo 'Timeout 300' >> /tmp/tp.conf &&
26+
echo 'MaxClients 100' >> /tmp/tp.conf &&
27+
echo 'Allow 172.16.0.0/12' >> /tmp/tp.conf &&
28+
echo 'ConnectPort 443' >> /tmp/tp.conf &&
29+
tinyproxy -d -c /tmp/tp.conf"
30+
31+
app:
32+
build: .
33+
container_name: proxyStressApp
34+
ports:
35+
- "8080:8080"
36+
environment:
37+
LISTEN_ADDR: ":8080"
38+
DATABASE_URL: "postgres://repro:repro@db:5432/reprodb?sslmode=disable"
39+
HTTPS_TARGET: "https://httpbin.org/get"
40+
HTTP_PROXY_URL: "http://proxy:3128"
41+
# High concurrency to stress-test cert generation and error channel.
42+
# 42 matches the production traffic pattern from Agoda travel-card-api.
43+
CONCURRENT_CONNS: "42"
44+
BATCH_SIZE: "42"
45+
# OTel enabled with no collector — generates mock-not-found errors
46+
# that stress the error channel during replay.
47+
OTEL_ENABLED: "true"
48+
OTEL_EXPORTER_OTLP_ENDPOINT: "localhost:4318"
49+
OTEL_EXPORT_INTERVAL: "500ms"
50+
# Background noise connections stress error channel between tests.
51+
BG_NOISE_CONNS: "2"
52+
depends_on:
53+
db:
54+
condition: service_healthy
55+
# CPU limit simulates a resource-constrained K8s pod. Without this,
56+
# cert storm and error channel bugs are masked by excess CPU.
57+
deploy:
58+
resources:
59+
limits:
60+
cpus: "0.50"
61+
memory: "512M"

proxy-stress-test/go.mod

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module github.com/keploy/samples-go/proxy-stress-test
2+
3+
go 1.22.0
4+
5+
require (
6+
github.com/lib/pq v1.10.9
7+
go.opentelemetry.io/otel v1.28.0
8+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0
9+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0
10+
go.opentelemetry.io/otel/sdk v1.28.0
11+
go.opentelemetry.io/otel/sdk/metric v1.28.0
12+
)
13+
14+
require (
15+
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
16+
github.com/go-logr/logr v1.4.2 // indirect
17+
github.com/go-logr/stdr v1.2.2 // indirect
18+
github.com/google/uuid v1.6.0 // indirect
19+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
20+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
21+
go.opentelemetry.io/otel/metric v1.28.0 // indirect
22+
go.opentelemetry.io/otel/trace v1.28.0 // indirect
23+
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
24+
golang.org/x/net v0.26.0 // indirect
25+
golang.org/x/sys v0.21.0 // indirect
26+
golang.org/x/text v0.16.0 // indirect
27+
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
28+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
29+
google.golang.org/grpc v1.64.0 // indirect
30+
google.golang.org/protobuf v1.34.2 // indirect
31+
)

proxy-stress-test/go.sum

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
2+
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
3+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
6+
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
7+
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
8+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
9+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
10+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
11+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
12+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
13+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
14+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
15+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
16+
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
17+
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
18+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
19+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
21+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
22+
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
23+
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
24+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 h1:aLmmtjRke7LPDQ3lvpFz+kNEH43faFhzW7v8BFIEydg=
25+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0/go.mod h1:TC1pyCt6G9Sjb4bQpShH+P5R53pO6ZuGnHuuln9xMeE=
26+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
27+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
28+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
29+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
30+
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
31+
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
32+
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
33+
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
34+
go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08=
35+
go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg=
36+
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
37+
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
38+
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
39+
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
40+
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
41+
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
42+
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
43+
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
44+
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
45+
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
46+
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
47+
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
48+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
49+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
50+
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
51+
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
52+
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
53+
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
54+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
55+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

proxy-stress-test/init.sql

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
-- Seed data for Issue 3: Postgres large DataRow responses
2+
--
3+
-- The Keploy Postgres wire protocol parser fails when DataRow packets
4+
-- exceed a single TCP segment (MSS ~1460 bytes on Docker bridge).
5+
-- Error: "incomplete or invalid response packet (DataRow): want N bytes, have M"
6+
--
7+
-- Strategy: Make each individual row ~100KB so a SINGLE DataRow packet
8+
-- spans dozens of TCP segments. Query 50 rows = 5MB+ response.
9+
-- This guarantees TCP fragmentation even on localhost.
10+
11+
CREATE TABLE IF NOT EXISTS large_records (
12+
id SERIAL PRIMARY KEY,
13+
name VARCHAR(255) NOT NULL,
14+
description TEXT NOT NULL,
15+
large_payload TEXT NOT NULL
16+
);
17+
18+
-- Generate 200 rows, each with a ~100KB random payload.
19+
-- A single DataRow at 100KB will span ~70 TCP segments (MSS=1460).
20+
-- This forces the Postgres wire protocol parser to buffer across segments.
21+
INSERT INTO large_records (name, description, large_payload)
22+
SELECT
23+
'record-' || i,
24+
'Test record ' || i || '' || repeat('description padding to increase row size ', 20),
25+
-- ~100KB per row: 64 chars per md5 pair × 1600 repeats = 102,400 chars
26+
repeat(
27+
md5(random()::text || i::text) || md5(random()::text || (i+1000)::text),
28+
1600
29+
)
30+
FROM generate_series(1, 200) AS i;
31+
32+
-- Also create a table with many small columns (wide rows)
33+
-- to trigger different DataRow encoding paths
34+
CREATE TABLE IF NOT EXISTS wide_records (
35+
id SERIAL PRIMARY KEY,
36+
col_01 TEXT, col_02 TEXT, col_03 TEXT, col_04 TEXT, col_05 TEXT,
37+
col_06 TEXT, col_07 TEXT, col_08 TEXT, col_09 TEXT, col_10 TEXT,
38+
col_11 TEXT, col_12 TEXT, col_13 TEXT, col_14 TEXT, col_15 TEXT,
39+
col_16 TEXT, col_17 TEXT, col_18 TEXT, col_19 TEXT, col_20 TEXT
40+
);
41+
42+
INSERT INTO wide_records (
43+
col_01, col_02, col_03, col_04, col_05,
44+
col_06, col_07, col_08, col_09, col_10,
45+
col_11, col_12, col_13, col_14, col_15,
46+
col_16, col_17, col_18, col_19, col_20
47+
)
48+
SELECT
49+
repeat(md5(random()::text), 100), -- ~3.2KB per column × 20 = ~64KB per row
50+
repeat(md5(random()::text), 100),
51+
repeat(md5(random()::text), 100),
52+
repeat(md5(random()::text), 100),
53+
repeat(md5(random()::text), 100),
54+
repeat(md5(random()::text), 100),
55+
repeat(md5(random()::text), 100),
56+
repeat(md5(random()::text), 100),
57+
repeat(md5(random()::text), 100),
58+
repeat(md5(random()::text), 100),
59+
repeat(md5(random()::text), 100),
60+
repeat(md5(random()::text), 100),
61+
repeat(md5(random()::text), 100),
62+
repeat(md5(random()::text), 100),
63+
repeat(md5(random()::text), 100),
64+
repeat(md5(random()::text), 100),
65+
repeat(md5(random()::text), 100),
66+
repeat(md5(random()::text), 100),
67+
repeat(md5(random()::text), 100),
68+
repeat(md5(random()::text), 100)
69+
FROM generate_series(1, 50) AS i;
70+
71+
-- Verify sizes
72+
SELECT 'large_records' AS tbl,
73+
count(*) AS rows,
74+
pg_size_pretty(avg(length(large_payload)::bigint)) AS avg_payload,
75+
pg_size_pretty(sum(length(large_payload)::bigint)) AS total_payload
76+
FROM large_records
77+
UNION ALL
78+
SELECT 'wide_records',
79+
count(*),
80+
pg_size_pretty(avg(octet_length(col_01)::bigint)),
81+
pg_size_pretty(sum(octet_length(col_01)::bigint) * 20)
82+
FROM wide_records;

0 commit comments

Comments
 (0)