/**
|
* 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);
|
}
|
};
|