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

497
dist-server/collector.js Normal file
View File

@@ -0,0 +1,497 @@
"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 });
exports.LogCollector = void 0;
const dgram = __importStar(require("dgram"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const zlib = __importStar(require("zlib"));
class LogCollector {
config;
streams = new Map();
buffers = new Map();
fileSizes = new Map();
fileCreatedAt = new Map();
rotationTimer = null;
udpServer = null;
forwardBuffers = new Map();
forwardTimers = new Map();
constructor(config = {}) {
this.config = {
storageDir: config.storageDir || path.join(process.cwd(), 'collector-data'),
maxLinesPerStream: config.maxLinesPerStream || 100000,
maxStreams: config.maxStreams || 50,
udpPort: config.udpPort || 5140,
udpEnabled: config.udpEnabled !== false,
rotation: config.rotation || {
maxSizeBytes: 5 * 1024 * 1024, // 5MB default
maxAgeMs: 10 * 60 * 1000, // 10 min default
compression: 'gzip',
fileExtension: '.log',
},
forwardTargets: config.forwardTargets || [],
persistToDisk: config.persistToDisk !== false,
};
if (!fs.existsSync(this.config.storageDir)) {
fs.mkdirSync(this.config.storageDir, { recursive: true });
}
this.loadExistingStreams();
// Start rotation check timer
if (this.config.rotation.maxAgeMs > 0) {
this.rotationTimer = setInterval(() => this.checkTimeRotation(), Math.min(this.config.rotation.maxAgeMs / 2, 30000));
}
// Start UDP server
if (this.config.udpEnabled) {
this.startUdpServer();
}
}
startUdpServer() {
try {
this.udpServer = dgram.createSocket('udp4');
this.udpServer.on('message', (msg, rinfo) => {
const message = msg.toString('utf-8').trim();
if (!message)
return;
// Parse stream ID from message or use sender IP as stream
// Format: <streamId>|<message> or just <message> (uses IP:port as stream)
let streamId;
let logLine;
const pipeIdx = message.indexOf('|');
if (pipeIdx > 0 && pipeIdx < 64 && !message.substring(0, pipeIdx).includes(' ')) {
streamId = message.substring(0, pipeIdx);
logLine = message.substring(pipeIdx + 1);
}
else {
streamId = `udp-${rinfo.address}`;
logLine = message;
}
this.ingest(streamId, [logLine], `UDP ${rinfo.address}:${rinfo.port}`);
});
this.udpServer.on('error', (err) => {
console.error(`UDP server error: ${err.message}`);
});
this.udpServer.bind(this.config.udpPort, '0.0.0.0', () => {
console.log(` UDP collector listening on port ${this.config.udpPort}`);
});
}
catch (err) {
console.error('Failed to start UDP server:', err.message);
}
}
loadExistingStreams() {
try {
const metaFile = path.join(this.config.storageDir, '_streams.json');
if (fs.existsSync(metaFile)) {
const data = JSON.parse(fs.readFileSync(metaFile, 'utf-8'));
for (const stream of data) {
this.streams.set(stream.id, stream);
const streamFile = this.getStreamFilePath(stream.id);
if (fs.existsSync(streamFile)) {
const content = fs.readFileSync(streamFile, 'utf-8');
const lines = content.split('\n').filter(l => l.length > 0);
this.buffers.set(stream.id, lines);
const stat = fs.statSync(streamFile);
this.fileSizes.set(stream.id, stat.size);
this.fileCreatedAt.set(stream.id, stat.birthtimeMs || Date.now());
}
else {
this.buffers.set(stream.id, []);
this.fileSizes.set(stream.id, 0);
this.fileCreatedAt.set(stream.id, Date.now());
}
}
}
}
catch (err) {
console.error('Error loading collector streams:', err);
}
}
saveMetadata() {
const metaFile = path.join(this.config.storageDir, '_streams.json');
fs.writeFileSync(metaFile, JSON.stringify(Array.from(this.streams.values()), null, 2));
}
getStreamFilePath(streamId) {
return path.join(this.config.storageDir, `${streamId}${this.config.rotation.fileExtension}`);
}
getRotatedFilePath(streamId) {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const ext = this.config.rotation.fileExtension;
const compExt = this.getCompressionExtension();
return path.join(this.config.storageDir, `${streamId}_${ts}${ext}${compExt}`);
}
getCompressionExtension() {
switch (this.config.rotation.compression) {
case 'gzip': return '.gz';
case 'bzip2': return '.bz2';
case 'xz': return '.xz';
case 'zstd': return '.zst';
default: return '';
}
}
saveStream(streamId) {
if (!this.config.persistToDisk)
return;
const lines = this.buffers.get(streamId) || [];
const streamFile = this.getStreamFilePath(streamId);
const content = lines.join('\n') + (lines.length > 0 ? '\n' : '');
fs.writeFileSync(streamFile, content);
this.fileSizes.set(streamId, Buffer.byteLength(content, 'utf-8'));
}
checkSizeRotation(streamId) {
if (this.config.rotation.maxSizeBytes <= 0)
return;
const size = this.fileSizes.get(streamId) || 0;
if (size >= this.config.rotation.maxSizeBytes) {
this.rotateStream(streamId);
}
}
checkTimeRotation() {
if (this.config.rotation.maxAgeMs <= 0)
return;
const now = Date.now();
for (const [streamId, createdAt] of this.fileCreatedAt) {
if (now - createdAt >= this.config.rotation.maxAgeMs) {
const buffer = this.buffers.get(streamId);
if (buffer && buffer.length > 0) {
this.rotateStream(streamId);
}
}
}
}
rotateStream(streamId) {
const srcFile = this.getStreamFilePath(streamId);
if (!fs.existsSync(srcFile))
return;
const content = fs.readFileSync(srcFile);
if (content.length === 0)
return;
const destFile = this.getRotatedFilePath(streamId);
// Compress and write
let compressed;
switch (this.config.rotation.compression) {
case 'gzip':
compressed = zlib.gzipSync(content);
break;
case 'bzip2':
case 'xz':
case 'zstd':
// For these, just copy without compression (would need external tools)
compressed = content;
break;
default:
compressed = content;
}
fs.writeFileSync(destFile, compressed);
console.log(` Rotated: ${streamId} -> ${path.basename(destFile)} (${compressed.length} bytes)`);
// Reset current file
fs.writeFileSync(srcFile, '');
this.buffers.set(streamId, []);
this.fileSizes.set(streamId, 0);
this.fileCreatedAt.set(streamId, Date.now());
const stream = this.streams.get(streamId);
if (stream) {
stream.lineCount = 0;
stream.currentFileSize = 0;
}
this.saveMetadata();
}
// ---- Forwarding ----
forwardLines(streamId, lines) {
for (const target of this.config.forwardTargets) {
const key = `${streamId}:${target.type}:${target.url || target.path || target.host}`;
if (!this.forwardBuffers.has(key))
this.forwardBuffers.set(key, []);
const buf = this.forwardBuffers.get(key);
buf.push(...lines);
const batchSize = target.batchSize || 100;
if (buf.length >= batchSize) {
this.flushForward(key, target, streamId);
}
else if (!this.forwardTimers.has(key)) {
const flushMs = target.flushIntervalMs || 5000;
this.forwardTimers.set(key, setTimeout(() => {
this.flushForward(key, target, streamId);
this.forwardTimers.delete(key);
}, flushMs));
}
}
}
flushForward(key, target, streamId) {
const lines = this.forwardBuffers.get(key) || [];
if (lines.length === 0)
return;
this.forwardBuffers.set(key, []);
switch (target.type) {
case 'http':
this.forwardHttp(target, streamId, lines);
break;
case 'file':
this.forwardFile(target, lines);
break;
case 'udp':
this.forwardUdp(target, streamId, lines);
break;
case 'stream':
this.forwardToStream(target, streamId, lines);
break;
case 'console':
for (const l of lines)
console.log(`[FWD:${streamId}] ${l}`);
break;
}
}
forwardHttp(target, streamId, lines) {
if (!target.url)
return;
const payload = target.format === 'json'
? JSON.stringify({ stream: streamId, lines, timestamp: new Date().toISOString() })
: lines.join('\n');
try {
const url = new URL(target.url);
const transport = url.protocol === 'https:' ? require('https') : require('http');
const req = transport.request({
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname,
method: 'POST',
headers: { 'Content-Type': target.format === 'json' ? 'application/json' : 'text/plain', 'X-Stream-Id': streamId },
});
req.on('error', (e) => console.error(`Forward HTTP error: ${e.message}`));
req.write(payload);
req.end();
}
catch (e) {
console.error(`Forward HTTP error: ${e.message}`);
}
}
forwardFile(target, lines) {
if (!target.path)
return;
try {
fs.appendFileSync(target.path, lines.join('\n') + '\n');
}
catch (e) {
console.error(`Forward file error: ${e.message}`);
}
}
forwardUdp(target, streamId, lines) {
if (!target.host || !target.port)
return;
try {
const client = dgram.createSocket('udp4');
for (const line of lines) {
const msg = Buffer.from(`${streamId}|${line}`);
client.send(msg, target.port, target.host);
}
setTimeout(() => client.close(), 1000);
}
catch (e) {
console.error(`Forward UDP error: ${e.message}`);
}
}
forwardToStream(target, sourceStreamId, lines) {
const targetId = target.targetStream;
if (!targetId || targetId === sourceStreamId)
return; // Prevent infinite loop
try {
// Prefix lines with source stream info
const prefixed = lines.map(l => `[fwd:${sourceStreamId}] ${l}`);
// Ingest directly into the target stream without triggering forwards again
const stream = this.getOrCreateStream(targetId, `Forwarded from ${sourceStreamId}`);
const buffer = this.buffers.get(targetId);
for (const line of prefixed) {
if (buffer.length >= this.config.maxLinesPerStream)
buffer.shift();
buffer.push(line);
}
stream.lineCount = buffer.length;
stream.lastReceived = new Date().toISOString();
if (this.config.persistToDisk)
this.saveStream(targetId);
this.saveMetadata();
}
catch (e) {
console.error(`Forward stream error: ${e.message}`);
}
}
// ---- Public API ----
getOrCreateStream(streamId, name) {
if (this.streams.has(streamId))
return this.streams.get(streamId);
if (this.streams.size >= this.config.maxStreams)
throw new Error(`Max streams (${this.config.maxStreams}) reached`);
const stream = {
id: streamId, name: name || streamId, createdAt: new Date().toISOString(),
lineCount: 0, lastReceived: null, currentFileSize: 0,
};
this.streams.set(streamId, stream);
this.buffers.set(streamId, []);
this.fileSizes.set(streamId, 0);
this.fileCreatedAt.set(streamId, Date.now());
this.saveMetadata();
return stream;
}
ingest(streamId, lines, streamName) {
const stream = this.getOrCreateStream(streamId, streamName);
const buffer = this.buffers.get(streamId);
let accepted = 0, dropped = 0;
const validLines = [];
for (const line of lines) {
if (line.trim() === '')
continue;
if (buffer.length >= this.config.maxLinesPerStream) {
buffer.shift();
dropped++;
}
buffer.push(line);
validLines.push(line);
accepted++;
}
stream.lineCount = buffer.length;
stream.lastReceived = new Date().toISOString();
if (this.config.persistToDisk) {
this.saveStream(streamId);
stream.currentFileSize = this.fileSizes.get(streamId) || 0;
this.checkSizeRotation(streamId);
}
// Forward to targets
if (validLines.length > 0 && this.config.forwardTargets.length > 0) {
this.forwardLines(streamId, validLines);
}
this.saveMetadata();
return { accepted, dropped };
}
getStreams() { return Array.from(this.streams.values()); }
readStream(streamId, tail) {
const buffer = this.buffers.get(streamId);
if (!buffer)
throw new Error(`Stream not found: ${streamId}`);
return tail && tail > 0 ? buffer.slice(-tail) : [...buffer];
}
deleteStream(streamId) {
if (!this.streams.has(streamId))
return false;
this.streams.delete(streamId);
this.buffers.delete(streamId);
this.fileSizes.delete(streamId);
this.fileCreatedAt.delete(streamId);
const f = this.getStreamFilePath(streamId);
if (fs.existsSync(f))
fs.unlinkSync(f);
this.saveMetadata();
return true;
}
getConfig() { return { ...this.config }; }
shutdown() {
if (this.rotationTimer)
clearInterval(this.rotationTimer);
if (this.udpServer)
this.udpServer.close();
for (const timer of this.forwardTimers.values())
clearTimeout(timer);
}
handleRequest(req, res, pathname) {
const method = req.method || 'GET';
// POST /collect/:streamId
const collectMatch = pathname.match(/^\/collect\/([a-zA-Z0-9_\-\.]+)(\/json)?$/);
if (collectMatch && method === 'POST') {
const streamId = collectMatch[1];
const isJson = !!collectMatch[2];
let body = '';
req.on('data', (chunk) => { body += chunk.toString(); });
req.on('end', () => {
try {
let lines;
if (isJson) {
const parsed = JSON.parse(body);
lines = Array.isArray(parsed) ? parsed.map((e) => typeof e === 'string' ? e : JSON.stringify(e)) : [JSON.stringify(parsed)];
}
else {
lines = body.split('\n');
}
const streamName = req.headers['x-stream-name'];
const result = this.ingest(streamId, lines, streamName);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, ...result }));
}
catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
}
});
return true;
}
// GET /collector/streams
if (pathname === '/collector/streams' && method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(this.getStreams()));
return true;
}
// GET /collector/config
if (pathname === '/collector/config' && method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(this.getConfig()));
return true;
}
// GET /collector/stream/:id
const readMatch = pathname.match(/^\/collector\/stream\/([a-zA-Z0-9_\-\.]+)$/);
if (readMatch && method === 'GET') {
const streamId = readMatch[1];
try {
const url = new URL(req.url || '/', `http://${req.headers.host}`);
const tail = url.searchParams.get('tail');
const lines = this.readStream(streamId, tail ? parseInt(tail, 10) : undefined);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ streamId, lineCount: lines.length, lines }));
}
catch (err) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.message }));
}
return true;
}
// DELETE /collector/stream/:id
const deleteMatch = pathname.match(/^\/collector\/stream\/([a-zA-Z0-9_\-\.]+)$/);
if (deleteMatch && method === 'DELETE') {
const deleted = this.deleteStream(deleteMatch[1]);
res.writeHead(deleted ? 200 : 404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: deleted }));
return true;
}
return false;
}
}
exports.LogCollector = LogCollector;
//# sourceMappingURL=collector.js.map

