Files
xiaocai/server/src/routes/logs.ts

233 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Router } from 'express'
import fs from 'fs'
import path from 'path'
import pool from '../db/connection'
import { requireAdmin } from '../middleware/requireAdmin'
const router = Router()
const LOG_DIR = path.resolve(process.env.LOG_DIR || './logs')
/** 获取今日日期字符串 YYYY-MM-DD服务器本地时间 */
function getTodayStr(): string {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
/** 获取日志列表最近7天 */
router.get('/files', requireAdmin, (_req, res) => {
try {
if (!fs.existsSync(LOG_DIR)) {
return res.json({ code: 0, data: [] })
}
const files = fs.readdirSync(LOG_DIR)
.filter(f => f.endsWith('.log'))
.sort()
.reverse()
.slice(0, 7) // 最多返回7天
const fileList = files.map(f => {
const filePath = path.join(LOG_DIR, f)
const stat = fs.statSync(filePath)
return {
name: f,
date: f.replace('.log', ''),
size: stat.size,
modified: stat.mtime.toISOString()
}
})
res.json({ code: 0, data: fileList })
} catch (err) {
console.error('[Logs] List error:', err)
res.status(500).json({ code: 50000, message: '获取日志列表失败' })
}
})
/** 获取日志统计 */
router.get('/stats', requireAdmin, (_req, res) => {
try {
if (!fs.existsSync(LOG_DIR)) {
return res.json({ code: 0, data: { today: { total: 0, errors: 0, slow: 0 }, files: [] } })
}
const dateStr = getTodayStr()
const todayFile = path.join(LOG_DIR, `${dateStr}.log`)
let todayStats = { total: 0, errors: 0, slow: 0, warns: 0 }
if (fs.existsSync(todayFile)) {
const content = fs.readFileSync(todayFile, 'utf-8')
const lines = content.split('\n').filter(l => l.trim())
todayStats.total = lines.length
todayStats.errors = lines.filter(l => l.includes(' ERROR ')).length
todayStats.slow = lines.filter(l => l.includes('Slow request')).length
todayStats.warns = lines.filter(l => l.includes(' WARN ')).length
}
// 最近7天统计
const files = fs.readdirSync(LOG_DIR)
.filter(f => f.endsWith('.log'))
.sort()
.reverse()
.slice(0, 7)
const dailyStats = files.map(f => {
const filePath = path.join(LOG_DIR, f)
const content = fs.readFileSync(filePath, 'utf-8')
const lines = content.split('\n').filter(l => l.trim())
return {
date: f.replace('.log', ''),
total: lines.length,
errors: lines.filter(l => l.includes(' ERROR ')).length,
slow: lines.filter(l => l.includes('Slow request')).length
}
})
res.json({
code: 0,
data: {
today: todayStats,
daily: dailyStats
}
})
} catch (err) {
console.error('[Logs] Stats error:', err)
res.status(500).json({ code: 50000, message: '获取日志统计失败' })
}
})
/** 获取埋点统计(必须在 /:date 之前注册) */
router.get('/track/stats', requireAdmin, async (_req, res) => {
try {
// 今日统计
const [todayRows] = await pool.query(
`SELECT event, COUNT(*) as count FROM track_events
WHERE DATE(created_at) = CURDATE()
GROUP BY event ORDER BY count DESC`
)
// 最近7天每日统计
const [dailyRows] = await pool.query(
`SELECT DATE(created_at) as date, COUNT(*) as count FROM track_events
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY DATE(created_at) ORDER BY date DESC`
)
// 最近7天事件分布
const [eventRows] = await pool.query(
`SELECT event, COUNT(*) as count FROM track_events
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY event ORDER BY count DESC LIMIT 10`
)
res.json({
code: 0,
data: {
today: todayRows,
daily: dailyRows,
events: eventRows
}
})
} catch (err) {
console.error('[Track] Stats error:', err)
res.status(500).json({ code: 50000, message: '获取埋点统计失败' })
}
})
/** 查询埋点事件(必须在 /:date 之前注册) */
router.get('/track/list', requireAdmin, async (req, res) => {
try {
const { event, page = '1', pageSize = '50' } = req.query
const pNum = Math.max(1, parseInt(page as string) || 1)
const pSize = Math.min(200, Math.max(1, parseInt(pageSize as string) || 50))
const offset = (pNum - 1) * pSize
let where = '1=1'
const params: any[] = []
if (event && typeof event === 'string') {
where += ' AND t.event = ?'
params.push(event)
}
const [countResult] = await pool.query(
`SELECT COUNT(*) as total FROM track_events t WHERE ${where}`,
params
)
const total = (countResult as any[])[0].total
const [rows] = await pool.query(
`SELECT t.*, u.nickname
FROM track_events t
LEFT JOIN users u ON t.user_id = u.id AND u.deleted_at IS NULL
WHERE ${where}
ORDER BY t.created_at DESC
LIMIT ? OFFSET ?`,
[...params, pSize, offset]
)
res.json({
code: 0,
data: { list: rows, total, page: pNum, pageSize: pSize }
})
} catch (err) {
console.error('[Track] List error:', err)
res.status(500).json({ code: 50000, message: '查询埋点失败' })
}
})
/** 查询指定日期的日志(必须在 /track/* 之后注册) */
router.get('/:date', requireAdmin, (req, res) => {
try {
const { date } = req.params
const { level, keyword, page = '1', pageSize = '100' } = req.query
// 日期格式校验
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
return res.status(400).json({ code: 40001, message: '日期格式无效' })
}
const logFile = path.join(LOG_DIR, `${date}.log`)
if (!fs.existsSync(logFile)) {
return res.json({ code: 0, data: { lines: [], total: 0 } })
}
const content = fs.readFileSync(logFile, 'utf-8')
let lines = content.split('\n').filter(l => l.trim())
// 按级别过滤
const levelStr = typeof level === 'string' ? level : ''
if (levelStr && levelStr !== 'all') {
lines = lines.filter(l => l.includes(` ${levelStr.toUpperCase()} `))
}
// 按关键词过滤
if (keyword && typeof keyword === 'string') {
const kw = keyword.toLowerCase()
lines = lines.filter(l => l.toLowerCase().includes(kw))
}
const total = lines.length
const pNum = Math.max(1, parseInt(page as string) || 1)
const pSize = Math.min(500, Math.max(1, parseInt(pageSize as string) || 100))
const offset = (pNum - 1) * pSize
const paged = lines.slice(offset, offset + pSize)
res.json({
code: 0,
data: {
lines: paged,
total,
page: pNum,
pageSize: pSize
}
})
} catch (err) {
console.error('[Logs] Query error:', err)
res.status(500).json({ code: 50000, message: '查询日志失败' })
}
})
export default router