feat(logging): add runtime log levels and debug log export (#241)

Add a System setting for Off/Normal/Debug logging, apply readable local timestamps to backend logs, and enable exporting buffered runtime logs to a file when debug mode is active.

Co-authored-by: Maxim Isaev <im@friclub.ru>
This commit is contained in:
Frank Stellmacher
2026-04-21 12:12:54 +02:00
committed by GitHub
parent fa21379dbb
commit 3b3833007b
16 changed files with 453 additions and 170 deletions
+5
View File
@@ -147,6 +147,7 @@ function AppShell() {
const setMusicFolders = useAuthStore(s => s.setMusicFolders);
const useCustomTitlebar = useAuthStore(s => s.useCustomTitlebar);
const linuxWebkitKineticScroll = useAuthStore(s => s.linuxWebkitKineticScroll);
const loggingMode = useAuthStore(s => s.loggingMode);
const setEntityRatingSupport = useAuthStore(s => s.setEntityRatingSupport);
const offlineAlbums = useOfflineStore(s => s.albums);
const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId);
@@ -176,6 +177,10 @@ function AppShell() {
invoke('set_linux_webkit_smooth_scrolling', { enabled: linuxWebkitKineticScroll }).catch(() => {});
}, [linuxWebkitKineticScroll]);
useEffect(() => {
invoke('set_logging_mode', { mode: loggingMode }).catch(() => {});
}, [loggingMode]);
useEffect(() => {
if (!isLoggedIn || !activeServerId) return;
const serverAtStart = activeServerId;
+8
View File
@@ -682,6 +682,14 @@ export const deTranslation = {
shortcutNativeFullscreen: 'Nativer Vollbildmodus',
shortcutOpenMiniPlayer: 'Mini-Player öffnen',
tabSystem: 'System',
loggingTitle: 'Protokollierung',
loggingModeDesc: 'Steuert die Ausführlichkeit der Backend-Protokolle im Terminal.',
loggingModeOff: 'Aus',
loggingModeNormal: 'Normal',
loggingModeDebug: 'Debug',
loggingExport: 'Protokolle exportieren',
loggingExportSuccess: 'Protokolle exportiert ({{count}} Zeilen).',
loggingExportError: 'Protokolle konnten nicht exportiert werden.',
tabGeneral: 'Allgemein',
ratingsSectionTitle: 'Bewertungen',
ratingsSkipStarTitle: 'Skip → 1 Stern',
+8
View File
@@ -666,6 +666,14 @@ export const enTranslation = {
tabServer: 'Server',
tabUsers: 'Users',
tabSystem: 'System',
loggingTitle: 'Logging',
loggingModeDesc: 'Controls backend log verbosity in the terminal.',
loggingModeOff: 'Off',
loggingModeNormal: 'Normal',
loggingModeDebug: 'Debug',
loggingExport: 'Export logs',
loggingExportSuccess: 'Logs exported ({{count}} lines).',
loggingExportError: 'Could not export logs.',
tabGeneral: 'General',
ratingsSectionTitle: 'Ratings',
ratingsSkipStarTitle: 'Skip for 1 star',
+8
View File
@@ -657,6 +657,14 @@ export const esTranslation = {
tabServer: 'Servidor',
tabUsers: 'Usuarios',
tabSystem: 'Sistema',
loggingTitle: 'Registro',
loggingModeDesc: 'Controla la verbosidad de los logs del backend en la terminal.',
loggingModeOff: 'Apagado',
loggingModeNormal: 'Normal',
loggingModeDebug: 'Depuración',
loggingExport: 'Exportar logs',
loggingExportSuccess: 'Logs exportados ({{count}} líneas).',
loggingExportError: 'No se pudieron exportar los logs.',
tabGeneral: 'General',
ratingsSectionTitle: 'Calificaciones',
ratingsSkipStarTitle: 'Saltar para 1 estrella',
+8
View File
@@ -670,6 +670,14 @@ export const frTranslation = {
shortcutNativeFullscreen: 'Plein écran natif',
shortcutOpenMiniPlayer: 'Ouvrir le mini-lecteur',
tabSystem: 'Système',
loggingTitle: 'Journalisation',
loggingModeDesc: 'Contrôle le niveau de verbosité des journaux backend dans le terminal.',
loggingModeOff: 'Désactivé',
loggingModeNormal: 'Normal',
loggingModeDebug: 'Débogage',
loggingExport: 'Exporter les journaux',
loggingExportSuccess: 'Journaux exportés ({{count}} lignes).',
loggingExportError: 'Impossible d\'exporter les journaux.',
tabGeneral: 'Général',
ratingsSectionTitle: 'Notes',
ratingsSkipStarTitle: 'Passer pour 1 étoile',
+8
View File
@@ -651,6 +651,14 @@ export const nbTranslation = {
tabServer: 'Tjener',
tabUsers: 'Brukere',
tabSystem: 'System',
loggingTitle: 'Loggføring',
loggingModeDesc: 'Styrer hvor detaljert backend-loggene i terminalen er.',
loggingModeOff: 'Av',
loggingModeNormal: 'Normal',
loggingModeDebug: 'Debug',
loggingExport: 'Eksporter logger',
loggingExportSuccess: 'Logger eksportert ({{count}} linjer).',
loggingExportError: 'Kunne ikke eksportere logger.',
tabGeneral: 'Generelt',
ratingsSectionTitle: 'Vurderinger',
ratingsSkipStarTitle: 'Hopp for 1 stjerne',
+8
View File
@@ -669,6 +669,14 @@ export const nlTranslation = {
shortcutNativeFullscreen: 'Systeemvolledig scherm',
shortcutOpenMiniPlayer: 'Mini-speler openen',
tabSystem: 'Systeem',
loggingTitle: 'Logboek',
loggingModeDesc: 'Bepaalt de uitgebreidheid van backendlogs in de terminal.',
loggingModeOff: 'Uit',
loggingModeNormal: 'Normaal',
loggingModeDebug: 'Debug',
loggingExport: 'Logs exporteren',
loggingExportSuccess: 'Logs geëxporteerd ({{count}} regels).',
loggingExportError: 'Kon logs niet exporteren.',
tabGeneral: 'Algemeen',
ratingsSectionTitle: 'Beoordelingen',
ratingsSkipStarTitle: 'Overslaan voor 1 ster',
+8
View File
@@ -679,6 +679,14 @@ export const ruTranslation = {
tabServer: 'Сервер',
tabUsers: 'Пользователи',
tabSystem: 'Система',
loggingTitle: 'Логирование',
loggingModeDesc: 'Управляет подробностью логов backend в терминале.',
loggingModeOff: 'Выключено',
loggingModeNormal: 'Обычное',
loggingModeDebug: 'Дебаг',
loggingExport: 'Экспорт логов',
loggingExportSuccess: 'Логи экспортированы ({{count}} строк).',
loggingExportError: 'Не удалось экспортировать логи.',
tabGeneral: 'Общие',
ratingsSectionTitle: 'Рейтинги',
ratingsSkipStarTitle: 'Скипнуть для 1 звезды',
+8
View File
@@ -647,6 +647,14 @@ export const zhTranslation = {
tabServer: '服务器',
tabUsers: '用户',
tabSystem: '系统',
loggingTitle: '日志记录',
loggingModeDesc: '控制终端中后端日志的详细程度。',
loggingModeOff: '关闭',
loggingModeNormal: '普通',
loggingModeDebug: '调试',
loggingExport: '导出日志',
loggingExportSuccess: '日志已导出({{count}} 行)。',
loggingExportError: '无法导出日志。',
tabGeneral: '通用',
ratingsSectionTitle: '评分',
ratingsSkipStarTitle: '跳过以评 1 星',
+48 -3
View File
@@ -22,7 +22,7 @@ import LastfmIcon from '../components/LastfmIcon';
import CustomSelect from '../components/CustomSelect';
import ThemePicker, { THEME_GROUPS } from '../components/ThemePicker';
import { useShallow } from 'zustand/react/shallow';
import { useAuthStore, ServerProfile, MIX_MIN_RATING_FILTER_MAX_STARS, type SeekbarStyle, type LyricsSourceId, type LyricsSourceConfig } from '../store/authStore';
import { useAuthStore, ServerProfile, MIX_MIN_RATING_FILTER_MAX_STARS, type SeekbarStyle, type LyricsSourceId, type LyricsSourceConfig, type LoggingMode } from '../store/authStore';
import { SeekbarPreview } from '../components/WaveformSeek';
import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform';
import { useThemeStore } from '../store/themeStore';
@@ -40,7 +40,7 @@ import {
type NdUser, type NdLibrary,
} from '../api/navidromeAdmin';
import { switchActiveServer } from '../utils/switchActiveServer';
import { open as openDialog } from '@tauri-apps/plugin-dialog';
import { open as openDialog, save as saveDialog } from '@tauri-apps/plugin-dialog';
import ConfirmModal from '../components/ConfirmModal';
import { Trans, useTranslation } from 'react-i18next';
import Equalizer from '../components/Equalizer';
@@ -1157,6 +1157,23 @@ export default function Settings() {
}
};
const exportRuntimeLogs = async () => {
const suggestedName = `psysonic-logs-${new Date().toISOString().replace(/[:.]/g, '-')}.log`;
const selected = await saveDialog({
defaultPath: suggestedName,
filters: [{ name: 'Log files', extensions: ['log', 'txt'] }],
title: t('settings.loggingExport'),
});
if (!selected || Array.isArray(selected)) return;
try {
const lines = await invoke<number>('export_runtime_logs', { path: selected });
showToast(t('settings.loggingExportSuccess', { count: lines }), 3500, 'info');
} catch (e) {
console.error(e);
showToast(t('settings.loggingExportError'), 4500, 'error');
}
};
const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
{ id: 'general', label: t('settings.tabGeneral'), icon: <AppWindow size={15} /> },
{ id: 'server', label: t('settings.tabServer'), icon: <Server size={15} /> },
@@ -2927,7 +2944,35 @@ export default function Settings() {
{activeTab === 'system' && (
<>
<BackupSection />
<BackupSection />
<section className="settings-section">
<div className="settings-section-header">
<Sliders size={18} />
<h2>{t('settings.loggingTitle')}</h2>
</div>
<div className="settings-card">
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
{t('settings.loggingModeDesc')}
</div>
<CustomSelect
value={auth.loggingMode}
onChange={(v) => auth.setLoggingMode(v as LoggingMode)}
options={[
{ value: 'off', label: t('settings.loggingModeOff') },
{ value: 'normal', label: t('settings.loggingModeNormal') },
{ value: 'debug', label: t('settings.loggingModeDebug') },
]}
/>
{auth.loggingMode === 'debug' && (
<div style={{ marginTop: '0.75rem' }}>
<button className="btn btn-surface" onClick={exportRuntimeLogs}>
<Download size={14} />
{t('settings.loggingExport')}
</button>
</div>
)}
</div>
</section>
<section className="settings-section">
<div className="settings-section-header">
<Info size={18} />
+6
View File
@@ -19,6 +19,7 @@ export interface ServerProfile {
}
export type SeekbarStyle = 'waveform' | 'linedot' | 'bar' | 'thick' | 'segmented' | 'neon' | 'pulsewave' | 'particletrail' | 'liquidfill' | 'retrotape';
export type LoggingMode = 'off' | 'normal' | 'debug';
export type LyricsSourceId = 'server' | 'lrclib' | 'netease';
export interface LyricsSourceConfig { id: LyricsSourceId; enabled: boolean; }
@@ -71,6 +72,8 @@ interface AuthState {
preloadMiniPlayer: boolean;
/** Linux WebKitGTK: smooth wheel on when true; off only after explicit opt-out in Settings. */
linuxWebkitKineticScroll: boolean;
/** Runtime backend logging level. */
loggingMode: LoggingMode;
nowPlayingEnabled: boolean;
lyricsServerFirst: boolean;
enableNeteaselyrics: boolean;
@@ -217,6 +220,7 @@ interface AuthState {
setUseCustomTitlebar: (v: boolean) => void;
setPreloadMiniPlayer: (v: boolean) => void;
setLinuxWebkitKineticScroll: (v: boolean) => void;
setLoggingMode: (v: LoggingMode) => void;
setNowPlayingEnabled: (v: boolean) => void;
setLyricsServerFirst: (v: boolean) => void;
setEnableNeteaselyrics: (v: boolean) => void;
@@ -324,6 +328,7 @@ export const useAuthStore = create<AuthState>()(
useCustomTitlebar: false,
preloadMiniPlayer: false,
linuxWebkitKineticScroll: true,
loggingMode: 'normal',
nowPlayingEnabled: false,
lyricsServerFirst: true,
enableNeteaselyrics: false,
@@ -455,6 +460,7 @@ export const useAuthStore = create<AuthState>()(
setUseCustomTitlebar: (v) => set({ useCustomTitlebar: v }),
setPreloadMiniPlayer: (v) => set({ preloadMiniPlayer: v }),
setLinuxWebkitKineticScroll: (v) => set({ linuxWebkitKineticScroll: v }),
setLoggingMode: (v) => set({ loggingMode: v }),
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
setLyricsServerFirst: (v: boolean) => set({ lyricsServerFirst: v }),
setEnableNeteaselyrics: (v: boolean) => set({ enableNeteaselyrics: v }),