mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-08 21:32:59 +02:00
improve app logging
- consolidate electron-log and expand support-oriented logging - consolidate log levels to info / trace only - add system/server diagnostics exporter
This commit is contained in:
@@ -4,6 +4,7 @@ import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-control
|
||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
import { mergeMusicFolderId } from '/@/renderer/api/utils-music-folder';
|
||||
import { getServerById, useAuthStore, useSettingsStore } from '/@/renderer/store';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
AuthenticationResponse,
|
||||
@@ -26,6 +27,95 @@ const endpoints: ApiController = {
|
||||
subsonic: SubsonicController,
|
||||
};
|
||||
|
||||
const SENSITIVE_KEY_PATTERN = /password|token|credential|authorization|secret|cookie/i;
|
||||
const MAX_ARRAY_ITEMS = 20;
|
||||
const MAX_STRING_LENGTH = 200;
|
||||
|
||||
const sanitizeValue = (value: unknown, depth = 0): unknown => {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value.length > MAX_STRING_LENGTH
|
||||
? `${value.slice(0, MAX_STRING_LENGTH)}…(${value.length})`
|
||||
: value;
|
||||
}
|
||||
|
||||
if (typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (depth >= 4) {
|
||||
return '[Truncated]';
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const items = value.slice(0, MAX_ARRAY_ITEMS).map((item) => sanitizeValue(item, depth + 1));
|
||||
if (value.length > MAX_ARRAY_ITEMS) {
|
||||
items.push(`…(+${value.length - MAX_ARRAY_ITEMS} more)`);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
|
||||
if (SENSITIVE_KEY_PATTERN.test(key)) {
|
||||
result[key] = '[Redacted]';
|
||||
continue;
|
||||
}
|
||||
result[key] = sanitizeValue(nested, depth + 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const OMITTED_CONTROLLER_ARG_KEYS = new Set([
|
||||
'apiClientProps',
|
||||
'body',
|
||||
'context',
|
||||
'query',
|
||||
'signal',
|
||||
]);
|
||||
|
||||
const sanitizeControllerArgs = (endpoint: string, args: unknown[]): unknown => {
|
||||
if (endpoint === 'authenticate') {
|
||||
const [url] = args;
|
||||
return { url: typeof url === 'string' ? url : undefined };
|
||||
}
|
||||
|
||||
const first = args[0];
|
||||
if (!first || typeof first !== 'object') {
|
||||
return args.length === 0 ? undefined : sanitizeValue(args);
|
||||
}
|
||||
|
||||
const input = first as Record<string, unknown>;
|
||||
const sanitized: Record<string, unknown> = {};
|
||||
|
||||
if (input.query !== undefined) {
|
||||
sanitized.query = sanitizeValue(input.query);
|
||||
}
|
||||
|
||||
if (input.body !== undefined) {
|
||||
sanitized.body = sanitizeValue(input.body);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (OMITTED_CONTROLLER_ARG_KEYS.has(key)) {
|
||||
continue;
|
||||
}
|
||||
sanitized[key] = sanitizeValue(value);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
const OMITTED_CONTROLLER_LOG_ENDPOINTS = new Set<keyof ControllerEndpoint>([
|
||||
'getDownloadUrl',
|
||||
'getImageRequest',
|
||||
'getImageUrl',
|
||||
'getStreamUrl',
|
||||
]);
|
||||
|
||||
const apiController = <K extends keyof ControllerEndpoint>(
|
||||
endpoint: K,
|
||||
type?: ServerType,
|
||||
@@ -37,27 +127,71 @@ const apiController = <K extends keyof ControllerEndpoint>(
|
||||
message: i18n.t('error.serverNotSelectedError') as string,
|
||||
title: i18n.t('error.apiRouteError') as string,
|
||||
});
|
||||
|
||||
logger.error('No server selected', {
|
||||
serverType,
|
||||
});
|
||||
throw new Error(`No server selected`);
|
||||
}
|
||||
|
||||
const controllerFn = endpoints?.[serverType]?.[endpoint];
|
||||
|
||||
if (typeof controllerFn !== 'function') {
|
||||
toast.error({
|
||||
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
|
||||
title: i18n.t('error.apiRouteError') as string,
|
||||
logger.error(`Endpoint ${endpoint} is not implemented for ${serverType}`, {
|
||||
endpoint,
|
||||
serverType,
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
i18n.t('error.endpointNotImplementedError', {
|
||||
endpoint,
|
||||
|
||||
serverType,
|
||||
}) as string,
|
||||
);
|
||||
}
|
||||
|
||||
return controllerFn;
|
||||
if (OMITTED_CONTROLLER_LOG_ENDPOINTS.has(endpoint)) {
|
||||
return controllerFn;
|
||||
}
|
||||
|
||||
return ((...args: unknown[]) => {
|
||||
const started = performance.now();
|
||||
const serverId = (args[0] as undefined | { apiClientProps?: { serverId?: string } })
|
||||
?.apiClientProps?.serverId;
|
||||
|
||||
const logResult = (error?: unknown, value?: unknown) => {
|
||||
logger.debug(`Controller ${String(endpoint)}${error != null ? ' failed' : ''}`, {
|
||||
args: sanitizeControllerArgs(String(endpoint), args),
|
||||
durationMs: Math.round(performance.now() - started),
|
||||
serverId,
|
||||
serverType,
|
||||
...(error != null
|
||||
? { error: error instanceof Error ? error.message : String(error) }
|
||||
: { result: sanitizeValue(value) }),
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const result = (controllerFn as (...a: unknown[]) => unknown)(...args);
|
||||
if (result instanceof Promise) {
|
||||
return result.then(
|
||||
(value) => {
|
||||
logResult(undefined, value);
|
||||
return value;
|
||||
},
|
||||
(error) => {
|
||||
logResult(error);
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
}
|
||||
logResult(undefined, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logResult(error);
|
||||
throw error;
|
||||
}
|
||||
}) as NonNullable<InternalControllerEndpoint[K]>;
|
||||
};
|
||||
|
||||
const getPathReplaceSettings = () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import qs from 'qs';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { authenticationFailure } from '/@/renderer/api/utils';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
|
||||
import { resultWithHeaders } from '/@/shared/api/utils';
|
||||
@@ -443,7 +444,7 @@ axiosClient.interceptors.response.use(
|
||||
console.error('Error when trying to reauthenticate: ', newError);
|
||||
|
||||
if (isAxiosError(newError) && newError.code === 'ERR_NETWORK') {
|
||||
console.log(
|
||||
logger.warn(
|
||||
'Network error during reauthentication - preserving credentials',
|
||||
);
|
||||
} else {
|
||||
@@ -460,7 +461,7 @@ axiosClient.interceptors.response.use(
|
||||
}
|
||||
|
||||
if (isAxiosError(error) && error.code === 'ERR_NETWORK') {
|
||||
console.log('Network error during authentication - preserving credentials');
|
||||
logger.warn('Network error during authentication - preserving credentials');
|
||||
} else {
|
||||
limitedFail(currentServer);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
getDirectPlayProfiles,
|
||||
} from '/@/renderer/features/player/components/audio-players';
|
||||
import { randomString } from '/@/renderer/utils';
|
||||
import { logFn } from '/@/renderer/utils/logger';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||
import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
|
||||
import {
|
||||
@@ -1426,13 +1426,13 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
if (jukeboxStatus.status === 200 && !(jukeboxStatus.body as any)?.error) {
|
||||
features[ServerFeature.JUKEBOX] = [1];
|
||||
} else {
|
||||
console.log(
|
||||
logger.warn(
|
||||
'Jukebox endpoint returned an error payload:',
|
||||
(jukeboxStatus.body as any)?.error,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Jukebox is not supported by this server:', error);
|
||||
logger.warn('Jukebox is not supported by this server:', error);
|
||||
}
|
||||
|
||||
return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion };
|
||||
@@ -1949,7 +1949,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
// If the server returns an error for transcodeDecision, fall back to direct stream so that we don't break the player
|
||||
if (transcodeDecision.status !== 200) {
|
||||
logFn.error(
|
||||
logger.error(
|
||||
`Failed to get transcode decision for song ${id}, falling back to direct stream`,
|
||||
);
|
||||
return streamUrl;
|
||||
@@ -1963,7 +1963,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
return streamUrl;
|
||||
}
|
||||
|
||||
logFn.info(`Song ${id} requires transcoding: ${[td.transcodeReason].join(', ')}`);
|
||||
logger.info(`Song ${id} requires transcoding: ${[td.transcodeReason].join(', ')}`);
|
||||
|
||||
// If the server does not return transcode params, manually create the transcode params
|
||||
if (!td.transcodeParams) {
|
||||
|
||||
Reference in New Issue
Block a user