File diff suppressed because one or more lines are too long

101
dist-server/configFile.js Normal file
View File

@@ -0,0 +1,101 @@
"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 });
exports.getConfigFilePath = getConfigFilePath;
exports.loadConfigFile = loadConfigFile;
exports.saveConfigFile = saveConfigFile;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const CONFIG_FILE = 'logviewer.config.json';
function getConfigFilePath() {
return path.join(process.cwd(), CONFIG_FILE);
}
function loadConfigFile() {
const defaults = {
mode: 'both',
port: 8080,
host: '0.0.0.0',
viewer: {
sources: [],
theme: 'dark',
refreshInterval: 2000,
tailMode: true,
maxLines: 50000,
defaultLevel: 'ALL',
},
collector: {
enabled: true,
storageDir: './collector-data',
maxLinesPerStream: 100000,
maxStreams: 50,
persistToDisk: true,
udp: { enabled: true, port: 5140 },
rotation: { maxSizeBytes: 5242880, maxAgeMs: 600000, compression: 'gzip', fileExtension: '.log' },
forwardTargets: [],
},
s3: { sources: {} },
remoteCollector: { url: '' },
};
const filePath = getConfigFilePath();
if (fs.existsSync(filePath)) {
try {
const fileContent = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
return deepMerge(defaults, fileContent);
}
catch (err) {
console.error(`Error reading ${CONFIG_FILE}:`, err.message);
}
}
return defaults;
}
function saveConfigFile(config) {
const filePath = getConfigFilePath();
const existing = loadConfigFile();
const merged = deepMerge(existing, config);
fs.writeFileSync(filePath, JSON.stringify(merged, null, 2));
}
function deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = deepMerge(target[key] || {}, source[key]);
}
else {
result[key] = source[key];
}
}
return result;
}
//# sourceMappingURL=configFile.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"configFile.js","sourceRoot":"","sources":["../src/server/configFile.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,97 @@
"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 });
exports.ConfigManager = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class ConfigManager {
config;
constructor() {
this.config = this.loadConfig();
}
loadConfig() {
this.loadEnvFile();
const sourcesRaw = process.env.LOG_SOURCES || '';
const sources = sourcesRaw
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0);
return {
sources,
refreshInterval: parseInt(process.env.LOG_REFRESH_INTERVAL || '2000', 10),
maxLines: parseInt(process.env.LOG_MAX_LINES || '50000', 10),
theme: process.env.LOG_THEME || 'dark',
defaultLevel: process.env.LOG_DEFAULT_LEVEL || 'ALL',
tailMode: process.env.LOG_TAIL_MODE !== 'false',
timestampRegex: process.env.LOG_TIMESTAMP_REGEX || null,
};
}
loadEnvFile() {
const envPaths = [
path.join(process.cwd(), '.env'),
path.join(__dirname, '..', '..', '.env'),
];
for (const envPath of envPaths) {
if (fs.existsSync(envPath)) {
const content = fs.readFileSync(envPath, 'utf-8');
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
const key = trimmed.substring(0, eqIndex).trim();
const value = trimmed.substring(eqIndex + 1).trim();
if (!process.env[key]) {
process.env[key] = value;
}
}
}
}
break;
}
}
}
getConfig() {
return { ...this.config };
}
updateSources(sources) {
this.config.sources = sources;
}
getSources() {
return [...this.config.sources];
}
}
exports.ConfigManager = ConfigManager;
//# sourceMappingURL=configManager.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"configManager.js","sourceRoot":"","sources":["../src/server/configManager.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,280 @@
"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 });
exports.LogSourceManager = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const zlib = __importStar(require("zlib"));
const child_process_1 = require("child_process");
class LogSourceManager {
configManager;
sources = new Map();
constructor(configManager) {
this.configManager = configManager;
this.initializeSources();
}
initializeSources() {
const configSources = this.configManager.getSources();
for (const source of configSources) {
this.addSource(source);
}
}
addSource(sourcePath) {
if (sourcePath.includes('*')) {
const resolvedPaths = this.resolveGlob(sourcePath);
for (const rp of resolvedPaths) {
this.addSingleSource(rp);
}
}
else {
this.addSingleSource(sourcePath);
}
this.configManager.updateSources(Array.from(this.sources.keys()));
}
addSingleSource(sourcePath) {
const resolved = path.resolve(sourcePath);
const type = this.detectType(resolved);
let size = 0;
let lastModified = null;
let exists = false;
try {
const stats = fs.statSync(resolved);
size = stats.size;
lastModified = stats.mtime.toISOString();
exists = true;
}
catch {
// File doesn't exist yet
}
this.sources.set(resolved, {
path: resolved,
displayName: path.basename(resolved),
type,
size,
lastModified,
exists,
});
}
removeSource(sourcePath) {
const resolved = path.resolve(sourcePath);
this.sources.delete(resolved);
this.configManager.updateSources(Array.from(this.sources.keys()));
}
getSources() {
for (const [key, source] of this.sources) {
try {
const stats = fs.statSync(key);
source.size = stats.size;
source.lastModified = stats.mtime.toISOString();
source.exists = true;
}
catch {
source.exists = false;
}
}
return Array.from(this.sources.values());
}
detectType(filePath) {
const ext = path.extname(filePath).toLowerCase();
switch (ext) {
case '.gz':
case '.gzip': return 'gzip';
case '.bz2':
case '.bzip2': return 'bzip2';
case '.xz': return 'xz';
case '.zst':
case '.zstd': return 'zstd';
default: return 'text';
}
}
async readLog(sourcePath) {
const resolved = path.resolve(sourcePath);
const source = this.sources.get(resolved);
if (!source)
throw new Error(`Source not found: ${sourcePath}`);
const config = this.configManager.getConfig();
const content = this.readFileContent(resolved, source.type);
const lines = content.split('\n');
const maxLines = config.maxLines;
const startIndex = lines.length > maxLines ? lines.length - maxLines : 0;
const entries = [];
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '')
continue;
entries.push({
line,
lineNumber: i + 1,
source: resolved,
timestamp: this.extractTimestamp(line, config.timestampRegex),
level: this.extractLevel(line),
});
}
return entries;
}
async readAllLogs() {
const allEntries = [];
for (const [sourcePath] of this.sources) {
try {
const entries = await this.readLog(sourcePath);
allEntries.push(...entries);
}
catch (err) {
allEntries.push({
line: `[ERROR] Could not read ${sourcePath}: ${err.message}`,
lineNumber: 0,
source: sourcePath,
timestamp: new Date().toISOString(),
level: 'ERROR',
});
}
}
allEntries.sort((a, b) => {
if (a.timestamp && b.timestamp)
return a.timestamp.localeCompare(b.timestamp);
return 0;
});
return allEntries;
}
readFileContent(filePath, type) {
switch (type) {
case 'gzip': return this.readGzip(filePath);
case 'bzip2': return this.readBzip2(filePath);
case 'xz': return this.readXz(filePath);
case 'zstd': return this.readZstd(filePath);
default: return fs.readFileSync(filePath, 'utf-8');
}
}
readGzip(filePath) {
const compressed = fs.readFileSync(filePath);
return zlib.gunzipSync(compressed).toString('utf-8');
}
readBzip2(filePath) {
try {
return (0, child_process_1.execSync)(`bzcat "${filePath}"`, { maxBuffer: 100 * 1024 * 1024 }).toString('utf-8');
}
catch {
throw new Error('Cannot decompress bzip2. Make sure bzip2 is installed.');
}
}
readXz(filePath) {
try {
return (0, child_process_1.execSync)(`xzcat "${filePath}"`, { maxBuffer: 100 * 1024 * 1024 }).toString('utf-8');
}
catch {
throw new Error('Cannot decompress xz. Make sure xz-utils is installed.');
}
}
readZstd(filePath) {
try {
return (0, child_process_1.execSync)(`zstdcat "${filePath}"`, { maxBuffer: 100 * 1024 * 1024 }).toString('utf-8');
}
catch {
throw new Error('Cannot decompress zstd. Make sure zstd is installed.');
}
}
extractTimestamp(line, customRegex) {
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})\]/,
/(\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2})/,
];
if (customRegex) {
try {
const match = line.match(new RegExp(customRegex));
if (match)
return match[1] || match[0];
}
catch { /* skip */ }
}
for (const pattern of patterns) {
const match = line.match(pattern);
if (match)
return match[1];
}
return null;
}
extractLevel(line) {
const upper = line.toUpperCase();
const levels = ['FATAL', 'ERROR', 'WARN', 'WARNING', 'INFO', 'DEBUG', 'TRACE', 'VERBOSE'];
for (const level of levels) {
if (new RegExp(`\\b${level}\\b`).test(upper)) {
return level === 'WARNING' ? 'WARN' : level;
}
}
return null;
}
resolveGlob(pattern) {
const dir = path.dirname(pattern);
const filePattern = path.basename(pattern);
const results = [];
try {
const files = fs.readdirSync(dir);
const regex = new RegExp('^' + filePattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
for (const file of files) {
if (regex.test(file)) {
const fullPath = path.join(dir, file);
try {
if (fs.statSync(fullPath).isFile())
results.push(fullPath);
}
catch { /* skip */ }
}
}
}
catch { /* dir doesn't exist */ }
return results;
}
async getFileStats(sourcePath) {
const resolved = path.resolve(sourcePath);
try {
const stats = fs.statSync(resolved);
const source = this.sources.get(resolved);
const content = this.readFileContent(resolved, source?.type || 'text');
return {
size: stats.size,
lastModified: stats.mtime.toISOString(),
lineCount: content.split('\n').length,
type: source?.type || 'text',
};
}
catch {
return null;
}
}
}
exports.LogSourceManager = LogSourceManager;
//# sourceMappingURL=logSourceManager.js.map

