refactor(logger): optimize logging architecture with 6 improvements
1. Extract shared module: consolidate getLogDir() and isProduction() into shared.ts, eliminate duplication across logger modules 2. Make retention config effective: delay file transport creation until config is loaded, apply appRetention/auditRetention from config.yaml 3. Add before-quit log flush: close logger and audit logger on app exit to prevent log loss 4. Unify logError entry point: remove duplicate logError from index.ts, re-export from error-utils.ts with richer error context 5. Renderer log level filtering: add client-side level check in preload to skip IPC for filtered-out messages 6. Child logger cache + audit cleanup: cache child loggers in IPC handler for performance, remove redundant timestamp format in audit logger Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,7 @@ graph TD
|
||||
## 文档职责一览
|
||||
|
||||
| 文档 | 主要回答的问题 |
|
||||
| --- | --- |
|
||||
| ------------------------- | -------------------------------------------------- |
|
||||
| `overview.md` | 这个项目整体是什么、做什么、核心目录和主链路是什么 |
|
||||
| `runtime-architecture.md` | `main / preload / renderer` 如何协作 |
|
||||
| `data-flow.md` | 核心业务数据如何在各层之间流动 |
|
||||
|
||||
@@ -67,7 +67,7 @@ flowchart TD
|
||||
## 当前文档一览
|
||||
|
||||
| 文档 | 主要内容 |
|
||||
| --- | --- |
|
||||
| ------------------------- | ---------------------------------------- |
|
||||
| `local-development.md` | 环境准备、启动、构建、常用命令、本地验证 |
|
||||
| `debugging.md` | 分层调试思路、调试入口、主链路定位方法 |
|
||||
| `renderer-development.md` | React 渲染层开发方式、页面/hook/组件边界 |
|
||||
|
||||
@@ -60,7 +60,7 @@ graph TD
|
||||
## 模块目录一览
|
||||
|
||||
| 模块 | 文档 | 核心职责 |
|
||||
| --- | --- | --- |
|
||||
| ---------- | --------------- | --------------------------------------------------------- |
|
||||
| Auth | `auth.md` | 登录、silent login、管理员代切用户、用户上下文同步 |
|
||||
| Extractor | `extractor.md` | 订单号输入、提取执行、日志与共享订单号同步 |
|
||||
| Validation | `validation.md` | 共享 Production IDs、校验查询、结果富化、Cleaner 数据准备 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { app } from 'electron'
|
||||
import logger from '../services/logger/index'
|
||||
import { logAudit } from '../services/logger/audit-logger'
|
||||
import { logAudit, closeAuditLogger } from '../services/logger/audit-logger'
|
||||
|
||||
export function setupProcessGuards(): void {
|
||||
process.on('uncaughtException', async (err) => {
|
||||
@@ -37,4 +37,10 @@ export function setupProcessGuards(): void {
|
||||
logger.error('Child process gone', { details })
|
||||
console.error('Child process gone:', details)
|
||||
})
|
||||
|
||||
// Flush and close loggers before quit to prevent log loss
|
||||
app.on('before-quit', () => {
|
||||
logger.close()
|
||||
closeAuditLogger()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -259,7 +259,13 @@ export function registerExtractorHandlers(): void {
|
||||
|
||||
// Write per-order record counts
|
||||
for (const { orderNumber, recordCount } of result.orderRecordCounts) {
|
||||
await historyDao.updateRecordStatus(batchId, orderNumber, status, undefined, recordCount)
|
||||
await historyDao.updateRecordStatus(
|
||||
batchId,
|
||||
orderNumber,
|
||||
status,
|
||||
undefined,
|
||||
recordCount
|
||||
)
|
||||
}
|
||||
|
||||
// Update batch status without recordCount (per-order counts are set individually)
|
||||
|
||||
@@ -68,15 +68,21 @@ export function withErrorHandling<T>(
|
||||
}
|
||||
|
||||
if (isBaseError(error)) {
|
||||
logError(log, `[${context}] ${error.name}`, error, {
|
||||
logError(log, error, {
|
||||
message: `[${context}] ${error.name}`,
|
||||
context: {
|
||||
code,
|
||||
cause: getErrorCauseMessage(error),
|
||||
handler: context
|
||||
}
|
||||
})
|
||||
} else {
|
||||
logError(log, `[${context}] Error`, error, {
|
||||
logError(log, error, {
|
||||
message: `[${context}] Error`,
|
||||
context: {
|
||||
code,
|
||||
handler: context
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
*/
|
||||
|
||||
import { ipcMain } from 'electron'
|
||||
import winston from 'winston'
|
||||
import { createLogger } from '../services/logger'
|
||||
import logger from '../services/logger'
|
||||
import { IPC_CHANNELS, type LogLevel } from '../../shared/ipc-channels'
|
||||
|
||||
const log = createLogger('LoggerHandler')
|
||||
@@ -41,6 +43,7 @@ class LoggerHandlerState {
|
||||
private buffer: LogEntry[] = []
|
||||
private debounceTimer: NodeJS.Timeout | null = null
|
||||
private discardedCount = 0
|
||||
private childLoggerCache = new Map<string, winston.Logger>()
|
||||
|
||||
/**
|
||||
* Add log entry to buffer
|
||||
@@ -131,16 +134,27 @@ class LoggerHandlerState {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a cached child logger for a component
|
||||
* Avoids creating a new child logger for every log entry
|
||||
* @param component - Component name for the child logger
|
||||
*/
|
||||
private getChildLogger(component: string): winston.Logger {
|
||||
let child = this.childLoggerCache.get(component)
|
||||
if (!child) {
|
||||
child = log.child({ source: 'renderer', component })
|
||||
this.childLoggerCache.set(component, child)
|
||||
}
|
||||
return child
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward a single log entry to Winston logger
|
||||
* @param entry - Log entry to forward
|
||||
*/
|
||||
private forwardToWinston(entry: LogEntry): void {
|
||||
const context = (entry.context?.component as string) || 'renderer'
|
||||
const childLogger = log.child({
|
||||
source: 'renderer',
|
||||
component: context
|
||||
})
|
||||
const childLogger = this.getChildLogger(context)
|
||||
|
||||
const message = entry.context?.message
|
||||
? `[${entry.context.message}] ${entry.message}`
|
||||
@@ -187,6 +201,7 @@ class LoggerHandlerState {
|
||||
}
|
||||
this.buffer = []
|
||||
this.discardedCount = 0
|
||||
this.childLoggerCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +212,11 @@ const state = new LoggerHandlerState()
|
||||
* Register IPC handlers for logger
|
||||
*/
|
||||
export function registerLoggerHandlers(): void {
|
||||
// Return current log level to preload for client-side filtering
|
||||
ipcMain.handle(IPC_CHANNELS.LOGGER_GET_LEVEL, () => {
|
||||
return logger.level as LogLevel
|
||||
})
|
||||
|
||||
// Use ipcMain.on with send() - fire-and-forget, non-blocking
|
||||
ipcMain.on(IPC_CHANNELS.LOGGER_FORWARD, (_event, entry: LogEntry) => {
|
||||
// Validate entry
|
||||
|
||||
@@ -27,16 +27,13 @@ export function registerSettingsHandlers(): void {
|
||||
const erpConfigService = UserErpConfigService.getInstance()
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.SETTINGS_GET_USER_TYPE, async (): Promise<IpcResult<UserType>> => {
|
||||
return withErrorHandling(
|
||||
async () => {
|
||||
return withErrorHandling(async () => {
|
||||
const userType = sessionManager.getUserType()
|
||||
if (!userType) {
|
||||
throw new ValidationError('未找到用户类型', 'VAL_INVALID_INPUT')
|
||||
}
|
||||
return userType as UserType
|
||||
},
|
||||
'settings:getUserType'
|
||||
)
|
||||
}, 'settings:getUserType')
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
|
||||
@@ -20,7 +20,8 @@ import { dirname } from 'path'
|
||||
import { app } from 'electron'
|
||||
import yaml from 'js-yaml'
|
||||
import { z } from 'zod'
|
||||
import { createLogger, setLogLevel } from '../logger'
|
||||
import { createLogger, applyLoggingConfig } from '../logger'
|
||||
import { applyAuditConfig } from '../logger/audit-logger'
|
||||
import {
|
||||
fullConfigSchema,
|
||||
type FullConfig,
|
||||
@@ -170,7 +171,8 @@ export class ConfigManager {
|
||||
await this.saveConfig(DEFAULT_CONFIG)
|
||||
this.config = DEFAULT_CONFIG
|
||||
// Apply logging configuration from default config
|
||||
setLogLevel(DEFAULT_CONFIG.logging.level)
|
||||
applyLoggingConfig(DEFAULT_CONFIG.logging)
|
||||
applyAuditConfig(DEFAULT_CONFIG.logging.auditRetention)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -190,7 +192,8 @@ export class ConfigManager {
|
||||
this.config = validated
|
||||
|
||||
// Apply logging configuration
|
||||
setLogLevel(validated.logging.level)
|
||||
applyLoggingConfig(validated.logging)
|
||||
applyAuditConfig(validated.logging.auditRetention)
|
||||
|
||||
log.info('Configuration loaded and validated successfully')
|
||||
} catch (error) {
|
||||
|
||||
@@ -123,9 +123,7 @@ export class ExtractorService {
|
||||
* @param filePaths - Array of downloaded Excel file paths
|
||||
* @returns Merged file path, total record count, and optional error message
|
||||
*/
|
||||
private async mergeFiles(
|
||||
filePaths: string[]
|
||||
): Promise<{
|
||||
private async mergeFiles(filePaths: string[]): Promise<{
|
||||
mergedFile: string | null
|
||||
recordCount: number
|
||||
error?: string
|
||||
@@ -197,7 +195,12 @@ export class ExtractorService {
|
||||
const errorStack = error instanceof Error ? error.stack : ''
|
||||
log.error('Failed to save merged file', { error: errorMsg, stack: errorStack })
|
||||
// Return parsed record count and error info even if save fails
|
||||
return { mergedFile: null, recordCount, orderRecordCounts, error: `保存合并文件失败:${errorMsg}` }
|
||||
return {
|
||||
mergedFile: null,
|
||||
recordCount,
|
||||
orderRecordCounts,
|
||||
error: `保存合并文件失败:${errorMsg}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
import winston from 'winston'
|
||||
import DailyRotateFile from 'winston-daily-rotate-file'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import { getLogDir } from './shared'
|
||||
|
||||
/**
|
||||
* Audit log entry structure
|
||||
@@ -32,22 +31,6 @@ export interface AuditEntry {
|
||||
metadata: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the log directory for audit logs
|
||||
* Uses app.getPath('logs') in production, local logs dir in development
|
||||
*/
|
||||
function getLogDir(): string {
|
||||
if (app && app.isReady()) {
|
||||
return app.getPath('logs')
|
||||
}
|
||||
// Fallback for development or before app is ready
|
||||
const devLogDir = path.join(process.cwd(), 'logs')
|
||||
if (!fs.existsSync(devLogDir)) {
|
||||
fs.mkdirSync(devLogDir, { recursive: true })
|
||||
}
|
||||
return devLogDir
|
||||
}
|
||||
|
||||
/**
|
||||
* JSONL formatter - outputs one JSON object per line
|
||||
* This is the key difference from the standard JSON formatter
|
||||
@@ -59,26 +42,40 @@ const jsonlFormat = winston.format.printf(({ message }) => {
|
||||
|
||||
/**
|
||||
* Create the audit logger instance with daily rotation
|
||||
* Configured for 30-day retention as per requirements
|
||||
* Initially silent (no transports). Call applyAuditConfig() after config is loaded.
|
||||
*/
|
||||
const auditLogger = winston.createLogger({
|
||||
level: 'info',
|
||||
silent: false,
|
||||
transports: [
|
||||
transports: []
|
||||
})
|
||||
|
||||
/**
|
||||
* Apply audit log retention configuration
|
||||
* Creates the DailyRotateFile transport with the configured retention period
|
||||
*
|
||||
* @param retentionDays - Number of days to retain audit logs
|
||||
*/
|
||||
export function applyAuditConfig(retentionDays: number): void {
|
||||
// Remove existing DailyRotateFile transports
|
||||
const existingTransports = auditLogger.transports.filter((t) => t instanceof DailyRotateFile)
|
||||
for (const transport of existingTransports) {
|
||||
auditLogger.remove(transport)
|
||||
}
|
||||
|
||||
// Add audit transport with configured retention
|
||||
auditLogger.add(
|
||||
new DailyRotateFile({
|
||||
filename: path.join(getLogDir(), 'audit-%DATE%.jsonl'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '30d', // 30-day retention
|
||||
maxFiles: `${retentionDays}d`,
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }),
|
||||
jsonlFormat
|
||||
format: jsonlFormat
|
||||
})
|
||||
)
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an audit event
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { ErrorLike, SerializedError } from '../../types/errors'
|
||||
import { isProduction } from './shared'
|
||||
|
||||
/**
|
||||
* Check if value is an Error or Error-like object
|
||||
@@ -84,7 +85,7 @@ export function sanitizeError(error: SerializedError): SerializedError {
|
||||
const sanitized: SerializedError = { ...error }
|
||||
|
||||
// Sanitize message in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (isProduction()) {
|
||||
// Keep error name and structure, but sanitize message
|
||||
if (sensitiveKeys.some((key) => error.message?.toLowerCase().includes(key))) {
|
||||
sanitized.message = 'An error occurred due to invalid credentials or configuration'
|
||||
@@ -165,7 +166,7 @@ export function formatErrorForLogging(
|
||||
metadata: Record<string, unknown>
|
||||
} {
|
||||
const serialized = serializeError(error)
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
const isProd = isProduction()
|
||||
const errorToLog = isProd ? sanitizeError(serialized) : serialized
|
||||
const errorContext = extractErrorContext(errorToLog)
|
||||
|
||||
|
||||
@@ -11,25 +11,8 @@
|
||||
import winston from 'winston'
|
||||
import DailyRotateFile from 'winston-daily-rotate-file'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import { serializeError, sanitizeError } from './error-utils'
|
||||
|
||||
// Get log directory - use app.getPath('logs') in production, or local logs dir in development
|
||||
function getLogDir(): string {
|
||||
if (app && app.isReady()) {
|
||||
return app.getPath('logs')
|
||||
}
|
||||
// Fallback for development or before app is ready
|
||||
const devLogDir = path.join(process.cwd(), 'logs')
|
||||
if (!fs.existsSync(devLogDir)) {
|
||||
fs.mkdirSync(devLogDir, { recursive: true })
|
||||
}
|
||||
return devLogDir
|
||||
}
|
||||
|
||||
// Check if running in production
|
||||
const isProduction = app?.isPackaged ?? process.env.NODE_ENV === 'production'
|
||||
import { getLogDir, isProduction } from './shared'
|
||||
|
||||
// Custom format for console output - includes full error details
|
||||
const consoleFormat = winston.format.combine(
|
||||
@@ -41,7 +24,9 @@ const consoleFormat = winston.format.combine(
|
||||
// Format error with full stack trace
|
||||
let errorStr = ''
|
||||
if (error) {
|
||||
const serialized = isProduction ? sanitizeError(serializeError(error)) : serializeError(error)
|
||||
const serialized = isProduction()
|
||||
? sanitizeError(serializeError(error))
|
||||
: serializeError(error)
|
||||
if (serialized.stack) {
|
||||
errorStr = `\n${serialized.stack}`
|
||||
} else {
|
||||
@@ -60,7 +45,7 @@ const fileFormat = winston.format.combine(
|
||||
winston.format((info) => {
|
||||
// Serialize errors in metadata
|
||||
if (info.error) {
|
||||
info.error = isProduction
|
||||
info.error = isProduction()
|
||||
? sanitizeError(serializeError(info.error))
|
||||
: serializeError(info.error)
|
||||
}
|
||||
@@ -68,7 +53,7 @@ const fileFormat = winston.format.combine(
|
||||
// Serialize any error in meta fields
|
||||
for (const key of Object.keys(info)) {
|
||||
if (key !== 'error' && info[key] instanceof Error) {
|
||||
info[key] = isProduction
|
||||
info[key] = isProduction()
|
||||
? sanitizeError(serializeError(info[key]))
|
||||
: serializeError(info[key])
|
||||
}
|
||||
@@ -80,19 +65,20 @@ const fileFormat = winston.format.combine(
|
||||
)
|
||||
|
||||
// Daily rotate file transport configuration
|
||||
const createFileTransport = (level?: string): DailyRotateFile => {
|
||||
const createFileTransport = (level?: string, maxFiles?: string): DailyRotateFile => {
|
||||
return new DailyRotateFile({
|
||||
filename: path.join(getLogDir(), 'app-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d',
|
||||
maxFiles: maxFiles || '14d',
|
||||
level,
|
||||
format: fileFormat
|
||||
})
|
||||
}
|
||||
|
||||
// Create the logger instance with default level
|
||||
// Create the logger instance with default level - Console only initially
|
||||
// File transports are added after config is loaded via applyLoggingConfig()
|
||||
const logger = winston.createLogger({
|
||||
level: 'info', // Default level, can be updated via setLogLevel()
|
||||
defaultMeta: { service: 'erpauto' },
|
||||
@@ -100,9 +86,7 @@ const logger = winston.createLogger({
|
||||
// Console transport - always enabled
|
||||
new winston.transports.Console({
|
||||
format: consoleFormat
|
||||
}),
|
||||
// File transport for all levels
|
||||
createFileTransport()
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
@@ -114,20 +98,41 @@ export function setLogLevel(level: string): void {
|
||||
logger.level = level
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply logging configuration from config file
|
||||
* Removes existing DailyRotateFile transports and recreates them with config values
|
||||
*
|
||||
* @param config - Logging configuration from config.yaml
|
||||
*/
|
||||
export function applyLoggingConfig(config: { level: string; appRetention: number }): void {
|
||||
// Update log level
|
||||
setLogLevel(config.level)
|
||||
|
||||
// Remove existing DailyRotateFile transports
|
||||
const existingFileTransports = logger.transports.filter((t) => t instanceof DailyRotateFile)
|
||||
for (const transport of existingFileTransports) {
|
||||
logger.remove(transport)
|
||||
}
|
||||
|
||||
// Add app log transport with configured retention
|
||||
const retentionStr = `${config.appRetention}d`
|
||||
logger.add(createFileTransport(undefined, retentionStr))
|
||||
|
||||
// Add error-specific file transport in production
|
||||
if (app?.isPackaged) {
|
||||
if (isProduction()) {
|
||||
logger.add(
|
||||
new DailyRotateFile({
|
||||
filename: path.join(getLogDir(), 'error-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d',
|
||||
maxFiles: retentionStr,
|
||||
level: 'error',
|
||||
format: fileFormat
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a child logger with a specific context
|
||||
@@ -138,23 +143,8 @@ export function createLogger(context: string): winston.Logger {
|
||||
return logger.child({ context })
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error with full context and stack trace
|
||||
* This is the recommended way to log errors in the application
|
||||
*
|
||||
* @param log - Logger instance
|
||||
* @param message - Error message
|
||||
* @param error - The error object (Error, BaseError, or any)
|
||||
* @param meta - Additional metadata to include
|
||||
*/
|
||||
export function logError(
|
||||
log: winston.Logger,
|
||||
message: string,
|
||||
error: unknown,
|
||||
meta?: Record<string, unknown>
|
||||
): void {
|
||||
log.error(message, { error, ...meta })
|
||||
}
|
||||
// Re-export error utilities for convenience
|
||||
export { logError, formatErrorForLogging, serializeError, extractErrorContext } from './error-utils'
|
||||
|
||||
// Export the main logger for direct use
|
||||
export default logger
|
||||
|
||||
53
src/main/services/logger/shared.ts
Normal file
53
src/main/services/logger/shared.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Shared Logger Utilities
|
||||
* Common functions used across logger modules
|
||||
*/
|
||||
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { app } from 'electron'
|
||||
|
||||
/**
|
||||
* Get log directory path
|
||||
* Uses app.getPath('logs') in production, local logs dir in development
|
||||
*/
|
||||
export function getLogDir(): string {
|
||||
if (app && app.isReady()) {
|
||||
return app.getPath('logs')
|
||||
}
|
||||
// Fallback for development or before app is ready
|
||||
const devLogDir = path.join(process.cwd(), 'logs')
|
||||
if (!fs.existsSync(devLogDir)) {
|
||||
fs.mkdirSync(devLogDir, { recursive: true })
|
||||
}
|
||||
return devLogDir
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running in production environment
|
||||
* Uses app.isPackaged as the single source of truth
|
||||
*/
|
||||
export function isProduction(): boolean {
|
||||
return app?.isPackaged ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* Log level priority mapping (higher number = more severe)
|
||||
*/
|
||||
export const LOG_LEVEL_PRIORITY: Record<string, number> = {
|
||||
verbose: 0,
|
||||
debug: 1,
|
||||
info: 2,
|
||||
warn: 3,
|
||||
error: 4
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a log level should be logged given a threshold
|
||||
* @param level - The log level of the message
|
||||
* @param threshold - The minimum log level threshold
|
||||
* @returns true if the message should be logged
|
||||
*/
|
||||
export function isLoggable(level: string, threshold: string): boolean {
|
||||
return (LOG_LEVEL_PRIORITY[level] ?? 0) >= (LOG_LEVEL_PRIORITY[threshold] ?? 2)
|
||||
}
|
||||
@@ -163,10 +163,10 @@ export class BIPUsersDAO {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
logError(log, 'Authenticate failed', error, {
|
||||
logError(log, error, {
|
||||
message: 'Authenticate failed',
|
||||
operation: 'authenticate',
|
||||
username,
|
||||
dbType: this.dbType
|
||||
context: { username, dbType: this.dbType }
|
||||
})
|
||||
return null
|
||||
}
|
||||
@@ -222,10 +222,10 @@ export class BIPUsersDAO {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
logError(log, 'Silent login failed', error, {
|
||||
logError(log, error, {
|
||||
message: 'Silent login failed',
|
||||
operation: 'authenticateByComputerName',
|
||||
computerName,
|
||||
dbType: this.dbType
|
||||
context: { computerName, dbType: this.dbType }
|
||||
})
|
||||
return null
|
||||
}
|
||||
@@ -258,9 +258,10 @@ export class BIPUsersDAO {
|
||||
createTime: row.CreateTime as Date | undefined
|
||||
}))
|
||||
} catch (error) {
|
||||
logError(log, 'Get all users failed', error, {
|
||||
logError(log, error, {
|
||||
message: 'Get all users failed',
|
||||
operation: 'getAllUsers',
|
||||
dbType: this.dbType
|
||||
context: { dbType: this.dbType }
|
||||
})
|
||||
return []
|
||||
}
|
||||
@@ -345,11 +346,10 @@ export class BIPUsersDAO {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
logError(log, 'Create user failed', error, {
|
||||
logError(log, error, {
|
||||
message: 'Create user failed',
|
||||
operation: 'createUser',
|
||||
username,
|
||||
userType,
|
||||
dbType: this.dbType
|
||||
context: { username, userType, dbType: this.dbType }
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -389,11 +389,10 @@ export class BIPUsersDAO {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
logError(log, 'Update user type failed', error, {
|
||||
logError(log, error, {
|
||||
message: 'Update user type failed',
|
||||
operation: 'updateUserType',
|
||||
username,
|
||||
userType,
|
||||
dbType: this.dbType
|
||||
context: { username, userType, dbType: this.dbType }
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -433,10 +432,10 @@ export class BIPUsersDAO {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
logError(log, 'Update password failed', error, {
|
||||
logError(log, error, {
|
||||
message: 'Update password failed',
|
||||
operation: 'updatePassword',
|
||||
username,
|
||||
dbType: this.dbType
|
||||
context: { username, dbType: this.dbType }
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -472,10 +471,10 @@ export class BIPUsersDAO {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
logError(log, 'Delete user failed', error, {
|
||||
logError(log, error, {
|
||||
message: 'Delete user failed',
|
||||
operation: 'deleteUser',
|
||||
username,
|
||||
dbType: this.dbType
|
||||
context: { username, dbType: this.dbType }
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -513,10 +512,10 @@ export class BIPUsersDAO {
|
||||
return result.rows.length > 0 && (result.rows[0].count as number) > 0
|
||||
}
|
||||
} catch (error) {
|
||||
logError(log, 'Check user exists failed', error, {
|
||||
logError(log, error, {
|
||||
message: 'Check user exists failed',
|
||||
operation: 'userExists',
|
||||
username,
|
||||
dbType: this.dbType
|
||||
context: { username, dbType: this.dbType }
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -574,10 +573,10 @@ export class BIPUsersDAO {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
logError(log, 'Get user ERP credentials failed', error, {
|
||||
logError(log, error, {
|
||||
message: 'Get user ERP credentials failed',
|
||||
operation: 'getUserErpCredentials',
|
||||
username,
|
||||
dbType: this.dbType
|
||||
context: { username, dbType: this.dbType }
|
||||
})
|
||||
return null
|
||||
}
|
||||
@@ -626,10 +625,10 @@ export class BIPUsersDAO {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
logError(log, 'Update user ERP credentials failed', error, {
|
||||
logError(log, error, {
|
||||
message: 'Update user ERP credentials failed',
|
||||
operation: 'updateUserErpCredentials',
|
||||
username,
|
||||
dbType: this.dbType
|
||||
context: { username, dbType: this.dbType }
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -668,9 +667,10 @@ export class BIPUsersDAO {
|
||||
erpUsername: (row[cols.ERP_USERNAME] as string) || ''
|
||||
}))
|
||||
} catch (error) {
|
||||
logError(log, 'Get all users ERP config failed', error, {
|
||||
logError(log, error, {
|
||||
message: 'Get all users ERP config failed',
|
||||
operation: 'getAllUsersErpConfig',
|
||||
dbType: this.dbType
|
||||
context: { dbType: this.dbType }
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -2,13 +2,42 @@ import type { LogLevel } from '../../shared/ipc-channels'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
import { ipcRenderer } from '../lib/ipc'
|
||||
|
||||
// Cached log level for client-side filtering (avoids IPC for filtered-out messages)
|
||||
let cachedLevel: LogLevel = 'info'
|
||||
|
||||
/**
|
||||
* Check if a message at the given level should be logged
|
||||
* Based on level priority: error > warn > info > debug > verbose
|
||||
*/
|
||||
function shouldLog(level: LogLevel): boolean {
|
||||
const priorities: Record<LogLevel, number> = {
|
||||
verbose: 0,
|
||||
debug: 1,
|
||||
info: 2,
|
||||
warn: 3,
|
||||
error: 4
|
||||
}
|
||||
return (priorities[level] ?? 0) >= (priorities[cachedLevel] ?? 2)
|
||||
}
|
||||
|
||||
export const loggerApi = {
|
||||
log: (level: LogLevel, message: string, context?: Record<string, unknown>): void => {
|
||||
// Drop messages below the configured log level
|
||||
if (!shouldLog(level)) return
|
||||
|
||||
ipcRenderer.send(IPC_CHANNELS.LOGGER_FORWARD, {
|
||||
level,
|
||||
message,
|
||||
context,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the current log level from main process and cache it
|
||||
* Should be called early in renderer initialization
|
||||
*/
|
||||
fetchLevel: async (): Promise<void> => {
|
||||
cachedLevel = (await ipcRenderer.invoke(IPC_CHANNELS.LOGGER_GET_LEVEL)) as LogLevel
|
||||
}
|
||||
} as const
|
||||
|
||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@@ -129,6 +129,7 @@ export interface ConfigAPI {
|
||||
|
||||
export interface LoggerAPI {
|
||||
log: (level: LogLevel, message: string, context?: Record<string, unknown>) => void
|
||||
fetchLevel: () => Promise<void>
|
||||
}
|
||||
|
||||
export interface UpdateAPI {
|
||||
|
||||
@@ -221,9 +221,7 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
||||
|
||||
const toggleUserFilter = (username: string) => {
|
||||
setSelectedUsers((prev) =>
|
||||
prev.includes(username)
|
||||
? prev.filter((u) => u !== username)
|
||||
: [...prev, username]
|
||||
prev.includes(username) ? prev.filter((u) => u !== username) : [...prev, username]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -274,7 +272,10 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
||||
<span className="text-sm text-gray-600">
|
||||
{isAdmin ? (
|
||||
<span className="text-amber-600 font-medium">
|
||||
管理员模式:{selectedUsers.length > 0 ? `已选择 ${selectedUsers.length} 个用户` : '显示所有用户记录'}
|
||||
管理员模式:
|
||||
{selectedUsers.length > 0
|
||||
? `已选择 ${selectedUsers.length} 个用户`
|
||||
: '显示所有用户记录'}
|
||||
</span>
|
||||
) : (
|
||||
<span>仅显示您的操作记录</span>
|
||||
|
||||
@@ -47,7 +47,10 @@ export const ComparisonTooltip = React.memo(
|
||||
{user || '未分配'}:
|
||||
</span>
|
||||
<span className="font-medium text-slate-900">
|
||||
{firstMetric === 'executionTimeSecs' ? Number(userEntry.value).toFixed(1) : userEntry.value} {firstMetric === 'executionTimeSecs' ? '秒' : ''}
|
||||
{firstMetric === 'executionTimeSecs'
|
||||
? Number(userEntry.value).toFixed(1)
|
||||
: userEntry.value}{' '}
|
||||
{firstMetric === 'executionTimeSecs' ? '秒' : ''}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -40,7 +40,10 @@ export const CustomTooltip = React.memo(
|
||||
{entry.name}:
|
||||
</span>
|
||||
<span className="font-medium text-slate-900">
|
||||
{entry.dataKey === 'executionTimeSecs' ? Number(entry.value).toFixed(1) : entry.value} {entry.dataKey === 'executionTimeSecs' ? '秒' : ''}
|
||||
{entry.dataKey === 'executionTimeSecs'
|
||||
? Number(entry.value).toFixed(1)
|
||||
: entry.value}{' '}
|
||||
{entry.dataKey === 'executionTimeSecs' ? '秒' : ''}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -68,6 +68,10 @@ export function useAppBootstrap() {
|
||||
|
||||
const initializeAuth = useCallback(async () => {
|
||||
logger.info('=== Starting initializeAuth ===')
|
||||
|
||||
// Fetch log level early so client-side filtering takes effect
|
||||
await window.electron.logger.fetchLevel()
|
||||
|
||||
try {
|
||||
logger.debug('Getting computer name...')
|
||||
const computerNameResult = await window.electron.auth.getComputerName()
|
||||
|
||||
@@ -92,6 +92,7 @@ export const IPC_CHANNELS = {
|
||||
|
||||
// Logger
|
||||
LOGGER_FORWARD: 'logger:forward',
|
||||
LOGGER_GET_LEVEL: 'logger:getLevel',
|
||||
|
||||
// Report
|
||||
REPORT_LIST_ALL: 'report:listAll',
|
||||
@@ -122,4 +123,4 @@ export const IPC_CHANNELS = {
|
||||
/**
|
||||
* Log level for logger service
|
||||
*/
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
||||
export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose'
|
||||
|
||||
Reference in New Issue
Block a user