mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-08 13:23:16 +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:
@@ -1,124 +0,0 @@
|
||||
import { LogCategory } from '/@/renderer/utils/logger';
|
||||
|
||||
export const logMsg = {
|
||||
[LogCategory.ANALYTICS]: {
|
||||
appTracked: 'Analytics sent',
|
||||
pageViewTracked: 'Page view tracked',
|
||||
},
|
||||
[LogCategory.API]: {},
|
||||
[LogCategory.EXTERNAL]: {
|
||||
discordRpcActivityCleared: 'Activity was cleared for Discord RPC',
|
||||
discordRpcInitialized: 'Discord RPC was initialized',
|
||||
discordRpcQuit: 'Discord RPC was quit',
|
||||
discordRpcSetActivity: 'Activity was set for Discord RPC',
|
||||
discordRpcTrackChanged: 'Track was changed for Discord RPC',
|
||||
discordRpcUpdateSkipped: 'Activity was not updated for Discord RPC',
|
||||
},
|
||||
[LogCategory.OTHER]: {
|
||||
error: 'An error occurred',
|
||||
warning: 'A warning occurred',
|
||||
},
|
||||
[LogCategory.PLAYER]: {
|
||||
addToQueueByData: 'Added to queue by data',
|
||||
addToQueueByFetch: 'Added to queue by fetch',
|
||||
addToQueueByListQuery: 'Added to queue by list query',
|
||||
addToQueueByType: 'Added to queue by type',
|
||||
autoPlayFailed: 'Auto play failed',
|
||||
autoPlayTriggered: 'Auto play triggered',
|
||||
cancelledFetch: 'Cancelled fetch',
|
||||
clearQueue: 'Cleared queue',
|
||||
clearSelected: 'Cleared selected',
|
||||
decreaseVolume: 'Decreased volume',
|
||||
increaseVolume: 'Increased volume',
|
||||
mediaNext: 'Media next',
|
||||
mediaPause: 'Media pause',
|
||||
mediaPlay: 'Media play',
|
||||
mediaPlayByIndex: 'Media play by index',
|
||||
mediaPrevious: 'Media previous',
|
||||
mediaSeekToTimestamp: 'Media seek to timestamp',
|
||||
mediaSkipBackward: 'Media skip backward',
|
||||
mediaSkipForward: 'Media skip forward',
|
||||
mediaStop: 'Media stop',
|
||||
mediaToggleMute: 'Media toggle mute',
|
||||
mediaTogglePlayPause: 'Media toggle play pause',
|
||||
moveSelectedTo: 'Moved selected to',
|
||||
moveSelectedToBottom: 'Moved selected to bottom',
|
||||
moveSelectedToNext: 'Moved selected to next',
|
||||
moveSelectedToTop: 'Moved selected to top',
|
||||
playbackError: 'An error occurred during playback',
|
||||
playerFiltersApplied: 'Player filters applied',
|
||||
setFavorite: 'Set favorite',
|
||||
setQueue: 'Set queue',
|
||||
setRating: 'Set rating',
|
||||
setRepeat: 'Set repeat',
|
||||
setShuffle: 'Set shuffle',
|
||||
setSpeed: 'Set speed',
|
||||
setVolume: 'Set volume',
|
||||
shuffle: 'Shuffle',
|
||||
shuffleAll: 'Shuffle all',
|
||||
shuffleSelected: 'Shuffle selected',
|
||||
toggleRepeat: 'Toggle repeat',
|
||||
toggleShuffle: 'Toggle shuffle',
|
||||
},
|
||||
[LogCategory.REMOTE]: {
|
||||
cannotSendEvent: 'Cannot send event - socket not available',
|
||||
closingExistingSocket: 'Closing existing socket',
|
||||
creatingWebSocket: 'Creating new WebSocket',
|
||||
credentialsFetched: 'Credentials fetched',
|
||||
failedToEnableRemote: 'Failed to enable remote',
|
||||
failedToGetCredentials: 'Failed to get credentials',
|
||||
favoriteEventReceived: 'Favorite event received',
|
||||
fetchingCredentials: 'Fetching credentials',
|
||||
initializingRemoteSettings: 'Initializing remote settings',
|
||||
playbackEventReceived: 'Playback event received',
|
||||
positionEventReceived: 'Position event received',
|
||||
proxyEventReceived: 'Proxy event received (image update)',
|
||||
ratingEventReceived: 'Rating event received',
|
||||
reconnectInitiated: 'Reconnect initiated',
|
||||
reloadingPage: 'Reloading page due to close code',
|
||||
repeatEventReceived: 'Repeat event received',
|
||||
requestFavoriteReceived: 'Request favorite received',
|
||||
requestPositionReceived: 'Request position received',
|
||||
requestRatingReceived: 'Request rating received',
|
||||
requestSeekReceived: 'Request seek received',
|
||||
requestVolumeReceived: 'Request volume received',
|
||||
sendingAuthentication: 'Sending authentication',
|
||||
sendingEventToServer: 'Sending event to server',
|
||||
sendingInitialSong: 'Sending initial song',
|
||||
serverIsDown: 'Server is down',
|
||||
shuffleEventReceived: 'Shuffle event received',
|
||||
socketClosedUnexpectedly: 'Socket closed unexpectedly',
|
||||
songEventReceived: 'Song event received',
|
||||
stateEventReceived: 'State event received (full state update)',
|
||||
updateFavoriteSent: 'Update favorite sent',
|
||||
updatePlaybackSent: 'Update playback sent',
|
||||
updatePositionSent: 'Update position sent',
|
||||
updateRatingSent: 'Update rating sent',
|
||||
updateRepeatSent: 'Update repeat sent',
|
||||
updateShuffleSent: 'Update shuffle sent',
|
||||
updateSongSent: 'Update song sent',
|
||||
updateVolumeSent: 'Update volume sent',
|
||||
volumeEventReceived: 'Volume event received',
|
||||
webSocketClosed: 'WebSocket closed',
|
||||
webSocketErrorEvent: 'WebSocket error event',
|
||||
webSocketMessageReceived: 'WebSocket message received',
|
||||
webSocketOpened: 'WebSocket opened',
|
||||
},
|
||||
[LogCategory.SCROBBLE]: {
|
||||
scrobbledPause: 'Scrobbled a pause event',
|
||||
scrobbledStart: 'Scrobbled a start event',
|
||||
scrobbledStop: 'Scrobbled a stop event',
|
||||
scrobbledSubmission: 'Scrobbled a submission event',
|
||||
scrobbledTimeupdate: 'Scrobbled a timeupdate event',
|
||||
scrobbledUnpause: 'Scrobbled an unpause event',
|
||||
},
|
||||
[LogCategory.SYSTEM]: {
|
||||
authenticatingServer: 'Authenticating server',
|
||||
serverAuthenticationAborted: 'Server authentication aborted',
|
||||
serverAuthenticationError: 'Server authentication error',
|
||||
serverAuthenticationFailed: 'Server authentication failed',
|
||||
serverAuthenticationInvalid: 'Server authentication invalid',
|
||||
serverAuthenticationSuccess: 'Server authentication successful',
|
||||
settingsSynchronized: 'Differences found between renderer and main process settings',
|
||||
},
|
||||
};
|
||||
+111
-66
@@ -1,27 +1,21 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { LogLevel, LogSeverity } from '/@/shared/logger/types';
|
||||
|
||||
export enum LogCategory {
|
||||
ANALYTICS = 'analytics',
|
||||
API = 'api',
|
||||
EXTERNAL = 'external',
|
||||
GENERAL = 'general',
|
||||
OTHER = 'other',
|
||||
PLAYER = 'player',
|
||||
REMOTE = 'remote',
|
||||
SCROBBLE = 'scrobble',
|
||||
SYSTEM = 'system',
|
||||
}
|
||||
export type { LogLevel, LogSeverity };
|
||||
|
||||
export type LogLevel = 'debug' | 'error' | 'info' | 'warn';
|
||||
type ElectronLogApi = {
|
||||
debug: (...params: any[]) => void;
|
||||
error: (...params: any[]) => void;
|
||||
info: (...params: any[]) => void;
|
||||
sendToMain?: (message: {
|
||||
data: any[];
|
||||
level: LogSeverity;
|
||||
variables?: { processType: string };
|
||||
}) => void;
|
||||
warn: (...params: any[]) => void;
|
||||
};
|
||||
|
||||
interface LogFn {
|
||||
(
|
||||
message?: string,
|
||||
options?: {
|
||||
category?: string;
|
||||
meta?: any;
|
||||
},
|
||||
): void;
|
||||
(message?: string, meta?: any): void;
|
||||
}
|
||||
|
||||
interface Logger {
|
||||
@@ -32,18 +26,89 @@ interface Logger {
|
||||
warn: LogFn;
|
||||
}
|
||||
|
||||
const DEFAULT_LOG_LEVEL = process.env.NODE_ENV === 'production' ? 'info' : 'debug';
|
||||
const DEFAULT_LOG_LEVEL: LogLevel = process.env.NODE_ENV === 'production' ? 'info' : 'debug';
|
||||
const PROCESS_LABEL = '[renderer]';
|
||||
const PROCESS_WIDTH = 10;
|
||||
const LEVEL_WIDTH = 5;
|
||||
const RESET = '\x1B[0m';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const NO_OP: LogFn = (_message?: string, ..._optionalParams: any[]) => {};
|
||||
|
||||
const colors = {
|
||||
const levelColors: Record<LogSeverity, string> = {
|
||||
debug: '\x1B[38;2;100;149;237m', // #6495ED
|
||||
error: '\x1B[38;2;255;100;100m', // #ff6464
|
||||
info: '\x1B[38;2;76;175;80m', // #4caf50
|
||||
warn: '\x1B[38;2;225;125;50m', // #e17d32
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const NO_OP: LogFn = (_message?: string, ..._optionalParams: any[]) => {};
|
||||
|
||||
const getElectronLog = (): ElectronLogApi | null => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const electronLog = (window as Window & { __electronLog?: ElectronLogApi }).__electronLog;
|
||||
return electronLog ?? null;
|
||||
};
|
||||
|
||||
const formatLogLine = (level: LogSeverity, message: string, count = 1): string => {
|
||||
const countStr = count > 1 ? ` (x${count})` : '';
|
||||
const levelLabel = `${levelColors[level]}${level.toUpperCase().padEnd(LEVEL_WIDTH, ' ')}${RESET}`;
|
||||
const processLabel = PROCESS_LABEL.padEnd(PROCESS_WIDTH, ' ');
|
||||
return `${new Date().toISOString()} ${levelLabel} ${processLabel} ${message}${countStr}`;
|
||||
};
|
||||
|
||||
const forwardToElectronLog = (level: LogSeverity, message: string, meta?: any, count = 1) => {
|
||||
const electronLog = getElectronLog();
|
||||
if (!electronLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const countStr = count > 1 ? ` (x${count})` : '';
|
||||
const forwardMessage = `${message}${countStr}`;
|
||||
const data = meta !== undefined ? [forwardMessage, meta] : [forwardMessage];
|
||||
|
||||
if (typeof electronLog.sendToMain === 'function') {
|
||||
electronLog.sendToMain({
|
||||
data,
|
||||
level,
|
||||
variables: { processType: 'renderer' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (meta !== undefined) {
|
||||
electronLog[level](forwardMessage, meta);
|
||||
} else {
|
||||
electronLog[level](forwardMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const syncLogLevelToMain = (level: LogLevel) => {
|
||||
if (typeof window === 'undefined' || !window.api?.ipc) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.api.ipc.send('logger-set-level', level);
|
||||
};
|
||||
|
||||
export const normalizeLogLevel = (value: null | string | undefined): LogLevel => {
|
||||
if (value === 'debug' || value === 'info') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Legacy warn/error/trace thresholds map to nearby levels.
|
||||
if (value === 'warn' || value === 'error') {
|
||||
return 'info';
|
||||
}
|
||||
|
||||
if (value === 'trace') {
|
||||
return 'debug';
|
||||
}
|
||||
|
||||
return DEFAULT_LOG_LEVEL;
|
||||
};
|
||||
|
||||
// Debounce configuration
|
||||
const DEBOUNCE_INTERVAL = 200; // milliseconds
|
||||
const DEBOUNCE_MAP = new Map<string, { count: number; lastLog: number }>();
|
||||
@@ -53,22 +118,18 @@ setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of DEBOUNCE_MAP.entries()) {
|
||||
if (now - value.lastLog >= DEBOUNCE_INTERVAL) {
|
||||
const [level, message, category, meta] = JSON.parse(key);
|
||||
const timestampStr = `${dayjs().format('HH:mm:ss')}`;
|
||||
const levelStr = `${colors[level as keyof typeof colors]}[${String(level).toUpperCase().padEnd(5, ' ')}]\x1B[0m`;
|
||||
const countStr = value.count > 1 ? ` (x${value.count})` : '';
|
||||
const categoryStr = category
|
||||
? String(`[${category.padEnd(9, ' ')}]`).toUpperCase()
|
||||
: '';
|
||||
const [level, message, meta] = JSON.parse(key) as [LogSeverity, string, any];
|
||||
const messageStr = message ? String(message) : '';
|
||||
const logStr = `[${timestampStr}] ${levelStr} ${categoryStr} ${messageStr}${countStr}`;
|
||||
const logStr = formatLogLine(level, messageStr, value.count);
|
||||
|
||||
if (meta) {
|
||||
if (meta !== undefined && meta !== null) {
|
||||
console.log(logStr, meta);
|
||||
} else {
|
||||
console.log(logStr);
|
||||
}
|
||||
|
||||
forwardToElectronLog(level, messageStr, meta, value.count);
|
||||
|
||||
DEBOUNCE_MAP.delete(key);
|
||||
}
|
||||
}
|
||||
@@ -82,19 +143,24 @@ class ConsoleLogger implements Logger {
|
||||
warn: LogFn = NO_OP;
|
||||
|
||||
constructor() {
|
||||
const level = (localStorage.getItem('log_level') || DEFAULT_LOG_LEVEL) as LogLevel;
|
||||
const level = normalizeLogLevel(localStorage.getItem('log_level'));
|
||||
if (localStorage.getItem('log_level') !== level) {
|
||||
localStorage.setItem('log_level', level);
|
||||
}
|
||||
|
||||
this.initializeLoggers(level);
|
||||
syncLogLevelToMain(level);
|
||||
|
||||
this.updateLogLevel = (newLevel: LogLevel) => {
|
||||
this.initializeLoggers(newLevel);
|
||||
syncLogLevelToMain(newLevel);
|
||||
};
|
||||
}
|
||||
|
||||
private initializeLoggers(level: LogLevel) {
|
||||
// Create timestamp wrapper function with colors and debouncing
|
||||
const withTimestamp = (logLevel: string): LogFn => {
|
||||
return (message?: any, options?: { category?: string; meta?: any }) => {
|
||||
const { category, meta } = options || {};
|
||||
const key = JSON.stringify([logLevel, message, category, meta]);
|
||||
const withDebounce = (logLevel: LogSeverity): LogFn => {
|
||||
return (message?: any, meta?: any) => {
|
||||
const key = JSON.stringify([logLevel, message, meta]);
|
||||
const now = Date.now();
|
||||
const existing = DEBOUNCE_MAP.get(key);
|
||||
|
||||
@@ -107,32 +173,11 @@ class ConsoleLogger implements Logger {
|
||||
};
|
||||
};
|
||||
|
||||
this.error = withTimestamp('error');
|
||||
|
||||
if (level === 'error') {
|
||||
this.warn = NO_OP;
|
||||
this.info = NO_OP;
|
||||
this.debug = NO_OP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.warn = withTimestamp('warn');
|
||||
|
||||
if (level === 'warn') {
|
||||
this.info = NO_OP;
|
||||
this.debug = NO_OP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.info = withTimestamp('info');
|
||||
|
||||
if (level === 'info') {
|
||||
this.debug = NO_OP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.debug = withTimestamp('debug');
|
||||
this.error = withDebounce('error');
|
||||
this.warn = withDebounce('warn');
|
||||
this.info = withDebounce('info');
|
||||
this.debug = level === 'debug' ? withDebounce('debug') : NO_OP;
|
||||
}
|
||||
}
|
||||
|
||||
export const logFn = new ConsoleLogger();
|
||||
export const logger = new ConsoleLogger();
|
||||
|
||||
Reference in New Issue
Block a user