313 lines
14 KiB
JavaScript
313 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
// ============================================
|
|
// Fresh Start: Configure, Test, and Run
|
|
// ============================================
|
|
const http = require('http');
|
|
const { spawn } = require('child_process');
|
|
|
|
const COLLECTOR_PORT = 8081;
|
|
const VIEWER_PORT = 9090;
|
|
const MONITOR_PORT = 8082;
|
|
|
|
// ---- Helpers ----
|
|
function req(port, method, path, data) {
|
|
return new Promise((resolve, reject) => {
|
|
const payload = data ? (typeof data === 'string' ? data : JSON.stringify(data)) : '';
|
|
const isJson = typeof data === 'object';
|
|
const r = http.request({
|
|
hostname: 'localhost', port, path, method,
|
|
headers: {
|
|
'Content-Type': isJson ? 'application/json' : 'text/plain',
|
|
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
},
|
|
}, (res) => {
|
|
let body = '';
|
|
res.on('data', c => body += c);
|
|
res.on('end', () => {
|
|
try { resolve({ status: res.statusCode, data: JSON.parse(body) }); }
|
|
catch { resolve({ status: res.statusCode, data: body }); }
|
|
});
|
|
});
|
|
r.on('error', reject);
|
|
if (payload) r.write(payload);
|
|
r.end();
|
|
});
|
|
}
|
|
|
|
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
let pass = 0, fail = 0;
|
|
function ok(name, cond) { if (cond) { console.log(` ✅ ${name}`); pass++; } else { console.log(` ❌ ${name}`); fail++; } }
|
|
|
|
// ---- Start Services ----
|
|
function startService(envFile) {
|
|
// Read env file and pass as environment
|
|
const fs = require('fs');
|
|
const env = { ...process.env };
|
|
try {
|
|
const content = fs.readFileSync(envFile, 'utf-8');
|
|
for (const line of content.split('\n')) {
|
|
const trimmed = line.trim();
|
|
if (trimmed && !trimmed.startsWith('#')) {
|
|
const eq = trimmed.indexOf('=');
|
|
if (eq > 0) env[trimmed.substring(0, eq).trim()] = trimmed.substring(eq + 1).trim();
|
|
}
|
|
}
|
|
} catch {}
|
|
const child = spawn('node', ['dist-server/server.js'], {
|
|
cwd: process.cwd(), stdio: 'pipe', env, detached: false,
|
|
});
|
|
child.stderr.on('data', () => {});
|
|
child.stdout.on('data', () => {});
|
|
return child;
|
|
}
|
|
|
|
async function waitForPort(port, maxWait = 15000) {
|
|
const start = Date.now();
|
|
while (Date.now() - start < maxWait) {
|
|
try { await req(port, 'GET', '/api/mode'); return true; } catch { await sleep(300); }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ---- Main ----
|
|
async function main() {
|
|
console.log('\n ╔══════════════════════════════════════════════╗');
|
|
console.log(' ║ LogViewer — Fresh Start & Full Test ║');
|
|
console.log(' ╚══════════════════════════════════════════════╝\n');
|
|
|
|
// Kill any existing processes on our ports
|
|
console.log(' Cleaning up old processes...');
|
|
const { execSync } = require('child_process');
|
|
for (const port of [COLLECTOR_PORT, VIEWER_PORT, MONITOR_PORT]) {
|
|
try { execSync(`cmd /c "for /f "tokens=5" %a in ('netstat -aon ^| findstr :${port} ^| findstr LISTENING') do taskkill /F /PID %a"`, { stdio: 'pipe' }); } catch {}
|
|
}
|
|
await sleep(1000);
|
|
|
|
// Start all 3 services
|
|
console.log(' Starting services...');
|
|
const procs = [];
|
|
procs.push(startService('.env.collector'));
|
|
procs.push(startService('.env.viewer'));
|
|
procs.push(startService('.env.monitor'));
|
|
|
|
const c = await waitForPort(COLLECTOR_PORT);
|
|
const v = await waitForPort(VIEWER_PORT);
|
|
const m = await waitForPort(MONITOR_PORT);
|
|
ok('Collector started (:8081)', c);
|
|
ok('Viewer started (:9090)', v);
|
|
ok('Monitor started (:8082)', m);
|
|
|
|
if (!c || !v || !m) { console.log('\n ❌ Services failed to start. Aborting.\n'); process.exit(1); }
|
|
|
|
// ═══════════════════════════════════════
|
|
// COLLECTOR TESTS
|
|
// ═══════════════════════════════════════
|
|
console.log('\n ── Collector Tests ──\n');
|
|
|
|
// Ingest plain text
|
|
const r1 = await req(COLLECTOR_PORT, 'POST', '/collect/web-app', '2026-05-01T08:00:00Z INFO [http] GET /dashboard 200 (12ms)\n2026-05-01T08:00:01Z INFO [http] GET /api/users 200 (45ms)\n2026-05-01T08:00:02Z WARN [http] Slow request: GET /api/reports took 3200ms\n2026-05-01T08:00:03Z ERROR [db] Query timeout: SELECT * FROM orders WHERE status=pending\n2026-05-01T08:00:04Z INFO [auth] User admin@corp.com logged in');
|
|
ok('Ingest plain text (5 lines)', r1.data.accepted === 5);
|
|
|
|
// Ingest JSON
|
|
const r2 = await req(COLLECTOR_PORT, 'POST', '/collect/auth-service/json', [
|
|
'2026-05-01T08:01:00Z INFO [auth] Token issued for user alice',
|
|
'2026-05-01T08:01:01Z WARN [auth] Failed login: bob (wrong password)',
|
|
'2026-05-01T08:01:02Z ERROR [auth] Account locked: bob (5 attempts)',
|
|
'2026-05-01T08:01:03Z INFO [auth] Password reset requested: bob',
|
|
]);
|
|
ok('Ingest JSON (4 lines)', r2.data.accepted === 4);
|
|
|
|
// Ingest to third stream
|
|
const r3 = await req(COLLECTOR_PORT, 'POST', '/collect/payment-api', '2026-05-01T08:02:00Z INFO [pay] Payment processed: order-1234 ($49.99)\n2026-05-01T08:02:01Z ERROR [pay] Payment failed: order-1235 (card declined)\n2026-05-01T08:02:02Z WARN [pay] Retry 1/3 for order-1235');
|
|
ok('Ingest third stream (3 lines)', r3.data.accepted === 3);
|
|
|
|
// List streams
|
|
const streams = await req(COLLECTOR_PORT, 'GET', '/collector/streams');
|
|
ok('3 streams created', streams.data.length === 3);
|
|
|
|
// Read stream
|
|
const read = await req(COLLECTOR_PORT, 'GET', '/collector/stream/web-app');
|
|
ok('Read web-app stream (5 lines)', read.data.lines.length === 5);
|
|
|
|
// Tail
|
|
const tail = await req(COLLECTOR_PORT, 'GET', '/collector/stream/auth-service?tail=2');
|
|
ok('Tail=2 works', tail.data.lines.length === 2);
|
|
|
|
// Config
|
|
const cfg = await req(COLLECTOR_PORT, 'GET', '/collector/config');
|
|
ok('Config has rotation', !!cfg.data.rotation);
|
|
ok('Config has UDP port', cfg.data.udpPort === 5140);
|
|
|
|
// Delete stream
|
|
const del = await req(COLLECTOR_PORT, 'DELETE', '/collector/stream/payment-api');
|
|
ok('Delete stream', del.data.ok === true);
|
|
const after = await req(COLLECTOR_PORT, 'GET', '/collector/streams');
|
|
ok('Stream removed', after.data.length === 2);
|
|
|
|
// ═══════════════════════════════════════
|
|
// VIEWER TESTS
|
|
// ═══════════════════════════════════════
|
|
console.log('\n ── Viewer Tests ──\n');
|
|
|
|
// HTML
|
|
const html = await req(VIEWER_PORT, 'GET', '/');
|
|
ok('Viewer HTML (200)', html.status === 200);
|
|
|
|
// Sources
|
|
const src = await req(VIEWER_PORT, 'GET', '/api/sources');
|
|
ok('File sources loaded', src.data.length >= 4);
|
|
|
|
// Read all logs
|
|
const logs = await req(VIEWER_PORT, 'GET', '/api/logs');
|
|
ok('Logs loaded (>100 lines)', logs.data.length > 100);
|
|
|
|
// Log structure
|
|
const entry = logs.data[0];
|
|
ok('Entry has line', !!entry.line);
|
|
ok('Entry has source', !!entry.source);
|
|
ok('Entry has level', entry.level !== undefined);
|
|
|
|
// Remote streams
|
|
const remote = await req(VIEWER_PORT, 'GET', '/api/remote/streams');
|
|
ok('Remote streams from collector', remote.data.length >= 2);
|
|
|
|
// Read remote stream
|
|
if (remote.data.length > 0) {
|
|
const rs = await req(VIEWER_PORT, 'GET', `/api/remote/stream/${remote.data[0].id}`);
|
|
ok('Read remote stream', Array.isArray(rs.data) && rs.data.length > 0);
|
|
}
|
|
|
|
// Config
|
|
const vcfg = await req(VIEWER_PORT, 'GET', '/api/config');
|
|
ok('Viewer config', vcfg.data.theme === 'dark');
|
|
|
|
// ═══════════════════════════════════════
|
|
// MONITOR TESTS
|
|
// ═══════════════════════════════════════
|
|
console.log('\n ── Monitor Tests ──\n');
|
|
|
|
// HTML
|
|
const mhtml = await req(MONITOR_PORT, 'GET', '/');
|
|
ok('Monitor HTML (200)', mhtml.status === 200);
|
|
|
|
// Create rules
|
|
const rule1 = await req(MONITOR_PORT, 'POST', '/api/monitor/rules', {
|
|
id: 'error-check', name: 'Error & Fatal Detection', enabled: true,
|
|
sources: ['sample-logs/app.log'],
|
|
patterns: ['\\bERROR\\b', '\\bFATAL\\b'],
|
|
patternLogic: 'any', timeRange: { type: 'all' },
|
|
notifications: [], maxLinesInMessage: 10, cooldownMs: 5000,
|
|
schedule: { mode: 'interval' },
|
|
});
|
|
ok('Create rule: Error Detection', rule1.data.ok);
|
|
|
|
const rule2 = await req(MONITOR_PORT, 'POST', '/api/monitor/rules', {
|
|
id: 'security-audit', name: 'Security: Root Access', enabled: true,
|
|
sources: ['sample-logs/security-audit.log'],
|
|
patterns: ['root.*login', 'GRANT.*PRIVILEGES', 'DROP TABLE'],
|
|
patternLogic: 'any', timeRange: { type: 'all' },
|
|
notifications: [{ type: 'telegram', botToken: '8116790669:AAE_llOFDFSwDhPx40vZMBa6dGv3Xxa9ZiQ', chatId: '8599028500' }],
|
|
maxLinesInMessage: 5, cooldownMs: 5000,
|
|
schedule: { mode: 'daily', value: '07:00' },
|
|
});
|
|
ok('Create rule: Security Audit (Telegram)', rule2.data.ok);
|
|
|
|
const rule3 = await req(MONITOR_PORT, 'POST', '/api/monitor/rules', {
|
|
id: 'slow-queries', name: 'Slow Query Detection', enabled: true,
|
|
sources: ['sample-logs/app.log'],
|
|
patterns: ['[Ss]low.*\\d{4,}ms', 'timeout'],
|
|
patternLogic: 'any', timeRange: { type: 'all' },
|
|
notifications: [], maxLinesInMessage: 15, cooldownMs: 5000,
|
|
schedule: { mode: 'hourly', value: '1' },
|
|
});
|
|
ok('Create rule: Slow Queries', rule3.data.ok);
|
|
|
|
// List rules
|
|
const rules = await req(MONITOR_PORT, 'GET', '/api/monitor/rules');
|
|
ok('3 rules created', rules.data.length === 3);
|
|
|
|
// Trigger each
|
|
const t1 = await req(MONITOR_PORT, 'POST', '/api/monitor/trigger/error-check', {});
|
|
ok(`Error Detection: ${t1.data.totalMatches || 0} matches`, (t1.data.totalMatches || 0) > 0);
|
|
|
|
const t2 = await req(MONITOR_PORT, 'POST', '/api/monitor/trigger/security-audit', {});
|
|
ok(`Security Audit: ${t2.data.totalMatches || 0} matches (Telegram sent)`, (t2.data.totalMatches || 0) > 0);
|
|
|
|
const t3 = await req(MONITOR_PORT, 'POST', '/api/monitor/trigger/slow-queries', {});
|
|
ok(`Slow Queries: ${t3.data.totalMatches || 0} matches`, (t3.data.totalMatches || 0) >= 0);
|
|
|
|
// Results
|
|
const results = await req(MONITOR_PORT, 'GET', '/api/monitor/results');
|
|
ok('Alert results stored', results.data.length >= 2);
|
|
|
|
// Test notification
|
|
const notif = await req(MONITOR_PORT, 'POST', '/api/monitor/test-notification', {
|
|
type: 'telegram', botToken: '8116790669:AAE_llOFDFSwDhPx40vZMBa6dGv3Xxa9ZiQ', chatId: '8599028500',
|
|
});
|
|
ok('Telegram test notification', notif.data.ok);
|
|
|
|
// Update rule
|
|
const upd = await req(MONITOR_PORT, 'PUT', '/api/monitor/rule/error-check', { name: 'Error Detection (updated)' });
|
|
ok('Update rule', upd.data.ok);
|
|
|
|
// Delete rule
|
|
const delr = await req(MONITOR_PORT, 'DELETE', '/api/monitor/rule/slow-queries');
|
|
ok('Delete rule', delr.data.ok);
|
|
|
|
// ═══════════════════════════════════════
|
|
// RESULTS
|
|
// ═══════════════════════════════════════
|
|
console.log('\n ══════════════════════════════════════════════');
|
|
console.log(` Results: ${pass} passed, ${fail} failed`);
|
|
console.log(' ══════════════════════════════════════════════');
|
|
|
|
console.log('\n Services running:');
|
|
console.log(' Collector → http://localhost:8081');
|
|
console.log(' Viewer → http://localhost:9090');
|
|
console.log(' Monitor → http://localhost:8082');
|
|
|
|
// ═══════════════════════════════════════
|
|
// START DUMMY LOG SENDER
|
|
// ═══════════════════════════════════════
|
|
console.log('\n Starting dummy log sender...\n');
|
|
|
|
const dummyLevels = ['DEBUG', 'INFO', 'INFO', 'INFO', 'WARN', 'ERROR', 'FATAL'];
|
|
const dummyComps = ['auth', 'db', 'http', 'cache', 'queue', 'api', 'scheduler'];
|
|
const dummyStreams = ['web-app', 'auth-service', 'payment-api', 'worker-1'];
|
|
const msgs = {
|
|
DEBUG: ['Cache hit key:user:42', 'Query 12ms', 'GC pause 8ms', 'Pool: active=3 idle=7'],
|
|
INFO: ['GET /api/users 200 (45ms)', 'User login: alice@corp.com', 'Job done: sync-data', 'Health OK', 'Payment processed $29.99'],
|
|
WARN: ['Slow query: 2340ms', 'Memory 82%', 'Rate limit 90%', 'Retry 2/3', 'Cert expires 7d'],
|
|
ERROR: ['Connection timeout', 'NullPointerException', '500 POST /api/orders', 'Disk write failed', 'Payment declined'],
|
|
FATAL: ['DB connection lost!', 'Out of memory!', 'Process crash'],
|
|
};
|
|
|
|
function rnd(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
|
|
let sent = 0;
|
|
|
|
setInterval(() => {
|
|
const stream = rnd(dummyStreams);
|
|
const level = rnd(dummyLevels);
|
|
const comp = rnd(dummyComps);
|
|
const msg = rnd(msgs[level] || msgs.INFO);
|
|
const line = `${new Date().toISOString()} ${level.padEnd(5)} [${comp}] ${msg}`;
|
|
|
|
const payload = line;
|
|
const r = http.request({
|
|
hostname: 'localhost', port: COLLECTOR_PORT,
|
|
path: `/collect/${stream}`, method: 'POST',
|
|
headers: { 'Content-Type': 'text/plain' },
|
|
});
|
|
r.on('error', () => {});
|
|
r.write(payload);
|
|
r.end();
|
|
sent++;
|
|
if (sent % 20 === 0) process.stdout.write(`\r 📡 Sent ${sent} logs to collector (${dummyStreams.length} streams)`);
|
|
}, 300);
|
|
|
|
// Keep running
|
|
console.log(' Sending logs every 300ms to 4 streams. Press Ctrl+C to stop.\n');
|
|
}
|
|
|
|
main().catch(e => { console.error('Fatal:', e.message); process.exit(1); });
|