Skip to content

Commit 93f6669

Browse files
committed
better demo
1 parent ec1019d commit 93f6669

4 files changed

Lines changed: 264 additions & 0 deletions

File tree

quotes-demo-app/DEMO.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Argus Demo — Simulation Scenarios
2+
3+
Run with: `node simulate.js` (requires Postgres; see `docker-compose-pg-only.yml`).
4+
5+
The simulator boots an Express server, attaches the DiagnosticAgent, then replays the traffic sequence below. Each scenario triggers a specific agent feature and prints a labelled event to the terminal.
6+
7+
---
8+
9+
## Traffic sequence
10+
11+
| Step | Route | Agent event / hint | Label |
12+
|------|-------|--------------------|-------|
13+
| 1 | `GET /` | health check (no event) ||
14+
| 2 | `GET /quotes` | `offset-pagination` hint | QUERY |
15+
| 3 | `GET /quotes?page=1` × 6 | `n-plus-one` hint (repeated identical queries) | QUERY |
16+
| 4 | `POST /quotes` | clean INSERT, no hints | QUERY |
17+
| 5 | `GET /debug/scrub` | high-entropy secret redacted from log | SCRUB |
18+
| 6 | `GET /debug/busy-spin` | 120ms synchronous busy-loop → `event-loop-lag` anomaly | ANOM |
19+
| 7 | `GET /debug/select-star` | `no-select-star` + `missing-limit` + `full-table-scan` hints | QUERY |
20+
| 8 | `POST /debug/update-all` | `missing-where-update` hint (critical) | QUERY |
21+
| 9 | `GET /debug/sync-read` × 5 | `synchronous-fs` hint (every call) + `missing-fs-cache` hint (5th call) | FS |
22+
| 10 | `GET /debug/log-unstructured` | `unstructured-log` hint | LOG |
23+
| 11 | `GET /debug/log-large` | `large-log-payload` hint | LOG |
24+
| 12 | `GET /debug/log-storm` | `log-error-storm` hint (6 errors in < 1 s) | LOG |
25+
| 13 | `GET /debug/path-traversal` | `path-traversal-risk` hint | FS |
26+
| 14 | `GET /debug/sensitive-file` | `sensitive-file-access` hint (.env) | FS |
27+
| 15 | `GET /debug/leak-memory` | ~12 MB V8 heap growth → `memory-leak` anomaly | ANOM |
28+
| 16 | `GET /debug/outbound` | `insecure-http` hint (plain http:// to remote host) | HTTP |
29+
| 17 | `POST /debug/delete-all` | `missing-where-delete` hint (critical — no WHERE clause) | QUERY |
30+
| 18 | `GET /debug/slow-query` | `pg_sleep(0.6)` exceeds 500 ms pg threshold → `slow-query` event | SLOW |
31+
| 19 | `GET /debug/transaction` | `BEGIN` / `SELECT` / `COMMIT` cycle → `transaction` event (committed) | TXN |
32+
| 20 | `GET /debug/rollback` | `BEGIN` / `SELECT` / `ROLLBACK` cycle → `transaction` event (aborted) | TXN |
33+
| 21 | `GET /debug/crash` | `Promise.reject()` with no `.catch()``unhandledRejection``crash` event | CRASH |
34+
| 22 | `GET /debug/dns-lookup` | `dns.lookup('example.com')``dns` event (+ `slow-dns` if > 100 ms) | DNS |
35+
| 23 | `GET /debug/error-500` | outbound HTTP call receives 500 → `http-server-error` hint | HTTP |
36+
| 24 | `GET /debug/rate-limited` | outbound HTTP call receives 429 → `http-rate-limited` hint | HTTP |
37+
| 25 | `GET /debug/slow-outbound` | outbound HTTP call takes ~2.5 s → `slow-http-request` hint | HTTP |
38+
39+
Startup also fires:
40+
- `audit` — npm audit scan for high/critical CVEs
41+
- `scan` — static analysis (TypeScript + ESLint diagnostics)
42+
43+
---
44+
45+
## Agent features exercised
46+
47+
| Feature | Triggered by |
48+
|---------|-------------|
49+
| Query analysis (7 rules) | steps 2–3, 7–8, 17 |
50+
| Slow query monitor | step 18 |
51+
| Transaction monitor | steps 19–20 |
52+
| Runtime monitor — event-loop lag | step 6 |
53+
| Runtime monitor — memory leak | step 15 |
54+
| Log instrumentation + scrubbing | steps 5, 10–12 |
55+
| FS instrumentation | steps 9, 13–14 |
56+
| HTTP instrumentation + analysis | steps 16, 23–25 |
57+
| DNS monitor | step 22 |
58+
| GC monitor | passive (fires if GC pauses exceed 10% of 10 s window) |
59+
| Crash guard | step 21 |
60+
| Resource leak monitor | passive (fires if OS handles exceed threshold) |
61+
| Audit scanner | startup |
62+
| Static scanner | startup |
63+
64+
---
65+
66+
## Monitors enabled but not directly triggered
67+
68+
- **GC pressure** (`gc-pressure`): enabled via `withGcMonitor()`. Fires automatically if accumulated GC pause time exceeds 10% of the 10-second sliding window. No synthetic trigger is needed.
69+
- **Pool exhaustion / slow-acquire**: requires calling `agent.watchPool(pool, 'pg')` with a registered pg pool. Not wired in this demo; add it in `diagnostic.js` to observe `pool-exhaustion` and `slow-acquire` events under load.

quotes-demo-app/diagnostic.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ const agent = DiagnosticAgent.createProfile({
2222
// (Normally reserved for the 'worker' profile — included here to showcase all free-mode features.)
2323
agent.withRuntimeMonitor();
2424

25+
// Additional monitors not included in the web/db profile — enabled here for the demo.
26+
agent.withTransactionMonitor();
27+
agent.withDnsMonitor();
28+
agent.withGcMonitor();
29+
2530
// ── live event listeners ──────────────────────────────────────────────────────
2631

2732
agent.on('query', (q) => {
@@ -106,6 +111,46 @@ agent.on('crash', (crash) => {
106111
);
107112
});
108113

114+
agent.on('slow-query', (q) => {
115+
console.warn(
116+
`${c.dim}${stamp()}${c.reset} ${tag(c.yellow,'SLOW ')} ` +
117+
`[${q.driver}] ${c.bold}${q.sanitizedQuery?.slice(0, 80)}${c.reset} ` +
118+
`${c.red}${q.durationMs.toFixed(1)}ms > ${q.thresholdMs}ms threshold${c.reset}`
119+
);
120+
});
121+
122+
agent.on('transaction', (t) => {
123+
const outcome = t.aborted ? `${c.red}ROLLBACK${c.reset}` : `${c.green}COMMIT${c.reset}`;
124+
console.log(
125+
`${c.dim}${stamp()}${c.reset} ${tag(c.cyan,'TXN ')} ` +
126+
`[${t.driver}] ${outcome}${t.queryCount} quer${t.queryCount !== 1 ? 'ies' : 'y'} ` +
127+
`${c.dim}(${t.durationMs.toFixed(1)}ms)${c.reset}`
128+
);
129+
});
130+
131+
agent.on('dns', (d) => {
132+
console.log(
133+
`${c.dim}${stamp()}${c.reset} ${tag(c.cyan,'DNS ')} ` +
134+
`${d.hostname}${(d.addresses || []).join(', ') || '(error)'} ` +
135+
`${c.dim}(${d.durationMs.toFixed(1)}ms)${c.reset}`
136+
);
137+
});
138+
139+
agent.on('slow-dns', (d) => {
140+
console.warn(
141+
`${c.dim}${stamp()}${c.reset} ${tag(c.yellow,'SDNS ')} ` +
142+
`${d.hostname} resolved in ${c.yellow}${d.durationMs.toFixed(1)}ms${c.reset} (slow-dns threshold exceeded)`
143+
);
144+
});
145+
146+
agent.on('gc-pressure', (g) => {
147+
console.warn(
148+
`${c.dim}${stamp()}${c.reset} ${tag(c.yellow,'GC ')} ` +
149+
`${g.gcCount} GC cycle(s), ${g.totalPauseMs.toFixed(1)}ms total ` +
150+
`${c.yellow}(${g.pausePct.toFixed(1)}% of ${g.windowMs}ms window)${c.reset}`
151+
);
152+
});
153+
109154
agent.on('audit', (result) => {
110155
const total = result.suggestions?.length ?? 0;
111156
const colour = total > 0 ? c.red : c.dim;

quotes-demo-app/routes/debug.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
const express = require('express');
99
const router = express.Router();
1010
const db = require('../services/db');
11+
const dns = require('dns');
1112
const fs = require('fs');
1213
const http = require('http');
1314
const path = require('path');
@@ -117,4 +118,117 @@ router.get('/outbound', (_req, res) => {
117118
}).on('error', () => res.json({ ok: true, note: 'httpbin unreachable' }));
118119
});
119120

121+
// ── Query hints: missing-where-delete ─────────────────────────────────────────
122+
123+
// Triggers: missing-where-delete (critical — deletes every row without WHERE)
124+
router.post('/delete-all', async (_req, res) => {
125+
try {
126+
await db.query('DELETE FROM quote');
127+
res.json({ deleted: 'all rows' });
128+
} catch (err) {
129+
res.status(500).json({ message: err.message });
130+
}
131+
});
132+
133+
// ── Slow query ────────────────────────────────────────────────────────────────
134+
135+
// Triggers: slow-query event (pg_sleep(0.6) exceeds the 500ms pg default threshold)
136+
router.get('/slow-query', async (_req, res) => {
137+
try {
138+
await db.query('SELECT pg_sleep(0.6)');
139+
res.json({ ok: true });
140+
} catch (err) {
141+
res.status(500).json({ message: err.message });
142+
}
143+
});
144+
145+
// ── Transaction monitoring ────────────────────────────────────────────────────
146+
147+
// Triggers: transaction event with aborted=false (COMMIT)
148+
// Note: pool.query() may use different connections per call; the DB-level transaction
149+
// may be a no-op, but TransactionMonitor correlates by SQL pattern, not connection.
150+
router.get('/transaction', async (_req, res) => {
151+
try {
152+
await db.query('BEGIN');
153+
await db.query('SELECT id FROM quote LIMIT 1');
154+
await db.query('COMMIT');
155+
res.json({ ok: true, outcome: 'committed' });
156+
} catch (err) {
157+
await db.query('ROLLBACK').catch(() => {});
158+
res.status(500).json({ message: err.message });
159+
}
160+
});
161+
162+
// Triggers: transaction event with aborted=true (ROLLBACK)
163+
router.get('/rollback', async (_req, res) => {
164+
try {
165+
await db.query('BEGIN');
166+
await db.query('SELECT id FROM quote LIMIT 1');
167+
await db.query('ROLLBACK');
168+
res.json({ ok: true, outcome: 'rolled back' });
169+
} catch (err) {
170+
res.status(500).json({ message: err.message });
171+
}
172+
});
173+
174+
// ── Crash detection ───────────────────────────────────────────────────────────
175+
176+
// Triggers: crash event via unhandledRejection.
177+
// CrashGuard intercepts the rejection and emits 'crash' without exiting the process
178+
// (unhandledRejection is recoverable since Node 15+ when a listener is registered).
179+
router.get('/crash', (_req, res) => {
180+
Promise.reject(new Error('[demo] Simulated unhandled rejection — .catch() was intentionally omitted'));
181+
res.json({ ok: true, note: 'unhandledRejection fired — watch for CRASH event' });
182+
});
183+
184+
// ── DNS monitoring ────────────────────────────────────────────────────────────
185+
186+
// Triggers: dns event (+ slow-dns if resolution exceeds the 100ms threshold)
187+
router.get('/dns-lookup', (_req, res) => {
188+
dns.lookup('example.com', (err, address) => {
189+
if (err) return res.json({ ok: false, error: err.message });
190+
res.json({ ok: true, address });
191+
});
192+
});
193+
194+
// ── HTTP hint sinks ────────────────────────────────────────────────────────────
195+
// These are internal targets used by the outbound routes below.
196+
// They are NOT meant to be called directly from traffic.js.
197+
198+
router.get('/sink-500', (_req, res) => res.status(500).json({ error: 'demo server error' }));
199+
router.get('/sink-429', (_req, res) => res.status(429).json({ error: 'demo rate limit' }));
200+
router.get('/sink-slow', (_req, res) => { setTimeout(() => res.json({ ok: true }), 2500); });
201+
202+
// ── HTTP hints via outbound calls ─────────────────────────────────────────────
203+
204+
function selfGet(urlPath) {
205+
const port = parseInt(process.env.TARGET_PORT || '3000', 10);
206+
return new Promise((resolve) => {
207+
const req = http.request({ host: 'localhost', port, path: urlPath, method: 'GET' }, (r) => {
208+
r.resume();
209+
r.on('end', resolve);
210+
});
211+
req.on('error', resolve);
212+
req.end();
213+
});
214+
}
215+
216+
// Triggers: http-server-error hint (outbound call receives 500)
217+
router.get('/error-500', async (_req, res) => {
218+
await selfGet('/debug/sink-500');
219+
res.json({ ok: true });
220+
});
221+
222+
// Triggers: http-rate-limited hint (outbound call receives 429)
223+
router.get('/rate-limited', async (_req, res) => {
224+
await selfGet('/debug/sink-429');
225+
res.json({ ok: true });
226+
});
227+
228+
// Triggers: slow-http-request hint (outbound call takes > 2000ms)
229+
router.get('/slow-outbound', async (_req, res) => {
230+
await selfGet('/debug/sink-slow');
231+
res.json({ ok: true });
232+
});
233+
120234
module.exports = router;

quotes-demo-app/traffic.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,42 @@ async function run() {
121121
await get('/debug/outbound');
122122
await wait(200);
123123

124+
console.log('\n[TRAFFIC] ── POST /debug/delete-all (missing-where-delete: critical) ────');
125+
await post('/debug/delete-all', {});
126+
await wait(100);
127+
128+
console.log('\n[TRAFFIC] ── GET /debug/slow-query (slow-query: 600ms > 500ms threshold) ─');
129+
await get('/debug/slow-query');
130+
await wait(200);
131+
132+
console.log('\n[TRAFFIC] ── GET /debug/transaction (transaction: COMMIT) ─────────────────');
133+
await get('/debug/transaction');
134+
await wait(100);
135+
136+
console.log('\n[TRAFFIC] ── GET /debug/rollback (transaction: ROLLBACK / aborted) ────────');
137+
await get('/debug/rollback');
138+
await wait(100);
139+
140+
console.log('\n[TRAFFIC] ── GET /debug/crash (crash: unhandledRejection) ──────────────────');
141+
await get('/debug/crash');
142+
await wait(200); // allow CrashGuard to emit the event
143+
144+
console.log('\n[TRAFFIC] ── GET /debug/dns-lookup (dns + possible slow-dns) ──────────────');
145+
await get('/debug/dns-lookup');
146+
await wait(200);
147+
148+
console.log('\n[TRAFFIC] ── GET /debug/error-500 (http-server-error hint) ─────────────────');
149+
await get('/debug/error-500');
150+
await wait(100);
151+
152+
console.log('\n[TRAFFIC] ── GET /debug/rate-limited (http-rate-limited hint) ──────────────');
153+
await get('/debug/rate-limited');
154+
await wait(100);
155+
156+
console.log('\n[TRAFFIC] ── GET /debug/slow-outbound (slow-http-request hint, ~2.5s) ──────');
157+
await get('/debug/slow-outbound');
158+
await wait(200);
159+
124160
console.log('\n[TRAFFIC] ── Done ───────────────────────────────────────────────────────\n');
125161
}
126162

0 commit comments

Comments
 (0)