feat: add endpoints to list and read log files; enhance log retrieval functionality #20
|
|
@ -338,7 +338,7 @@ app.get('/api/config', (_req, res) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get recent logs (dev/debug only)
|
* Get recent logs from memory (dev/debug only)
|
||||||
* GET /api/logs?limit=100&level=debug|info|warn|error&q=keyword
|
* GET /api/logs?limit=100&level=debug|info|warn|error&q=keyword
|
||||||
*/
|
*/
|
||||||
app.get('/api/logs', (req, res) => {
|
app.get('/api/logs', (req, res) => {
|
||||||
|
|
@ -360,6 +360,70 @@ app.get('/api/logs', (req, res) => {
|
||||||
res.json({ count: sliced.length, items: sliced })
|
res.json({ count: sliced.length, items: sliced })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all log files in logs directory
|
||||||
|
* GET /api/logs/files
|
||||||
|
*/
|
||||||
|
app.get('/api/logs/files', (req, res) => {
|
||||||
|
if (!LOG_EXPOSE_API) {
|
||||||
|
return res.status(403).json({ error: 'FORBIDDEN', message: 'Log API disabled. Set LOG_EXPOSE_API=true to enable.' })
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(LOG_DIR)
|
||||||
|
.filter(f => f.startsWith('LOGS_') && f.endsWith('.log'))
|
||||||
|
.map(f => {
|
||||||
|
const stats = fs.statSync(path.join(LOG_DIR, f))
|
||||||
|
return {
|
||||||
|
filename: f,
|
||||||
|
size: stats.size,
|
||||||
|
modified: stats.mtime,
|
||||||
|
path: `/api/logs/files/${f}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.modified - a.modified)
|
||||||
|
res.json({ count: files.length, files })
|
||||||
|
} catch (e) {
|
||||||
|
logError('logs.files.error', { message: e?.message })
|
||||||
|
res.status(500).json({ error: 'READ_ERROR', message: e?.message || 'Failed to read log files' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read specific log file
|
||||||
|
* GET /api/logs/files/:filename
|
||||||
|
*/
|
||||||
|
app.get('/api/logs/files/:filename', (req, res) => {
|
||||||
|
if (!LOG_EXPOSE_API) {
|
||||||
|
return res.status(403).json({ error: 'FORBIDDEN', message: 'Log API disabled. Set LOG_EXPOSE_API=true to enable.' })
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { filename } = req.params
|
||||||
|
|
||||||
|
// SECURITY: VALIDATE FILENAME TO PREVENT DIRECTORY TRAVERSAL
|
||||||
|
if (!filename.match(/^LOGS_\d{8}\.log$/)) {
|
||||||
|
return res.status(400).json({ error: 'INVALID_FILENAME', message: 'Invalid log filename format' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(LOG_DIR, filename)
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return res.status(404).json({ error: 'NOT_FOUND', message: 'Log file not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8')
|
||||||
|
const lines = content.split('\n').filter(Boolean)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
filename,
|
||||||
|
lines: lines.length,
|
||||||
|
content: lines
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
logError('logs.file.read.error', { message: e?.message, filename: req.params.filename })
|
||||||
|
res.status(500).json({ error: 'READ_ERROR', message: e?.message || 'Failed to read log file' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update payment toggles at runtime (dev only)
|
* Update payment toggles at runtime (dev only)
|
||||||
* POST /api/config
|
* POST /api/config
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue