Files
LogMaster/dist-server/server.js
2026-05-29 18:43:53 +02:00

484 lines
22 KiB
JavaScript

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const http = __importStar(require("http"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const url = __importStar(require("url"));
const logSourceManager_1 = require("./logSourceManager");
const configManager_1 = require("./configManager");
const collector_1 = require("./collector");
const s3Source_1 = require("./s3Source");
const configFile_1 = require("./configFile");
const monitor_1 = require("./monitor");
const configManager = new configManager_1.ConfigManager();
const logSourceManager = new logSourceManager_1.LogSourceManager(configManager);
// Mode: "viewer" | "collector" | "monitor" | "both" | "all"
const MODE = (process.env.LOG_MODE || 'both').toLowerCase();
const PORT = parseInt(process.env.PORT || '8080', 10);
const HOST = process.env.HOST || '0.0.0.0';
// Collector setup
let collector = null;
if (MODE === 'collector' || MODE === 'both') {
let forwardTargets = [];
try {
forwardTargets = JSON.parse(process.env.COLLECTOR_FORWARD_TARGETS || '[]');
}
catch { }
collector = new collector_1.LogCollector({
storageDir: process.env.COLLECTOR_STORAGE_DIR || path.join(process.cwd(), 'collector-data'),
maxLinesPerStream: parseInt(process.env.COLLECTOR_MAX_LINES || '100000', 10),
maxStreams: parseInt(process.env.COLLECTOR_MAX_STREAMS || '50', 10),
udpEnabled: process.env.COLLECTOR_UDP_ENABLED !== 'false',
udpPort: parseInt(process.env.COLLECTOR_UDP_PORT || '5140', 10),
persistToDisk: process.env.COLLECTOR_PERSIST !== 'false',
rotation: {
maxSizeBytes: parseInt(process.env.COLLECTOR_ROTATION_MAX_SIZE || '5242880', 10),
maxAgeMs: parseInt(process.env.COLLECTOR_ROTATION_MAX_AGE || '600000', 10),
compression: process.env.COLLECTOR_ROTATION_COMPRESSION || 'gzip',
fileExtension: process.env.COLLECTOR_ROTATION_EXTENSION || '.log',
},
forwardTargets,
});
}
// S3 sources from env
const s3Sources = new Map();
function loadS3SourcesFromEnv() {
// Format: S3_SOURCE_<name>=bucket:prefix
// S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_REGION (global)
// Or per-source: S3_SOURCE_<name>_ACCESS_KEY, S3_SOURCE_<name>_SECRET_KEY, etc.
const globalAccessKey = process.env.S3_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID || '';
const globalSecretKey = process.env.S3_SECRET_ACCESS_KEY || process.env.AWS_SECRET_ACCESS_KEY || '';
const globalRegion = process.env.S3_REGION || process.env.AWS_REGION || 'us-east-1';
const globalEndpoint = process.env.S3_ENDPOINT || '';
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith('S3_SOURCE_') && value && !key.includes('_ACCESS_KEY') && !key.includes('_SECRET_KEY') && !key.includes('_REGION') && !key.includes('_ENDPOINT')) {
const name = key.replace('S3_SOURCE_', '').toLowerCase();
const parts = value.split(':');
const bucket = parts[0];
const prefix = parts.slice(1).join(':') || '';
const accessKey = process.env[`S3_SOURCE_${name.toUpperCase()}_ACCESS_KEY`] || globalAccessKey;
const secretKey = process.env[`S3_SOURCE_${name.toUpperCase()}_SECRET_KEY`] || globalSecretKey;
const region = process.env[`S3_SOURCE_${name.toUpperCase()}_REGION`] || globalRegion;
const endpoint = process.env[`S3_SOURCE_${name.toUpperCase()}_ENDPOINT`] || globalEndpoint;
if (accessKey && secretKey && bucket) {
const config = { accessKeyId: accessKey, secretAccessKey: secretKey, region, bucket, prefix: prefix || undefined, endpoint: endpoint || undefined };
s3Sources.set(name, new s3Source_1.S3Source(config));
}
}
}
}
loadS3SourcesFromEnv();
// Monitor setup
let monitor = null;
if (MODE === 'monitor' || MODE === 'both' || MODE === 'all') {
let monitorRules = [];
try {
monitorRules = JSON.parse(process.env.MONITOR_RULES || '[]');
}
catch { }
monitor = new monitor_1.LogMonitor({
enabled: process.env.MONITOR_ENABLED !== 'false',
scanIntervalMs: parseInt(process.env.MONITOR_SCAN_INTERVAL || '60000', 10),
rules: monitorRules,
storageDir: process.env.MONITOR_STORAGE_DIR || path.join(process.cwd(), 'monitor-data'),
});
}
// Remote collector URL (for viewer mode connecting to a remote collector)
const REMOTE_COLLECTOR_URL = process.env.REMOTE_COLLECTOR_URL || '';
const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
};
function sendJson(res, data, status = 200) {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(data));
}
function sendError(res, message, status = 500) {
sendJson(res, { error: message }, status);
}
async function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
req.on('error', reject);
});
}
async function fetchRemote(remotePath) {
if (!REMOTE_COLLECTOR_URL)
throw new Error('No remote collector configured');
const fullUrl = REMOTE_COLLECTOR_URL.replace(/\/$/, '') + remotePath;
return new Promise((resolve, reject) => {
const transport = fullUrl.startsWith('https') ? require('https') : require('http');
transport.get(fullUrl, (res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')));
}
catch {
resolve(Buffer.concat(chunks).toString('utf-8'));
}
});
}).on('error', reject);
});
}
const server = http.createServer(async (req, res) => {
const parsedUrl = url.parse(req.url || '/', true);
const pathname = parsedUrl.pathname || '/';
const method = req.method || 'GET';
// CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Stream-Name');
if (method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// ---- Collector endpoints ----
if (collector && (pathname.startsWith('/collect/') || pathname.startsWith('/collector/'))) {
const handled = collector.handleRequest(req, res, pathname);
if (handled)
return;
}
// ---- Monitor endpoints ----
if (monitor && pathname.startsWith('/api/monitor/')) {
const handled = monitor.handleRequest(req, res, pathname);
if (handled)
return;
}
// ---- API Routes ----
if (pathname.startsWith('/api/')) {
try {
// GET /api/health
if (pathname === '/api/health' && method === 'GET') {
return sendJson(res, {
status: 'ok',
mode: MODE,
uptime: process.uptime(),
collector: collector ? 'active' : 'off',
monitor: monitor ? 'active' : 'off',
sources: logSourceManager.getSources().length,
s3Sources: s3Sources.size,
});
}
// GET /api/mode
if (pathname === '/api/mode' && method === 'GET') {
return sendJson(res, { mode: MODE, hasCollector: !!collector, hasRemoteCollector: !!REMOTE_COLLECTOR_URL, s3Sources: Array.from(s3Sources.keys()) });
}
// GET /api/config
if (pathname === '/api/config' && method === 'GET') {
return sendJson(res, { ...configManager.getConfig(), mode: MODE });
}
// GET /api/sources
if (pathname === '/api/sources' && method === 'GET') {
return sendJson(res, logSourceManager.getSources());
}
// POST /api/sources { path: "..." }
if (pathname === '/api/sources' && method === 'POST') {
const body = JSON.parse(await readBody(req));
logSourceManager.addSource(body.path);
return sendJson(res, logSourceManager.getSources());
}
// DELETE /api/sources?path=...
if (pathname === '/api/sources' && method === 'DELETE') {
const sourcePath = parsedUrl.query.path;
if (!sourcePath)
return sendError(res, 'Missing path parameter', 400);
logSourceManager.removeSource(sourcePath);
return sendJson(res, logSourceManager.getSources());
}
// GET /api/logs?source=...&level=...&offset=...
if (pathname === '/api/logs' && method === 'GET') {
const source = parsedUrl.query.source;
const levelFilter = parsedUrl.query.level;
const offsetParam = parsedUrl.query.offset;
const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
let entries;
if (source && source !== 'ALL') {
entries = await logSourceManager.readLog(source);
}
else {
entries = await logSourceManager.readAllLogs();
}
// Apply level filter
if (levelFilter && levelFilter !== 'ALL') {
const filterUpper = levelFilter.toUpperCase();
entries = entries.filter((e) => e.level && e.level.toUpperCase() === filterUpper);
}
// Apply offset
if (offset > 0 && offset < entries.length) {
entries = entries.slice(offset);
}
return sendJson(res, entries);
}
// ---- S3 endpoints ----
// POST /api/s3/add { name, bucket, prefix, accessKeyId, secretAccessKey, region, endpoint? }
if (pathname === '/api/s3/add' && method === 'POST') {
const body = JSON.parse(await readBody(req));
const { name, bucket, prefix, accessKeyId, secretAccessKey, region, endpoint } = body;
if (!name || !bucket || !accessKeyId || !secretAccessKey || !region) {
return sendError(res, 'Missing required fields: name, bucket, accessKeyId, secretAccessKey, region', 400);
}
const config = { accessKeyId, secretAccessKey, region, bucket, prefix, endpoint };
s3Sources.set(name, new s3Source_1.S3Source(config));
return sendJson(res, { ok: true, name });
}
// GET /api/s3/sources
if (pathname === '/api/s3/sources' && method === 'GET') {
return sendJson(res, Array.from(s3Sources.keys()));
}
// GET /api/s3/list?name=...&prefix=...
if (pathname === '/api/s3/list' && method === 'GET') {
const name = parsedUrl.query.name;
const prefix = parsedUrl.query.prefix;
const s3 = s3Sources.get(name);
if (!s3)
return sendError(res, `S3 source not found: ${name}`, 404);
try {
const objects = await s3.listObjects(prefix);
return sendJson(res, objects);
}
catch (err) {
console.warn(`[S3] Connection error for source "${name}":`, err.message);
return sendError(res, `S3 connection failed for "${name}": ${err.message}`, 502);
}
}
// GET /api/s3/read?name=...&key=...
if (pathname === '/api/s3/read' && method === 'GET') {
const name = parsedUrl.query.name;
const key = parsedUrl.query.key;
const s3 = s3Sources.get(name);
if (!s3)
return sendError(res, `S3 source not found: ${name}`, 404);
if (!key)
return sendError(res, 'Missing key parameter', 400);
try {
const content = await s3.getObject(key);
const lines = content.split('\n').filter(l => l.trim() !== '');
const entries = lines.map((line, i) => ({
line,
lineNumber: i + 1,
source: `s3://${name}/${key}`,
timestamp: extractTimestamp(line),
level: extractLevel(line),
}));
return sendJson(res, entries);
}
catch (err) {
console.warn(`[S3] Read error for "${name}/${key}":`, err.message);
return sendError(res, `S3 read failed: ${err.message}`, 502);
}
}
// DELETE /api/s3/remove?name=...
if (pathname === '/api/s3/remove' && method === 'DELETE') {
const name = parsedUrl.query.name;
if (!name)
return sendError(res, 'Missing name parameter', 400);
s3Sources.delete(name);
return sendJson(res, { ok: true });
}
// ---- Remote Collector endpoints (viewer reads from remote collector) ----
// GET /api/remote/streams
if (pathname === '/api/remote/streams' && method === 'GET') {
if (!REMOTE_COLLECTOR_URL)
return sendJson(res, []);
try {
const streams = await fetchRemote('/collector/streams');
return sendJson(res, streams);
}
catch (err) {
return sendError(res, `Cannot reach remote collector: ${err.message}`);
}
}
// GET /api/remote/stream/:id?tail=N
const remoteStreamMatch = pathname.match(/^\/api\/remote\/stream\/([a-zA-Z0-9_\-\.]+)$/);
if (remoteStreamMatch && method === 'GET') {
const streamId = remoteStreamMatch[1];
const tail = parsedUrl.query.tail;
const tailParam = tail ? `?tail=${tail}` : '';
try {
const data = await fetchRemote(`/collector/stream/${streamId}${tailParam}`);
// Convert to LogEntry format
const lines = data.lines || [];
const entries = lines.map((line, i) => ({
line,
lineNumber: i + 1,
source: `collector://${streamId}`,
timestamp: extractTimestamp(line),
level: extractLevel(line),
}));
return sendJson(res, entries);
}
catch (err) {
return sendError(res, err.message);
}
}
// ---- Collector streams as log source (local) ----
// GET /api/collector/streams
if (pathname === '/api/collector/streams' && method === 'GET') {
if (!collector)
return sendJson(res, []);
return sendJson(res, collector.getStreams());
}
// GET /api/collector/stream/:id
const localStreamMatch = pathname.match(/^\/api\/collector\/stream\/([a-zA-Z0-9_\-\.]+)$/);
if (localStreamMatch && method === 'GET') {
const streamId = localStreamMatch[1];
if (!collector)
return sendError(res, 'Collector not enabled', 400);
try {
const tail = parsedUrl.query.tail;
const lines = collector.readStream(streamId, tail ? parseInt(tail, 10) : undefined);
const entries = lines.map((line, i) => ({
line,
lineNumber: i + 1,
source: `stream://${streamId}`,
timestamp: extractTimestamp(line),
level: extractLevel(line),
}));
return sendJson(res, entries);
}
catch (err) {
return sendError(res, err.message, 404);
}
}
// POST /api/config/save - Save config to file
if (pathname === '/api/config/save' && method === 'POST') {
const body = JSON.parse(await readBody(req));
(0, configFile_1.saveConfigFile)(body);
return sendJson(res, { ok: true });
}
return sendError(res, 'Not found', 404);
}
catch (err) {
console.error('API error:', err);
return sendError(res, err.message);
}
}
// ---- Static Files ----
const webDir = path.join(__dirname, '..', 'src', 'web');
let filePath;
// Serve collector UI when in collector mode, monitor UI when in monitor mode
if (pathname === '/' || pathname === '/index.html') {
if (MODE === 'collector') {
filePath = path.join(webDir, 'collector.html');
}
else if (MODE === 'monitor') {
filePath = path.join(webDir, 'monitor.html');
}
else {
filePath = path.join(webDir, 'index.html');
}
}
else {
filePath = path.join(webDir, pathname);
}
const webRoot = path.resolve(webDir);
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(webRoot)) {
res.writeHead(403);
res.end('Forbidden');
return;
}
try {
if (!fs.existsSync(resolvedPath) || fs.statSync(resolvedPath).isDirectory()) {
res.writeHead(404);
res.end('Not found');
return;
}
const ext = path.extname(resolvedPath);
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': contentType });
res.end(fs.readFileSync(resolvedPath));
}
catch {
res.writeHead(500);
res.end('Internal server error');
}
});
// Helper functions
function extractTimestamp(line) {
const patterns = [
/(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)/,
/^([A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})/,
/\[(\d{2}\/[A-Z][a-z]{2}\/\d{4}:\d{2}:\d{2}:\d{2}\s[+-]\d{4})\]/,
];
for (const p of patterns) {
const m = line.match(p);
if (m)
return m[1];
}
return null;
}
function extractLevel(line) {
const upper = line.toUpperCase();
for (const level of ['FATAL', 'ERROR', 'WARN', 'WARNING', 'INFO', 'DEBUG', 'TRACE']) {
if (new RegExp(`\\b${level}\\b`).test(upper))
return level === 'WARNING' ? 'WARN' : level;
}
return null;
}
server.listen(PORT, HOST, () => {
console.log('');
console.log(' ╔══════════════════════════════════════════════════╗');
console.log(` ║ 📋 Log Viewer - Mode: ${MODE.toUpperCase().padEnd(24)}`);
console.log(' ╠══════════════════════════════════════════════════╣');
console.log(` ║ URL: http://${HOST}:${PORT}`);
console.log(` ║ Sources: ${String(logSourceManager.getSources().length).padEnd(3)} file(s) ║`);
console.log(` ║ S3 Sources: ${String(s3Sources.size).padEnd(3)} configured ║`);
console.log(` ║ Collector: ${collector ? 'ACTIVE' : 'OFF'}`);
console.log(` ║ Monitor: ${monitor ? 'ACTIVE' : 'OFF'}`);
if (REMOTE_COLLECTOR_URL) {
console.log(` ║ Remote: ${REMOTE_COLLECTOR_URL.substring(0, 30).padEnd(30)}`);
}
console.log(' ╚══════════════════════════════════════════════════╝');
console.log('');
if (collector) {
console.log(' Collector endpoints:');
console.log(` POST http://${HOST}:${PORT}/collect/<stream-id> (plain text, one line per line)`);
console.log(` POST http://${HOST}:${PORT}/collect/<stream-id>/json (JSON array of entries)`);
console.log('');
}
});
//# sourceMappingURL=server.js.map