-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathserver.js
More file actions
369 lines (342 loc) · 11.3 KB
/
Copy pathserver.js
File metadata and controls
369 lines (342 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
// Tiny Express target for LoadCell sanity checks. Exposes a handful of
// REST-shaped routes (GET / POST / PUT / DELETE) each with its own status
// mix and latency profile so LoadCell's chart shows distinctly different
// shapes per endpoint:
//
// GET /health fast, almost always 200 — quiet baseline
// GET /users mostly 200, occasional 5xx, medium latency
// POST /users validation-heavy: 70% 2xx, 20% 4xx, 10% 5xx
// PUT /users/:id 85% 2xx, 10% 4xx (not found), 5% 5xx
// DELETE /users/:id 80% 2xx, 15% 4xx, 5% 5xx
// POST /login auth-heavy: 60% 2xx, 35% 4xx, 5% 429
// GET /products/search 90% 2xx, 8% 429, 2% 5xx, slow tail
// POST /checkout risky: 50% 2xx, 30% 4xx, 15% 5xx, 5% 429
// GET /reports/heavy 95% 2xx but expensive (200-500ms)
// ANY /echo always 200, reflects request back (debug)
//
// Anything else falls through to a catch-all that honors the legacy
// env-tunables (PROB_500/PROB_429/PROB_404 etc.) so old scripts still work.
//
// node tools/echo-server/server.js
//
// Catch-all env knobs (only applied to the fallback handler):
// PROB_500=0.03 PROB_429=0.05 PROB_404=0.02
// LATENCY_MIN_MS=1 LATENCY_MAX_MS=8
// SLOW_PROB=0.05 SLOW_MAX_MS=400
const express = require('express');
const PORT = 4466;
const PROB_500 = clampProb(parseFloat(process.env.PROB_500 ?? '0.03'));
const PROB_429 = clampProb(parseFloat(process.env.PROB_429 ?? '0.05'));
const PROB_404 = clampProb(parseFloat(process.env.PROB_404 ?? '0.02'));
const LATENCY_MIN_MS = Math.max(0, parseInt(process.env.LATENCY_MIN_MS ?? '1', 10));
const LATENCY_MAX_MS = Math.max(
LATENCY_MIN_MS,
parseInt(process.env.LATENCY_MAX_MS ?? '8', 10)
);
const SLOW_PROB = clampProb(parseFloat(process.env.SLOW_PROB ?? '0.05'));
const SLOW_MAX_MS = Math.max(0, parseInt(process.env.SLOW_MAX_MS ?? '400', 10));
const PROB_OK = Math.max(0, 1 - PROB_500 - PROB_429 - PROB_404);
function clampProb(p) {
if (Number.isNaN(p)) return 0;
if (p < 0) return 0;
if (p > 1) return 1;
return p;
}
const app = express();
// /echo captures the raw body before the global parsers run so we can
// reflect it back verbatim — even when LoadCell sends no Content-Type
// (which body-parser would otherwise ignore). Signals _body=true so the
// downstream json/text middlewares skip the already-consumed stream.
app.use('/echo', (req, res, next) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
req.rawBody = Buffer.concat(chunks).toString('utf8');
req._body = true;
next();
});
req.on('error', next);
});
// Capture body so we can inspect what LoadCell actually sent.
app.use(express.json({ limit: '1mb', strict: false }));
app.use(express.text({ limit: '1mb', type: '*/*' }));
let total = 0;
let lastWindowTotal = 0;
let firstHitAt = null;
let slowResponses = 0;
const counts = { ok: 0, c404: 0, e500: 0, r429: 0 };
const methodCounts = {};
const firstByMethod = {};
// Generic responder. Picks a status from the supplied bucket weights, then
// sleeps for a uniform [latMin..latMax] interval (with a slowProb chance of
// an extra slowMax tail) before sending.
function respond(req, res, opts) {
if (firstHitAt === null) firstHitAt = Date.now();
total++;
methodCounts[req.method] = (methodCounts[req.method] || 0) + 1;
const key = req.method + ' ' + req.route?.path || req.method + ' ' + req.path;
if (!firstByMethod[key]) {
firstByMethod[key] = {
method: req.method,
url: req.url,
headers: { ...req.headers },
bodyPreview:
typeof req.body === 'string'
? req.body.slice(0, 200)
: req.body
? JSON.stringify(req.body).slice(0, 200)
: '',
at: new Date().toISOString(),
};
}
const {
ok = 1,
c4xx = 0,
e5xx = 0,
r429 = 0,
latMin = 1,
latMax = 8,
slowProb = 0,
slowMax = 200,
okBody = 'ok',
c4xxBody = 'bad request',
e5xxBody = 'internal server error',
r429Body = 'too many requests',
} = opts;
const r = Math.random();
let status;
let body;
if (r < e5xx) {
status = 500;
body = e5xxBody;
counts.e500++;
} else if (r < e5xx + c4xx) {
// Mix of 400/404 — route can override via c4xxStatus if it cares.
status = opts.c4xxStatus ?? 400;
body = c4xxBody;
counts.c404++;
} else if (r < e5xx + c4xx + r429) {
status = 429;
body = r429Body;
res.set('Retry-After', '1');
counts.r429++;
} else {
status = 200;
body = okBody;
counts.ok++;
}
const baseline = latMin + Math.random() * (latMax - latMin);
const slow = slowProb > 0 && Math.random() < slowProb;
if (slow) slowResponses++;
const delay = baseline + (slow ? Math.random() * slowMax : 0);
const send = () =>
res
.status(status)
.type(typeof body === 'string' ? 'text/plain' : 'application/json')
.send(body);
if (delay > 0) setTimeout(send, delay);
else send();
}
// ─── Explicit routes ────────────────────────────────────────────────
// Quiet baseline: nearly always 200, very fast. Good for warming up.
app.get('/health', (req, res) =>
respond(req, res, {
ok: 0.99,
e5xx: 0.01,
latMin: 1,
latMax: 3,
okBody: JSON.stringify({ status: 'ok' }),
})
);
// Read-heavy: mostly 200, occasional 5xx blip, modest latency.
app.get('/users', (req, res) =>
respond(req, res, {
ok: 0.95,
e5xx: 0.05,
latMin: 2,
latMax: 10,
slowProb: 0.05,
slowMax: 100,
okBody: JSON.stringify({ users: [], page: 1 }),
})
);
// Validation-heavy: client-error spike on bad bodies.
app.post('/users', (req, res) =>
respond(req, res, {
ok: 0.70,
c4xx: 0.20,
e5xx: 0.10,
c4xxStatus: 422,
latMin: 3,
latMax: 15,
okBody: JSON.stringify({ id: Math.floor(Math.random() * 10000) }),
c4xxBody: 'validation failed',
})
);
// Update: targets often missing → 404-heavy.
app.put('/users/:id', (req, res) =>
respond(req, res, {
ok: 0.85,
c4xx: 0.10,
e5xx: 0.05,
c4xxStatus: 404,
latMin: 5,
latMax: 20,
okBody: JSON.stringify({ id: req.params.id, updated: true }),
c4xxBody: 'user not found',
})
);
// Delete: similar 404 pattern.
app.delete('/users/:id', (req, res) =>
respond(req, res, {
ok: 0.80,
c4xx: 0.15,
e5xx: 0.05,
c4xxStatus: 404,
latMin: 3,
latMax: 12,
okBody: '',
c4xxBody: 'user not found',
})
);
// Auth: lots of 401s and a sprinkle of rate limits.
app.post('/login', (req, res) =>
respond(req, res, {
ok: 0.60,
c4xx: 0.35,
r429: 0.05,
c4xxStatus: 401,
latMin: 10,
latMax: 50,
okBody: JSON.stringify({ token: 'fake.jwt.token' }),
c4xxBody: 'invalid credentials',
})
);
// Search: rate-limit dominant, with a long tail.
app.get('/products/search', (req, res) =>
respond(req, res, {
ok: 0.90,
r429: 0.08,
e5xx: 0.02,
latMin: 20,
latMax: 80,
slowProb: 0.20,
slowMax: 300,
okBody: JSON.stringify({ q: req.query.q ?? '', results: [] }),
})
);
// Checkout: messy multi-failure mode.
app.post('/checkout', (req, res) =>
respond(req, res, {
ok: 0.50,
c4xx: 0.30,
e5xx: 0.15,
r429: 0.05,
c4xxStatus: 402,
latMin: 50,
latMax: 200,
slowProb: 0.30,
slowMax: 500,
okBody: JSON.stringify({ orderId: `ord_${Date.now()}` }),
c4xxBody: 'payment declined',
})
);
// Heavy report: success rate is fine but each call is expensive.
app.get('/reports/heavy', (req, res) =>
respond(req, res, {
ok: 0.95,
e5xx: 0.05,
latMin: 200,
latMax: 500,
slowProb: 0.50,
slowMax: 1500,
okBody: JSON.stringify({ rows: 0, generatedAt: new Date().toISOString() }),
})
);
// Debug echo: always 200, no latency, no fault injection. Reflects the
// rendered method/url/query/headers/body back so you can confirm what
// LoadCell actually sent (e.g. that {{uuid}} got substituted to a real
// value). Use any HTTP verb.
app.all('/echo', (req, res) => {
if (firstHitAt === null) firstHitAt = Date.now();
total++;
counts.ok++;
methodCounts[req.method] = (methodCounts[req.method] || 0) + 1;
// Try to pretty-print JSON bodies so {{uuid}} renders as a value in
// the response viewer, but fall back to the raw string for anything
// else (form-encoded, plain text, malformed JSON, etc.).
let body = req.rawBody || null;
if (body) {
const t = body.trim();
if (t.startsWith('{') || t.startsWith('[')) {
try { body = JSON.parse(t); } catch { /* keep as string */ }
}
}
res.status(200).json({
method: req.method,
url: req.url,
path: req.path,
query: req.query,
headers: req.headers,
body,
});
});
// ─── Catch-all — preserves legacy env-tunable behavior ─────────────
app.use((req, res) =>
respond(req, res, {
ok: PROB_OK,
c4xx: PROB_404,
e5xx: PROB_500,
r429: PROB_429,
c4xxStatus: 404,
latMin: LATENCY_MIN_MS,
latMax: LATENCY_MAX_MS,
slowProb: SLOW_PROB,
slowMax: SLOW_MAX_MS,
c4xxBody: 'not found',
})
);
setInterval(() => {
const windowCount = total - lastWindowTotal;
lastWindowTotal = total;
if (total === 0) return;
const elapsedSec = firstHitAt ? (Date.now() - firstHitAt) / 1000 : 0;
const avgRps = elapsedSec > 0 ? total / elapsedSec : 0;
const ts = new Date().toISOString().slice(11, 19);
const pct = (n) => `${((n / total) * 100).toFixed(1).padStart(4)}%`;
const methodSummary = Object.entries(methodCounts)
.sort((a, b) => b[1] - a[1])
.map(([m, c]) => `${m}:${c}`)
.join(' ');
console.log(
`[${ts}] total=${total.toString().padStart(8)} ` +
`2xx=${pct(counts.ok)} 4xx=${pct(counts.c404)} 429=${pct(counts.r429)} 5xx=${pct(counts.e500)} ` +
`slow=${pct(slowResponses)} methods[${methodSummary}] ` +
`last1s=${windowCount.toString().padStart(6)} avg=${avgRps.toFixed(1).padStart(8)} rps`
);
}, 1000);
// Tiny side-channel on port 4467 for inspecting the first request seen per
// method+path. curl http://127.0.0.1:4467/ to see the headers/body LoadCell sent.
const inspectApp = express();
inspectApp.get('/', (_req, res) => res.json(firstByMethod));
inspectApp.listen(4467, '127.0.0.1', () => {
console.log('inspect → http://127.0.0.1:4467/ (first request per route)');
});
app.listen(PORT, '127.0.0.1', () => {
const pp = (p) => `${(p * 100).toFixed(1)}%`;
console.log(`echo-server listening on http://127.0.0.1:${PORT}`);
console.log('routes:');
console.log(' GET /health 99% ok, fast');
console.log(' GET /users 95% ok, occasional 5xx');
console.log(' POST /users 70% 2xx, 20% 4xx, 10% 5xx');
console.log(' PUT /users/:id 85% 2xx, 10% 404, 5% 5xx');
console.log(' DELETE /users/:id 80% 2xx, 15% 404, 5% 5xx');
console.log(' POST /login 60% 2xx, 35% 401, 5% 429');
console.log(' GET /products/search 90% 2xx, 8% 429, 2% 5xx, slow tail');
console.log(' POST /checkout 50% 2xx, 30% 402, 15% 5xx, 5% 429');
console.log(' GET /reports/heavy 95% 2xx, 200-500ms baseline');
console.log(' ANY /echo always 200, reflects request');
console.log(
`fallback → 200:${pp(PROB_OK)} 404:${pp(PROB_404)} 429:${pp(PROB_429)} 500:${pp(PROB_500)}`
);
});