优化:降低访问日志噪声并增加日志文件轮转

This commit is contained in:
yml2213
2026-08-30 16:59:11 +08:00
parent 6ca54e468e
commit 25b89bb503
20 changed files with 2334 additions and 4 deletions
+10
View File
@@ -77,6 +77,16 @@ test('resolveExpiredLogFilenames supports explicit retention days', () => {
assert.deepEqual(expired, ['app-2026-04-07.log'])
})
test('resolveExpiredLogFilenames includes rotated files', () => {
const expired = resolveExpiredLogFilenames(
['app-2026-04-14.1.log', 'app-2026-04-07.5.log', 'app-2026-04-14.log'],
'2026-04-14',
7,
)
assert.deepEqual(expired, ['app-2026-04-07.5.log'])
})
test('formatLogTimestamp renders readable local timestamp with timezone offset', () => {
const rendered = formatLogTimestamp('2026-04-14T10:36:24.974Z')
assert.match(rendered, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2}$/)
+73 -1
View File
@@ -56,6 +56,11 @@ const ACTIVE_INTEGRATION_LOG_LEVEL = normalizeLogLevel(
)
const DEFAULT_LOG_RETENTION_DAYS = 30
const LOG_RETENTION_DAYS = normalizeLogRetentionDays(runtimeConfig.logging?.retentionDays)
const DEFAULT_LOG_MAX_FILE_SIZE_MB = 20
const DEFAULT_LOG_MAX_FILES = 5
const LOG_MAX_FILE_SIZE_BYTES =
normalizeLogMaxFileSizeMb(runtimeConfig.logging?.maxFileSizeMb) * 1024 * 1024
const LOG_MAX_FILES = normalizeLogMaxFiles(runtimeConfig.logging?.maxFiles)
const LEGACY_LOG_FILES = new Set(['app.log', 'integration.log'])
const SENSITIVE_KEY_PATTERN =
/token|secret|password|passwd|pwd|cookie|authorization|credential|card|cards|cardno|cardpwd|apikey|api_key|devicekey|session|x-sign|(?:^|[_-])sign(?:ature)?(?:[_-]|$)/i
@@ -181,6 +186,7 @@ function enqueueFileWrite(
writeQueue = writeQueue
.then(async () => {
await fs.mkdir(LOG_DIR, { recursive: true })
await rotateLogFileIfNeeded(filePath, Buffer.byteLength(`${line}\n`, 'utf8'))
await fs.appendFile(filePath, `${line}\n`, 'utf8')
await cleanupExpiredLogsIfNeeded(dateKey)
})
@@ -666,12 +672,78 @@ function normalizeLogRetentionDays(value: unknown): number {
return Math.max(1, Math.floor(parsed))
}
function normalizeLogMaxFileSizeMb(value: unknown): number {
const parsed = Number(value)
if (!Number.isFinite(parsed)) {
return DEFAULT_LOG_MAX_FILE_SIZE_MB
}
return Math.max(1, Math.floor(parsed))
}
function normalizeLogMaxFiles(value: unknown): number {
const parsed = Number(value)
if (!Number.isFinite(parsed)) {
return DEFAULT_LOG_MAX_FILES
}
return Math.max(2, Math.floor(parsed))
}
async function rotateLogFileIfNeeded(filePath: string, nextLineBytes: number): Promise<void> {
if (!filePath.endsWith('.log') || LOG_MAX_FILE_SIZE_BYTES <= 0) {
return
}
let currentSize = 0
try {
currentSize = (await fs.stat(filePath)).size
} catch (error) {
if (isFileMissingError(error)) {
return
}
throw error
}
if (currentSize === 0 || currentSize + nextLineBytes <= LOG_MAX_FILE_SIZE_BYTES) {
return
}
for (let index = LOG_MAX_FILES - 1; index >= 1; index -= 1) {
await renameIfPresent(
rotatedLogFilePath(filePath, index),
rotatedLogFilePath(filePath, index + 1),
)
}
await renameIfPresent(filePath, rotatedLogFilePath(filePath, 1))
}
function rotatedLogFilePath(filePath: string, index: number): string {
return filePath.replace(/\.log$/, `.${index}.log`)
}
async function renameIfPresent(source: string, target: string): Promise<void> {
try {
await fs.rename(source, target)
} catch (error) {
if (!isFileMissingError(error)) {
throw error
}
}
}
function isFileMissingError(error: unknown): boolean {
return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
}
function shouldDeleteLogFileByDate(fileName: unknown, cutoffDateKey: string): boolean {
if (LEGACY_LOG_FILES.has(String(fileName || '').trim())) {
return true
}
const matched = /^([a-z0-9-]{1,40})-(\d{4}-\d{2}-\d{2})\.log$/.exec(String(fileName || '').trim())
const matched = /^([a-z0-9-]{1,40})-(\d{4}-\d{2}-\d{2})(?:\.\d+)?\.log$/.exec(
String(fileName || '').trim(),
)
if (!matched) {
return false