| | 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; |