File diff suppressed because one or more lines are too long

516
dist-server/monitor.js Normal file
View File

@@ -0,0 +1,516 @@
"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 });
exports.LogMonitor = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const zlib = __importStar(require("zlib"));
const http = __importStar(require("http"));
const https = __importStar(require("https"));
// ---- Monitor Engine ----
class LogMonitor {
config;
scanTimer = null;
results = [];
constructor(config = {}) {
this.config = {
enabled: config.enabled !== false,
scanIntervalMs: config.scanIntervalMs || 60000,
rules: config.rules || [],
storageDir: config.storageDir || path.join(process.cwd(), 'monitor-data'),
};
if (!fs.existsSync(this.config.storageDir)) {
fs.mkdirSync(this.config.storageDir, { recursive: true });
}
this.loadState();
if (this.config.enabled && this.config.rules.length > 0) {
this.startScanning();
}
}
loadState() {
const stateFile = path.join(this.config.storageDir, '_monitor_state.json');
if (fs.existsSync(stateFile)) {
try {
const data = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
this.results = data.results || [];
// Restore lastTriggered from state
if (data.rules) {
for (const saved of data.rules) {
const rule = this.config.rules.find(r => r.id === saved.id);
if (rule) {
rule.lastTriggered = saved.lastTriggered;
rule.matchCount = saved.matchCount || 0;
}
}
}
}
catch { }
}
}
saveState() {
const stateFile = path.join(this.config.storageDir, '_monitor_state.json');
fs.writeFileSync(stateFile, JSON.stringify({
results: this.results.slice(-100),
rules: this.config.rules.map(r => ({ id: r.id, lastTriggered: r.lastTriggered, matchCount: r.matchCount })),
}, null, 2));
}
startScanning() {
if (this.scanTimer)
clearInterval(this.scanTimer);
console.log(` Monitor scanning every ${this.config.scanIntervalMs / 1000}s with ${this.config.rules.length} rule(s)`);
this.scanTimer = setInterval(() => this.runScan(), this.config.scanIntervalMs);
// Run immediately
setTimeout(() => this.runScan(), 2000);
}
stopScanning() {
if (this.scanTimer) {
clearInterval(this.scanTimer);
this.scanTimer = null;
}
}
async runScan() {
for (const rule of this.config.rules) {
if (!rule.enabled)
continue;
// Check schedule
if (rule.schedule && rule.schedule.mode !== 'interval') {
if (!this.shouldRunBySchedule(rule))
continue;
}
// Check cooldown
if (rule.lastTriggered) {
const elapsed = Date.now() - new Date(rule.lastTriggered).getTime();
if (elapsed < rule.cooldownMs)
continue;
}
try {
const matches = this.scanRule(rule);
if (matches.length > 0) {
const result = {
ruleId: rule.id,
ruleName: rule.name,
matchedLines: matches,
totalMatches: matches.length,
sources: rule.sources,
timestamp: new Date().toISOString(),
};
rule.lastTriggered = result.timestamp;
rule.matchCount += matches.length;
this.results.push(result);
// Send notifications
await this.notify(rule, result);
this.saveState();
}
}
catch (err) {
console.error(` Monitor rule "${rule.name}" error:`, err.message);
}
}
}
shouldRunBySchedule(rule) {
const schedule = rule.schedule;
if (!schedule)
return true;
const now = new Date();
const lastRun = rule.lastTriggered ? new Date(rule.lastTriggered) : null;
switch (schedule.mode) {
case 'daily': {
// Run once per day at specified time (HH:MM)
const [h, m] = (schedule.value || '07:00').split(':').map(Number);
const targetToday = new Date(now);
targetToday.setHours(h, m, 0, 0);
// Already ran today?
if (lastRun && lastRun.toDateString() === now.toDateString())
return false;
// Is it past the target time?
return now >= targetToday;
}
case 'hourly': {
// Run every N hours
const hours = parseInt(schedule.value || '2', 10);
if (!lastRun)
return true;
return (now.getTime() - lastRun.getTime()) >= hours * 3600000;
}
case 'cron': {
// Simple cron: just check if enough time passed based on scan interval
// Full cron parsing would need a library, so we approximate
return true;
}
default: return true;
}
}
scanRule(rule) {
const allMatches = [];
const regexes = rule.patterns.map(p => new RegExp(p, 'i'));
for (const source of rule.sources) {
const files = this.resolveFiles(source);
for (const file of files) {
if (!this.isInTimeRange(file, rule.timeRange))
continue;
try {
const content = this.readFile(file);
const lines = content.split('\n');
for (const line of lines) {
if (line.trim() === '')
continue;
const matched = rule.patternLogic === 'all'
? regexes.every(r => r.test(line))
: regexes.some(r => r.test(line));
if (matched)
allMatches.push(line);
}
}
catch { }
}
}
return allMatches;
}
resolveFiles(pattern) {
if (pattern.includes('*')) {
const dir = path.dirname(pattern);
const filePattern = path.basename(pattern);
try {
const files = fs.readdirSync(dir);
const regex = new RegExp('^' + filePattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
return files.filter(f => regex.test(f)).map(f => path.join(dir, f));
}
catch {
return [];
}
}
return fs.existsSync(pattern) ? [pattern] : [];
}
isInTimeRange(file, range) {
if (range.type === 'all')
return true;
try {
const stat = fs.statSync(file);
const fileTime = stat.mtime.getTime();
const now = Date.now();
switch (range.type) {
case 'today': {
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
return fileTime >= startOfDay.getTime();
}
case 'week': return fileTime >= now - 7 * 24 * 60 * 60 * 1000;
case 'month': return fileTime >= now - 30 * 24 * 60 * 60 * 1000;
case 'hours': return fileTime >= now - (range.hours || 24) * 60 * 60 * 1000;
default: return true;
}
}
catch {
return false;
}
}
readFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
if (ext === '.gz' || ext === '.gzip') {
return zlib.gunzipSync(fs.readFileSync(filePath)).toString('utf-8');
}
return fs.readFileSync(filePath, 'utf-8');
}
// ---- Notifications ----
async notify(rule, result) {
const summary = this.buildSummary(rule, result);
const attachment = result.matchedLines.length > rule.maxLinesInMessage
? this.buildAttachment(result)
: null;
for (const channel of rule.notifications) {
try {
switch (channel.type) {
case 'webhook':
await this.sendWebhook(channel, summary, attachment);
break;
case 'telegram':
await this.sendTelegram(channel, summary, attachment);
break;
case 'teams':
await this.sendTeams(channel, summary);
break;
case 'email':
console.log(` [Monitor] Email notification not yet implemented`);
break;
}
}
catch (err) {
console.error(` [Monitor] Notification error (${channel.type}):`, err.message);
}
}
}
buildSummary(rule, result) {
const lines = result.matchedLines.slice(0, rule.maxLinesInMessage);
let msg = `🚨 **Log Monitor Alert: ${rule.name}**\n\n`;
msg += `**Matches:** ${result.totalMatches} lines\n`;
msg += `**Sources:** ${result.sources.join(', ')}\n`;
msg += `**Time:** ${result.timestamp}\n`;
msg += `**Patterns:** ${rule.patterns.join(' | ')}\n\n`;
msg += `**Sample (first ${lines.length} of ${result.totalMatches}):**\n`;
msg += '```\n' + lines.join('\n') + '\n```';
if (result.totalMatches > rule.maxLinesInMessage) {
msg += `\n\n📎 Full results (${result.totalMatches} lines) attached as .gz file.`;
}
return msg;
}
buildAttachment(result) {
const content = result.matchedLines.join('\n');
return zlib.gzipSync(Buffer.from(content, 'utf-8'));
}
async sendWebhook(channel, message, attachment) {
if (!channel.url)
return;
const payload = JSON.stringify({
text: message,
matches: attachment ? `(${attachment.length} bytes gzipped attachment)` : undefined,
});
await this.httpPost(channel.url, payload, 'application/json');
console.log(` [Monitor] Webhook sent to ${channel.url}`);
}
async sendTelegram(channel, message, attachment) {
if (!channel.botToken || !channel.chatId)
return;
// Send text message - use plain text to avoid Markdown parsing issues
const payload = JSON.stringify({
chat_id: channel.chatId,
text: message.replace(/[*_`\[]/g, '').substring(0, 4000), // Strip markdown, respect limit
});
await this.httpPost(`https://api.telegram.org/bot${channel.botToken}/sendMessage`, payload, 'application/json');
console.log(` [Monitor] Telegram sent to chat ${channel.chatId}`);
}
async sendTeams(channel, message) {
if (!channel.url)
return;
const payload = JSON.stringify({
'@type': 'MessageCard',
summary: 'Log Monitor Alert',
text: message.replace(/\n/g, '<br>').replace(/```/g, ''),
});
await this.httpPost(channel.url, payload, 'application/json');
console.log(` [Monitor] Teams webhook sent`);
}
httpPost(targetUrl, body, contentType) {
return new Promise((resolve, reject) => {
const parsed = new URL(targetUrl);
const isHttps = parsed.protocol === 'https:';
const transport = isHttps ? https : http;
const req = transport.request({
hostname: parsed.hostname,
port: parsed.port || (isHttps ? 443 : 80),
path: parsed.pathname + parsed.search,
method: 'POST',
headers: { 'Content-Type': contentType, 'Content-Length': Buffer.byteLength(body) },
}, (res) => {
let responseBody = '';
res.on('data', (c) => { responseBody += c; });
res.on('end', () => {
if (res.statusCode && res.statusCode >= 400) {
reject(new Error(`HTTP ${res.statusCode}: ${responseBody.substring(0, 200)}`));
}
else {
resolve();
}
});
});
req.on('error', reject);
req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); });
req.write(body);
req.end();
});
}
// ---- Public API ----
getRules() { return this.config.rules; }
getResults() { return this.results; }
getConfig() { return { ...this.config }; }
addRule(rule) {
this.config.rules.push(rule);
this.saveState();
if (this.config.rules.length === 1 && !this.scanTimer)
this.startScanning();
}
updateRule(id, updates) {
const rule = this.config.rules.find(r => r.id === id);
if (!rule)
return false;
Object.assign(rule, updates);
this.saveState();
return true;
}
deleteRule(id) {
const idx = this.config.rules.findIndex(r => r.id === id);
if (idx === -1)
return false;
this.config.rules.splice(idx, 1);
this.saveState();
return true;
}
triggerRule(id) {
const rule = this.config.rules.find(r => r.id === id);
if (!rule)
return null;
const matches = this.scanRule(rule);
if (matches.length === 0)
return null;
const result = {
ruleId: rule.id, ruleName: rule.name, matchedLines: matches,
totalMatches: matches.length, sources: rule.sources, timestamp: new Date().toISOString(),
};
this.results.push(result);
rule.matchCount += matches.length;
this.saveState();
return result;
}
getAttachment(ruleId) {
const result = [...this.results].reverse().find(r => r.ruleId === ruleId);
if (!result)
return null;
return zlib.gzipSync(Buffer.from(result.matchedLines.join('\n'), 'utf-8'));
}
handleRequest(req, res, pathname) {
const method = req.method || 'GET';
if (pathname === '/api/monitor/rules' && method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(this.getRules()));
return true;
}
if (pathname === '/api/monitor/rules' && method === 'POST') {
let body = '';
req.on('data', c => body += c);
req.on('end', () => {
try {
const rule = JSON.parse(body);
if (!rule.id)
rule.id = 'rule-' + Date.now();
if (!rule.maxLinesInMessage)
rule.maxLinesInMessage = 20;
if (!rule.cooldownMs)
rule.cooldownMs = 300000;
if (!rule.matchCount)
rule.matchCount = 0;
if (!rule.lastTriggered)
rule.lastTriggered = null;
this.addRule(rule);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, rule }));
}
catch (e) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: e.message }));
}
});
return true;
}
// Test notification endpoint
if (pathname === '/api/monitor/test-notification' && method === 'POST') {
let body = '';
req.on('data', c => body += c);
req.on('end', async () => {
try {
const channel = JSON.parse(body);
const testMsg = `🧪 **Test Notification**\n\nThis is a test from LogViewer Monitor.\nTimestamp: ${new Date().toISOString()}\n\nIf you see this, notifications are working! ✅`;
await this.sendTestNotification(channel, testMsg);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
}
catch (e) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: false, error: e.message }));
}
});
return true;
}
const ruleMatch = pathname.match(/^\/api\/monitor\/rule\/([a-zA-Z0-9_\-]+)$/);
if (ruleMatch && method === 'DELETE') {
const ok = this.deleteRule(ruleMatch[1]);
res.writeHead(ok ? 200 : 404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok }));
return true;
}
if (ruleMatch && method === 'PUT') {
let body = '';
req.on('data', c => body += c);
req.on('end', () => {
const updates = JSON.parse(body);
const ok = this.updateRule(ruleMatch[1], updates);
res.writeHead(ok ? 200 : 404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok }));
});
return true;
}
const triggerMatch = pathname.match(/^\/api\/monitor\/trigger\/([a-zA-Z0-9_\-]+)$/);
if (triggerMatch && method === 'POST') {
const result = this.triggerRule(triggerMatch[1]);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result || { matches: 0 }));
return true;
}
if (pathname === '/api/monitor/results' && method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(this.results.slice(-50)));
return true;
}
if (pathname === '/api/monitor/config' && method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(this.getConfig()));
return true;
}
return false;
}
async sendTestNotification(channel, message) {
switch (channel.type) {
case 'webhook':
if (!channel.url)
throw new Error('No webhook URL provided');
await this.sendWebhook(channel, message, null);
break;
case 'telegram':
if (!channel.botToken || !channel.chatId)
throw new Error('Bot token and chat ID required');
await this.sendTelegram(channel, message, null);
break;
case 'teams':
if (!channel.url)
throw new Error('No Teams webhook URL provided');
await this.sendTeams(channel, message);
break;
case 'email':
throw new Error('Email test not yet implemented');
default:
throw new Error(`Unknown channel type: ${channel.type}`);
}
}
}
exports.LogMonitor = LogMonitor;
//# sourceMappingURL=monitor.js.map

File diff suppressed because one or more lines are too long

192
dist-server/s3Source.js Normal file
View File

@@ -0,0 +1,192 @@
"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 });
exports.S3Source = void 0;
const https = __importStar(require("https"));
const http = __importStar(require("http"));
const crypto = __importStar(require("crypto"));
const zlib = __importStar(require("zlib"));
/**
* Minimal S3 client without external dependencies.
* Supports AWS S3 and S3-compatible services (MinIO, DigitalOcean Spaces, etc.)
*/
class S3Source {
config;
host;
useHttps;
constructor(config) {
this.config = config;
if (config.endpoint) {
const url = new URL(config.endpoint);
this.host = url.host;
this.useHttps = url.protocol === 'https:';
}
else {
this.host = `${config.bucket}.s3.${config.region}.amazonaws.com`;
this.useHttps = true;
}
}
/**
* List objects in the bucket with optional prefix
*/
async listObjects(prefix) {
const usedPrefix = prefix || this.config.prefix || '';
const queryParams = usedPrefix ? `list-type=2&prefix=${encodeURIComponent(usedPrefix)}` : 'list-type=2';
const path = this.config.endpoint ? `/${this.config.bucket}/?${queryParams}` : `/?${queryParams}`;
const response = await this.request('GET', path, '');
const objects = [];
// Simple XML parsing for S3 ListObjectsV2 response
const contentRegex = /<Contents>([\s\S]*?)<\/Contents>/g;
let match;
while ((match = contentRegex.exec(response)) !== null) {
const content = match[1];
const key = this.extractXmlValue(content, 'Key');
const size = parseInt(this.extractXmlValue(content, 'Size') || '0', 10);
const lastModified = this.extractXmlValue(content, 'LastModified') || '';
if (key && !key.endsWith('/')) {
objects.push({ key, size, lastModified });
}
}
return objects;
}
/**
* Download an object's content as string
*/
async getObject(key) {
const path = this.config.endpoint
? `/${this.config.bucket}/${encodeURIComponent(key)}`
: `/${encodeURIComponent(key)}`;
const response = await this.requestRaw('GET', path, '');
// Auto-decompress if gzipped
if (key.endsWith('.gz') || key.endsWith('.gzip')) {
try {
const decompressed = zlib.gunzipSync(response);
return decompressed.toString('utf-8');
}
catch {
return response.toString('utf-8');
}
}
return response.toString('utf-8');
}
extractXmlValue(xml, tag) {
const regex = new RegExp(`<${tag}>(.*?)</${tag}>`);
const match = xml.match(regex);
return match ? match[1] : null;
}
async request(method, path, body) {
const buf = await this.requestRaw(method, path, body);
return buf.toString('utf-8');
}
async requestRaw(method, path, body) {
const now = new Date();
const dateStamp = now.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
const shortDate = dateStamp.substring(0, 8);
const headers = {
'host': this.host,
'x-amz-date': dateStamp,
'x-amz-content-sha256': this.sha256(body),
};
// Create canonical request
const [pathPart, queryPart] = path.split('?');
const canonicalQueryString = queryPart || '';
const signedHeaderKeys = Object.keys(headers).sort();
const signedHeaders = signedHeaderKeys.join(';');
const canonicalHeaders = signedHeaderKeys.map(k => `${k}:${headers[k]}\n`).join('');
const canonicalRequest = [
method,
pathPart,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
headers['x-amz-content-sha256'],
].join('\n');
// Create string to sign
const credentialScope = `${shortDate}/${this.config.region}/s3/aws4_request`;
const stringToSign = [
'AWS4-HMAC-SHA256',
dateStamp,
credentialScope,
this.sha256(canonicalRequest),
].join('\n');
// Calculate signature
const signingKey = this.getSignatureKey(shortDate);
const signature = this.hmac(signingKey, stringToSign).toString('hex');
// Authorization header
headers['authorization'] = `AWS4-HMAC-SHA256 Credential=${this.config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
return new Promise((resolve, reject) => {
const options = {
hostname: this.host.split(':')[0],
port: this.host.includes(':') ? parseInt(this.host.split(':')[1]) : (this.useHttps ? 443 : 80),
path: path,
method: method,
headers: headers,
};
const transport = this.useHttps ? https : http;
const req = transport.request(options, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const data = Buffer.concat(chunks);
if (res.statusCode && res.statusCode >= 400) {
reject(new Error(`S3 error ${res.statusCode}: ${data.toString('utf-8').substring(0, 200)}`));
}
else {
resolve(data);
}
});
});
req.on('error', reject);
req.setTimeout(30000, () => { req.destroy(); reject(new Error('S3 request timeout')); });
if (body)
req.write(body);
req.end();
});
}
sha256(data) {
return crypto.createHash('sha256').update(data, 'utf8').digest('hex');
}
hmac(key, data) {
return crypto.createHmac('sha256', key).update(data, 'utf8').digest();
}
getSignatureKey(dateStamp) {
const kDate = this.hmac(`AWS4${this.config.secretAccessKey}`, dateStamp);
const kRegion = this.hmac(kDate, this.config.region);
const kService = this.hmac(kRegion, 's3');
return this.hmac(kService, 'aws4_request');
}
}
exports.S3Source = S3Source;
//# sourceMappingURL=s3Source.js.map

File diff suppressed because one or more lines are too long

448
dist-server/server.js Normal file
View File

@@ -0,0 +1,448 @@
"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/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=...
if (pathname === '/api/logs' && method === 'GET') {
const source = parsedUrl.query.source;
if (source && source !== 'ALL') {
const entries = await logSourceManager.readLog(source);
return sendJson(res, entries);
}
else {
const entries = await logSourceManager.readAllLogs();
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);
const objects = await s3.listObjects(prefix);
return sendJson(res, objects);
}
// 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);
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);
}
// 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

File diff suppressed because one or more lines are too long