/** * Winston 日志配置模块 * 提供详细的日志记录,用于问题排查和系统监控 */ const winston = require('winston'); const path = require('path'); const fs = require('fs'); const config = require('./config.json'); // 创建日志目录 const logsDir = path.join(__dirname, 'logs'); if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir, { recursive: true }); } // 获取日志配置 const logConfig = config.logging || { level: 'info', maxFileSize: 10485760, maxFiles: 20 }; /** * 日志格式化函数 - 统一使用 */ const formatLog = (info) => { let log = `${info.timestamp} [${info.level.toUpperCase()}]`; // 添加IP前缀(最高优先级) if (info.ip) { log += ` [IP: ${info.ip}]`; } // 添加设备前缀 if (info.device) { log += ` [设备: ${info.device}]`; } if (info.module) { log += ` [${info.module}]`; } if (info.clientId) { log += ` [客户端#${info.clientId}]`; } log += `: ${info.message}`; // 添加元数据 if (Object.keys(info).length > 0) { const metaKeys = ['module', 'device', 'clientId', 'timestamp', 'level', 'message', 'service', 'splat']; const meta = Object.entries(info) .filter(([key]) => !metaKeys.includes(key) && !key.startsWith('Symbol')) .map(([key, val]) => { if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') { return `${key}=${val}`; } else if (typeof val === 'object' && val !== null) { return `${key}=${JSON.stringify(val)}`; } return ''; }) .filter(m => m) .join(', '); if (meta) { log += ` {${meta}}`; } } // 添加堆栈跟踪 if (info.stack) { log += `\n${info.stack}`; } return log; }; /** * 创建 Winston Logger 实例 */ const logger = winston.createLogger({ level: process.env.LOG_LEVEL || logConfig.level || 'info', format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), winston.format.errors({ stack: true }) ), defaultMeta: { service: 'dialysis-server' }, transports: [ // 控制台输出 new winston.transports.Console({ format: winston.format.combine( winston.format.colorize({ all: true }), winston.format.printf(info => formatLog(info)) ), level: 'debug' }), // 所有日志文件 new winston.transports.File({ filename: path.join(logsDir, 'all.log'), maxsize: logConfig.maxFileSize || 10485760, maxFiles: logConfig.maxFiles || 20, tailable: true, format: winston.format.printf(info => formatLog(info)) }), // 错误日志文件 new winston.transports.File({ filename: path.join(logsDir, 'error.log'), level: 'error', maxsize: logConfig.maxFileSize || 10485760, maxFiles: logConfig.maxFiles || 20, tailable: true, format: winston.format.printf(info => formatLog(info)) }) ] }); // 创建自定义 Transport 过滤器函数 const createModuleTransport = (filename, moduleName) => { class ModuleFilterTransport extends winston.transports.File { log(info, callback) { if (info.module === moduleName) { super.log(info, callback); } else { if (callback) callback(); } } } return new ModuleFilterTransport({ filename: filename, maxsize: logConfig.maxFileSize || 10485760, maxFiles: logConfig.maxFiles || 20, tailable: true, format: winston.format.printf(info => formatLog(info)) }); }; // 添加模块级别的 log 文件 logger.add(createModuleTransport( path.join(logsDir, 'tcp-connections.log'), 'tcp' )); logger.add(createModuleTransport( path.join(logsDir, 'data-parsing.log'), 'parser' )); logger.add(createModuleTransport( path.join(logsDir, 'mqtt.log'), 'mqtt' )); logger.add(createModuleTransport( path.join(logsDir, 'aliyun-iot.log'), 'aliyun' )); /** * 导出日志接口 */ module.exports = { logger, // TCP 连接日志 logTcpConnection: (ip, port, clientId) => { logger.info(`客户端连接建立`, { module: 'tcp', clientId, ip, port, timestamp: new Date().toISOString() }); }, logTcpDisconnection: (clientId, recordCount, deviceSN = null, ip = null) => { logger.info(`客户端连接断开`, { module: 'tcp', clientId, device: deviceSN, ip, recordCount, timestamp: new Date().toISOString() }); }, logTcpError: (clientId, error, deviceSN = null, ip = null) => { logger.error(`TCP 连接错误`, { module: 'tcp', clientId, device: deviceSN, ip, error: error.message, stack: error.stack }); }, // 数据解析日志 logDataReceived: (clientId, deviceSN, dataSize) => { logger.debug(`收到设备数据`, { module: 'parser', clientId, device: deviceSN, dataSize: `${dataSize} bytes` }); }, logDataParsed: (clientId, deviceSN, recordCount, parameters) => { logger.info(`数据解析成功`, { module: 'parser', clientId, device: deviceSN, recordCount, parameterCount: Object.keys(parameters || {}).length }); }, logParsingError: (clientId, error) => { logger.error(`数据解析失败`, { module: 'parser', clientId, error: error.message, stack: error.stack }); }, // MQTT 日志 logMqttConnecting: (deviceSN) => { logger.info(`正在连接 MQTT 服务`, { module: 'mqtt', device: deviceSN }); }, logMqttConnected: (deviceSN, brokerUrl) => { logger.info(`MQTT 连接成功`, { module: 'mqtt', device: deviceSN, broker: brokerUrl }); }, logMqttConnectionError: (deviceSN, error) => { logger.error(`MQTT 连接失败`, { module: 'mqtt', device: deviceSN, error: error.message }); }, logMqttPublishing: (deviceSN, topic, dataSize) => { logger.debug(`发布 MQTT 消息`, { module: 'mqtt', device: deviceSN, topic, size: `${dataSize} bytes` }); }, logMqttPublishError: (deviceSN, topic, error, payload) => { logger.error(`MQTT 发布失败`, { module: 'mqtt', device: deviceSN, topic, errorMessage: error.message || String(error), errorCode: error.code || 'UNKNOWN', payloadSize: payload ? `${payload.length} bytes` : 'N/A', payloadPreview: payload ? payload.substring(0, 150) : 'N/A', timestamp: new Date().toISOString() }); }, logMqttOffline: (deviceSN) => { logger.warn(`MQTT 离线`, { module: 'mqtt', device: deviceSN }); }, logMqttReconnecting: (deviceSN) => { logger.info(`MQTT 重新连接中`, { module: 'mqtt', device: deviceSN }); }, /** * 记录 MQTT 数据发送前的信息 */ logMqttDataSending: (deviceSN, topic, messageType, payload) => { logger.info(`发送 MQTT 消息前`, { module: 'mqtt', device: deviceSN, topic, type: messageType, size: `${payload.length} bytes`, payload: payload.substring(0, 200) // 记录前200个字符预览 }); }, /** * 记录 MQTT 发布成功 */ logMqttPublishSuccess: (deviceSN, topic, messageType, payload) => { logger.info(`MQTT 消息发布成功`, { module: 'mqtt', device: deviceSN, topic, type: messageType, size: `${payload.length} bytes`, timestamp: new Date().toISOString() }); }, /** * 记录 MQTT 发布失败 - 增强版,包含发送内容 */ logMqttPublishFailure: (deviceSN, topic, messageType, error, payload) => { logger.error(`MQTT 消息发布失败`, { module: 'mqtt', device: deviceSN, topic, type: messageType, errorMessage: error.message || error, errorCode: error.code || 'UNKNOWN', size: `${payload.length} bytes`, payload: payload.substring(0, 200), // 记录前200个字符预览 timestamp: new Date().toISOString() }); }, // 阿里云 IoT 日志 logAliyunConnecting: (deviceSN) => { logger.info(`正在连接阿里云 IoT`, { module: 'aliyun', device: deviceSN }); }, logAliyunFetchingTriplet: (deviceSN) => { logger.info(`正在从 API 获取设备三元组`, { module: 'aliyun', device: deviceSN }); }, logAliyunTripletFetched: (deviceSN, triplet) => { logger.info(`设备三元组获取成功`, { module: 'aliyun', device: deviceSN, deviceName: triplet.deviceName, hasSecret: !!triplet.deviceSecret }); }, logAliyunTripletFetchError: (deviceSN, error) => { logger.warn(`从 API 获取三元组失败,尝试本地配置`, { module: 'aliyun', device: deviceSN, error: error.message || error }); }, logAliyunConnected: (deviceSN, deviceName) => { logger.info(`阿里云 IoT 连接成功`, { module: 'aliyun', device: deviceSN, deviceName }); }, logAliyunConnectionError: (deviceSN, error) => { logger.error(`阿里云 IoT 连接失败`, { module: 'aliyun', device: deviceSN, error: error.message }); }, logAliyunPublishing: (deviceSN, type, dataSize) => { logger.debug(`发布阿里云 IoT 消息`, { module: 'aliyun', device: deviceSN, type, size: `${dataSize} bytes` }); }, logAliyunPublishError: (deviceSN, type, error) => { logger.error(`阿里云 IoT 发布失败`, { module: 'aliyun', device: deviceSN, type, error: error.message }); }, logAliyunOffline: (deviceSN) => { logger.warn(`阿里云 IoT 离线`, { module: 'aliyun', device: deviceSN }); }, // 业务逻辑日志 logStateEvent: (deviceSN, eventType, eventDescription) => { logger.info(`检测到设备状态事件`, { module: 'parser', device: deviceSN, eventType, eventDescription }); }, logDeviceRegistration: (deviceSN, ip) => { logger.info(`新设备已注册`, { device: deviceSN, ip, timestamp: new Date().toISOString() }); }, // TCP 原始消息日志 logRawTcpMessage: (clientId, data, deviceSN = null, ip = null) => { // 记录原始二进制数据的十六进制表示和 ASCII 表示 let hexStr = ''; let asciiStr = ''; for (let i = 0; i < data.length; i++) { const byte = data[i]; hexStr += byte.toString(16).padStart(2, '0').toUpperCase() + ' '; // 可打印的 ASCII 字符直接显示,否则显示 . asciiStr += (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.'; } // 如果数据较长,分段显示 let message = `收到原始 TCP 消息 (${data.length} bytes)`; if (data.length > 100) { message += `\n 十六进制 (前100字节): ${hexStr.substring(0, 300)}...`; message += `\n ASCII (前100字节): ${asciiStr.substring(0, 100)}...`; } else { message += `\n 十六进制: ${hexStr}`; message += `\n ASCII: ${asciiStr}`; } logger.debug(message, { module: 'tcp', clientId, device: deviceSN, ip, totalBytes: data.length, rawData: data.toString('hex') // 存储完整的十六进制数据供查询 }); }, // 通用日志 logInfo: (message, meta = {}) => { logger.info(message, meta); }, logWarn: (message, meta = {}) => { logger.warn(message, meta); }, logError: (message, error, meta = {}) => { logger.error(message, { ...meta, error: error.message, stack: error.stack }); }, logDebug: (message, meta = {}) => { logger.debug(message, meta); } };