fix(logger): cache isProduction and prevent error double-serialization

Cache isProduction() result at module load to avoid repeated property
lookups. Add isSerializedError() check in format functions to skip
re-serialization when error objects have already been processed by
logError/formatErrorForLogging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-04-03 21:16:04 +08:00
parent 0a1181fecd
commit 21359b31c6

View File

@@ -14,6 +14,23 @@ import path from 'path'
import { serializeError, sanitizeError } from './error-utils' import { serializeError, sanitizeError } from './error-utils'
import { getLogDir, isProduction } from './shared' import { getLogDir, isProduction } from './shared'
// Cache isProduction() at module load — app.isPackaged never changes at runtime
const IS_PROD = isProduction()
/**
* Check if an error has already been serialized (plain object with name/message but not an Error instance).
* Prevents double-serialization when logError() output passes through the format pipeline.
*/
function isSerializedError(value: unknown): boolean {
return (
typeof value === 'object' &&
value !== null &&
!(value instanceof Error) &&
'name' in value &&
'message' in value
)
}
// Custom format for console output - includes full error details // Custom format for console output - includes full error details
const consoleFormat = winston.format.combine( const consoleFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
@@ -24,9 +41,12 @@ const consoleFormat = winston.format.combine(
// Format error with full stack trace // Format error with full stack trace
let errorStr = '' let errorStr = ''
if (error) { if (error) {
const serialized = isProduction() // Skip re-serialization if already a serialized error object
? sanitizeError(serializeError(error)) const serialized: { stack?: string; message: string } = isSerializedError(error)
: serializeError(error) ? (error as { stack?: string; message: string })
: IS_PROD
? sanitizeError(serializeError(error))
: serializeError(error)
if (serialized.stack) { if (serialized.stack) {
errorStr = `\n${serialized.stack}` errorStr = `\n${serialized.stack}`
} else { } else {
@@ -43,17 +63,19 @@ const consoleFormat = winston.format.combine(
const fileFormat = winston.format.combine( const fileFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format((info) => { winston.format((info) => {
// Serialize errors in metadata // Serialize errors in metadata (skip if already serialized)
if (info.error) { if (info.error) {
info.error = isProduction() if (!isSerializedError(info.error)) {
? sanitizeError(serializeError(info.error)) info.error = IS_PROD
: serializeError(info.error) ? sanitizeError(serializeError(info.error))
: serializeError(info.error)
}
} }
// Serialize any error in meta fields // Serialize any error in meta fields (skip if already serialized)
for (const key of Object.keys(info)) { for (const key of Object.keys(info)) {
if (key !== 'error' && info[key] instanceof Error) { if (key !== 'error' && info[key] instanceof Error) {
info[key] = isProduction() info[key] = IS_PROD
? sanitizeError(serializeError(info[key])) ? sanitizeError(serializeError(info[key]))
: serializeError(info[key]) : serializeError(info[key])
} }
@@ -119,7 +141,7 @@ export function applyLoggingConfig(config: { level: string; appRetention: number
logger.add(createFileTransport(undefined, retentionStr)) logger.add(createFileTransport(undefined, retentionStr))
// Add error-specific file transport in production // Add error-specific file transport in production
if (isProduction()) { if (IS_PROD) {
logger.add( logger.add(
new DailyRotateFile({ new DailyRotateFile({
filename: path.join(getLogDir(), 'error-%DATE%.log'), filename: path.join(getLogDir(), 'error-%DATE%.log'),