From 8c7cac369a582562f41b198b0e6de86220f3ed76 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 13 Jul 2025 01:16:55 -0700 Subject: [PATCH] add experimental request logger --- src/renderer/api/api-controller-logger.ts | 174 ++++++++++++++++++ src/renderer/api/api-controller.ts | 5 +- .../api/subsonic/subsonic-controller.ts | 8 +- 3 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 src/renderer/api/api-controller-logger.ts diff --git a/src/renderer/api/api-controller-logger.ts b/src/renderer/api/api-controller-logger.ts new file mode 100644 index 000000000..4b4759517 --- /dev/null +++ b/src/renderer/api/api-controller-logger.ts @@ -0,0 +1,174 @@ +import { + ApiController, + ApiControllerError, + ApiControllerFn, +} from '/@/shared/types/adapter/api-controller-types'; +import { ServerListItem } from '/@/shared/types/domain/server-domain-types'; +import { logger } from '/@/shared/utils/logger'; + +export interface LoggingOptions { + logErrors?: boolean; + logPerformance?: boolean; + logRequests?: boolean; + logResponses?: boolean; + maxRequestSize?: number; + maxResponseSize?: number; +} + +type LoggedApiControllerFn = ( + request: TRequest, + server: ServerListItem, + options?: any, +) => Promise<[ApiControllerError, null] | [null, TResponse]>; + +export function createLoggedApiController( + controller: ApiController, + options: LoggingOptions = {}, +): ApiController { + const loggedController: any = {}; + + // Log utility functions + loggedController._utility = createLoggedUtility(controller._utility); + + // Log all controller methods + for (const [sectionKey, section] of Object.entries(controller)) { + if (sectionKey === '_utility') continue; + + loggedController[sectionKey] = {}; + + for (const [methodKey, method] of Object.entries(section as Record)) { + if (typeof method === 'function') { + const functionName = `${sectionKey}.${methodKey}`; + + if (methodKey === 'authenticate' || methodKey === 'getType') { + // Special handling for non-standard API functions + loggedController[sectionKey][methodKey] = (...args: any[]) => { + logger.info(`[API] ${functionName} called`, { + args: JSON.stringify(args, null, 2), + }); + return method(...args); + }; + } else { + loggedController[sectionKey][methodKey] = createLoggedFunction( + method as ApiControllerFn, + functionName, + options, + ); + } + } else { + loggedController[sectionKey][methodKey] = method; + } + } + } + + return loggedController as ApiController; +} + +function createLoggedFunction( + originalFn: ApiControllerFn | undefined, + functionName: string, + options: LoggingOptions = {}, +): LoggedApiControllerFn | undefined { + if (!originalFn) { + return undefined; + } + + return async (request: TRequest, server: ServerListItem, options?: any) => { + const startTime = Date.now(); + const requestId = Math.random().toString(36).substring(2, 15); + + const { + logErrors = true, + logPerformance = true, + logRequests = true, + logResponses = true, + maxRequestSize = 1000, + maxResponseSize = 1000, + } = options; + + if (logRequests) { + const requestStr = JSON.stringify(request, null, 2); + const truncatedRequest = + requestStr.length > maxRequestSize + ? requestStr.substring(0, maxRequestSize) + '...' + : requestStr; + + logger.info(`[API] ${functionName} called`, { + request: truncatedRequest, + requestId, + serverId: server.id, + serverType: server.type, + }); + } + + try { + const result = await originalFn(request, server, options); + const duration = Date.now() - startTime; + + if (result[0]) { + // Error response + if (logErrors) { + const error = result[0] as ApiControllerError; + logger.error(`[API] ${functionName} failed`, { + duration: logPerformance ? `${duration}ms` : undefined, + error: { + code: error.code, + message: error.message, + }, + requestId, + }); + } + } else { + // Success response + if (logResponses) { + const response = result[1]; + const responseStr = JSON.stringify(response); + const truncatedResponse = + responseStr.length > maxResponseSize + ? responseStr.substring(0, maxResponseSize) + '...' + : responseStr; + + logger.info(`[API] ${functionName} succeeded`, { + duration: logPerformance ? `${duration}ms` : undefined, + requestId, + response: truncatedResponse, + responseSize: responseStr.length, + responseType: typeof response, + }); + } + } + + return result; + } catch (error) { + const duration = Date.now() - startTime; + if (logErrors) { + logger.error(`[API] ${functionName} threw exception`, { + duration: logPerformance ? `${duration}ms` : undefined, + error: error instanceof Error ? error.message : String(error), + requestId, + stack: error instanceof Error ? error.stack : undefined, + }); + } + throw error; + } + }; +} + +function createLoggedUtility>(utility: T): T { + const loggedUtility: any = {}; + + for (const [key, value] of Object.entries(utility)) { + if (typeof value === 'function') { + loggedUtility[key] = (...args: any[]) => { + logger.debug(`[API] _utility.${key} called`, { + args: JSON.stringify(args, null, 2), + }); + return value(...args); + }; + } else { + loggedUtility[key] = value; + } + } + + return loggedUtility as T; +} diff --git a/src/renderer/api/api-controller.ts b/src/renderer/api/api-controller.ts index 74ed21e72..010d73667 100644 --- a/src/renderer/api/api-controller.ts +++ b/src/renderer/api/api-controller.ts @@ -1,7 +1,8 @@ +import { createLoggedApiController } from '/@/renderer/api/api-controller-logger'; import { getServerById } from '/@/renderer/store'; import { - controller as subsonicAdapter, apiClient as subsonicApiClient, + controller as subsonicBaseAdapter, middleware as subsonicMiddleware, } from '/@/shared/api/subsonic/subsonic-controller'; import { ApiController } from '/@/shared/types/adapter/api-controller-types'; @@ -20,7 +21,7 @@ export const serverApi = { }, [ServerType.SUBSONIC]: { apiClient: subsonicApiClient, - controller: subsonicAdapter, + controller: createLoggedApiController(subsonicBaseAdapter), middleware: subsonicMiddleware, }, }; diff --git a/src/shared/api/subsonic/subsonic-controller.ts b/src/shared/api/subsonic/subsonic-controller.ts index 246035f4d..5472eb626 100644 --- a/src/shared/api/subsonic/subsonic-controller.ts +++ b/src/shared/api/subsonic/subsonic-controller.ts @@ -47,7 +47,7 @@ export const middleware: (server: ServerListItem) => Middleware = (server: Serve }, }); -const client: SubsonicClient = createClient({ +const client = createClient({ querySerializer: (params) => qs.stringify(params, { arrayFormat: 'repeat' }), }); @@ -56,8 +56,6 @@ type ErrorResponseArgs = { message?: string; }; -type SubsonicClient = Client; - function errorResponse(args: ErrorResponseArgs): [ApiControllerError, null] { const message = `${i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string}${ args.message ? `: ${args.message}` : '' @@ -131,7 +129,7 @@ function toHttpErrorCode(subsonicErrorCode: number): number { } } -export const controller: ApiController = { +const baseController: ApiController = { _utility: { getImageUrl: ( args: { id: string; size?: number; type: LibraryItem }, @@ -206,3 +204,5 @@ export const controller: ApiController = { // TODO: Implement user methods }, }; + +export const controller = baseController;