185 líneas
5.5 KiB
JavaScript
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)
|
|
})
|
|
})
|