| | import path from 'path' |
| | import { getLoggerContext } from '@/observability/logger/lib/logger-context' |
| | import { |
| | getLogLevelNumber, |
| | LOG_LEVELS, |
| | useProductionLogging, |
| | } from '@/observability/logger/lib/log-levels' |
| | import { toLogfmt } from '@/observability/logger/lib/to-logfmt' |
| |
|
| | type IncludeContext = { [key: string]: any } |
| |
|
| | |
| | interface LoggerMethod { |
| | |
| | (message: string): void |
| | |
| | (message: string, extraData: IncludeContext): void |
| | |
| | (message: string, ...messageParts: (string | number | boolean)[]): void |
| | |
| | |
| | |
| | ( |
| | message: string, |
| | ...args: [...messageParts: (string | number | boolean)[], extraData: IncludeContext] |
| | ): void |
| | |
| | |
| | |
| | (message: string, error: Error): void |
| | |
| | |
| | (message: string, ...args: (string | number | boolean | Error | IncludeContext)[]): void |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export function createLogger(filePath: string) { |
| | if (!filePath) { |
| | throw new Error('createLogger must be called with the import.meta.url argument') |
| | } |
| |
|
| | |
| | function isPlainObject(value: any): boolean { |
| | return ( |
| | value !== null && |
| | typeof value === 'object' && |
| | value.constructor === Object && |
| | !(value instanceof Error) && |
| | !(value instanceof Array) && |
| | !(value instanceof Date) |
| | ) |
| | } |
| |
|
| | |
| | function logMessage(level: keyof typeof LOG_LEVELS, message: string, ...args: any[]) { |
| | |
| | let finalMessage: string |
| | let includeContext: IncludeContext = {} |
| |
|
| | |
| | const errorObjects: Error[] = [] |
| | const nonErrorArgs: any[] = [] |
| |
|
| | for (const arg of args) { |
| | if (arg instanceof Error) { |
| | errorObjects.push(arg) |
| | } else { |
| | nonErrorArgs.push(arg) |
| | } |
| | } |
| |
|
| | |
| | if (nonErrorArgs.length > 0 && isPlainObject(nonErrorArgs[nonErrorArgs.length - 1])) { |
| | |
| | includeContext = { ...nonErrorArgs[nonErrorArgs.length - 1] } |
| | const messageParts = nonErrorArgs.slice(0, -1) |
| | if (messageParts.length > 0) { |
| | |
| | const allMessageParts = [ |
| | message, |
| | ...messageParts.map((arg) => (typeof arg === 'string' ? arg : String(arg))), |
| | ] |
| | finalMessage = allMessageParts.join(' ') |
| | } else { |
| | |
| | finalMessage = message |
| | } |
| | } else if (nonErrorArgs.length > 0) { |
| | |
| | const allMessageParts = [ |
| | message, |
| | ...nonErrorArgs.map((arg) => (typeof arg === 'string' ? arg : String(arg))), |
| | ] |
| | finalMessage = allMessageParts.join(' ') |
| | } else { |
| | |
| | finalMessage = message |
| | } |
| |
|
| | |
| | if (errorObjects.length > 0) { |
| | if (errorObjects.length === 1) { |
| | |
| | includeContext.error = errorObjects[0] |
| | finalMessage = `${finalMessage}: ${errorObjects[0].message}` |
| | } else { |
| | |
| | for (let index = 0; index < errorObjects.length; index++) { |
| | const error = errorObjects[index] |
| | includeContext[`error_${index + 1}`] = error |
| | } |
| | const errorMessages = errorObjects.map((err) => err.message).join(', ') |
| | finalMessage = `${finalMessage}: ${errorMessages}` |
| | } |
| | } |
| | |
| | const currentLogLevel = getLogLevelNumber() |
| | if (LOG_LEVELS[level] > currentLogLevel) { |
| | return |
| | } |
| |
|
| | const loggerContext = getLoggerContext() |
| | const timestamp = new Date().toISOString() |
| |
|
| | if (useProductionLogging()) { |
| | |
| | const logObject: IncludeContext = { |
| | ...loggerContext, |
| | timestamp, |
| | level, |
| | file: path.relative(process.cwd(), new URL(filePath).pathname), |
| | message: finalMessage, |
| | } |
| |
|
| | |
| | const includedContextWithFormattedError = {} as IncludeContext |
| | for (const [key, value] of Object.entries(includeContext)) { |
| | if (typeof value === 'object' && value instanceof Error) { |
| | |
| | includedContextWithFormattedError[key] = value.message |
| | includedContextWithFormattedError[`${key}_stack`] = value.stack |
| | } else { |
| | includedContextWithFormattedError[key] = value |
| | } |
| | } |
| |
|
| | |
| | logObject.included = includedContextWithFormattedError |
| |
|
| | console.log(toLogfmt(logObject)) |
| | } else { |
| | |
| | let wasErrorLog = false |
| | for (const [, value] of Object.entries(includeContext)) { |
| | if (typeof value === 'object' && value instanceof Error) { |
| | wasErrorLog = true |
| | console.log(`[${level.toUpperCase()}] ${finalMessage}`) |
| | console.error(value) |
| | } |
| | } |
| | if (!wasErrorLog) { |
| | console.log(`[${level.toUpperCase()}] ${finalMessage}`) |
| | } |
| | } |
| | } |
| |
|
| | return { |
| | error: logMessage.bind(null, 'error') as LoggerMethod, |
| | warn: logMessage.bind(null, 'warn') as LoggerMethod, |
| | info: logMessage.bind(null, 'info') as LoggerMethod, |
| | debug: logMessage.bind(null, 'debug') as LoggerMethod, |
| | } |
| | } |
| |
|