日志管理
日志管理的重要性
日志是应用程序运行过程中产生的记录,用于追踪系统行为、排查问题和分析性能。良好的日志管理能帮助开发者快速定位问题,提高系统可维护性。在Node.js中,日志管理尤为重要,因为Node.js的单线程特性使得错误追踪更加依赖日志记录。
常见的日志级别
日志通常分为几个级别,每个级别对应不同的重要性:
const levels = {
error: 0, // 错误,需要立即处理
warn: 1, // 警告,潜在问题
info: 2, // 重要信息
verbose: 3, // 详细信息
debug: 4, // 调试信息
silly: 5 // 最详细的日志
};
Node.js中的日志模块
console模块
Node.js内置的console模块是最基础的日志工具:
console.error('错误信息');
console.warn('警告信息');
console.info('普通信息');
console.log('等同于info');
console.debug('调试信息');
Winston日志库
Winston是Node.js中最流行的日志库之一:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
Bunyan日志库
另一个流行的选择是Bunyan,它特别适合结构化日志:
const bunyan = require('bunyan');
const log = bunyan.createLogger({
name: 'myapp',
streams: [
{
level: 'error',
path: '/var/tmp/myapp-error.log'
},
{
level: 'info',
stream: process.stdout
}
]
});
log.info('服务器启动');
log.error({err: new Error('错误示例')}, '发生错误');
日志格式最佳实践
结构化日志
结构化日志比纯文本日志更易于分析和处理:
// 不好的做法
logger.info('用户登录: 张三');
// 好的做法
logger.info({
event: 'user_login',
username: '张三',
ip: '192.168.1.1',
timestamp: new Date().toISOString()
});
包含上下文信息
日志应包含足够的上下文信息:
function processOrder(order) {
const logContext = {
orderId: order.id,
userId: order.userId,
amount: order.amount
};
logger.info(logContext, '开始处理订单');
try {
// 处理订单逻辑
logger.info(logContext, '订单处理成功');
} catch (err) {
logger.error({...logContext, error: err.message}, '订单处理失败');
throw err;
}
}
日志存储策略
文件存储
最基本的日志存储方式是写入文件:
const fs = require('fs');
const path = require('path');
function writeLog(level, message) {
const logFile = path.join(__dirname, 'app.log');
const logEntry = `[${new Date().toISOString()}] [${level}] ${message}\n`;
fs.appendFile(logFile, logEntry, (err) => {
if (err) console.error('写入日志失败:', err);
});
}
日志轮转
防止日志文件过大,需要实现日志轮转:
const { createGzip } = require('zlib');
const { pipeline } = require('stream');
function rotateLogs() {
const currentDate = new Date();
const oldFile = 'app.log';
const newFile = `app.${currentDate.toISOString().split('T')[0]}.log.gz`;
pipeline(
fs.createReadStream(oldFile),
createGzip(),
fs.createWriteStream(newFile),
(err) => {
if (err) {
console.error('日志轮转失败:', err);
} else {
fs.truncate(oldFile, 0, (err) => {
if (err) console.error('清空日志文件失败:', err);
});
}
}
);
}
// 每天午夜执行日志轮转
setInterval(rotateLogs, 24 * 60 * 60 * 1000);
日志分析与监控
ELK Stack
ELK(Elasticsearch, Logstash, Kibana)是流行的日志分析解决方案:
- Logstash配置示例:
input {
file {
path => "/var/log/node-app/*.log"
start_position => "beginning"
}
}
filter {
grok {
match => { "message" => "\[%{TIMESTAMP_ISO8601:timestamp}\] \[%{LOGLEVEL:level}\] %{GREEDYDATA:message}" }
}
}
output {
elasticsearch {
hosts => ["localhost:9200"]
}
}
实时日志监控
使用Socket.IO实现实时日志监控:
const io = require('socket.io')(3001);
const tail = require('tail').Tail;
const logFile = new tail('app.log');
io.on('connection', (socket) => {
logFile.on('line', (data) => {
socket.emit('log', data);
});
});
性能考虑
异步日志记录
同步日志会阻塞事件循环,应尽可能使用异步:
// 同步方式 - 不推荐
fs.writeFileSync('sync.log', logEntry);
// 异步方式 - 推荐
fs.writeFile('async.log', logEntry, (err) => {
if (err) console.error('异步写入日志失败:', err);
});
批量写入
高频日志场景应考虑批量写入:
let logBuffer = [];
const BATCH_SIZE = 100;
const BATCH_INTERVAL = 5000; // 5秒
function addToBuffer(logEntry) {
logBuffer.push(logEntry);
if (logBuffer.length >= BATCH_SIZE) {
flushLogs();
}
}
function flushLogs() {
if (logBuffer.length === 0) return;
const logsToWrite = logBuffer.join('\n');
logBuffer = [];
fs.appendFile('batch.log', logsToWrite + '\n', (err) => {
if (err) console.error('批量写入日志失败:', err);
});
}
// 定时刷新缓冲区
setInterval(flushLogs, BATCH_INTERVAL);
安全考虑
敏感信息过滤
日志中不应包含敏感信息:
function sanitizeLog(data) {
const sensitiveFields = ['password', 'creditCard', 'ssn'];
return JSON.parse(JSON.stringify(data, (key, value) => {
if (sensitiveFields.includes(key)) {
return '[REDACTED]';
}
return value;
}));
}
logger.info(sanitizeLog({
username: 'user1',
password: 'secret123',
action: 'login'
}));
日志访问控制
确保日志文件有适当的权限:
// 设置日志文件权限为640 (rw-r-----)
fs.chmod('app.log', 0o640, (err) => {
if (err) console.error('设置文件权限失败:', err);
});
多环境日志配置
不同环境应有不同的日志配置:
function createLogger(env) {
const commonTransports = [
new winston.transports.File({ filename: 'errors.log', level: 'error' })
];
if (env === 'production') {
return winston.createLogger({
level: 'info',
transports: [
...commonTransports,
new winston.transports.File({ filename: 'combined.log' })
]
});
} else {
return winston.createLogger({
level: 'debug',
transports: [
...commonTransports,
new winston.transports.Console()
]
});
}
}
请求追踪
在Web应用中,为每个请求分配唯一ID便于追踪:
const uuid = require('uuid');
app.use((req, res, next) => {
req.requestId = uuid.v4();
logger.info({
requestId: req.requestId,
method: req.method,
url: req.url,
ip: req.ip
}, '收到请求');
const originalEnd = res.end;
res.end = function(...args) {
logger.info({
requestId: req.requestId,
statusCode: res.statusCode,
responseTime: Date.now() - req.startTime
}, '请求完成');
originalEnd.apply(res, args);
};
req.startTime = Date.now();
next();
});
错误处理与日志
正确处理错误并记录:
process.on('uncaughtException', (err) => {
logger.error({
error: err.message,
stack: err.stack
}, '未捕获的异常');
// 根据严重程度决定是否退出
if (err.isFatal) {
process.exit(1);
}
});
process.on('unhandledRejection', (reason, promise) => {
logger.error({
reason: reason instanceof Error ? reason.stack : reason,
promise
}, '未处理的Promise拒绝');
});
日志测试
确保日志系统正常工作:
describe('日志系统', () => {
let logOutput;
const originalWrite = process.stdout.write;
beforeEach(() => {
logOutput = '';
process.stdout.write = (chunk) => {
logOutput += chunk;
};
});
afterEach(() => {
process.stdout.write = originalWrite;
});
it('应正确记录错误', () => {
logger.error('测试错误');
expect(logOutput).to.contain('测试错误');
expect(logOutput).to.contain('error');
});
});
日志与性能监控集成
将日志与APM工具集成:
const apm = require('elastic-apm-node').start({
serviceName: 'my-node-app'
});
function trackError(err) {
apm.captureError(err);
logger.error({
error: err.message,
stack: err.stack,
transactionId: apm.currentTransaction?.ids['transaction.id']
}, '应用程序错误');
}
try {
// 可能出错的代码
} catch (err) {
trackError(err);
}
自定义日志格式
创建自定义日志格式:
const { format } = require('winston');
const util = require('util');
const customFormat = format.printf(({ level, message, timestamp, ...metadata }) => {
let msg = `${timestamp} [${level}] ${message}`;
if (Object.keys(metadata).length > 0) {
msg += ' ' + util.inspect(metadata, { colors: true, depth: null });
}
return msg;
});
const logger = winston.createLogger({
format: format.combine(
format.timestamp(),
format.colorize(),
customFormat
),
transports: [new winston.transports.Console()]
});
日志采样
高流量环境下可考虑日志采样:
const sampledLogger = winston.createLogger({
transports: [
new winston.transports.Console({
level: 'info',
sampleRate: 0.1 // 只记录10%的日志
}),
new winston.transports.File({
filename: 'important.log',
level: 'error' // 错误日志全部记录
})
]
});
分布式系统日志
在微服务架构中,需要集中式日志管理:
const { createLogger } = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');
const esTransport = new ElasticsearchTransport({
level: 'info',
clientOpts: { node: 'http://localhost:9200' }
});
const logger = createLogger({
transports: [esTransport]
});
// 添加服务标识
logger.defaultMeta = { service: 'order-service' };
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn