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:
jeffvli
2026-07-20 22:25:44 -07:00
parent 99258c92fc
commit cbf8037099
45 changed files with 1429 additions and 1067 deletions
-124
View File
@@ -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
View File
@@ -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();