Initial commit: LogMaster with Viewer Collector Monitor modes
Some checks failed
LogMaster CI/CD / build-and-test (push) Has been cancelled
LogMaster CI/CD / docker (push) Has been cancelled

This commit is contained in:
2026-05-04 13:50:15 +02:00
commit 44cd9ab001
231 changed files with 29018 additions and 0 deletions

306
test-all.js Normal file
View File

@@ -0,0 +1,306 @@
#!/usr/bin/env node
// ============================================
// Full Integration Test for LogViewer
// Tests: Collector, Viewer, Monitor, Self-Forward, Telegram
// ============================================
const http = require('http');
const COLLECTOR = 'http://localhost:8081';
const VIEWER = 'http://localhost:9090';
const MONITOR = 'http://localhost:8082';
let passed = 0, failed = 0;
function post(base, path, data) {
return new Promise((resolve, reject) => {
const payload = JSON.stringify(data);
const url = new URL(path, base);
const req = http.request({
hostname: url.hostname, port: url.port, path: url.pathname + url.search,
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
}, (res) => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => { try { resolve(JSON.parse(body)); } catch { resolve(body); } });
});
req.on('error', reject);
req.write(payload);
req.end();
});
}
function postText(base, path, text) {
return new Promise((resolve, reject) => {
const url = new URL(path, base);
const req = http.request({
hostname: url.hostname, port: url.port, path: url.pathname,
method: 'POST',
headers: { 'Content-Type': 'text/plain', 'Content-Length': Buffer.byteLength(text) },
}, (res) => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => { try { resolve(JSON.parse(body)); } catch { resolve(body); } });
});
req.on('error', reject);
req.write(text);
req.end();
});
}
function get(base, path) {
return new Promise((resolve, reject) => {
const url = new URL(path, base);
http.get({ hostname: url.hostname, port: url.port, path: url.pathname + url.search }, (res) => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => { try { resolve(JSON.parse(body)); } catch { resolve(body); } });
}).on('error', reject);
});
}
function del(base, path) {
return new Promise((resolve, reject) => {
const url = new URL(path, base);
const req = http.request({ hostname: url.hostname, port: url.port, path: url.pathname + url.search, method: 'DELETE' }, (res) => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => { try { resolve(JSON.parse(body)); } catch { resolve(body); } });
});
req.on('error', reject);
req.end();
});
}
function check(name, condition) {
if (condition) { console.log(`${name}`); passed++; }
else { console.log(`${name}`); failed++; }
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
async function testCollector() {
console.log('\n ═══ COLLECTOR TESTS ═══\n');
// 1. Health check
const mode = await get(COLLECTOR, '/api/mode');
check('Collector is running', mode && mode.mode === 'collector');
check('Collector is active', mode && mode.hasCollector === true);
// 2. Send plain text logs
const r1 = await postText(COLLECTOR, '/collect/test-plain', '2026-05-01T10:00:00Z INFO Test line 1\n2026-05-01T10:00:01Z ERROR Test line 2');
check('Plain text ingest', r1 && r1.ok === true && r1.accepted === 2);
// 3. Send JSON logs
const r2 = await post(COLLECTOR, '/collect/test-json/json', ['INFO json line 1', 'WARN json line 2', 'ERROR json line 3']);
check('JSON ingest', r2 && r2.ok === true && r2.accepted === 3);
// 4. List streams
const streams = await get(COLLECTOR, '/collector/streams');
check('Streams listed', Array.isArray(streams) && streams.length >= 2);
check('test-plain stream exists', streams.some(s => s.id === 'test-plain'));
check('test-json stream exists', streams.some(s => s.id === 'test-json'));
// 5. Read stream
const data = await get(COLLECTOR, '/collector/stream/test-plain');
check('Read stream', data && data.lines && data.lines.length === 2);
// 6. Read with tail
const tail = await get(COLLECTOR, '/collector/stream/test-json?tail=1');
check('Tail parameter works', tail && tail.lines && tail.lines.length === 1);
// 7. Self-forward: send to stream-a, should appear in stream-all
// First we need to configure forward targets - this requires restart, so we test via direct ingest
const r3 = await postText(COLLECTOR, '/collect/stream-a', 'INFO from stream-a');
check('Stream-a ingest', r3 && r3.ok === true);
// 8. Delete stream
const delResult = await del(COLLECTOR, '/collector/stream/test-plain');
check('Delete stream', delResult && delResult.ok === true);
const afterDel = await get(COLLECTOR, '/collector/streams');
check('Stream removed', !afterDel.some(s => s.id === 'test-plain'));
// 9. Collector config endpoint
const config = await get(COLLECTOR, '/collector/config');
check('Config endpoint', config && config.storageDir && config.rotation);
// 10. Bulk ingest
const lines = [];
for (let i = 0; i < 50; i++) lines.push(`2026-05-01T10:${String(i).padStart(2,'0')}:00Z INFO Bulk line ${i}`);
const r4 = await post(COLLECTOR, '/collect/bulk-test/json', lines);
check('Bulk ingest (50 lines)', r4 && r4.accepted === 50);
}
async function testViewer() {
console.log('\n ═══ VIEWER TESTS ═══\n');
// 1. Health check
const mode = await get(VIEWER, '/api/mode');
check('Viewer is running', mode && mode.mode === 'viewer');
// 2. HTML page served
const html = await new Promise((resolve, reject) => {
http.get(`${VIEWER}/`, (res) => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => resolve({ status: res.statusCode, body }));
}).on('error', reject);
});
check('HTML page served (200)', html.status === 200);
check('HTML contains Log Viewer', html.body.includes('Log Viewer'));
// 3. Sources listed
const sources = await get(VIEWER, '/api/sources');
check('File sources listed', Array.isArray(sources) && sources.length > 0);
// 4. Read logs
const logs = await get(VIEWER, '/api/logs');
check('Logs readable', Array.isArray(logs) && logs.length > 0);
check('Log entries have structure', logs[0] && logs[0].line && logs[0].source);
// 5. Read specific source
if (sources.length > 0) {
const src = sources[0].path;
const srcLogs = await get(VIEWER, `/api/logs?source=${encodeURIComponent(src)}`);
check('Source-specific logs', Array.isArray(srcLogs) && srcLogs.length > 0);
}
// 6. Remote streams from collector
const remote = await get(VIEWER, '/api/remote/streams');
check('Remote streams accessible', Array.isArray(remote));
if (remote.length > 0) {
const stream = remote[0];
const remoteData = await get(VIEWER, `/api/remote/stream/${stream.id}`);
check('Remote stream readable', Array.isArray(remoteData) && remoteData.length > 0);
} else {
check('Remote stream readable (skipped - no streams)', true);
}
// 7. Config endpoint
const config = await get(VIEWER, '/api/config');
check('Config endpoint', config && config.theme);
// 8. Add source
const addResult = await post(VIEWER, '/api/sources', { path: 'sample-logs/security-audit.log' });
check('Add source', Array.isArray(addResult));
}
async function testMonitor() {
console.log('\n ═══ MONITOR TESTS ═══\n');
// 1. Health check
const mode = await get(MONITOR, '/api/mode');
check('Monitor is running', mode && mode.mode === 'monitor');
// 2. HTML page served (monitor UI)
const html = await new Promise((resolve, reject) => {
http.get(`${MONITOR}/`, (res) => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => resolve({ status: res.statusCode, body }));
}).on('error', reject);
});
check('Monitor HTML served (200)', html.status === 200);
check('Monitor HTML contains Monitor', html.body.includes('Log Monitor'));
// 3. Create rule
const rule = await post(MONITOR, '/api/monitor/rules', {
id: 'test-rule-1',
name: 'Test: Error 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' },
});
check('Create rule', rule && rule.ok === true);
// 4. Create second rule
const rule2 = await post(MONITOR, '/api/monitor/rules', {
id: 'test-rule-2',
name: 'Test: Security Audit',
enabled: true,
sources: ['sample-logs/security-audit.log'],
patterns: ['root.*login', 'DROP TABLE', 'GRANT'],
patternLogic: 'any',
timeRange: { type: 'all' },
notifications: [{ type: 'telegram', botToken: '8116790669:AAE_llOFDFSwDhPx40vZMBa6dGv3Xxa9ZiQ', chatId: '8599028500' }],
maxLinesInMessage: 5,
cooldownMs: 5000,
schedule: { mode: 'daily', value: '07:00' },
});
check('Create rule with Telegram', rule2 && rule2.ok === true);
// 5. List rules
const rules = await get(MONITOR, '/api/monitor/rules');
check('Rules listed', Array.isArray(rules) && rules.length >= 2);
// 6. Trigger rule 1
const result1 = await post(MONITOR, '/api/monitor/trigger/test-rule-1', {});
check('Trigger rule 1 (errors in app.log)', result1 && result1.totalMatches > 0);
if (result1 && result1.totalMatches) {
console.log(` → Found ${result1.totalMatches} ERROR/FATAL lines`);
}
// 7. Trigger rule 2 (with Telegram)
const result2 = await post(MONITOR, '/api/monitor/trigger/test-rule-2', {});
check('Trigger rule 2 (security + Telegram)', result2 && result2.totalMatches > 0);
if (result2 && result2.totalMatches) {
console.log(` → Found ${result2.totalMatches} security matches, Telegram alert sent`);
}
// 8. Check results
const results = await get(MONITOR, '/api/monitor/results');
check('Results stored', Array.isArray(results) && results.length >= 2);
// 9. Update rule
const updated = await new Promise((resolve, reject) => {
const payload = JSON.stringify({ name: 'Test: Error Detection (updated)' });
const req = http.request({
hostname: 'localhost', port: 8082, path: '/api/monitor/rule/test-rule-1',
method: 'PUT', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
}, (res) => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve(JSON.parse(b))); });
req.on('error', reject);
req.write(payload);
req.end();
});
check('Update rule', updated && updated.ok === true);
// 10. Test notification endpoint
const notifTest = await post(MONITOR, '/api/monitor/test-notification', {
type: 'telegram',
botToken: '8116790669:AAE_llOFDFSwDhPx40vZMBa6dGv3Xxa9ZiQ',
chatId: '8599028500',
});
check('Test notification (Telegram)', notifTest && notifTest.ok === true);
// 11. Delete rule
const delResult = await del(MONITOR, '/api/monitor/rule/test-rule-1');
check('Delete rule', delResult && delResult.ok === true);
// 12. Monitor config
const config = await get(MONITOR, '/api/monitor/config');
check('Monitor config endpoint', config && config.scanIntervalMs);
}
async function main() {
console.log('\n ╔══════════════════════════════════════════╗');
console.log(' ║ LogViewer Full Integration Test ║');
console.log(' ╚══════════════════════════════════════════╝');
try { await testCollector(); } catch (e) { console.log(` ❌ Collector tests failed: ${e.message}`); failed++; }
try { await testViewer(); } catch (e) { console.log(` ❌ Viewer tests failed: ${e.message}`); failed++; }
try { await testMonitor(); } catch (e) { console.log(` ❌ Monitor tests failed: ${e.message}`); failed++; }
console.log('\n ═══════════════════════════════════════════');
console.log(` Results: ${passed} passed, ${failed} failed`);
console.log(' ═══════════════════════════════════════════\n');
process.exit(failed > 0 ? 1 : 0);
}
main();