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 log4js from "logger"; |
||||
|
import fs from "node:fs"; |
||||
|
import path from "node:path"; |
||||
|
|
||||
const logger = log4js.getLogger("SCHEDULER"); |
const logger = log4js.getLogger("SCHEDULER"); |
||||
|
|
||||
|
const LOG_DIR = path.resolve(process.cwd(), "logs"); |
||||
|
|
||||
export default async function cleanupLogs(payload?: Record<string, unknown>) { |
export default async function cleanupLogs(payload?: Record<string, unknown>) { |
||||
const days = (payload?.days as number) ?? 30; |
const days = (payload?.days as number) ?? 30; |
||||
logger.info("Log cleanup placeholder: would remove logs older than %d days", days); |
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000; |
||||
return { success: true, message: `Cleaned logs older than ${days} days` }; |
|
||||
|
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