http-log-replay 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/replayer.js ADDED
@@ -0,0 +1,232 @@
1
+ /* eslint-disable no-console */
2
+ const axios = require('axios');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { diffJson } = require('diff');
6
+ require('colors');
7
+ const readline = require('readline');
8
+
9
+ async function replayAndDiff(logFile, primaryUrl, secondaryUrl, reportFile, ignoreFields = [], injectedHeaders = {}, excludeEndpoints = [], onEvent = () => { }, concurrency = 1) {
10
+ console.log(`\nšŸ”„ Replaying logs from: ${logFile}`);
11
+ console.log(`šŸ…°ļø Primary: ${primaryUrl}`);
12
+ console.log(`šŸ…±ļø Secondary: ${secondaryUrl}`);
13
+ console.log(`šŸš€ Concurrency: ${concurrency}`);
14
+
15
+ const logs = [];
16
+ const results = [];
17
+ let passed = 0;
18
+ let failed = 0;
19
+
20
+ // 1. Read the JSONL file safely
21
+ try {
22
+ const fileStream = fs.createReadStream(logFile);
23
+ const rl = readline.createInterface({
24
+ input: fileStream,
25
+ crlfDelay: Infinity
26
+ });
27
+
28
+ for await (const line of rl) {
29
+ if (line.trim()) {
30
+ try {
31
+ const entry = JSON.parse(line);
32
+ if (entry.status >= 200 && entry.status < 300) {
33
+ logs.push(entry);
34
+ }
35
+ } catch (e) {
36
+ console.warn("āš ļø Skipping malformed log line".yellow);
37
+ }
38
+ }
39
+ }
40
+ } catch (e) {
41
+ console.error(`āŒ Failed to read log file: ${e.message}`.red);
42
+ onEvent({ type: 'error', message: `Failed to read log file: ${e.message}` });
43
+ return;
44
+ }
45
+
46
+ console.log(`šŸ“Š Loaded ${logs.length} valid requests to replay.`.cyan);
47
+ onEvent({ type: 'start', total: logs.length });
48
+
49
+ // 2. Iterate and Replay with Concurrency
50
+ const processLog = async (entry, index) => {
51
+ const endpoint = entry.url;
52
+
53
+ // SKIP LOGIC
54
+ if (excludeEndpoints.includes(endpoint)) {
55
+ console.log(`ā© Skipping excluded endpoint: ${endpoint}`.gray);
56
+ return null;
57
+ }
58
+
59
+ console.log(`\n[${index + 1}/${logs.length}] ${entry.method} ${endpoint}`.bold);
60
+
61
+ const headers = {
62
+ ...injectedHeaders,
63
+ 'Content-Type': 'application/json',
64
+ 'User-Agent': 'Traffic-Replayer/1.0'
65
+ };
66
+
67
+ const config = {
68
+ method: entry.method,
69
+ headers: headers,
70
+ data: entry.requestBody ? JSON.parse(entry.requestBody) : undefined,
71
+ validateStatus: () => true
72
+ };
73
+
74
+ try {
75
+ const start1 = Date.now();
76
+ const res1 = await axios({ ...config, url: `${primaryUrl}${endpoint}` });
77
+ const time1 = Date.now() - start1;
78
+
79
+ const start2 = Date.now();
80
+ const res2 = await axios({ ...config, url: `${secondaryUrl}${endpoint}` });
81
+ const time2 = Date.now() - start2;
82
+
83
+ const cleanBody1 = clean(res1.data, ignoreFields);
84
+ const cleanBody2 = clean(res2.data, ignoreFields);
85
+
86
+ const differences = diffJson(cleanBody1, cleanBody2);
87
+ const hasDiff = differences.length > 1;
88
+
89
+ const isSuccess = !hasDiff && res1.status === res2.status;
90
+
91
+ if (isSuccess) {
92
+ console.log(`āœ… MATCH (${res1.status})`.green);
93
+ passed++;
94
+ } else {
95
+ console.log(`āŒ MISMATCH`.red);
96
+ failed++;
97
+ }
98
+
99
+ const resultEntry = {
100
+ id: index + 1,
101
+ method: entry.method,
102
+ url: endpoint,
103
+ status1: res1.status,
104
+ status2: res2.status,
105
+ time1,
106
+ time2,
107
+ diff: hasDiff ? differences : null,
108
+ match: isSuccess
109
+ };
110
+
111
+ results.push(resultEntry);
112
+ onEvent({ type: 'progress', current: index + 1, total: logs.length, result: resultEntry });
113
+ return resultEntry;
114
+
115
+ } catch (err) {
116
+ console.error(`šŸ’„ Error replaying ${endpoint}: ${err.message}`.red);
117
+ onEvent({ type: 'error', message: `Error on ${endpoint}: ${err.message}` });
118
+ return null;
119
+ }
120
+ };
121
+
122
+ // Queue mechanism: Process logic in chunks based on concurrency
123
+ for (let i = 0; i < logs.length; i += concurrency) {
124
+ const batch = logs.slice(i, i + concurrency);
125
+ // Map batch to promises
126
+ await Promise.all(batch.map((log, idx) => processLog(log, i + idx)));
127
+ }
128
+
129
+ generateReport(results, reportFile, primaryUrl, secondaryUrl);
130
+
131
+ console.log(`\nšŸ Replay Complete. passed: ${passed}, failed: ${failed}`.bold);
132
+
133
+ onEvent({ type: 'complete', passed, failed, report: reportFile, results: results });
134
+ }
135
+
136
+ function clean(obj, ignoreList) {
137
+ if (!obj || typeof obj !== 'object') return obj;
138
+ const copy = JSON.parse(JSON.stringify(obj));
139
+ const sanitize = (o) => {
140
+ for (const key in o) {
141
+ if (ignoreList.includes(key)) {
142
+ delete o[key];
143
+ } else if (typeof o[key] === 'object' && o[key] !== null) {
144
+ sanitize(o[key]);
145
+ }
146
+ }
147
+ };
148
+ sanitize(copy);
149
+ return copy;
150
+ }
151
+
152
+ function generateReport(results, filePath, url1, url2) {
153
+ const fs = require('fs');
154
+
155
+ const html = `
156
+ <!DOCTYPE html>
157
+ <html>
158
+ <head>
159
+ <title>Traffic Replay Report</title>
160
+ <style>
161
+ body { font-family: sans-serif; padding: 20px; background: #f4f4f9; }
162
+ h1 { text-align: center; }
163
+ .summary { display: flex; justify-content: center; gap: 20px; margin-bottom: 20px; }
164
+ .card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
165
+ .success { border-left: 5px solid #2ecc71; color: #2ecc71; }
166
+ .failure { border-left: 5px solid #e74c3c; color: #e74c3c; }
167
+ table { width: 100%; border-collapse: collapse; background: white; }
168
+ th, td { padding: 10px; border-bottom: 1px solid #ddd; text-align: left; }
169
+ th { background: #eee; }
170
+ .diff-pre { background: #2d2d2d; color: #ccc; padding: 10px; border-radius: 4px; overflow-x: auto; }
171
+ .diff-added { color: #2ecc71; }
172
+ .diff-removed { color: #e74c3c; }
173
+ tr.fail-row { background-color: #ffe6e6; }
174
+ </style>
175
+ </head>
176
+ <body>
177
+ <h1>🚦 Traffic Replay Report</h1>
178
+ <div class="summary">
179
+ <div class="card success">
180
+ <h2>Passed</h2>
181
+ <p>${results.filter(r => r.match).length}</p>
182
+ </div>
183
+ <div class="card failure">
184
+ <h2>Failed</h2>
185
+ <p>${results.filter(r => !r.match).length}</p>
186
+ </div>
187
+ </div>
188
+
189
+ <table>
190
+ <thead>
191
+ <tr>
192
+ <th>Method</th>
193
+ <th>URL</th>
194
+ <th>Env 1 Status</th>
195
+ <th>Env 2 Status</th>
196
+ <th>Time (ms)</th>
197
+ <th>Result</th>
198
+ </tr>
199
+ </thead>
200
+ <tbody>
201
+ ${results.map(r => `
202
+ <tr class="${!r.match ? 'fail-row' : ''}">
203
+ <td>${r.method}</td>
204
+ <td>${r.url}</td>
205
+ <td>${r.status1}</td>
206
+ <td>${r.status2}</td>
207
+ <td>${r.time1} / ${r.time2}</td>
208
+ <td>${r.match ? 'āœ… Match' : 'āŒ Mismatch'}</td>
209
+ </tr>
210
+ ${!r.match && r.diff ? `
211
+ <tr>
212
+ <td colspan="6">
213
+ <pre class="diff-pre">${r.diff.map(d =>
214
+ d.added ? `<span class="diff-added">+ ${d.value}</span>` :
215
+ d.removed ? `<span class="diff-removed">- ${d.value}</span>` :
216
+ `<span> ${d.value}</span>`
217
+ ).join('')}</pre>
218
+ </td>
219
+ </tr>
220
+ ` : ''}
221
+ `).join('')}
222
+ </tbody>
223
+ </table>
224
+ </body>
225
+ </html>
226
+ `;
227
+
228
+ fs.writeFileSync(filePath, html);
229
+ console.log(`šŸ“„ Report saved to ${filePath}`.green);
230
+ }
231
+
232
+ module.exports = replayAndDiff;
package/server.js ADDED
@@ -0,0 +1,129 @@
1
+ /* eslint-disable no-console */
2
+ // ... existing imports
3
+ const express = require('express');
4
+ const http = require('http');
5
+ const { Server } = require('socket.io');
6
+ const cors = require('cors');
7
+ const recorder = require('./recorder');
8
+ const replayer = require('./replayer');
9
+ const generator = require('./traffic-generator');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+
13
+ function startServer(port = 4200) {
14
+ const app = express();
15
+ const server = http.createServer(app);
16
+ const io = new Server(server, { cors: { origin: '*' } });
17
+
18
+ app.use(cors());
19
+ app.use(express.json());
20
+
21
+ // --- Recorder API ---
22
+ app.post('/api/record/start', (req, res) => {
23
+ const { target, port, file } = req.body;
24
+ try {
25
+ const msg = recorder.start(target, port, file, (log) => {
26
+ io.emit('record-log', log);
27
+ });
28
+ res.json({ status: 'ok', message: msg });
29
+ } catch (e) {
30
+ res.status(400).json({ error: e.message });
31
+ }
32
+ });
33
+
34
+ app.post('/api/record/stop', (req, res) => {
35
+ recorder.stop();
36
+ res.json({ status: 'ok', message: 'Stopped' });
37
+ });
38
+
39
+ // --- Replayer API (FIXED) ---
40
+ app.post('/api/replay', (req, res) => {
41
+ // Added 'exclude' to destructuring
42
+ const { file, env1, env2, ignore, auth, exclude } = req.body;
43
+
44
+ const injectedHeaders = {};
45
+ if (auth && auth.trim() !== '') {
46
+ injectedHeaders['Authorization'] = auth;
47
+ }
48
+
49
+ // Fixed Argument Order:
50
+ // 1. file, 2. env1, 3. env2, 4. reportPath, 5. ignore, 6. headers, 7. EXCLUDE, 8. callback
51
+ replayer(
52
+ file,
53
+ env1,
54
+ env2,
55
+ 'last-report.html',
56
+ ignore || [],
57
+ injectedHeaders,
58
+ exclude || [], // <--- Pass exclude array here
59
+ (event) => {
60
+ io.emit('replay-event', event);
61
+ }
62
+ );
63
+
64
+ res.json({ status: 'ok', message: 'Replay started' });
65
+ });
66
+
67
+ // --- Generator API ---
68
+ app.post('/api/generate', async (req, res) => {
69
+ // 1. Get arguments (Added 'target' and 'port' so we can start the recorder)
70
+ const { proxyUrl, swaggerFile, exclude, target, port } = req.body;
71
+
72
+ const targetProxy = proxyUrl || 'http://localhost:3000';
73
+ const targetFile = swaggerFile || './full_documentation.json';
74
+ const targetExclude = Array.isArray(exclude) ? exclude : [];
75
+ const recTarget = target || 'http://localhost:8080';
76
+ const recPort = port || 3000;
77
+
78
+ console.log('šŸš€ Starting Traffic Generation Workflow...');
79
+
80
+ // --- STEP A: Start the Recorder (The Proxy) ---
81
+ try {
82
+ // We force start the recorder so there is something listening on port 3000
83
+ recorder.start(recTarget, recPort, 'ui-traffic.jsonl', (log) => {
84
+ io.emit('record-log', log);
85
+ });
86
+ console.log(`šŸŽ™ļø Auto-started Recorder on port ${recPort} -> ${recTarget}`);
87
+ } catch (e) {
88
+ console.log('āš ļø Recorder might already be running, proceeding...');
89
+ }
90
+
91
+ res.json({ status: 'started', message: 'Traffic generation started' });
92
+
93
+ try {
94
+ // --- STEP B: Run the Generator ---
95
+ // Wait a moment for the server to bind the port
96
+ await new Promise((r) => setTimeout(r, 500));
97
+
98
+ await generator.run(targetProxy, targetFile, targetExclude, (log) => {
99
+ // If the log is an object, emit it; if string, just log console
100
+ if (typeof log === 'object' && log.message) {
101
+ // specific generator status updates
102
+ }
103
+ });
104
+
105
+ console.log('šŸ›‘ Generation finished. Stopping Recorder...');
106
+
107
+ // --- STEP C: Stop the Recorder ---
108
+ recorder.stop();
109
+ io.emit('record-stopped');
110
+ } catch (e) {
111
+ console.error('Generator Error:', e);
112
+ recorder.stop(); // Ensure we stop even if generator crashes
113
+ io.emit('record-stopped');
114
+ }
115
+ });
116
+
117
+ // ... Serve Angular logic (keep existing) ...
118
+ const frontendPath = path.join(__dirname, 'ui/dist/ui/browser');
119
+ if (fs.existsSync(frontendPath)) {
120
+ app.use(express.static(frontendPath));
121
+ app.get('/', (req, res) => res.sendFile(path.join(frontendPath, 'index.html')));
122
+ }
123
+
124
+ server.listen(port, () => {
125
+ console.log(`\n🌐 TrafficMirror running at http://localhost:${port}`);
126
+ });
127
+ }
128
+
129
+ module.exports = startServer;
@@ -0,0 +1,149 @@
1
+ /* eslint-disable no-console */
2
+ const fs = require('fs');
3
+ const http = require('http');
4
+ const https = require('https');
5
+
6
+ class TrafficGenerator {
7
+ /**
8
+ * Fixed signature to match index.js:
9
+ * 1. proxyUrl (from options.target)
10
+ * 2. swaggerPath (from options.file)
11
+ * 3. exclude (from options.exclude)
12
+ * 4. onProgress (the callback)
13
+ * 5. sourceUrl (from options.source)
14
+ */
15
+ async run(proxyUrl, swaggerPath, exclude = [], onProgress = () => { }, sourceUrl) {
16
+ // --- STEP 1: Health Checks ---
17
+ onProgress({ message: `🩺 Starting Pre-flight Health Checks...` });
18
+
19
+ // Check Proxy Health
20
+ onProgress({ message: ` - Checking Proxy: ${proxyUrl}` });
21
+ const isProxyUp = await this.checkConnection(proxyUrl);
22
+
23
+ // Check Source Health (if provided)
24
+ let isSourceUp = true;
25
+ if (sourceUrl) {
26
+ onProgress({ message: ` - Checking Source: ${sourceUrl}` });
27
+ isSourceUp = await this.checkConnection(sourceUrl);
28
+ }
29
+
30
+ if (!isProxyUp) {
31
+ const msg = `šŸ›‘ ABORTING: Proxy is unreachable at ${proxyUrl}`;
32
+ onProgress({ message: msg });
33
+ throw new Error(msg);
34
+ }
35
+
36
+ if (sourceUrl && !isSourceUp) {
37
+ const msg = `šŸ›‘ ABORTING: Source Server is unreachable at ${sourceUrl}`;
38
+ onProgress({ message: msg });
39
+ throw new Error(msg);
40
+ }
41
+
42
+ onProgress({ message: `āœ… Systems Nominal. Proxy and Source are UP.` });
43
+ // -----------------------------
44
+
45
+ onProgress({ message: `šŸ“– Reading Swagger file: ${swaggerPath}` });
46
+
47
+ let doc;
48
+ try {
49
+ if (!fs.existsSync(swaggerPath)) {
50
+ throw new Error(`Swagger file not found at ${swaggerPath}`);
51
+ }
52
+ const raw = fs.readFileSync(swaggerPath);
53
+ doc = JSON.parse(raw);
54
+ } catch (e) {
55
+ throw new Error(`Failed to load Swagger: ${e.message}`);
56
+ }
57
+
58
+ // Validate Swagger structure
59
+ if (!doc.paths) {
60
+ throw new Error("Invalid Swagger file: 'paths' property missing.");
61
+ }
62
+
63
+ const paths = Object.keys(doc.paths);
64
+ const getEndpoints = paths.filter((p) => doc.paths[p].get);
65
+
66
+ onProgress({
67
+ message: `šŸ” Found ${getEndpoints.length} GET endpoints. Applying filters...`,
68
+ });
69
+
70
+ const rawExclude = Array.isArray(exclude) ? exclude : [exclude];
71
+ const cleanExclude = rawExclude.map((e) => (e ? e.trim() : '')).filter((e) => e !== '');
72
+
73
+ const validEndpoints = getEndpoints.filter((endpoint) => {
74
+ if (cleanExclude.includes(endpoint)) return false;
75
+ const isExcluded = cleanExclude.some((excludedItem) => {
76
+ return excludedItem.endsWith(endpoint) || endpoint.endsWith(excludedItem);
77
+ });
78
+ if (isExcluded) return false;
79
+ return true;
80
+ });
81
+
82
+ onProgress({
83
+ message: `⚔ Generating traffic for ${validEndpoints.length
84
+ } endpoints (Skipped ${getEndpoints.length - validEndpoints.length})`,
85
+ });
86
+
87
+ // Ensure we send traffic to the PROXY
88
+ const cleanProxyUrl = proxyUrl.replace(/\/$/, '');
89
+ let count = 0;
90
+
91
+ for (const endpoint of validEndpoints) {
92
+ // Skip parameterized endpoints like /users/{id} for now as we can't guess IDs
93
+ if (endpoint.includes('{')) {
94
+ onProgress({ message: `āš ļø Skipping parameterized path: ${endpoint}` });
95
+ continue;
96
+ }
97
+
98
+ const url = `${cleanProxyUrl}/api${endpoint}`;
99
+ console.log(`[Generator] Requesting: ${url}`);
100
+ count++;
101
+
102
+ try {
103
+ await this.sendRequest(url);
104
+ onProgress({
105
+ message: `[${count}/${validEndpoints.length}] šŸš€ HIT: ${endpoint}`,
106
+ });
107
+ // Small delay to prevent overwhelming the server
108
+ await new Promise((r) => setTimeout(r, 50));
109
+ } catch (err) {
110
+ onProgress({ message: `āŒ FAIL: ${endpoint} - ${err.message}` });
111
+ }
112
+ }
113
+
114
+ onProgress({ message: 'āœ… Traffic generation complete!' });
115
+ return true;
116
+ }
117
+
118
+ checkConnection(url) {
119
+ return new Promise((resolve) => {
120
+ if (!url) resolve(true);
121
+
122
+ const client = url.startsWith('https') ? https : http;
123
+ const req = client.get(url, (res) => {
124
+ res.resume();
125
+ resolve(true);
126
+ });
127
+
128
+ req.on('error', () => resolve(false));
129
+
130
+ req.setTimeout(5000, () => {
131
+ req.destroy();
132
+ resolve(false);
133
+ });
134
+ });
135
+ }
136
+
137
+ sendRequest(url) {
138
+ return new Promise((resolve, reject) => {
139
+ const client = url.startsWith('https') ? https : http;
140
+ const req = client.get(url, { headers: { 'User-Agent': 'Traffic-Mirror-Bot' } }, (res) => {
141
+ res.resume();
142
+ resolve();
143
+ });
144
+ req.on('error', (e) => reject(e));
145
+ });
146
+ }
147
+ }
148
+
149
+ module.exports = new TrafficGenerator();