增加feifei 日志
This commit is contained in:
@@ -11,6 +11,7 @@ type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
||||
type LogChannel = 'app' | 'integration'
|
||||
type LogDetail = unknown
|
||||
type InlineFields = Record<string, string | number | boolean | null | undefined>
|
||||
type ExternalHttpPacketDetail = Record<string, unknown>
|
||||
type LogEntry = {
|
||||
time: string
|
||||
level: LogLevel
|
||||
@@ -51,7 +52,12 @@ const ACTIVE_LOG_LEVEL = normalizeLogLevel(runtimeConfig.logging?.level)
|
||||
const DEFAULT_LOG_RETENTION_DAYS = 30
|
||||
const LOG_RETENTION_DAYS = normalizeLogRetentionDays(runtimeConfig.logging?.retentionDays)
|
||||
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/i
|
||||
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
|
||||
const SENSITIVE_URL_PARAM_PATTERN =
|
||||
/([?&](?:token|secret|password|passwd|pwd|cookie|authorization|api[_-]?key|device[_-]?key|sign|signature|code)=)([^&\s,;"']+)/gi
|
||||
const SENSITIVE_JSON_FIELD_PATTERN =
|
||||
/("(?:token|secret|password|passwd|pwd|cookie|authorization|credential|card|cards|cardNo|cardPwd|apiKey|api_key|deviceKey|device_key|session|x-sign|sign|signature)"\s*:\s*")([^"]*)(")/gi
|
||||
const MAX_SANITIZE_DEPTH = 8
|
||||
|
||||
let writeQueue = Promise.resolve()
|
||||
@@ -86,6 +92,15 @@ export function logIntegration(
|
||||
return writeLog(level, scope, message, detail, { channel: 'integration' })
|
||||
}
|
||||
|
||||
export function logExternalHttpPacket(
|
||||
scope: unknown,
|
||||
message: unknown,
|
||||
detail: ExternalHttpPacketDetail = {},
|
||||
{ level = 'info' }: { level?: LogLevel } = {},
|
||||
) {
|
||||
return logIntegration(scope, message, normalizeExternalHttpPacketDetail(detail), { level })
|
||||
}
|
||||
|
||||
function writeLog(
|
||||
level: LogLevel,
|
||||
scope: unknown,
|
||||
@@ -147,18 +162,26 @@ function resolveConsoleMethod(level: unknown) {
|
||||
}
|
||||
|
||||
export function normalizeLogLevel(level: unknown): LogLevel {
|
||||
const normalized = String(level || '').trim().toLowerCase()
|
||||
const normalized = String(level || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return isLogLevel(normalized) ? normalized : 'info'
|
||||
}
|
||||
|
||||
export function shouldWriteLog(level: unknown, configuredLevel: unknown = ACTIVE_LOG_LEVEL): boolean {
|
||||
export function shouldWriteLog(
|
||||
level: unknown,
|
||||
configuredLevel: unknown = ACTIVE_LOG_LEVEL,
|
||||
): boolean {
|
||||
const normalizedLevel = normalizeLogLevel(level)
|
||||
const normalizedConfigured = normalizeLogLevel(configuredLevel)
|
||||
|
||||
return LOG_LEVEL_PRIORITY[normalizedLevel] >= LOG_LEVEL_PRIORITY[normalizedConfigured]
|
||||
}
|
||||
|
||||
export function formatLogEntry(entry: Partial<LogEntry>, { color = false }: { color?: boolean } = {}): string {
|
||||
export function formatLogEntry(
|
||||
entry: Partial<LogEntry>,
|
||||
{ color = false }: { color?: boolean } = {},
|
||||
): string {
|
||||
const normalizedEntry = {
|
||||
time: String(entry?.time || new Date().toISOString()),
|
||||
level: normalizeLogLevel(entry?.level),
|
||||
@@ -168,7 +191,11 @@ export function formatLogEntry(entry: Partial<LogEntry>, { color = false }: { co
|
||||
detail: sanitizeLogValue(entry?.detail),
|
||||
}
|
||||
const timestamp = colorize(formatLogTimestamp(normalizedEntry.time), ANSI.gray, color)
|
||||
const level = colorize(LEVEL_LABELS[normalizedEntry.level], resolveLevelColor(normalizedEntry.level), color)
|
||||
const level = colorize(
|
||||
LEVEL_LABELS[normalizedEntry.level],
|
||||
resolveLevelColor(normalizedEntry.level),
|
||||
color,
|
||||
)
|
||||
const scope = colorize(normalizedEntry.scope, ANSI.cyan, color)
|
||||
const message = colorize(normalizedEntry.message, ANSI.bold, color)
|
||||
const separator = colorize('|', ANSI.dim, color)
|
||||
@@ -177,7 +204,9 @@ export function formatLogEntry(entry: Partial<LogEntry>, { color = false }: { co
|
||||
pid: normalizedEntry.pid,
|
||||
...inline,
|
||||
})
|
||||
const firstLine = [timestamp, level, scope, message, separator, inlineFields].filter(Boolean).join(' ')
|
||||
const firstLine = [timestamp, level, scope, message, separator, inlineFields]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
if (!block) {
|
||||
return firstLine
|
||||
@@ -207,8 +236,9 @@ export function resolveExpiredLogFilenames(
|
||||
const normalizedRetentionDays = normalizeLogRetentionDays(retentionDays)
|
||||
const cutoffDateKey = toDateKey(offsetDate(referenceTime, -(normalizedRetentionDays - 1)))
|
||||
|
||||
return (Array.isArray(fileNames) ? fileNames : [])
|
||||
.filter((fileName) => shouldDeleteLogFileByDate(fileName, cutoffDateKey))
|
||||
return (Array.isArray(fileNames) ? fileNames : []).filter((fileName) =>
|
||||
shouldDeleteLogFileByDate(fileName, cutoffDateKey),
|
||||
)
|
||||
}
|
||||
|
||||
export function formatLogTimestamp(value: unknown): string {
|
||||
@@ -232,27 +262,29 @@ function normalizeLogValue(value: unknown): unknown {
|
||||
}
|
||||
|
||||
try {
|
||||
return sanitizeLogValue(JSON.parse(
|
||||
JSON.stringify(value, (_key, current) => {
|
||||
if (current instanceof Error) {
|
||||
const currentError = current as Error & HttpErrorLike
|
||||
return {
|
||||
name: current.name,
|
||||
message: sanitizeLogString(current.message),
|
||||
stack: sanitizeLogString(current.stack),
|
||||
statusCode: currentError.statusCode,
|
||||
errorCode: currentError.errorCode,
|
||||
context: sanitizeLogValue(currentError.context),
|
||||
return sanitizeLogValue(
|
||||
JSON.parse(
|
||||
JSON.stringify(value, (_key, current) => {
|
||||
if (current instanceof Error) {
|
||||
const currentError = current as Error & HttpErrorLike
|
||||
return {
|
||||
name: current.name,
|
||||
message: sanitizeLogString(current.message),
|
||||
stack: sanitizeLogString(current.stack),
|
||||
statusCode: currentError.statusCode,
|
||||
errorCode: currentError.errorCode,
|
||||
context: sanitizeLogValue(currentError.context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof current === 'bigint') {
|
||||
return String(current)
|
||||
}
|
||||
if (typeof current === 'bigint') {
|
||||
return String(current)
|
||||
}
|
||||
|
||||
return current
|
||||
}),
|
||||
))
|
||||
return current
|
||||
}),
|
||||
),
|
||||
)
|
||||
} catch {
|
||||
return sanitizeLogString(String(value))
|
||||
}
|
||||
@@ -305,7 +337,18 @@ function sanitizeLogValue(value: unknown, key = '', depth = 0): unknown {
|
||||
|
||||
function sanitizeLogString(value: unknown): string {
|
||||
return String(value || '')
|
||||
.replace(/(Bearer\s+)([A-Za-z0-9._~+/=-]+)/gi, (_matched, prefix, secret) => `${prefix}${maskSecret(secret)}`)
|
||||
.replace(
|
||||
/(Bearer\s+)([A-Za-z0-9._~+/=-]+)/gi,
|
||||
(_matched, prefix, secret) => `${prefix}${maskSecret(secret)}`,
|
||||
)
|
||||
.replace(
|
||||
SENSITIVE_URL_PARAM_PATTERN,
|
||||
(_matched, prefix, secret) => `${prefix}${maskSecret(secret)}`,
|
||||
)
|
||||
.replace(
|
||||
SENSITIVE_JSON_FIELD_PATTERN,
|
||||
(_matched, prefix, secret, suffix) => `${prefix}${maskSecret(secret)}${suffix}`,
|
||||
)
|
||||
.replace(
|
||||
/\b((?:token|secret|password|passwd|pwd|cookie|authorization|api[_-]?key|device[_-]?key)=)([^&\s,;]+)/gi,
|
||||
(_matched, prefix, secret) => `${prefix}${maskSecret(secret)}`,
|
||||
@@ -316,7 +359,61 @@ function isSensitiveLogKey(key: unknown): boolean {
|
||||
return SENSITIVE_KEY_PATTERN.test(String(key || '').trim())
|
||||
}
|
||||
|
||||
function splitDetailPayload(detail: unknown): { inline: InlineFields, block: unknown | null } {
|
||||
function normalizeExternalHttpPacketDetail(
|
||||
detail: ExternalHttpPacketDetail,
|
||||
): ExternalHttpPacketDetail {
|
||||
const source = detail && typeof detail === 'object' && !Array.isArray(detail) ? detail : {}
|
||||
const normalized: ExternalHttpPacketDetail = {}
|
||||
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
normalized[key] = normalizeExternalPacketValue(value)
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
function normalizeExternalPacketValue(value: unknown): unknown {
|
||||
if (typeof value === 'undefined' || value === null) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (value instanceof Error) {
|
||||
const currentError = value as Error & HttpErrorLike
|
||||
return {
|
||||
name: value.name,
|
||||
message: value.message,
|
||||
stack: value.stack,
|
||||
statusCode: currentError.statusCode,
|
||||
errorCode: currentError.errorCode,
|
||||
context: normalizeExternalPacketValue(currentError.context),
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => normalizeExternalPacketValue(item))
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, currentValue]) => [
|
||||
key,
|
||||
normalizeExternalPacketValue(currentValue),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function splitDetailPayload(detail: unknown): { inline: InlineFields; block: unknown | null } {
|
||||
if (typeof detail === 'undefined') {
|
||||
return {
|
||||
inline: {},
|
||||
@@ -446,7 +543,9 @@ async function cleanupExpiredLogsIfNeeded(dateKey: string): Promise<void> {
|
||||
try {
|
||||
const fileNames = await fs.readdir(LOG_DIR)
|
||||
const expired = resolveExpiredLogFilenames(fileNames, dateKey, LOG_RETENTION_DAYS)
|
||||
await Promise.all(expired.map((fileName) => fs.rm(path.join(LOG_DIR, fileName), { force: true })))
|
||||
await Promise.all(
|
||||
expired.map((fileName) => fs.rm(path.join(LOG_DIR, fileName), { force: true })),
|
||||
)
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error || '未知错误')
|
||||
console.error('[logger] failed to cleanup old log files:', reason)
|
||||
|
||||
Reference in New Issue
Block a user