在Node.js应用中,通过日志库(如winston、morgan)记录请求的方法、URL、时间戳、状态码、响应时间等关键信息,是追踪链路的基础。
morgan(HTTP请求专用中间件):morgan提供预定义格式(如dev、combined)和自定义格式,可快速记录HTTP请求详情。例如:const morgan = require('morgan');
app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); // 自定义格式
输出示例:GET /api/users 200 123 - 45ms(表示GET请求/api/users,状态码200,响应时间45ms)。winston(结构化日志库):winston支持JSON格式、多传输目标(文件、控制台),适合生产环境。配置示例如下:const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
new winston.transports.Console() // 开发环境输出到控制台
]
});
// 请求日志中间件
app.use((req, res, next) => {
logger.info({
message: 'Incoming request',
method: req.method,
url: req.originalUrl,
headers: req.headers
});
next();
});
结构化日志便于后续通过ELK、Grafana等工具分析。为了在分布式系统或异步调用中追踪完整请求链路,需为每个请求生成唯一的traceId,并通过**异步本地存储(AsyncLocalStorage)**或第三方库(如cls-hooked)在上下文中传递。
AsyncLocalStorage(Node.js原生API,v14.17.0+):AsyncLocalStorage可在异步调用链中保持上下文一致性,无需手动传递traceId。示例:const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
// 请求中间件:生成并绑定traceId
app.use((req, res, next) => {
const traceId = req.headers['x-trace-id'] || generateUniqueId(); // 从请求头获取或生成
asyncLocalStorage.run(traceId, () => {
req.traceId = traceId; // 将traceId挂载到请求对象
next();
});
});
// 日志中间件:从上下文中获取traceId
app.use((req, res, next) => {
const traceId = asyncLocalStorage.getStore();
logger.info({
traceId,
method: req.method,
url: req.originalUrl
});
next();
});
function generateUniqueId() {
return Math.random().toString(36).substr(2, 9);
}
cls-hooked(第三方库,兼容旧版本):cls-hooked基于async_hooks封装,提供更简单的API。示例:const cls = require('cls-hooked');
const session = cls.createNamespace('request');
app.use((req, res, next) => {
session.run(() => {
session.set('traceId', req.headers['x-trace-id'] || generateUniqueId());
next();
});
});
// 日志中间件:从session中获取traceId
app.use((req, res, next) => {
const traceId = session.get('traceId');
logger.info({ traceId, method: req.method, url: req.originalUrl });
next();
});
通过traceId可将不同服务的日志关联起来,形成完整链路。在请求开始和结束时记录请求参数、响应数据、耗时,帮助定位性能瓶颈或错误。
res.on('finish')(响应结束时触发)计算耗时:app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info({
traceId: req.traceId,
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration
});
});
next();
});
输出示例:{"traceId":"abc123","method":"GET","url":"/api/users","status":200,"duration":45}。app.use((req, res, next) => {
if (req.method === 'POST' || req.method === 'PUT') {
req.body = JSON.parse(JSON.stringify(req.body)); // 深拷贝避免修改原数据
logger.debug({ traceId: req.traceId, body: req.body });
}
next();
});
app.use((req, res, next) => {
const originalSend = res.send;
res.send = function(data) {
res.locals.responseBody = data;
originalSend.call(this, data);
};
next();
});
app.use((req, res, next) => {
if (res.locals.responseBody) {
logger.debug({ traceId: req.traceId, responseBody: res.locals.responseBody });
}
});
仅在开发环境开启debug级别日志,避免泄露敏感信息。将日志发送到集中式系统(如ELK Stack、Prometheus+Grafana),便于大规模日志的存储、检索和可视化。
/var/log/my-js-app.log),解析JSON格式并发送到Elasticsearch。input {
file {
path => "/var/log/my-js-app.log"
start_position => "beginning"
codec => "json"
}
}
output {
elasticsearch {
hosts => ["localhost:9200"]
index => "nodejs-logs-%{+YYYY.MM.dd}"
}
}
prom-client库定义指标(如http_requests_total),暴露/metrics接口。const promClient = require('prom-client');
const httpRequestCounter = new promClient.Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'path', 'status']
});
app.use((req, res, next) => {
httpRequestCounter.inc({ method: req.method, path: req.originalUrl, status: res.statusCode });
next();
});
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
对于分布式系统,使用OpenTelemetry或Jaeger实现端到端链路追踪,覆盖多个服务的请求流程。
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(new JaegerExporter({ endpoint: 'http://localhost:14268/api/traces' })));
provider.register();
registerInstrumentations({ instrumentations: [new HttpInstrumentation()] }); // 自动追踪HTTP请求
// 手动创建span
const tracer = provider.getTracer('my-app');
app.get('/api/users', (req, res) => {
const span = tracer.startSpan('get-users');
// 业务逻辑
span.end();
res.send([{ id: 1, name: 'Alice' }]);
});
通过Jaeger UI查看完整的调用链路,包括服务间的依赖关系和耗时。通过以上方法,可在Ubuntu环境下通过JS日志实现请求链路的完整追踪,从基础日志记录到高级链路分析,满足不同场景的需求。