/* eslint-disable no-console */ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { Optional } from '@speckle/shared' import type * as Observability from '@speckle/shared/observability' import { upperFirst, get, isBoolean, isNumber, isObjectLike, isString, noop } from 'lodash-es' import type { Logger, Level } from 'pino' import { ResourceLoadError } from '~/lib/core/errors/base' /** * Add pino-pretty like formatting */ export const prettify = (log: object, msg: string) => msg.replace(/{([^{}]+)}/g, (match: string, p1: string) => { const val = get(log, p1) if (val === undefined) return match const formattedValue = isString(val) || isNumber(val) || isBoolean(val) ? val : JSON.stringify(val) return formattedValue as string }) /** * Wrap any logger call w/ logic that prettifies the error message like pino-pretty does * and emits bindings if they are provided */ const prettifiedLoggerFactory = (logger: (...args: unknown[]) => void, bindings?: () => Record) => (...vals: unknown[]) => { const finalVals = vals.slice() const firstObject = finalVals.find((v) => isObjectLike(v) && !Array.isArray(v)) const firstMessageIdx = finalVals.findIndex(isString) if (firstMessageIdx !== -1) { const msg = finalVals[firstMessageIdx] as string finalVals.splice(firstMessageIdx, 1) // remove from array const finalMsg = prettify(firstObject || {}, msg) finalVals.unshift(finalMsg) } if (bindings) { const boundVals = JSON.parse(JSON.stringify(bindings())) finalVals.push(boundVals) } logger(...finalVals) } export function buildFakePinoLogger( options?: Partial<{ /** * Returns an object that will be merged into the log context when outputting to the console. * These will not be sent to seq! */ consoleBindings: () => Record }> ) { const bindings = options?.consoleBindings const logger = { debug: prettifiedLoggerFactory(console.debug, bindings), info: prettifiedLoggerFactory(console.info, bindings), warn: prettifiedLoggerFactory(console.warn, bindings), error: prettifiedLoggerFactory(console.error, bindings), fatal: prettifiedLoggerFactory(console.error, bindings), trace: prettifiedLoggerFactory(console.trace, bindings), silent: noop } as unknown as ReturnType logger.child = () => logger as any return logger } export type SimpleError = { statusCode: number message: string stack?: string } export const formatAppError = (err: SimpleError): SimpleError => { const { statusCode, message, stack } = err let finalMessage = message || '' let finalStatusCode = statusCode || 500 if (finalMessage.match(/^fetch failed$/i)) { finalMessage = 'Internal API call failed, please contact site administrators' } if (finalMessage.match(/status code 429/i)) { finalMessage = 'You are sending too many requests. You have been rate limited. Please try again later.' finalStatusCode = 429 } if (finalMessage.match(/\/_nuxt\/builds\/meta.*?404/i)) { finalMessage = 'Speckle is currently upgrading to a newer version. Please reload the page in a few seconds.' finalStatusCode = 500 } finalMessage = upperFirst(finalMessage) return { statusCode: finalStatusCode, message: finalMessage, stack } } export type AbstractLoggerHandler = ( params: { args: unknown[] firstString: Optional firstError: Optional otherData: Record nonObjectOtherData: unknown[] level: Level }, helpers: { prettifyMessage: (msg: string) => string } ) => void export type AbstractUnhandledErrorHandler = (params: { event: ErrorEvent | PromiseRejectionEvent isUnhandledRejection: boolean error: Error | unknown message: string }) => void export type AbstractLoggerHandlerParams = Parameters[0] /** * Adds proxy that intercepts logger calls so that they can be sent to any transport */ export function enableCustomLoggerHandling(params: { logger: Logger handler: AbstractLoggerHandler }): Logger { const { logger, handler } = params return new Proxy(logger, { get(target, prop) { if ( ['trace', 'debug', 'info', 'warn', 'error', 'fatal'].includes(prop as string) ) { const logMethod = get(target, prop) as (...args: unknown[]) => void return (...args: unknown[]) => { const log = logMethod.bind(target) // Format passed in data, if needed args = args .map((arg) => { // Convert error events to error type if (arg instanceof Event && arg.type === 'error') { return new ResourceLoadError() } return arg }) .filter((arg) => { // Filter out falsy values return !!arg && !(['null', 'undefined'] as unknown[]).includes(arg) }) // If nothing valid to log, skip entirely if (args.length === 0) return const level = prop as Level const firstError = args.find((arg): arg is Error => arg instanceof Error) const firstString = args.find(isString) const otherData: unknown[] = args.filter( (o) => o !== firstString && o !== firstError ) const errorMessage = firstError?.message ?? firstString ?? `Unknown error` if (errorMessage !== firstString) { otherData.unshift(firstString) } const otherDataObjects = otherData.filter(isObjectLike) const otherDataNonObjects = otherData.filter((o) => !isObjectLike(o)) const mergedOtherDataObject = Object.assign( {}, ...otherDataObjects ) as Record handler( { args, firstError, firstString, otherData: mergedOtherDataObject, nonObjectOtherData: otherDataNonObjects, level }, { prettifyMessage: (msg) => prettify(mergedOtherDataObject, msg) } ) return log(...args) } } return get(target, prop) } }) }