29 changed files with 322 additions and 21 deletions
@ -0,0 +1,60 @@ |
|||
<template> |
|||
<div class="error-page"> |
|||
<div class="error-card"> |
|||
<div class="error-code">{{ statusCode }}</div> |
|||
<div class="error-message">{{ statusMessage }}</div> |
|||
<BoButton type="primary" @click="handleHome">返回首页</BoButton> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { BoButton } from 'bolt-ui' |
|||
|
|||
const props = defineProps<{ |
|||
statusCode: number |
|||
statusMessage?: string |
|||
}>() |
|||
|
|||
const statusMessage = computed(() => { |
|||
if (props.statusMessage) return props.statusMessage |
|||
if (props.statusCode === 404) return '页面不存在' |
|||
if (props.statusCode >= 500) return '服务器开小差了' |
|||
return '出错了' |
|||
}) |
|||
|
|||
function handleHome() { |
|||
clearError({ redirect: '/' }) |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.error-page { |
|||
min-height: 100vh; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background-color: var(--color-canvas); |
|||
padding: 2rem; |
|||
} |
|||
|
|||
.error-card { |
|||
text-align: center; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
gap: 1.5rem; |
|||
} |
|||
|
|||
.error-code { |
|||
font-family: var(--font-display); |
|||
font-size: 5rem; |
|||
color: var(--color-primary); |
|||
line-height: 1; |
|||
} |
|||
|
|||
.error-message { |
|||
font-size: 1.125rem; |
|||
color: var(--color-body); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,62 @@ |
|||
interface ClientErrorPayload { |
|||
message: string; |
|||
stack?: string; |
|||
url?: string; |
|||
line?: number; |
|||
column?: number; |
|||
userAgent?: string; |
|||
tags?: string[]; |
|||
} |
|||
|
|||
let reportEndpoint = "/api/_error-report"; |
|||
|
|||
function reportError(payload: ClientErrorPayload) { |
|||
try { |
|||
$fetch(reportEndpoint, { |
|||
method: "POST", |
|||
body: payload, |
|||
}).catch(() => {}); |
|||
} catch { |
|||
// 静默失败,避免错误上报本身导致循环
|
|||
} |
|||
} |
|||
|
|||
export default defineNuxtPlugin((nuxtApp) => { |
|||
// Vue 运行时错误
|
|||
nuxtApp.vueApp.config.errorHandler = (err, _instance, info) => { |
|||
const error = err instanceof Error ? err : new Error(String(err)); |
|||
reportError({ |
|||
message: error.message, |
|||
stack: error.stack, |
|||
url: window.location.href, |
|||
userAgent: navigator.userAgent, |
|||
tags: ["vue", info], |
|||
}); |
|||
}; |
|||
|
|||
// 未捕获的 Promise rejection
|
|||
window.addEventListener("unhandledrejection", (event) => { |
|||
const reason = event.reason; |
|||
const error = reason instanceof Error ? reason : new Error(String(reason)); |
|||
reportError({ |
|||
message: error.message, |
|||
stack: error.stack, |
|||
url: window.location.href, |
|||
userAgent: navigator.userAgent, |
|||
tags: ["unhandledrejection"], |
|||
}); |
|||
}); |
|||
|
|||
// 未捕获的同步错误
|
|||
window.addEventListener("error", (event) => { |
|||
if (!event.message) return; |
|||
reportError({ |
|||
message: event.message, |
|||
url: event.filename || window.location.href, |
|||
line: event.lineno, |
|||
column: event.colno, |
|||
userAgent: navigator.userAgent, |
|||
tags: ["window"], |
|||
}); |
|||
}); |
|||
}); |
|||
Binary file not shown.
@ -0,0 +1,38 @@ |
|||
import log4js from "logger"; |
|||
|
|||
const logger = log4js.getLogger("ERROR"); |
|||
|
|||
interface ClientErrorReport { |
|||
message: string; |
|||
stack?: string; |
|||
url?: string; |
|||
line?: number; |
|||
column?: number; |
|||
userAgent?: string; |
|||
tags?: string[]; |
|||
} |
|||
|
|||
export default defineEventHandler(async (event) => { |
|||
const body = await readBody<ClientErrorReport>(event).catch(() => null); |
|||
if (!body || typeof body.message !== "string") { |
|||
return R.success({ ok: false }); |
|||
} |
|||
|
|||
const requestId = event.context.requestId ?? "(no-request-id)"; |
|||
const ip = getRequestIP(event, { xForwardedFor: true }) ?? "unknown"; |
|||
const tags = Array.isArray(body.tags) ? body.tags.join(",") : "client"; |
|||
|
|||
logger.error( |
|||
`[${requestId}]`, |
|||
`[CLIENT-ERROR]`, |
|||
`[${tags}]`, |
|||
`url=${body.url ?? "-"}`, |
|||
`ip=${ip}`, |
|||
`ua=${body.userAgent ?? "-"}`, |
|||
"\n", |
|||
body.message, |
|||
body.stack ?? "", |
|||
); |
|||
|
|||
return R.success({ ok: true }); |
|||
}); |
|||
@ -1,9 +1,43 @@ |
|||
import log4js from "logger"; |
|||
import fs from "node:fs"; |
|||
import path from "node:path"; |
|||
|
|||
const logger = log4js.getLogger("SCHEDULER"); |
|||
|
|||
const LOG_DIR = path.resolve(process.cwd(), "logs"); |
|||
|
|||
export default async function cleanupLogs(payload?: Record<string, unknown>) { |
|||
const days = (payload?.days as number) ?? 30; |
|||
logger.info("Log cleanup placeholder: would remove logs older than %d days", days); |
|||
return { success: true, message: `Cleaned logs older than ${days} days` }; |
|||
} |
|||
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000; |
|||
|
|||
let deletedCount = 0; |
|||
|
|||
try { |
|||
if (!fs.existsSync(LOG_DIR)) { |
|||
logger.info("Log cleanup: log dir does not exist, skipping"); |
|||
return { success: true, message: "Log dir not found", deleted: 0 }; |
|||
} |
|||
|
|||
const files = fs.readdirSync(LOG_DIR); |
|||
for (const file of files) { |
|||
if (!file.endsWith(".log") && !file.endsWith(".log.gz")) continue; |
|||
|
|||
const filePath = path.join(LOG_DIR, file); |
|||
try { |
|||
const stat = fs.statSync(filePath); |
|||
if (stat.mtimeMs < cutoff) { |
|||
fs.unlinkSync(filePath); |
|||
deletedCount++; |
|||
} |
|||
} catch { |
|||
// 单个文件删除失败不中断整体清理
|
|||
} |
|||
} |
|||
|
|||
logger.info("Log cleanup: removed %d files older than %d days", deletedCount, days); |
|||
return { success: true, message: `Cleaned ${deletedCount} log files older than ${days} days`, deleted: deletedCount }; |
|||
} catch (err) { |
|||
logger.error("Log cleanup failed: %s", err instanceof Error ? err.message : String(err)); |
|||
return { success: false, message: "Log cleanup failed", deleted: 0 }; |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue