diff --git a/packages/toolpad-app/pages/api/rpc.ts b/packages/toolpad-app/pages/api/rpc.ts index a0f56d9fe38..fe0d660d39e 100644 --- a/packages/toolpad-app/pages/api/rpc.ts +++ b/packages/toolpad-app/pages/api/rpc.ts @@ -24,7 +24,8 @@ import { } from '../../src/server/data'; import { getLatestToolpadRelease } from '../../src/server/getLatestRelease'; import { hasOwnProperty } from '../../src/utils/collections'; -import { withRpcReqResLogs } from '../../src/server/logs/withLogs'; +import { errorFrom, serializeError } from '../../src/utils/errors'; +import logger from '../../src/server/logs/logger'; export interface Method

{ (...params: P): Promise; @@ -72,6 +73,7 @@ function createRpcHandler(definition: Definition): NextApiHandler { res.status(405).end(); return; } + const { type, name, params } = req.body as RpcRequest; if (!hasOwnProperty(definition, type) || !hasOwnProperty(definition[type], name)) { @@ -82,20 +84,29 @@ function createRpcHandler(definition: Definition): NextApiHandler { const method: MethodResolver = definition[type][name]; let rawResult; + let error: Error | null = null; try { rawResult = await method({ params, req, res }); - } catch (error) { - console.error(error); - if (error instanceof Error) { - res.json({ error: { message: error.message, code: error.code, stack: error.stack } }); - } else { - res.status(500).end(); - } - - return; + } catch (rawError) { + error = errorFrom(rawError); } - const responseData: RpcResponse = { result: superjson.stringify(rawResult) }; + + const responseData: RpcResponse = error + ? { error: serializeError(error) } + : { result: superjson.stringify(rawResult) }; + res.json(responseData); + + const logLevel = error ? 'warn' : 'trace'; + logger[logLevel]( + { + key: 'rpc', + type, + name, + error, + }, + 'Handled RPC request', + ); }; } @@ -182,4 +193,4 @@ const rpcServer = { export type ServerDefinition = MethodsOf; -export default withRpcReqResLogs(createRpcHandler(rpcServer)); +export default createRpcHandler(rpcServer); diff --git a/packages/toolpad-app/src/server/config.ts b/packages/toolpad-app/src/server/config.ts index 66147f068c5..58ba56a314e 100644 --- a/packages/toolpad-app/src/server/config.ts +++ b/packages/toolpad-app/src/server/config.ts @@ -17,7 +17,6 @@ export type ServerConfig = { encryptionKeys: string[]; basicAuthUser?: string; basicAuthPassword?: string; - serverLogsEnabled: boolean; recaptchaV2SecretKey?: string; recaptchaV3SecretKey?: string; ecsNodeUrl?: string; @@ -55,7 +54,6 @@ function readConfig(): ServerConfig & typeof sharedConfig { databaseUrl: process.env.TOOLPAD_DATABASE_URL, googleSheetsClientId: process.env.TOOLPAD_DATASOURCE_GOOGLESHEETS_CLIENT_ID, googleSheetsClientSecret: process.env.TOOLPAD_DATASOURCE_GOOGLESHEETS_CLIENT_SECRET, - serverLogsEnabled: !!process.env.TOOLPAD_SERVER_LOGS_ENABLED, recaptchaV2SecretKey: process.env.TOOLPAD_RECAPTCHA_V2_SECRET_KEY, recaptchaV3SecretKey: process.env.TOOLPAD_RECAPTCHA_V3_SECRET_KEY, ecsNodeUrl: process.env.TOOLPAD_ECS_NODE_URL, diff --git a/packages/toolpad-app/src/server/logs/logInfo.ts b/packages/toolpad-app/src/server/logs/logInfo.ts deleted file mode 100644 index 30f63500ccb..00000000000 --- a/packages/toolpad-app/src/server/logs/logInfo.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import logger from './logger'; -import type { RpcResponse } from '../../../pages/api/rpc'; - -type ReqResLogPayload = { - key: 'apiReqRes'; - req: NextApiRequest; - res: NextApiResponse; -}; - -type RpcReqResLogPayload = { - key: 'rpcReqRes'; - req: NextApiRequest; - res: NextApiResponse; - resErr?: RpcResponse['error']; -}; - -type LogPayload = ReqResLogPayload | RpcReqResLogPayload; - -function logInfo(payload: LogPayload, message?: string): void { - logger.info(payload, message); -} - -export default logInfo; diff --git a/packages/toolpad-app/src/server/logs/logSerializers.ts b/packages/toolpad-app/src/server/logs/logSerializers.ts index ed1e030b408..d245ed65288 100644 --- a/packages/toolpad-app/src/server/logs/logSerializers.ts +++ b/packages/toolpad-app/src/server/logs/logSerializers.ts @@ -1,4 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next'; +import { errorFrom } from '../../utils/errors'; function getReqLoggableIPAddress(req: NextApiRequest): string | null { const forwardedHeader = req.headers['x-forwarded-for']; @@ -38,9 +39,12 @@ export function resSerializer(res: NextApiResponse) { }; } -export function resErrSerializer(error: Error) { +export function errSerializer(rawError: unknown) { + const error = errorFrom(rawError); return { message: error.message, + name: error.name, + stack: error.stack, code: error.code, }; } diff --git a/packages/toolpad-app/src/server/logs/logger.ts b/packages/toolpad-app/src/server/logs/logger.ts index 23f13e9a0bd..9cadc767d90 100644 --- a/packages/toolpad-app/src/server/logs/logger.ts +++ b/packages/toolpad-app/src/server/logs/logger.ts @@ -1,8 +1,8 @@ import pino from 'pino'; import ecsFormat from '@elastic/ecs-pino-format'; - +import type { NextApiRequest, NextApiResponse } from 'next'; import config from '../config'; -import { reqSerializer, resSerializer, resErrSerializer } from './logSerializers'; +import { errSerializer, reqSerializer, resSerializer } from './logSerializers'; let transport; if (config.ecsNodeUrl) { @@ -21,17 +21,42 @@ if (config.ecsNodeUrl) { const logger = pino( { - enabled: config.serverLogsEnabled, level: process.env.LOG_LEVEL || 'info', redact: { paths: [] }, serializers: { + err: errSerializer, + error: errSerializer, req: reqSerializer, res: resSerializer, - resErr: resErrSerializer, }, ...(config.ecsNodeUrl ? ecsFormat() : {}), }, transport, ); -export default logger; +interface ReqResLogPayload { + key: 'apiReqRes'; + req: NextApiRequest; + res: NextApiResponse; +} + +interface RpcReqResLogPayload { + key: 'rpc'; + type: 'query' | 'mutation'; + name: string; + error: Error | null; +} + +type LogPayload = ReqResLogPayload | RpcReqResLogPayload; + +function logMethod(method: 'info' | 'trace' | 'error' | 'warn' | 'fatal') { + return (obj: LogPayload, msg: string) => logger[method](obj, msg); +} + +export default { + info: logMethod('info'), + trace: logMethod('trace'), + error: logMethod('error'), + warn: logMethod('warn'), + fatal: logMethod('fatal'), +}; diff --git a/packages/toolpad-app/src/server/logs/withLogs.ts b/packages/toolpad-app/src/server/logs/withLogs.ts index baf16e5c201..b6efbc49b35 100644 --- a/packages/toolpad-app/src/server/logs/withLogs.ts +++ b/packages/toolpad-app/src/server/logs/withLogs.ts @@ -1,44 +1,10 @@ import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; -import logInfo from './logInfo'; -import type { RpcResponse } from '../../../pages/api/rpc'; - -type RestArgs = [ - chunk: any, - encoding: BufferEncoding, - callback?: ((error?: Error | null) => void) | undefined, -]; - -const logWithResponseBody = ( - res: NextApiResponse, - logHandler: (responseBody: Record) => void, -): void => { - const oldWrite = res.write; - const oldEnd = res.end; - - const chunks: Buffer[] = []; - res.write = (...restArgs: any[]) => { - chunks.push(Buffer.from(restArgs[0])); - return oldWrite.apply(res, restArgs as RestArgs); - }; - - res.end = (...restArgs: any[]) => { - if (restArgs[0]) { - chunks.push(Buffer.from(restArgs[0])); - } - - const loggableResponseBody = JSON.parse(Buffer.concat(chunks).toString('utf8'), (key, value) => - key === 'stack' ? undefined : value, - ); - logHandler(loggableResponseBody); - - return oldEnd.apply(res, restArgs as RestArgs); - }; -}; +import logger from './logger'; export const withReqResLogs = (apiHandler: NextApiHandler) => (req: NextApiRequest, res: NextApiResponse): unknown | Promise => { - logInfo( + logger.info( { key: 'apiReqRes', req, @@ -49,23 +15,3 @@ export const withReqResLogs = return apiHandler(req, res); }; - -export const withRpcReqResLogs = - (apiHandler: NextApiHandler) => - async (req: NextApiRequest, res: NextApiResponse): Promise => { - logWithResponseBody(res, (resBody) => { - const error = (resBody as RpcResponse).error; - - logInfo( - { - key: 'rpcReqRes', - req, - res, - ...(error ? { resErr: error } : {}), - }, - 'Handled RPC request', - ); - }); - - return apiHandler(req, res); - };