const fs = require('fs'); const path = require('path'); const { createServer } = require('http'); const { createWriteStream } = require('fs'); const os = require('os'); // Configuration const LOG_DIR = process.env.LOG_DIR || '/tmp/.stremio-logs'; const LOG_RETENTION_DAYS = parseInt(process.env.LOG_RETENTION_DAYS || '7'); const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; const MAX_LOG_SIZE = parseInt(process.env.MAX_LOG_SIZE || '10485760'); // 10MB const MONITORING_PORT = parseInt(process.env.MONITORING_PORT || '7861'); // Create log directory if (!fs.existsSync(LOG_DIR)) { fs.mkdirSync(LOG_DIR, { recursive: true }); } // Setup log file paths const logFile = path.join(LOG_DIR, 'stremio.log'); const errorLogFile = path.join(LOG_DIR, 'error.log'); const accessLogFile = path.join(LOG_DIR, 'access.log'); const metricsFile = path.join(LOG_DIR, 'metrics.json'); // Create log streams const logStream = createWriteStream(logFile, { flags: 'a' }); const errorLogStream = createWriteStream(errorLogFile, { flags: 'a' }); const accessLogStream = createWriteStream(accessLogFile, { flags: 'a' }); // Log levels const LOG_LEVELS = { error: 0, warn: 1, info: 2, debug: 3, }; // In-memory logs for UI access const memoryLogs = { general: [], error: [], access: [], MAX_ENTRIES: 1000, }; // System metrics let metrics = { startTime: Date.now(), requestsTotal: 0, requestsSuccess: 0, requestsError: 0, proxyErrors: 0, lastUpdate: Date.now(), systemInfo: { platform: os.platform(), arch: os.arch(), cpus: os.cpus().length, totalMem: os.totalmem(), } }; // Save metrics periodically const saveMetrics = () => { metrics.lastUpdate = Date.now(); metrics.uptime = Date.now() - metrics.startTime; metrics.memoryUsage = process.memoryUsage(); metrics.systemLoad = os.loadavg(); metrics.freeMem = os.freemem(); fs.writeFileSync(metricsFile, JSON.stringify(metrics, null, 2)); }; // Initialize metrics file saveMetrics(); setInterval(saveMetrics, 60000); // Save every minute // Log rotation check const checkLogRotation = () => { try { const stats = fs.statSync(logFile); if (stats.size > MAX_LOG_SIZE) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); fs.renameSync(logFile, `${logFile}.${timestamp}`); logStream.end(); logStream = createWriteStream(logFile, { flags: 'a' }); } } catch (err) { console.error('Error rotating logs:', err); } }; // Log rotation and cleanup setInterval(() => { checkLogRotation(); // Clean old log files fs.readdir(LOG_DIR, (err, files) => { if (err) return; const now = Date.now(); const maxAge = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000; files.forEach(file => { if (file.endsWith('.log.') || file.endsWith('.log-')) { const filePath = path.join(LOG_DIR, file); fs.stat(filePath, (err, stats) => { if (err) return; if (now - stats.mtime.getTime() > maxAge) { fs.unlink(filePath, () => {}); } }); } }); }); }, 3600000); // Check every hour // Logger function const logger = { log: (level, message, meta = {}) => { if (LOG_LEVELS[level] > LOG_LEVELS[LOG_LEVEL]) return; const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, message, ...meta }; const logString = JSON.stringify(logEntry); // Write to file logStream.write(`${logString}\n`); // Keep in memory for UI memoryLogs.general.unshift(logEntry); if (memoryLogs.general.length > memoryLogs.MAX_ENTRIES) { memoryLogs.general.pop(); } // Also log errors to error log if (level === 'error') { errorLogStream.write(`${logString}\n`); memoryLogs.error.unshift(logEntry); if (memoryLogs.error.length > memoryLogs.MAX_ENTRIES) { memoryLogs.error.pop(); } } // Console output for immediate feedback console[level](message); }, access: (req, res, responseTime) => { const timestamp = new Date().toISOString(); const logEntry = { timestamp, method: req.method, url: req.url, statusCode: res.statusCode, userAgent: req.headers['user-agent'], responseTime, remoteAddress: req.headers['x-forwarded-for'] || req.socket.remoteAddress }; const logString = JSON.stringify(logEntry); // Write to access log file accessLogStream.write(`${logString}\n`); // Keep in memory for UI memoryLogs.access.unshift(logEntry); if (memoryLogs.access.length > memoryLogs.MAX_ENTRIES) { memoryLogs.access.pop(); } // Update metrics metrics.requestsTotal++; if (res.statusCode >= 200 && res.statusCode < 400) { metrics.requestsSuccess++; } else { metrics.requestsError++; } } }; // Add convenience methods logger.error = (message, meta) => logger.log('error', message, meta); logger.warn = (message, meta) => logger.log('warn', message, meta); logger.info = (message, meta) => logger.log('info', message, meta); logger.debug = (message, meta) => logger.log('debug', message, meta); // Create HTTP server for log UI and monitoring const monitoringServer = createServer((req, res) => { // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } // Health check endpoint if (req.url === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'up', timestamp: new Date().toISOString() })); return; } // Metrics endpoint if (req.url === '/metrics') { res.writeHead(200, { 'Content-Type': 'application/json' }); // Update metrics before sending metrics.uptime = Date.now() - metrics.startTime; metrics.memoryUsage = process.memoryUsage(); metrics.systemLoad = os.loadavg(); metrics.freeMem = os.freemem(); res.end(JSON.stringify(metrics)); return; } // Logs endpoint if (req.url === '/logs' || req.url.startsWith('/logs?')) { const url = new URL(`http://localhost${req.url}`); const type = url.searchParams.get('type') || 'general'; const limit = parseInt(url.searchParams.get('limit') || '100'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(memoryLogs[type]?.slice(0, limit) || [])); return; } // UI endpoint if (req.url === '/' || req.url === '/index.html') { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(getLogUIHtml()); return; } // Not found res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found' })); }); // Generate HTML for log UI function getLogUIHtml() { return ` Stremio Logs & Monitoring

Stremio Logs & Monitoring

System Online
Dashboard
Logs
Access Logs
Error Logs
Settings

System Overview

Request Statistics

Recent Logs

`; } // Start monitoring server monitoringServer.listen(MONITORING_PORT, () => { logger.info(`Monitoring server listening on port ${MONITORING_PORT}`, { service: 'logger' }); }); // Handle exit process.on('SIGINT', () => { logger.info('Received SIGINT, shutting down...', { service: 'logger' }); logStream.end(); errorLogStream.end(); accessLogStream.end(); monitoringServer.close(); process.exit(0); }); process.on('SIGTERM', () => { logger.info('Received SIGTERM, shutting down...', { service: 'logger' }); logStream.end(); errorLogStream.end(); accessLogStream.end(); monitoringServer.close(); process.exit(0); }); // Export logger module.exports = logger;