add experimental request logger

This commit is contained in:
jeffvli
2025-07-13 01:16:55 -07:00
parent f1c011f677
commit 8c7cac369a
3 changed files with 181 additions and 6 deletions
+174
View File
@@ -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<TRequest, TResponse> = (
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<string, any>)) {
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<any, any>,
functionName,
options,
);
}
} else {
loggedController[sectionKey][methodKey] = method;
}
}
}
return loggedController as ApiController;
}
function createLoggedFunction<TRequest, TResponse>(
originalFn: ApiControllerFn<TRequest, TResponse> | undefined,
functionName: string,
options: LoggingOptions = {},
): LoggedApiControllerFn<TRequest, TResponse> | 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<T extends Record<string, any>>(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;
}
+3 -2
View File
@@ -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,
},
};
@@ -47,7 +47,7 @@ export const middleware: (server: ServerListItem) => Middleware = (server: Serve
},
});
const client: SubsonicClient = createClient<paths>({
const client = createClient<paths>({
querySerializer: (params) => qs.stringify(params, { arrayFormat: 'repeat' }),
});
@@ -56,8 +56,6 @@ type ErrorResponseArgs = {
message?: string;
};
type SubsonicClient = Client<paths, `${string}/${string}`>;
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;