Files
tail-monitor/index.js
2025-07-10 23:07:01 +02:00

185 líneas
5.5 KiB
JavaScript

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)
// 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'))
}
})
const stream = ts.createReadStream(file, {
beginAt: 'end',
endOnError: true,
detectTruncate: true
})
// Send initial message
res.write(Buffer.from(`data: Tailing ${endpoint}...\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)
})
})