中间件的日志记录与监控
日志记录的必要性
日志记录是中间件开发中不可或缺的一环。它帮助开发者追踪应用运行状态,定位问题根源。在Express应用中,没有完善的日志系统就像在黑暗中调试代码,难以发现潜在的错误和性能瓶颈。
Express中的日志中间件
Express生态中有多个成熟的日志中间件可供选择。最常用的是morgan,它专门为HTTP请求日志设计。安装方式很简单:
npm install morgan
基本使用示例如下:
const express = require('express');
const morgan = require('morgan');
const app = express();
// 使用预定义的日志格式
app.use(morgan('combined'));
// 自定义日志格式
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
morgan支持多种预定义格式:
- 'combined':标准Apache组合日志格式
- 'common':基本日志格式
- 'dev':彩色开发日志
- 'short':极简格式
- 'tiny':最简格式
自定义日志中间件
有时预定义中间件不能满足需求,可以创建自定义日志中间件:
function requestLogger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms`);
});
next();
}
app.use(requestLogger);
这个自定义中间件记录了请求方法、URL、状态码和响应时间。
日志分级管理
生产环境需要区分日志级别,常用的有:
- error:错误日志
- warn:警告日志
- info:普通信息日志
- debug:调试信息
- verbose:详细日志
可以使用winston库实现分级日志:
const winston = require('winston');
const logger = winston.createLogger({
levels: winston.config.syslog.levels,
transports: [
new winston.transports.Console({
level: 'debug',
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
new winston.transports.File({
filename: 'error.log',
level: 'error'
})
]
});
// 使用示例
logger.error('数据库连接失败');
logger.info('服务器启动在3000端口');
日志存储策略
日志存储需要考虑几个方面:
- 本地文件存储
- 数据库存储
- 云服务存储
文件轮转是常见需求,可以使用winston-daily-rotate-file:
const DailyRotateFile = require('winston-daily-rotate-file');
logger.add(new DailyRotateFile({
filename: 'application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d'
}));
监控中间件性能
除了日志记录,性能监控同样重要。可以使用express-status-monitor:
const monitor = require('express-status-monitor')();
app.use(monitor);
这个中间件提供了可视化面板,展示:
- 请求响应时间
- 内存使用情况
- CPU负载
- 事件循环延迟
自定义性能监控
对于特定路由的性能监控,可以创建中间件:
function performanceMonitor(req, res, next) {
const start = process.hrtime();
res.on('finish', () => {
const diff = process.hrtime(start);
const duration = diff[0] * 1e3 + diff[1] * 1e-6; // 毫秒
if(duration > 500) {
logger.warn(`慢请求: ${req.method} ${req.url} 耗时 ${duration.toFixed(2)}ms`);
}
});
next();
}
// 应用到特定路由
app.get('/api/complex', performanceMonitor, (req, res) => {
// 复杂处理逻辑
});
错误追踪与日志关联
当应用出错时,需要将错误信息与请求关联。可以扩展错误处理中间件:
app.use((err, req, res, next) => {
const requestId = req.headers['x-request-id'] || require('crypto').randomBytes(8).toString('hex');
logger.error({
requestId,
method: req.method,
url: req.url,
error: err.stack,
user: req.user ? req.user.id : 'anonymous'
});
res.status(500).json({
error: 'Internal Server Error',
requestId
});
});
日志分析与可视化
收集日志后,可以使用ELK栈(Elasticsearch, Logstash, Kibana)进行分析:
- 使用Filebeat收集日志
- Logstash处理日志数据
- Elasticsearch存储日志
- Kibana可视化展示
配置示例(filebeat.yml):
filebeat.inputs:
- type: log
paths:
- /var/log/node-app/*.log
output.logstash:
hosts: ["logstash:5044"]
实时监控与告警
对于关键指标,需要设置实时告警。可以使用Prometheus和Grafana:
- 使用express-prom-bundle收集指标
- Prometheus存储时间序列数据
- Grafana展示仪表盘
const promBundle = require("express-prom-bundle");
const metricsMiddleware = promBundle({
includeMethod: true,
includePath: true,
customLabels: { project: 'my-app' }
});
app.use(metricsMiddleware);
日志安全考虑
日志记录需要注意安全事项:
- 不要记录敏感信息(密码、token等)
- 对用户数据进行脱敏处理
- 设置适当的日志访问权限
function sanitizeData(data) {
if (typeof data !== 'object') return data;
const sensitiveKeys = ['password', 'creditCard', 'token'];
const sanitized = {...data};
sensitiveKeys.forEach(key => {
if (sanitized[key]) {
sanitized[key] = '******';
}
});
return sanitized;
}
// 使用示例
logger.info('用户登录', {
user: req.body.username,
...sanitizeData(req.body)
});
分布式系统日志追踪
在微服务架构中,需要跨服务追踪请求。可以使用OpenTelemetry:
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const provider = new NodeTracerProvider();
provider.addSpanProcessor(
new SimpleSpanProcessor(
new JaegerExporter({
serviceName: 'express-app'
})
)
);
provider.register();
容器环境日志处理
在Docker或Kubernetes环境中,日志处理有所不同:
- 将日志输出到stdout/stderr
- 使用Fluentd或Fluent Bit收集日志
- 配置适当的日志驱动
FROM node:14
# 创建非root用户
RUN useradd -m appuser
USER appuser
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]
性能优化技巧
日志记录本身也会影响性能,需要注意:
- 避免同步日志写入
- 生产环境减少debug日志
- 使用批量写入代替频繁IO
// 批量写入示例
const logQueue = [];
let isWriting = false;
async function batchWriteLogs() {
if (isWriting || logQueue.length === 0) return;
isWriting = true;
const logsToWrite = [...logQueue];
logQueue.length = 0;
try {
await writeToDatabase(logsToWrite);
} catch (err) {
console.error('日志写入失败', err);
// 将失败的日志重新加入队列
logQueue.unshift(...logsToWrite);
} finally {
isWriting = false;
if (logQueue.length > 0) {
setImmediate(batchWriteLogs);
}
}
}
function logToQueue(message) {
logQueue.push(message);
if (logQueue.length >= 100) {
batchWriteLogs();
}
}
日志采样策略
高流量应用需要日志采样避免存储爆炸:
function shouldSample(req) {
// 重要请求总是记录
if (req.url.startsWith('/api/payment')) return true;
// 错误请求总是记录
if (res.statusCode >= 500) return true;
// 其他请求按10%采样
return Math.random() < 0.1;
}
app.use((req, res, next) => {
if (shouldSample(req)) {
logger.info(`${req.method} ${req.url}`);
}
next();
});
上下文增强日志
为日志添加更多上下文信息有助于调试:
const cls = require('cls-hooked');
const namespace = cls.createNamespace('app');
function contextLogger(req, res, next) {
namespace.run(() => {
namespace.set('requestId', req.headers['x-request-id'] || require('crypto').randomBytes(8).toString('hex'));
namespace.set('userId', req.user?.id || 'anonymous');
next();
});
}
function logWithContext(message) {
const requestId = namespace.get('requestId');
const userId = namespace.get('userId');
logger.info(`${requestId} [${userId}] ${message}`);
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:中间件的安全注意事项
下一篇:中间件的版本兼容性问题