feat(logging): Wave 2 - integrate logging throughout application
This commit integrates the logging infrastructure across the entire application: IPC Layer: - Add logger-handler.ts with centralized IPC logging channels - Integrate audit logging into auth, cleaner, extractor handlers - Add structured logging for IPC operations and data flow Service Layer: - Add logger integration to ERP services (extractor, cleaner) - Integrate logging into excel-parser and user DAO - Add operation tracking and error logging Renderer Layer: - Add useLogger hook for component-level logging - Update App.tsx with session and user activity logging - Enable frontend audit trail for critical actions Testing: - Add comprehensive IPC logging integration tests - Enhance unit test coverage for logger and audit-logger - Add end-to-end logging flow validation Types: - Update preload type definitions for logging APIs Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { LayoutDashboard, Download, Trash2, Settings, Database, User, LogOut } from 'lucide-react'
|
||||
import { useLogger } from './hooks/useLogger'
|
||||
import LoginDialog from './components/LoginDialog'
|
||||
import UserSelectionDialog, {
|
||||
type UserInfo as SelectedUserInfo
|
||||
@@ -26,6 +27,9 @@ interface CurrentUser {
|
||||
}
|
||||
|
||||
function App(): React.JSX.Element {
|
||||
// Create logger instance for App component
|
||||
const logger = useLogger('App')
|
||||
|
||||
// Authentication state
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(true)
|
||||
@@ -52,28 +56,30 @@ function App(): React.JSX.Element {
|
||||
|
||||
// Initialize authentication on mount
|
||||
useEffect(() => {
|
||||
console.log('=== App: Initializing auth... ===')
|
||||
logger.info('=== Initializing auth... ===')
|
||||
initializeAuth()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const initializeAuth = async () => {
|
||||
console.log('=== App: Starting initializeAuth ===')
|
||||
logger.info('=== Starting initializeAuth ===')
|
||||
try {
|
||||
// Get computer name
|
||||
console.log('Getting computer name...')
|
||||
logger.debug('Getting computer name...')
|
||||
const computerNameResult = await window.electron.auth.getComputerName()
|
||||
const name = computerNameResult.success && computerNameResult.data ? computerNameResult.data : ''
|
||||
console.log('Computer name:', name)
|
||||
const name =
|
||||
computerNameResult.success && computerNameResult.data ? computerNameResult.data : ''
|
||||
logger.debug('Computer name obtained', { name })
|
||||
setComputerName(name)
|
||||
|
||||
// Try silent login
|
||||
console.log('Trying silent login...')
|
||||
logger.debug('Trying silent login...')
|
||||
const silentLoginResult = await window.electron.auth.silentLogin()
|
||||
const result = silentLoginResult.data
|
||||
console.log('Silent login result:', result)
|
||||
logger.debug('Silent login result', { result })
|
||||
|
||||
if (silentLoginResult.success && result?.success && result.userInfo) {
|
||||
console.log('Silent login success:', result.userInfo)
|
||||
logger.info('Silent login success', { username: result.userInfo.username })
|
||||
setCurrentUser({
|
||||
username: result.userInfo.username,
|
||||
userType: result.userInfo.userType
|
||||
@@ -81,28 +87,30 @@ function App(): React.JSX.Element {
|
||||
|
||||
// Check if admin needs user selection
|
||||
if (result.requiresUserSelection) {
|
||||
console.log('Admin user needs to select user')
|
||||
logger.info('Admin user needs to select user')
|
||||
// Load all users for selection
|
||||
const usersResult = await window.electron.auth.getAllUsers()
|
||||
setAllUsers(usersResult.success && usersResult.data ? usersResult.data : [])
|
||||
setShowUserSelection(true)
|
||||
} else {
|
||||
console.log('Setting authenticated to true')
|
||||
logger.info('Setting authenticated to true')
|
||||
setIsAuthenticated(true)
|
||||
}
|
||||
} else {
|
||||
console.log('Silent login failed, showing login dialog')
|
||||
logger.info('Silent login failed, showing login dialog')
|
||||
// Silent login failed, show login dialog
|
||||
setShowLoginDialog(true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth initialization error:', error)
|
||||
logger.error('Auth initialization error', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
setShowLoginDialog(true)
|
||||
} finally {
|
||||
console.log('Setting isAuthenticating to false')
|
||||
logger.debug('Setting isAuthenticating to false')
|
||||
setIsAuthenticating(false)
|
||||
}
|
||||
console.log('=== App: Auth initialization complete ===')
|
||||
logger.info('=== Auth initialization complete ===')
|
||||
}
|
||||
|
||||
// Handle login dialog submit
|
||||
@@ -132,7 +140,7 @@ function App(): React.JSX.Element {
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
logger.error('Login error', { error: error instanceof Error ? error.message : String(error) })
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -159,7 +167,9 @@ function App(): React.JSX.Element {
|
||||
setIsSwitchedByAdmin(true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('User selection error:', error)
|
||||
logger.error('User selection error', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
showError('切换用户失败')
|
||||
}
|
||||
}
|
||||
@@ -223,12 +233,7 @@ function App(): React.JSX.Element {
|
||||
|
||||
// Show login dialog if not authenticated
|
||||
if (!isAuthenticated) {
|
||||
console.log(
|
||||
'Render: not authenticated, showLoginDialog:',
|
||||
showLoginDialog,
|
||||
'computerName:',
|
||||
computerName
|
||||
)
|
||||
logger.debug('Render: not authenticated', { showLoginDialog, computerName })
|
||||
return (
|
||||
<>
|
||||
<LoginDialog
|
||||
@@ -299,7 +304,7 @@ function App(): React.JSX.Element {
|
||||
}
|
||||
|
||||
// Show main content when authenticated
|
||||
console.log('Render: authenticated, currentUser:', currentUser, 'currentPage:', currentPage)
|
||||
logger.debug('Render: authenticated', { currentUser, currentPage })
|
||||
|
||||
const navItems = [
|
||||
{ id: 'extractor', label: '数据提取 (Extractor)', icon: <Download size={18} /> },
|
||||
|
||||
156
src/renderer/src/hooks/useLogger.ts
Normal file
156
src/renderer/src/hooks/useLogger.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { useRef, useCallback } from 'react'
|
||||
import type { LogLevel } from '../../../shared/ipc-channels'
|
||||
|
||||
/**
|
||||
* Logger interface for renderer process
|
||||
* Provides type-safe logging with component context
|
||||
*/
|
||||
export interface RendererLogger {
|
||||
/**
|
||||
* Log a message at specified level
|
||||
*/
|
||||
log: (level: LogLevel, message: string, meta?: Record<string, unknown>) => void
|
||||
/**
|
||||
* Log an info level message (business operations)
|
||||
*/
|
||||
info: (message: string, meta?: Record<string, unknown>) => void
|
||||
/**
|
||||
* Log a warning level message (recoverable issues)
|
||||
*/
|
||||
warn: (message: string, meta?: Record<string, unknown>) => void
|
||||
/**
|
||||
* Log an error level message (failures)
|
||||
*/
|
||||
error: (message: string, meta?: Record<string, unknown>) => void
|
||||
/**
|
||||
* Log a debug level message (technical details)
|
||||
*/
|
||||
debug: (message: string, meta?: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook for logging from renderer process to main process Winston logger
|
||||
*
|
||||
* @param context - Component or feature context for log messages
|
||||
* @returns Logger instance with component context
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* function MyComponent() {
|
||||
* const logger = useLogger('MyComponent')
|
||||
*
|
||||
* const handleClick = () => {
|
||||
* logger.info('User clicked button', { buttonId: 'submit' })
|
||||
* }
|
||||
*
|
||||
* const handleError = (err: Error) => {
|
||||
* logger.error('Operation failed', { error: err.message })
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useLogger(context: string): RendererLogger {
|
||||
// Use ref to store context to avoid recreating logger on re-renders
|
||||
const contextRef = useRef(context)
|
||||
contextRef.current = context
|
||||
|
||||
// Create stable logger instance using useCallback
|
||||
const logger = useCallback((level: LogLevel, message: string, meta?: Record<string, unknown>) => {
|
||||
// Check if window.electron is available (safety check)
|
||||
if (typeof window !== 'undefined' && window.electron?.logger?.log) {
|
||||
window.electron.logger.log(level, message, {
|
||||
...meta,
|
||||
context: contextRef.current
|
||||
})
|
||||
} else {
|
||||
// Fallback to console in development if IPC not available
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const consoleMethod = console[level] || console.log
|
||||
consoleMethod(`[${contextRef.current}] ${message}`, meta || '')
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Return memoized logger methods
|
||||
return {
|
||||
log: logger,
|
||||
info: useCallback(
|
||||
(message: string, meta?: Record<string, unknown>) => {
|
||||
logger('info', message, meta)
|
||||
},
|
||||
[logger]
|
||||
),
|
||||
warn: useCallback(
|
||||
(message: string, meta?: Record<string, unknown>) => {
|
||||
logger('warn', message, meta)
|
||||
},
|
||||
[logger]
|
||||
),
|
||||
error: useCallback(
|
||||
(message: string, meta?: Record<string, unknown>) => {
|
||||
logger('error', message, meta)
|
||||
},
|
||||
[logger]
|
||||
),
|
||||
debug: useCallback(
|
||||
(message: string, meta?: Record<string, unknown>) => {
|
||||
logger('debug', message, meta)
|
||||
},
|
||||
[logger]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FPS monitoring utility for detecting UI lag from excessive logging
|
||||
*
|
||||
* @param threshold - FPS threshold below which to warn (default: 30)
|
||||
* @param logger - Logger instance to use for warnings
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* useEffect(() => {
|
||||
* const stopMonitoring = monitorFps(30, logger)
|
||||
* return () => stopMonitoring()
|
||||
* }, [logger])
|
||||
* ```
|
||||
*/
|
||||
export function monitorFps(threshold: number = 30, logger: RendererLogger): () => void {
|
||||
let frames = 0
|
||||
let lastFpsCheck = 0
|
||||
let warningCooldown = 0
|
||||
let animationFrameId: number
|
||||
|
||||
const measureFps = (currentTime: number) => {
|
||||
frames++
|
||||
|
||||
// Check FPS every second
|
||||
const elapsed = currentTime - lastFpsCheck
|
||||
if (elapsed >= 1000) {
|
||||
const fps = Math.round((frames * 1000) / elapsed)
|
||||
|
||||
if (fps < threshold && warningCooldown <= 0) {
|
||||
logger.warn('Low FPS detected - logging may be causing UI lag', {
|
||||
fps,
|
||||
threshold,
|
||||
context: 'FPSMonitor'
|
||||
})
|
||||
warningCooldown = 5 // Don't warn again for 5 seconds
|
||||
}
|
||||
|
||||
warningCooldown = Math.max(0, warningCooldown - 1)
|
||||
frames = 0
|
||||
lastFpsCheck = currentTime
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(measureFps)
|
||||
}
|
||||
|
||||
// Start monitoring
|
||||
animationFrameId = requestAnimationFrame(measureFps)
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user