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' // 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', (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') 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 }) // Send initial message const initialMessage = isReconnect ? `Reconnected to ${endpoint}...` : `Tailing ${endpoint}...` res.write(Buffer.from(`data: ${initialMessage}\n\n`)) // 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) }) })