const http = require('http'), express = require('express'), app = express(), path = require('path'), fs = require('fs'), ts = require('tail-stream'), Transform = require('stream').Transform, server = http.createServer(app).listen(3000, () => { console.log(`🚀 Log Tail Monitor running on: http://${server.address().address}:${server.address().port}/tail`) console.log(`📁 Monitoring directory: ${directory}`) }), directory = process.argv[2] || './logs' // Helper function to read last N lines from a file function readLastLines(filePath, numLines = 100) { return new Promise((resolve, reject) => { try { const stats = fs.statSync(filePath) const fileSize = stats.size if (fileSize === 0) { resolve([]) return } const buffer = Buffer.alloc(Math.min(fileSize, 64 * 1024)) // Read max 64KB const fd = fs.openSync(filePath, 'r') // Calculate read position (start from end, go backwards) const readSize = Math.min(fileSize, buffer.length) const position = Math.max(0, fileSize - readSize) fs.readSync(fd, buffer, 0, readSize, position) fs.closeSync(fd) // Convert to string and split by lines const content = buffer.toString('utf8', 0, readSize) let lines = content.split('\n') // Remove last empty line if exists if (lines[lines.length - 1] === '') { lines.pop() } // If we read from the middle of the file, remove the first partial line if (position > 0 && lines.length > 0) { lines.shift() } // Return last N lines const result = lines.slice(-numLines) resolve(result) } catch (error) { reject(error) } }) } // Middleware app.disable('x-powered-by') app.use((req, res, next) => { // CORS headers res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept') next() }) // Health check endpoint app.get('/tail/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), directory: directory, directoryExists: fs.existsSync(directory) }) }) // Get list of files app.get('/tail/api/files', (req, res) => { try { if (fs.existsSync(directory)) { const files = fs.readdirSync(directory, { withFileTypes: true }) .filter(file => file.isFile()) .map(file => ({ name: file.name, size: fs.statSync(path.join(directory, file.name)).size, modified: fs.statSync(path.join(directory, file.name)).mtime })) .sort((a, b) => b.modified - a.modified) // Sort by most recently modified res.json({ status: 'ok', files: files, directory: directory, count: files.length }) } else { res.status(404).json({ status: 'error', message: 'Directory not found', directory: directory, files: [] }) } } catch (error) { console.error('Error reading directory:', error) res.status(500).json({ status: 'error', message: 'Internal server error', files: [] }) } }) // Tail a specific file app.get('/tail/api/files/:endpoint', async (req, res) => { const endpoint = path.normalize(req.params.endpoint) const file = path.join(directory, endpoint) const isReconnect = req.query.reconnect === 'true' // Security check - ensure file is within the directory if (!file.startsWith(path.resolve(directory))) { return res.status(403).end('Access denied') } if (!fs.existsSync(file)) { return res.status(404).end('File not found') } try { // Set SSE headers res.header('Content-Type', 'text/event-stream') res.header('Connection', 'keep-alive') res.header('Cache-Control', 'no-cache') res.header('X-Accel-Buffering', 'no') // Send initial message const initialMessage = isReconnect ? `Reconnected to ${endpoint}...` : `Tailing ${endpoint}...` res.write(Buffer.from(`data: ${initialMessage}\n\n`)) // If it's not a reconnection, send the last 100 lines first if (!isReconnect) { try { const lastLines = await readLastLines(file, 100) if (lastLines.length > 0) { // Send a separator message res.write(Buffer.from(`data: ===== Last ${lastLines.length} lines =====\n\n`)) // Send each line with a marker that it's historical lastLines.forEach(line => { if (line.trim()) { res.write(Buffer.from(`data: [HISTORY] ${line}\n\n`)) } }) // Send separator for new content res.write(Buffer.from(`data: ===== Live tail starts here =====\n\n`)) } } catch (error) { console.warn('Could not read file history:', error.message) res.write(Buffer.from(`data: Could not load file history: ${error.message}\n\n`)) } } const transform = new Transform({ transform: (chunk, enc, cb) => { cb(null, Buffer.from('data: ' + chunk.toString() + '\n\n')) } }) // For reconnections, start from current end, for new connections start from 'end' const beginAt = isReconnect ? fs.statSync(file).size : 'end' const stream = ts.createReadStream(file, { beginAt: beginAt, endOnError: true, detectTruncate: true }) // Handle stream events stream.on('error', error => { console.error('Stream error:', error) transform.destroy(error) res.end(Buffer.from(`data: Error: ${error.message}\n\n`)) }) stream.on('truncate', () => { res.write(Buffer.from('data: File was truncated, continuing from beginning...\n\n')) }) // Pipe stream to response stream.pipe(transform).pipe(res) // Keep connection alive with periodic pings const pingInterval = setInterval(() => { if (!res.writableEnded) { res.write(Buffer.from('data: \n\n')) // Empty ping } else { clearInterval(pingInterval) } }, 30000) // 30 second ping // Clean up on client disconnect req.on('close', () => { clearInterval(pingInterval) stream.destroy() transform.destroy() }) } catch (error) { console.error('Error setting up tail stream:', error) res.status(500).end('Internal server error') } }) // Serve static files app.use('/tail', express.static(path.join(__dirname, 'public'))) // Root redirect app.get('/', (req, res) => { res.redirect('/tail') }) // Handle 404s app.use((req, res) => { res.status(404).json({ status: 'error', message: 'Endpoint not found', path: req.path }) }) // Global error handler app.use((err, req, res, next) => { console.error('Unhandled error:', err) res.status(500).json({ status: 'error', message: 'Internal server error' }) }) // Graceful shutdown process.on('SIGTERM', () => { console.log('Received SIGTERM, shutting down gracefully...') server.close(() => { console.log('Server closed') process.exit(0) }) }) process.on('SIGINT', () => { console.log('Received SIGINT, shutting down gracefully...') server.close(() => { console.log('Server closed') process.exit(0) }) })