优化:降低访问日志噪声并增加日志文件轮转
This commit is contained in:
@@ -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}$/)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user