您现在的位置是:网站首页 > 日志系统的集成与配置文章详情
日志系统的集成与配置
陈川
【
Node.js
】
45668人已围观
8476字
日志系统是Express应用中不可或缺的一部分,它帮助开发者追踪请求、调试错误并监控应用状态。合理的日志配置能显著提升开发效率和运维能力,尤其是在生产环境中。
日志系统的重要性
日志记录是应用运行时的关键数据来源。通过日志可以分析用户行为、排查错误、监控性能瓶颈。在Express中,常见的日志需求包括:
- 记录HTTP请求的详细信息(方法、路径、状态码)
- 捕获未处理的异常和错误
- 区分不同环境(开发/生产)的日志级别
- 长期存储和日志轮转
没有日志的系统就像黑箱,出现问题难以定位。例如当API响应异常时,通过访问日志可以快速判断是客户端参数错误还是服务端处理失败。
常用日志库选择
Express生态中有多个成熟的日志解决方案:
- morgan - 专注于HTTP请求日志的中间件
const morgan = require('morgan');
app.use(morgan('combined')); // 使用Apache标准格式
- winston - 功能全面的日志库,支持多传输方式
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' })
]
});
- pino - 高性能JSON格式日志
const pino = require('pino');
const logger = pino({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug'
});
- bunyan - 结构化日志库,适合复杂系统
const bunyan = require('bunyan');
const log = bunyan.createLogger({
name: 'myapp',
streams: [
{ level: 'error', path: '/var/log/myapp-error.log' }
]
});
选择时需考虑性能需求(如pino在高压环境下表现优异)和功能需求(如winston的多传输支持)。
集成morgan进行请求日志记录
morgan是Express最常用的请求日志中间件,支持多种预定义格式:
const express = require('express');
const morgan = require('morgan');
const app = express();
// 开发环境使用带颜色的简洁格式
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
} else {
// 生产环境使用标准Apache组合格式
app.use(morgan('combined'));
}
// 自定义token
morgan.token('host', (req) => req.headers['host']);
app.use(morgan(':method :host :url :status'));
高级配置示例,将日志写入文件并实现日志轮转:
const fs = require('fs');
const path = require('path');
const rfs = require('rotating-file-stream');
const logDirectory = path.join(__dirname, 'log');
fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory);
const accessLogStream = rfs.createStream('access.log', {
interval: '1d', // 每天轮转
path: logDirectory
});
app.use(morgan('combined', { stream: accessLogStream }));
使用winston构建完整日志系统
winston的强大之处在于其灵活的配置能力:
const { createLogger, format, transports } = require('winston');
const { combine, timestamp, printf, colorize } = format;
const logFormat = printf(({ level, message, timestamp }) => {
return `${timestamp} [${level}]: ${message}`;
});
const logger = createLogger({
level: 'debug',
format: combine(
colorize(),
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
logFormat
),
transports: [
new transports.Console(),
new transports.File({
filename: 'logs/combined.log',
level: 'info'
}),
new transports.File({
filename: 'logs/errors.log',
level: 'error',
maxsize: 1024 * 1024 * 5 // 5MB
})
],
exceptionHandlers: [
new transports.File({ filename: 'logs/exceptions.log' })
]
});
// 在Express中使用
app.use((err, req, res, next) => {
logger.error(`${err.status || 500} - ${err.message}`);
next(err);
});
实现按日期分文件的日志存储:
const { createLogger, transports, format } = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const logger = createLogger({
transports: [
new DailyRotateFile({
filename: 'logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d'
})
]
});
错误处理与日志关联
将错误日志与请求上下文关联非常重要:
const uuid = require('uuid');
app.use((req, res, next) => {
req.requestId = uuid.v4(); // 为每个请求生成唯一ID
next();
});
app.use(morgan(':requestId :method :url :status', {
stream: {
write: (message) => logger.info(message.trim())
}
}));
app.use((err, req, res, next) => {
logger.error({
requestId: req.requestId,
error: err.stack,
route: req.originalUrl
});
res.status(500).send('Internal Server Error');
});
日志级别与分类控制
合理的日志分级能有效管理日志量:
const logger = winston.createLogger({
levels: {
emergency: 0,
alert: 1,
critical: 2,
error: 3,
warning: 4,
notice: 5,
info: 6,
debug: 7
},
transports: [
new transports.Console({ level: 'info' }),
new transports.File({
filename: 'debug.log',
level: 'debug'
})
]
});
// 业务代码中使用不同级别
logger.debug('Debugging info');
logger.info('User login');
logger.warn('Deprecated API called');
logger.error('Database connection failed');
生产环境最佳实践
生产环境中的日志配置需要考虑:
- 敏感信息过滤
morgan.token('body', (req) => {
if (req.body.password) {
const filtered = { ...req.body, password: '******' };
return JSON.stringify(filtered);
}
return JSON.stringify(req.body);
});
app.use(morgan(':method :url :status :body'));
- 日志采样 - 在高流量时避免日志爆炸
app.use(morgan('combined', {
skip: (req, res) => Math.random() > 0.5 // 50%采样率
}));
- 集中式日志管理 - 使用ELK或Splunk等工具
const { ElasticsearchTransport } = require('winston-elasticsearch');
const esTransport = new ElasticsearchTransport({
level: 'info',
clientOpts: { node: 'http://localhost:9200' }
});
logger.add(esTransport);
- 性能考量 - 使用异步日志写入
const asyncLogger = winston.createLogger({
transports: [
new winston.transports.File({
filename: 'async.log',
handleExceptions: true,
maxsize: 5242880, // 5MB
maxFiles: 5,
tailable: true,
zippedArchive: true,
options: { flags: 'a' }
})
],
exitOnError: false
});
自定义日志格式与扩展
创建符合业务需求的日志格式:
const { format } = require('winston');
const util = require('util');
const customFormat = format((info) => {
info.appName = 'MyExpressApp';
info.environment = process.env.NODE_ENV || 'development';
if (info.message instanceof Error) {
info.stack = info.message.stack;
info.message = info.message.message;
}
return info;
});
const logger = winston.createLogger({
format: format.combine(
customFormat(),
format.timestamp(),
format.json()
),
transports: [new transports.Console()]
});
// 使用util.inspect深度打印对象
logger.debug('Current session: %o', req.session);
日志与监控系统集成
将日志指标导入监控系统:
const Prometheus = require('prom-client');
const httpRequestDurationMicroseconds = new Prometheus.Histogram({
name: 'http_request_duration_ms',
help: 'Duration of HTTP requests in ms',
labelNames: ['method', 'route', 'code'],
buckets: [0.1, 5, 15, 50, 100, 300, 500, 1000, 3000, 5000]
});
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
httpRequestDurationMicroseconds
.labels(req.method, req.route.path, res.statusCode)
.observe(duration);
});
next();
});
多环境日志策略
不同环境应采用不同的日志策略:
function createLogger(env) {
const commonTransports = [
new transports.File({ filename: 'logs/errors.log', level: 'error' })
];
if (env === 'development') {
return winston.createLogger({
level: 'debug',
format: format.combine(
format.colorize(),
format.simple()
),
transports: [
...commonTransports,
new transports.Console()
]
});
} else {
return winston.createLogger({
level: 'info',
format: format.json(),
transports: [
...commonTransports,
new transports.File({ filename: 'logs/combined.log' }),
process.env.LOGSTASH_HOST && new LogstashTransport({
host: process.env.LOGSTASH_HOST,
port: 5044
})
].filter(Boolean)
});
}
}
日志性能优化技巧
在高性能场景下的日志优化:
- 批量写入 - 减少IO操作
const { BatchTransport } = require('winston-transport');
class MemoryTransport extends BatchTransport {
constructor(opts) {
super(opts);
this.batch = [];
setInterval(() => this.flush(), 5000); // 每5秒刷一次
}
logBatch(batch, callback) {
fs.appendFile('bulk.log', batch.join('\n'), callback);
}
}
logger.add(new MemoryTransport());
- 使用worker线程 - 避免阻塞主线程
const { Worker } = require('worker_threads');
class WorkerTransport extends winston.Transport {
constructor() {
super();
this.worker = new Worker('./logger-worker.js');
}
log(info, callback) {
this.worker.postMessage(info);
callback();
}
}
- 选择性日志记录
app.use((req, res, next) => {
// 跳过健康检查的日志
if (req.path === '/health') return next();
morgan('combined')(req, res, next);
});