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:
@@ -3,6 +3,9 @@ import { ErpAuthService } from './erp-auth'
|
||||
import type { CleanerInput, CleanerResult, OrderCleanDetail } from '../../types/cleaner.types'
|
||||
import type { ErpSession } from '../../types/erp.types'
|
||||
import type { FrameLocator, Locator, Page } from 'playwright'
|
||||
import { createLogger } from '../logger'
|
||||
|
||||
const log = createLogger('CleanerService')
|
||||
|
||||
/**
|
||||
* Cleaner Service Options
|
||||
@@ -103,6 +106,13 @@ export class CleanerService {
|
||||
}
|
||||
|
||||
const totalOrders = input.orderNumbers.length
|
||||
const dryRun = input.dryRun ?? this.dryRun
|
||||
|
||||
log.info('Starting cleaner', {
|
||||
totalOrders,
|
||||
materialCount: input.materialCodes.length,
|
||||
dryRun
|
||||
})
|
||||
|
||||
// Create delete set for O(1) lookup
|
||||
const deleteSet = new Set(input.materialCodes)
|
||||
@@ -121,6 +131,7 @@ export class CleanerService {
|
||||
const orderNumber = input.orderNumbers[i]
|
||||
|
||||
try {
|
||||
log.debug('Processing order', { orderNumber, index: i + 1, total: totalOrders })
|
||||
const detail = await this.processOrder({
|
||||
workFrame,
|
||||
popupPage,
|
||||
@@ -138,6 +149,7 @@ export class CleanerService {
|
||||
result.materialsSkipped += detail.materialsSkipped
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Order processing failed', { orderNumber, error: message })
|
||||
result.errors.push(`Order ${orderNumber}: ${message}`)
|
||||
|
||||
// Add error detail
|
||||
@@ -153,8 +165,15 @@ export class CleanerService {
|
||||
|
||||
// Close popup page
|
||||
await popupPage.close()
|
||||
log.info('Cleaner completed', {
|
||||
ordersProcessed: result.ordersProcessed,
|
||||
materialsDeleted: result.materialsDeleted,
|
||||
materialsSkipped: result.materialsSkipped,
|
||||
errorCount: result.errors.length
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Cleaner failed', { error: message })
|
||||
result.errors.push(`Clean failed: ${message}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import type {
|
||||
ExtractorCoreResult,
|
||||
ExtractionProgress
|
||||
} from '../../types/extractor.types'
|
||||
import { createLogger } from '../logger'
|
||||
|
||||
const log = createLogger('ExtractorCore')
|
||||
|
||||
/**
|
||||
* ExtractorCore - Handles all web page operations for data extraction
|
||||
|
||||
@@ -11,6 +11,9 @@ import type {
|
||||
ExtractionProgress
|
||||
} from '../../types/extractor.types'
|
||||
import { DataImportService } from '../database/data-importer'
|
||||
import { createLogger } from '../logger'
|
||||
|
||||
const log = createLogger('ExtractorService')
|
||||
|
||||
/**
|
||||
* ERP Data Extractor Service
|
||||
@@ -127,7 +130,7 @@ export class ExtractorService {
|
||||
return { mergedFile: null, recordCount: 0 }
|
||||
}
|
||||
|
||||
console.log(`[Extractor] Starting merge of ${filePaths.length} files`)
|
||||
log.info('Starting merge', { fileCount: filePaths.length })
|
||||
const parser = new ExcelParser({ verbose: true })
|
||||
|
||||
// Collect all orders with full order info and materials
|
||||
@@ -137,17 +140,17 @@ export class ExtractorService {
|
||||
// Parse each downloaded file and collect orders
|
||||
for (const filePath of filePaths) {
|
||||
try {
|
||||
console.log(`[Extractor] Parsing file: ${filePath}`)
|
||||
log.debug('Parsing file', { filePath })
|
||||
await parser.parse(filePath)
|
||||
// After parse(), the parser stores orders internally as lastOrders
|
||||
// After parse(), the parser store orders internally as lastOrders
|
||||
const orders = (parser as any).lastOrders
|
||||
console.log(`[Extractor] Parsed ${orders?.length || 0} orders from ${filePath}`)
|
||||
log.debug('File parsed', { filePath, orderCount: orders?.length || 0 })
|
||||
if (orders && Array.isArray(orders)) {
|
||||
allOrders.push(...orders)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[Extractor] Failed to parse file ${filePath}:`, errorMsg)
|
||||
log.error('Failed to parse file', { filePath, error: errorMsg })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,10 +160,10 @@ export class ExtractorService {
|
||||
recordCount += order.materials.length
|
||||
}
|
||||
|
||||
console.log(`[Extractor] Total orders: ${allOrders.length}, total records: ${recordCount}`)
|
||||
log.info('Merge summary', { orderCount: allOrders.length, recordCount })
|
||||
|
||||
if (recordCount === 0) {
|
||||
console.warn('[Extractor] No records found in any of the downloaded files')
|
||||
log.warn('No records found in any downloaded files')
|
||||
return { mergedFile: null, recordCount: 0 }
|
||||
}
|
||||
|
||||
@@ -174,17 +177,16 @@ export class ExtractorService {
|
||||
|
||||
// Save with error handling
|
||||
try {
|
||||
console.log(`[Extractor] Saving merged file to: ${outputPath}`)
|
||||
log.info('Saving merged file', { outputPath })
|
||||
await this.saveMergedOrders(allOrders, outputPath)
|
||||
console.log(`[Extractor] Successfully saved merged file with ${recordCount} records`)
|
||||
log.info('Merged file saved successfully', { recordCount })
|
||||
return { mergedFile: outputPath, recordCount }
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
const errorStack = error instanceof Error ? error.stack : ''
|
||||
console.error(`[Extractor] Failed to save merged file: ${errorMsg}`)
|
||||
console.error(`[Extractor] Error stack: ${errorStack}`)
|
||||
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, error: `保存合并文件失败: ${errorMsg}` }
|
||||
return { mergedFile: null, recordCount, error: `保存合并文件失败:${errorMsg}` }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,11 +198,11 @@ export class ExtractorService {
|
||||
orders: Array<{ orderInfo: any; materials: any[] }>,
|
||||
outputPath: string
|
||||
): Promise<void> {
|
||||
console.log(`[Extractor] Loading ExcelJS...`)
|
||||
log.debug('Loading ExcelJS')
|
||||
const ExcelJSModule = await import('exceljs')
|
||||
// Handle both ESM and CommonJS module formats
|
||||
const ExcelJS = ExcelJSModule.default || ExcelJSModule
|
||||
console.log(`[Extractor] ExcelJS loaded, creating workbook...`)
|
||||
log.debug('ExcelJS loaded, creating workbook')
|
||||
|
||||
const workbook = new ExcelJS.Workbook()
|
||||
const worksheet = workbook.addWorksheet('Data')
|
||||
@@ -239,7 +241,7 @@ export class ExtractorService {
|
||||
{ header: '打印日期', key: 'printDate', width: 20 }
|
||||
]
|
||||
|
||||
console.log(`[Extractor] Adding ${orders.length} orders to worksheet...`)
|
||||
log.debug('Adding orders to worksheet', { orderCount: orders.length })
|
||||
// Add data rows - merge orderInfo with each material
|
||||
for (const order of orders) {
|
||||
const { orderInfo, materials } = order
|
||||
@@ -283,9 +285,9 @@ export class ExtractorService {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Extractor] Writing file to ${outputPath}...`)
|
||||
log.debug('Writing file', { outputPath })
|
||||
await workbook.xlsx.writeFile(outputPath)
|
||||
console.log(`[Extractor] File saved successfully: ${outputPath}`)
|
||||
log.debug('File saved successfully', { outputPath })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,10 +298,10 @@ export class ExtractorService {
|
||||
for (const filePath of filePaths) {
|
||||
try {
|
||||
await fs.unlink(filePath)
|
||||
console.log(`Deleted temporary file: ${filePath}`)
|
||||
log.debug('Deleted temporary file', { filePath })
|
||||
} catch (error) {
|
||||
// Log error but don't fail the main process
|
||||
console.error(`Failed to delete temporary file ${filePath}:`, error)
|
||||
log.error('Failed to delete temporary file', { filePath, error })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,14 +312,14 @@ export class ExtractorService {
|
||||
* @returns Import result with statistics
|
||||
*/
|
||||
private async importToDatabase(filePath: string): Promise<ImportResult> {
|
||||
console.log(`[Extractor] Starting database import from: ${filePath}`)
|
||||
log.info('Starting database import', { filePath })
|
||||
|
||||
const importService = new DataImportService()
|
||||
|
||||
try {
|
||||
const result = await importService.importFromExcel(filePath, 1000)
|
||||
|
||||
console.log(`[Extractor] Import completed`, {
|
||||
log.info('Import completed', {
|
||||
success: result.success,
|
||||
recordsRead: result.recordsRead,
|
||||
recordsDeleted: result.recordsDeleted,
|
||||
@@ -327,7 +329,7 @@ export class ExtractorService {
|
||||
return result
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[Extractor] Import failed: ${errorMsg}`)
|
||||
log.error('Import failed', { error: errorMsg })
|
||||
|
||||
return {
|
||||
success: false,
|
||||
@@ -350,7 +352,7 @@ export class ExtractorService {
|
||||
filePath: string,
|
||||
onLog?: (level: LogLevel, message: string) => void
|
||||
): Promise<ImportResult> {
|
||||
console.log(`[Extractor] Starting database import from: ${filePath}`)
|
||||
log.info('Starting database import', { filePath })
|
||||
onLog?.('info', `开始导入数据到数据库...`)
|
||||
|
||||
const importService = new DataImportService()
|
||||
@@ -358,7 +360,7 @@ export class ExtractorService {
|
||||
try {
|
||||
const result = await importService.importFromExcel(filePath, 1000)
|
||||
|
||||
console.log(`[Extractor] Import completed`, {
|
||||
log.info('Import completed', {
|
||||
success: result.success,
|
||||
recordsRead: result.recordsRead,
|
||||
recordsDeleted: result.recordsDeleted,
|
||||
@@ -377,7 +379,7 @@ export class ExtractorService {
|
||||
return result
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[Extractor] Import failed: ${errorMsg}`)
|
||||
log.error('Import failed', { error: errorMsg })
|
||||
onLog?.('error', `导入失败:${errorMsg}`)
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import ExcelJS from 'exceljs'
|
||||
import type { DiscreteMaterialPlan, ExcelParseOptions, OrderHeader } from '../../types/excel.types'
|
||||
import path from 'path'
|
||||
import { createLogger } from '../logger'
|
||||
|
||||
const log = createLogger('ExcelParser')
|
||||
|
||||
/**
|
||||
* Excel Parser Service
|
||||
@@ -56,17 +59,11 @@ export class ExcelParser {
|
||||
this.verbose = options.verbose || false
|
||||
}
|
||||
|
||||
private log(...args: any[]): void {
|
||||
if (this.verbose) {
|
||||
console.log('[ExcelParser]', ...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Excel file and extract material plans
|
||||
*/
|
||||
async parse(filePath: string, options: ExcelParseOptions = {}): Promise<DiscreteMaterialPlan[]> {
|
||||
this.log('Parsing Excel file:', filePath)
|
||||
log.debug('Parsing Excel file:', filePath)
|
||||
|
||||
const workbook = new ExcelJS.Workbook()
|
||||
await workbook.xlsx.readFile(filePath)
|
||||
@@ -84,7 +81,7 @@ export class ExcelParser {
|
||||
allRows.push(row.values as any[])
|
||||
})
|
||||
|
||||
this.log(`Total rows read: ${allRows.length} (worksheet has ${worksheet.rowCount} rows)`)
|
||||
log.debug(`Total rows read: ${allRows.length} (worksheet has ${worksheet.rowCount} rows)`)
|
||||
|
||||
// Parse orders from rows
|
||||
const orders = this.parseOrders(allRows)
|
||||
@@ -98,7 +95,7 @@ export class ExcelParser {
|
||||
|
||||
// Skip empty orders if option is set
|
||||
if (options.skipEmptyOrders && materials.length === 0) {
|
||||
this.log('Skipping empty order:', orderInfo.productionOrder)
|
||||
log.debug('Skipping empty order:', orderInfo.productionOrder)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -125,7 +122,7 @@ export class ExcelParser {
|
||||
}
|
||||
}
|
||||
|
||||
this.log(`Parsed ${plans.length} material plans from ${orders.length} orders`)
|
||||
log.debug(`Parsed ${plans.length} material plans from ${orders.length} orders`)
|
||||
|
||||
return plans
|
||||
}
|
||||
@@ -143,7 +140,7 @@ export class ExcelParser {
|
||||
throw new Error('No parsed data available. Call parse() first.')
|
||||
}
|
||||
|
||||
this.log('Saving parsed data to Excel:', outputPath)
|
||||
log.debug('Saving parsed data to Excel:', outputPath)
|
||||
|
||||
const workbook = new ExcelJS.Workbook()
|
||||
const worksheet = workbook.addWorksheet('Data')
|
||||
@@ -226,7 +223,7 @@ export class ExcelParser {
|
||||
|
||||
// Save workbook
|
||||
await workbook.xlsx.writeFile(outputPath)
|
||||
this.log(
|
||||
log.debug(
|
||||
`Excel file saved: ${outputPath} (${orders.length} orders, ${worksheet.rowCount - 1} data rows)`
|
||||
)
|
||||
}
|
||||
@@ -253,7 +250,7 @@ export class ExcelParser {
|
||||
}
|
||||
|
||||
// Debug: check productionOrder extraction
|
||||
this.log(`Order ${orders.length + 1}: productionOrder="${orderInfo.productionOrder}"`)
|
||||
log.debug(`Order ${orders.length + 1}: productionOrder="${orderInfo.productionOrder}"`)
|
||||
|
||||
// Find table header row dynamically (look for "序号" in index 1)
|
||||
// Note: worksheet.eachRow() skips empty rows, so we can't use fixed offsets
|
||||
@@ -263,7 +260,7 @@ export class ExcelParser {
|
||||
}
|
||||
|
||||
if (tableRow >= allRows.length || !allRows[tableRow]) {
|
||||
this.log(' ⚠️ Table header not found, skipping this order')
|
||||
log.debug(' ⚠️ Table header not found, skipping this order')
|
||||
i++
|
||||
continue
|
||||
}
|
||||
@@ -280,7 +277,7 @@ export class ExcelParser {
|
||||
|
||||
if (isEmptyRow) {
|
||||
// No data, find footer info
|
||||
this.log('Order has no material data')
|
||||
log.debug('Order has no material data')
|
||||
const materials: any[] = []
|
||||
const footerInfo: OrderHeader = {}
|
||||
let dataRow = nextRow + 1
|
||||
@@ -306,7 +303,7 @@ export class ExcelParser {
|
||||
})
|
||||
} else {
|
||||
// Has data, extract materials
|
||||
this.log('Order has material data')
|
||||
log.debug('Order has material data')
|
||||
const materials: any[] = []
|
||||
const footerInfo: OrderHeader = {}
|
||||
let dataRow = tableRow + 1
|
||||
@@ -332,7 +329,7 @@ export class ExcelParser {
|
||||
if (dataRow + 1 < allRows.length && allRows[dataRow + 1]) {
|
||||
this.parseHeaderRow(allRows[dataRow + 1], footerInfo)
|
||||
}
|
||||
this.log(' Found footer row, stopping material parsing')
|
||||
log.debug(' Found footer row, stopping material parsing')
|
||||
break
|
||||
}
|
||||
|
||||
@@ -340,7 +337,7 @@ export class ExcelParser {
|
||||
// Next row is footer, parse current row as material first
|
||||
const material = this.parseMaterialRowInternal(allRows[dataRow], dataRow + 1)
|
||||
if (material) {
|
||||
this.log(' Parsed material:', material.materialCode)
|
||||
log.debug(' Parsed material:', material.materialCode)
|
||||
materials.push(material)
|
||||
}
|
||||
|
||||
@@ -349,17 +346,17 @@ export class ExcelParser {
|
||||
if (dataRow + 2 < allRows.length && allRows[dataRow + 2]) {
|
||||
this.parseHeaderRow(allRows[dataRow + 2], footerInfo)
|
||||
}
|
||||
this.log(' Found footer in next row, stopping material parsing')
|
||||
log.debug(' Found footer in next row, stopping material parsing')
|
||||
break
|
||||
}
|
||||
|
||||
// Extract material data
|
||||
const material = this.parseMaterialRowInternal(allRows[dataRow], dataRow + 1)
|
||||
if (material) {
|
||||
//this.log(' Parsed material:', material.materialCode)
|
||||
//log.debug(' Parsed material:', material.materialCode)
|
||||
materials.push(material)
|
||||
} else {
|
||||
this.log(' Skipped material row at', dataRow + 1)
|
||||
log.debug(' Skipped material row at', dataRow + 1)
|
||||
}
|
||||
|
||||
dataRow++
|
||||
@@ -371,7 +368,7 @@ export class ExcelParser {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.log(
|
||||
log.debug(
|
||||
` ⚠️ Table header check failed at row ${tableRow + 1}, value="${allRows[tableRow] ? allRows[tableRow][1] : 'null'}"`
|
||||
)
|
||||
}
|
||||
@@ -383,7 +380,7 @@ export class ExcelParser {
|
||||
}
|
||||
}
|
||||
|
||||
this.log(`parseOrders: Returning ${orders.length} orders`)
|
||||
log.debug(`parseOrders: Returning ${orders.length} orders`)
|
||||
return orders
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ import { SqlServerService } from '../database/sql-server'
|
||||
import { ConfigManager } from '../config/config-manager'
|
||||
import sql from 'mssql'
|
||||
import type { UserInfo } from '../../types/user.types'
|
||||
import { createLogger } from '../logger'
|
||||
|
||||
const log = createLogger('BipUsersDao')
|
||||
|
||||
/**
|
||||
* Database configuration for BIPUsers table
|
||||
@@ -160,7 +163,7 @@ export class BIPUsersDAO {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BIPUsersDAO] Authenticate error:', error)
|
||||
log.error('Authenticate error:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -215,7 +218,7 @@ export class BIPUsersDAO {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BIPUsersDAO] Authenticate by computer name error:', error)
|
||||
log.error('Authenticate by computer name error:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -247,7 +250,7 @@ export class BIPUsersDAO {
|
||||
createTime: row.CreateTime as Date | undefined
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('[BIPUsersDAO] Get all users error:', error)
|
||||
log.error('Get all users error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -331,7 +334,7 @@ export class BIPUsersDAO {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BIPUsersDAO] Create user error:', error)
|
||||
log.error('Create user error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -370,7 +373,7 @@ export class BIPUsersDAO {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BIPUsersDAO] Update user type error:', error)
|
||||
log.error('Update user type error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -409,7 +412,7 @@ export class BIPUsersDAO {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BIPUsersDAO] Update password error:', error)
|
||||
log.error('Update password error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -444,7 +447,7 @@ export class BIPUsersDAO {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BIPUsersDAO] Delete user error:', error)
|
||||
log.error('Delete user error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -481,7 +484,7 @@ export class BIPUsersDAO {
|
||||
return result.rows.length > 0 && (result.rows[0].count as number) > 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BIPUsersDAO] User exists error:', error)
|
||||
log.error('User exists error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -538,7 +541,7 @@ export class BIPUsersDAO {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BIPUsersDAO] Get user ERP credentials error:', error)
|
||||
log.error('Get user ERP credentials error:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -586,7 +589,7 @@ export class BIPUsersDAO {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BIPUsersDAO] Update user ERP credentials error:', error)
|
||||
log.error('Update user ERP credentials error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -624,7 +627,7 @@ export class BIPUsersDAO {
|
||||
erpUsername: (row[cols.ERP_USERNAME] as string) || ''
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('[BIPUsersDAO] Get all users ERP config error:', error)
|
||||
log.error('Get all users ERP config error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user