| const fs = require('fs'); |
| const path = require('path'); |
| const { createServer } = require('http'); |
| const { createWriteStream } = require('fs'); |
| const os = require('os'); |
|
|
| |
| 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'); |
| const MONITORING_PORT = parseInt(process.env.MONITORING_PORT || '7861'); |
|
|
| |
| if (!fs.existsSync(LOG_DIR)) { |
| fs.mkdirSync(LOG_DIR, { recursive: true }); |
| } |
|
|
| |
| 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'); |
|
|
| |
| const logStream = createWriteStream(logFile, { flags: 'a' }); |
| const errorLogStream = createWriteStream(errorLogFile, { flags: 'a' }); |
| const accessLogStream = createWriteStream(accessLogFile, { flags: 'a' }); |
|
|
| |
| const LOG_LEVELS = { |
| error: 0, |
| warn: 1, |
| info: 2, |
| debug: 3, |
| }; |
|
|
| |
| const memoryLogs = { |
| general: [], |
| error: [], |
| access: [], |
| MAX_ENTRIES: 1000, |
| }; |
|
|
| |
| 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(), |
| } |
| }; |
|
|
| |
| 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)); |
| }; |
|
|
| |
| saveMetrics(); |
| setInterval(saveMetrics, 60000); |
|
|
| |
| 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); |
| } |
| }; |
|
|
| |
| setInterval(() => { |
| checkLogRotation(); |
| |
| |
| 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); |
|
|
| |
| 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); |
| |
| |
| logStream.write(`${logString}\n`); |
| |
| |
| memoryLogs.general.unshift(logEntry); |
| if (memoryLogs.general.length > memoryLogs.MAX_ENTRIES) { |
| memoryLogs.general.pop(); |
| } |
| |
| |
| if (level === 'error') { |
| errorLogStream.write(`${logString}\n`); |
| |
| memoryLogs.error.unshift(logEntry); |
| if (memoryLogs.error.length > memoryLogs.MAX_ENTRIES) { |
| memoryLogs.error.pop(); |
| } |
| } |
| |
| |
| 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); |
| |
| |
| accessLogStream.write(`${logString}\n`); |
| |
| |
| memoryLogs.access.unshift(logEntry); |
| if (memoryLogs.access.length > memoryLogs.MAX_ENTRIES) { |
| memoryLogs.access.pop(); |
| } |
| |
| |
| metrics.requestsTotal++; |
| if (res.statusCode >= 200 && res.statusCode < 400) { |
| metrics.requestsSuccess++; |
| } else { |
| metrics.requestsError++; |
| } |
| } |
| }; |
|
|
| |
| 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); |
|
|
| |
| const monitoringServer = createServer((req, res) => { |
| |
| 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; |
| } |
| |
| |
| if (req.url === '/health') { |
| res.writeHead(200, { 'Content-Type': 'application/json' }); |
| res.end(JSON.stringify({ status: 'up', timestamp: new Date().toISOString() })); |
| return; |
| } |
| |
| |
| if (req.url === '/metrics') { |
| res.writeHead(200, { 'Content-Type': 'application/json' }); |
| |
| metrics.uptime = Date.now() - metrics.startTime; |
| metrics.memoryUsage = process.memoryUsage(); |
| metrics.systemLoad = os.loadavg(); |
| metrics.freeMem = os.freemem(); |
| res.end(JSON.stringify(metrics)); |
| return; |
| } |
| |
| |
| 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; |
| } |
| |
| |
| if (req.url === '/' || req.url === '/index.html') { |
| res.writeHead(200, { 'Content-Type': 'text/html' }); |
| res.end(getLogUIHtml()); |
| return; |
| } |
| |
| |
| res.writeHead(404, { 'Content-Type': 'application/json' }); |
| res.end(JSON.stringify({ error: 'Not found' })); |
| }); |
|
|
| |
| function getLogUIHtml() { |
| return `<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Stremio Logs & Monitoring</title> |
| <style> |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; |
| margin: 0; |
| padding: 0; |
| background: #f5f5f5; |
| color: #333; |
| } |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| padding: 20px; |
| } |
| header { |
| background: #2b2b2b; |
| color: white; |
| padding: 1rem; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| h1 { |
| margin: 0; |
| font-size: 1.5rem; |
| } |
| .tabs { |
| display: flex; |
| background: white; |
| margin-bottom: 20px; |
| border-radius: 4px; |
| overflow: hidden; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| } |
| .tab { |
| padding: 10px 20px; |
| cursor: pointer; |
| border-bottom: 2px solid transparent; |
| } |
| .tab.active { |
| background: #f0f0f0; |
| border-bottom: 2px solid #ff6600; |
| } |
| .card { |
| background: white; |
| border-radius: 4px; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| margin-bottom: 20px; |
| padding: 20px; |
| } |
| .card h2 { |
| margin-top: 0; |
| border-bottom: 1px solid #eee; |
| padding-bottom: 10px; |
| } |
| .metrics { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); |
| gap: 15px; |
| } |
| .metric-card { |
| background: #f9f9f9; |
| padding: 15px; |
| border-radius: 4px; |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
| } |
| .metric-title { |
| font-size: 0.9rem; |
| color: #666; |
| margin-bottom: 5px; |
| } |
| .metric-value { |
| font-size: 1.4rem; |
| font-weight: bold; |
| } |
| table { |
| width: 100%; |
| border-collapse: collapse; |
| } |
| table th, table td { |
| text-align: left; |
| padding: 8px; |
| border-bottom: 1px solid #eee; |
| } |
| table th { |
| background: #f0f0f0; |
| } |
| .log-row { |
| font-family: monospace; |
| font-size: 0.9rem; |
| } |
| .log-row.error { |
| background-color: #ffecec; |
| } |
| .log-row.warn { |
| background-color: #fffbec; |
| } |
| .controls { |
| margin-bottom: 15px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| select, button { |
| padding: 8px 12px; |
| border-radius: 4px; |
| border: 1px solid #ddd; |
| background: white; |
| } |
| button { |
| cursor: pointer; |
| } |
| button:hover { |
| background: #f0f0f0; |
| } |
| .log-count { |
| font-size: 0.9rem; |
| color: #666; |
| } |
| .timestamp { |
| font-size: 0.8rem; |
| color: #888; |
| } |
| .status-indicator { |
| display: inline-block; |
| width: 10px; |
| height: 10px; |
| border-radius: 50%; |
| margin-right: 5px; |
| } |
| .status-up { |
| background: #4caf50; |
| } |
| .status-down { |
| background: #f44336; |
| } |
| #lastUpdated { |
| font-size: 0.8rem; |
| color: #888; |
| } |
| </style> |
| </head> |
| <body> |
| <header> |
| <h1>Stremio Logs & Monitoring</h1> |
| <div> |
| <span class="status-indicator status-up" id="statusIndicator"></span> |
| <span id="statusText">System Online</span> |
| </div> |
| </header> |
| |
| <div class="container"> |
| <div class="tabs"> |
| <div class="tab active" data-tab="dashboard">Dashboard</div> |
| <div class="tab" data-tab="logs">Logs</div> |
| <div class="tab" data-tab="access">Access Logs</div> |
| <div class="tab" data-tab="errors">Error Logs</div> |
| <div class="tab" data-tab="settings">Settings</div> |
| </div> |
| |
| <div id="dashboard" class="tab-content"> |
| <div class="card"> |
| <h2>System Overview</h2> |
| <div class="metrics" id="systemMetrics"> |
| <!-- Metrics will be inserted here --> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <h2>Request Statistics</h2> |
| <div class="metrics" id="requestMetrics"> |
| <!-- Request metrics will be inserted here --> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <h2>Recent Logs</h2> |
| <div id="recentLogs"> |
| <!-- Recent logs will be inserted here --> |
| </div> |
| </div> |
| </div> |
| |
| <div id="logs" class="tab-content" style="display:none"> |
| <div class="card"> |
| <h2>General Logs</h2> |
| <div class="controls"> |
| <div> |
| <select id="logLevel"> |
| <option value="all">All Levels</option> |
| <option value="error">Error</option> |
| <option value="warn">Warning</option> |
| <option value="info">Info</option> |
| <option value="debug">Debug</option> |
| </select> |
| <button id="refreshLogs">Refresh</button> |
| </div> |
| <div class="log-count"><span id="logCount">0</span> logs</div> |
| </div> |
| <div id="logTableContainer" style="max-height: 600px; overflow-y: auto;"> |
| <table id="logTable"> |
| <thead> |
| <tr> |
| <th>Timestamp</th> |
| <th>Level</th> |
| <th>Message</th> |
| </tr> |
| </thead> |
| <tbody> |
| <!-- Logs will be inserted here --> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| |
| <div id="access" class="tab-content" style="display:none"> |
| <div class="card"> |
| <h2>Access Logs</h2> |
| <div class="controls"> |
| <div> |
| <select id="statusFilter"> |
| <option value="all">All Status Codes</option> |
| <option value="200">200 Success</option> |
| <option value="300">300 Redirects</option> |
| <option value="400">400 Client Errors</option> |
| <option value="500">500 Server Errors</option> |
| </select> |
| <button id="refreshAccessLogs">Refresh</button> |
| </div> |
| <div class="log-count"><span id="accessLogCount">0</span> requests</div> |
| </div> |
| <div id="accessLogTableContainer" style="max-height: 600px; overflow-y: auto;"> |
| <table id="accessLogTable"> |
| <thead> |
| <tr> |
| <th>Timestamp</th> |
| <th>Method</th> |
| <th>URL</th> |
| <th>Status</th> |
| <th>Response Time</th> |
| </tr> |
| </thead> |
| <tbody> |
| <!-- Access logs will be inserted here --> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| |
| <div id="errors" class="tab-content" style="display:none"> |
| <div class="card"> |
| <h2>Error Logs</h2> |
| <div class="controls"> |
| <div> |
| <button id="refreshErrorLogs">Refresh</button> |
| </div> |
| <div class="log-count"><span id="errorLogCount">0</span> errors</div> |
| </div> |
| <div id="errorLogTableContainer" style="max-height: 600px; overflow-y: auto;"> |
| <table id="errorLogTable"> |
| <thead> |
| <tr> |
| <th>Timestamp</th> |
| <th>Message</th> |
| <th>Details</th> |
| </tr> |
| </thead> |
| <tbody> |
| <!-- Error logs will be inserted here --> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| |
| <div id="settings" class="tab-content" style="display:none"> |
| <div class="card"> |
| <h2>Settings</h2> |
| <p>Log settings are configured via environment variables:</p> |
| <table> |
| <tr> |
| <th>Setting</th> |
| <th>Current Value</th> |
| <th>Description</th> |
| </tr> |
| <tr> |
| <td>LOG_LEVEL</td> |
| <td id="settingLogLevel"></td> |
| <td>Minimum log level to record</td> |
| </tr> |
| <tr> |
| <td>LOG_DIR</td> |
| <td id="settingLogDir"></td> |
| <td>Directory where logs are stored</td> |
| </tr> |
| <tr> |
| <td>LOG_RETENTION_DAYS</td> |
| <td id="settingRetention"></td> |
| <td>Number of days to keep log files</td> |
| </tr> |
| <tr> |
| <td>MAX_LOG_SIZE</td> |
| <td id="settingMaxSize"></td> |
| <td>Maximum size of log file before rotation</td> |
| </tr> |
| </table> |
| </div> |
| </div> |
| </div> |
| |
| <div style="text-align: center; margin-top: 20px; padding-bottom: 20px;"> |
| <div id="lastUpdated"></div> |
| </div> |
| |
| <script> |
| // Element references |
| const tabs = document.querySelectorAll('.tab'); |
| const tabContents = document.querySelectorAll('.tab-content'); |
| const logTable = document.getElementById('logTable').querySelector('tbody'); |
| const accessLogTable = document.getElementById('accessLogTable').querySelector('tbody'); |
| const errorLogTable = document.getElementById('errorLogTable').querySelector('tbody'); |
| const systemMetricsEl = document.getElementById('systemMetrics'); |
| const requestMetricsEl = document.getElementById('requestMetrics'); |
| const recentLogsEl = document.getElementById('recentLogs'); |
| const lastUpdatedEl = document.getElementById('lastUpdated'); |
| const logLevelSelect = document.getElementById('logLevel'); |
| const statusFilterSelect = document.getElementById('statusFilter'); |
| const refreshLogsBtn = document.getElementById('refreshLogs'); |
| const refreshAccessLogsBtn = document.getElementById('refreshAccessLogs'); |
| const refreshErrorLogsBtn = document.getElementById('refreshErrorLogs'); |
| const logCountEl = document.getElementById('logCount'); |
| const accessLogCountEl = document.getElementById('accessLogCount'); |
| const errorLogCountEl = document.getElementById('errorLogCount'); |
| const settingLogLevelEl = document.getElementById('settingLogLevel'); |
| const settingLogDirEl = document.getElementById('settingLogDir'); |
| const settingRetentionEl = document.getElementById('settingRetention'); |
| const settingMaxSizeEl = document.getElementById('settingMaxSize'); |
| const statusIndicator = document.getElementById('statusIndicator'); |
| const statusText = document.getElementById('statusText'); |
| |
| // Tab switching |
| tabs.forEach(tab => { |
| tab.addEventListener('click', () => { |
| tabs.forEach(t => t.classList.remove('active')); |
| tab.classList.add('active'); |
| |
| const tabName = tab.getAttribute('data-tab'); |
| tabContents.forEach(content => { |
| content.style.display = content.id === tabName ? 'block' : 'none'; |
| }); |
| }); |
| }); |
| |
| // Format date |
| function formatDate(dateString) { |
| const date = new Date(dateString); |
| return date.toLocaleString(); |
| } |
| |
| // Format bytes |
| function formatBytes(bytes, decimals = 2) { |
| if (bytes === 0) return '0 Bytes'; |
| const k = 1024; |
| const dm = decimals < 0 ? 0 : decimals; |
| const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; |
| } |
| |
| // Format duration |
| function formatDuration(ms) { |
| const seconds = Math.floor(ms / 1000); |
| const minutes = Math.floor(seconds / 60); |
| const hours = Math.floor(minutes / 60); |
| const days = Math.floor(hours / 24); |
| |
| if (days > 0) return days + 'd ' + (hours % 24) + 'h'; |
| if (hours > 0) return hours + 'h ' + (minutes % 60) + 'm'; |
| if (minutes > 0) return minutes + 'm ' + (seconds % 60) + 's'; |
| return seconds + 's'; |
| } |
| |
| // Fetch metrics |
| async function fetchMetrics() { |
| try { |
| const response = await fetch('/metrics'); |
| const metrics = await response.json(); |
| |
| // Update system metrics |
| let systemMetricsHtml = ''; |
| |
| systemMetricsHtml += createMetricCard('Uptime', formatDuration(metrics.uptime)); |
| systemMetricsHtml += createMetricCard('Platform', metrics.systemInfo.platform); |
| systemMetricsHtml += createMetricCard('CPU Cores', metrics.systemInfo.cpus); |
| systemMetricsHtml += createMetricCard('Memory Total', formatBytes(metrics.systemInfo.totalMem)); |
| systemMetricsHtml += createMetricCard('Memory Free', formatBytes(metrics.freeMem)); |
| systemMetricsHtml += createMetricCard('Memory Usage', formatBytes(metrics.memoryUsage.rss)); |
| systemMetricsHtml += createMetricCard('Heap Used', formatBytes(metrics.memoryUsage.heapUsed)); |
| systemMetricsHtml += createMetricCard('CPU Load', metrics.systemLoad[0].toFixed(2)); |
| |
| systemMetricsEl.innerHTML = systemMetricsHtml; |
| |
| // Update request metrics |
| let requestMetricsHtml = ''; |
| |
| requestMetricsHtml += createMetricCard('Total Requests', metrics.requestsTotal); |
| requestMetricsHtml += createMetricCard('Successful', metrics.requestsSuccess); |
| requestMetricsHtml += createMetricCard('Errors', metrics.requestsError); |
| requestMetricsHtml += createMetricCard('Success Rate', ((metrics.requestsSuccess / metrics.requestsTotal) * 100 || 0).toFixed(1) + '%'); |
| requestMetricsHtml += createMetricCard('Proxy Errors', metrics.proxyErrors); |
| |
| requestMetricsEl.innerHTML = requestMetricsHtml; |
| |
| // Update settings |
| settingLogLevelEl.textContent = '${LOG_LEVEL}'; |
| settingLogDirEl.textContent = '${LOG_DIR}'; |
| settingRetentionEl.textContent = '${LOG_RETENTION_DAYS} days'; |
| settingMaxSizeEl.textContent = formatBytes(${MAX_LOG_SIZE}); |
| |
| // Update system status |
| statusIndicator.className = 'status-indicator status-up'; |
| statusText.textContent = 'System Online'; |
| |
| lastUpdatedEl.textContent = 'Last updated: ' + new Date().toLocaleString(); |
| } catch (err) { |
| console.error('Error fetching metrics:', err); |
| statusIndicator.className = 'status-indicator status-down'; |
| statusText.textContent = 'System Error'; |
| } |
| } |
| |
| // Create metric card |
| function createMetricCard(title, value) { |
| return \` |
| <div class="metric-card"> |
| <div class="metric-title">\${title}</div> |
| <div class="metric-value">\${value}</div> |
| </div> |
| \`; |
| } |
| |
| // Fetch and render logs |
| async function fetchLogs(type = 'general', filter = 'all') { |
| try { |
| const response = await fetch(\`/logs?type=\${type}\`); |
| const logs = await response.json(); |
| |
| let filteredLogs = logs; |
| let tableEl; |
| let countEl; |
| |
| if (type === 'general') { |
| if (filter !== 'all') { |
| filteredLogs = logs.filter(log => log.level === filter); |
| } |
| tableEl = logTable; |
| countEl = logCountEl; |
| } else if (type === 'access') { |
| if (filter !== 'all') { |
| const statusPrefix = filter.charAt(0); |
| filteredLogs = logs.filter(log => |
| log.statusCode.toString().charAt(0) === statusPrefix); |
| } |
| tableEl = accessLogTable; |
| countEl = accessLogCountEl; |
| } else if (type === 'error') { |
| tableEl = errorLogTable; |
| countEl = errorLogCountEl; |
| } |
| |
| // Update count |
| countEl.textContent = filteredLogs.length; |
| |
| // Render logs |
| let html = ''; |
| |
| if (type === 'general') { |
| filteredLogs.forEach(log => { |
| html += \` |
| <tr class="log-row \${log.level}"> |
| <td class="timestamp">\${formatDate(log.timestamp)}</td> |
| <td>\${log.level.toUpperCase()}</td> |
| <td>\${log.message}</td> |
| </tr> |
| \`; |
| }); |
| } else if (type === 'access') { |
| filteredLogs.forEach(log => { |
| const statusClass = log.statusCode >= 400 ? 'error' : |
| log.statusCode >= 300 ? 'warn' : ''; |
| html += \` |
| <tr class="log-row \${statusClass}"> |
| <td class="timestamp">\${formatDate(log.timestamp)}</td> |
| <td>\${log.method}</td> |
| <td>\${log.url}</td> |
| <td>\${log.statusCode}</td> |
| <td>\${log.responseTime}ms</td> |
| </tr> |
| \`; |
| }); |
| } else if (type === 'error') { |
| filteredLogs.forEach(log => { |
| html += \` |
| <tr class="log-row error"> |
| <td class="timestamp">\${formatDate(log.timestamp)}</td> |
| <td>\${log.message}</td> |
| <td>\${JSON.stringify(log.meta || {})}</td> |
| </tr> |
| \`; |
| }); |
| } |
| |
| tableEl.innerHTML = html; |
| |
| // Also update recent logs on dashboard |
| if (type === 'general' && recentLogsEl) { |
| let recentHtml = '<table><thead><tr><th>Time</th><th>Level</th><th>Message</th></tr></thead><tbody>'; |
| logs.slice(0, 5).forEach(log => { |
| recentHtml += \` |
| <tr class="log-row \${log.level}"> |
| <td class="timestamp">\${formatDate(log.timestamp)}</td> |
| <td>\${log.level.toUpperCase()}</td> |
| <td>\${log.message}</td> |
| </tr> |
| \`; |
| }); |
| recentHtml += '</tbody></table>'; |
| recentLogsEl.innerHTML = recentHtml; |
| } |
| } catch (err) { |
| console.error(\`Error fetching \${type} logs:\`, err); |
| } |
| } |
| |
| // Event listeners |
| refreshLogsBtn.addEventListener('click', () => { |
| fetchLogs('general', logLevelSelect.value); |
| }); |
| |
| refreshAccessLogsBtn.addEventListener('click', () => { |
| fetchLogs('access', statusFilterSelect.value); |
| }); |
| |
| refreshErrorLogsBtn.addEventListener('click', () => { |
| fetchLogs('error'); |
| }); |
| |
| logLevelSelect.addEventListener('change', () => { |
| fetchLogs('general', logLevelSelect.value); |
| }); |
| |
| statusFilterSelect.addEventListener('change', () => { |
| fetchLogs('access', statusFilterSelect.value); |
| }); |
| |
| // Initial data fetch |
| fetchMetrics(); |
| fetchLogs('general'); |
| fetchLogs('access'); |
| fetchLogs('error'); |
| |
| // Refresh data periodically |
| setInterval(fetchMetrics, 10000); |
| setInterval(() => fetchLogs('general', logLevelSelect.value), 10000); |
| setInterval(() => fetchLogs('access', statusFilterSelect.value), 10000); |
| setInterval(() => fetchLogs('error'), 10000); |
| </script> |
| </body> |
| </html>`; |
| } |
|
|
| |
| monitoringServer.listen(MONITORING_PORT, () => { |
| logger.info(`Monitoring server listening on port ${MONITORING_PORT}`, { service: 'logger' }); |
| }); |
|
|
| |
| 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); |
| }); |
|
|
| |
| module.exports = logger; |