WEBKT

NestJS 微服务日志追踪:Winston 与 Pino 的分布式实践

37 0 0 0

为什么微服务需要分布式日志追踪?

NestJS 日志基础

Winston

Pino

实现分布式日志追踪的关键

生成 Trace ID

传递 Trace ID

在日志中包含 Trace ID

Winston 示例

Pino 示例

日志收集与聚合

总结

“哎,小王,你上次那个接口又出问题了,我这儿查日志,根本看不出来是哪儿的问题啊!请求转了好几个服务,日志都散了,头疼!”

相信不少做微服务的兄弟都遇到过类似上面老李这样的抱怨。在单体应用时代,日志通常集中在一个地方,排查问题相对容易。但到了微服务架构下,一个请求可能跨越多个服务,日志分散在各个服务的“角落”里,想找到问题根源,无异于大海捞针。

别担心,今天咱们就来聊聊,如何在 NestJS 构建的微服务中,利用 Winston 或 Pino 这两个流行的日志库,实现分布式日志追踪,让问题无处遁形!

为什么微服务需要分布式日志追踪?

在深入探讨技术细节之前,咱们先来明确一下,为什么分布式日志追踪在微服务架构中如此重要?

想象一下,你的电商应用被拆分成了用户服务、商品服务、订单服务、支付服务等多个微服务。一个用户下单的请求,可能会依次经过这些服务:

  1. 用户服务:验证用户信息。
  2. 商品服务:检查商品库存。
  3. 订单服务:创建订单。
  4. 支付服务:发起支付。

如果其中任何一个环节出错,而你没有一个有效的日志追踪机制,你将不得不:

  1. 登录到每个服务的服务器。
  2. 查看每个服务的日志文件。
  3. 手动关联不同服务中的日志条目,尝试拼凑出完整的请求流程。

这不仅效率低下,而且容易出错。分布式日志追踪的目标就是解决这个问题,它能够:

  • 集中收集日志: 将各个微服务的日志收集到一个集中的地方,方便查看和分析。
  • 关联请求: 为每个请求生成一个唯一的追踪 ID(Trace ID),并将该 ID 贯穿整个请求链路,关联不同服务中的日志条目。
  • 可视化追踪: 提供友好的界面,让你能够清晰地看到请求在各个服务中的流转过程,以及每个环节的耗时、状态等信息。

有了分布式日志追踪,当问题发生时,你只需要输入 Trace ID,就能快速定位到问题所在的服务和代码位置,大大提高了排查问题的效率。

NestJS 日志基础

在 NestJS 中,内置了一个简单的 Logger 服务,可以用于基本的日志记录。但它功能有限,通常不足以满足微服务场景的需求。因此,我们需要引入更强大的日志库,比如 Winston 或 Pino。

Winston

Winston 是一个非常流行的 Node.js 日志库,它支持多种传输方式(Transports),可以将日志输出到控制台、文件、数据库等不同的目的地。Winston 还支持自定义日志格式、日志级别等功能。

Pino

Pino 是另一个备受推崇的 Node.js 日志库,它以高性能著称。Pino 的核心理念是尽可能减少日志记录对应用程序性能的影响。Pino 同样支持多种传输方式和自定义配置。

在 NestJS 中使用 Winston 或 Pino 都非常简单,通常的步骤是:

  1. 安装相应的 npm 包。
  2. 创建一个自定义的 Logger 服务,封装 Winston 或 Pino 的实例。
  3. 在需要记录日志的地方,注入自定义的 Logger 服务,并调用相应的方法。

实现分布式日志追踪的关键

要在微服务中实现分布式日志追踪,关键在于以下几点:

  1. 生成唯一的 Trace ID: 为每个请求生成一个唯一的 Trace ID,并在整个请求链路中传递该 ID。
  2. 在日志中包含 Trace ID: 在每个服务的日志记录中,都包含当前请求的 Trace ID。
  3. 日志收集与聚合: 将各个服务的日志收集到一个集中的地方,并根据 Trace ID 进行关联。

生成 Trace ID

生成 Trace ID 的方法有很多,可以使用 UUID、Snowflake 算法等。在 NestJS 中,我们可以借助一些现成的库来简化这一过程,比如 uuid

// 安装 uuid
npm install uuid
// 生成 Trace ID
import { v4 as uuidv4 } from 'uuid';
const traceId = uuidv4();

传递 Trace ID

在微服务架构中,请求通常会跨越多个服务。因此,我们需要一种机制来传递 Trace ID。常见的做法是:

  • HTTP 请求头: 在 HTTP 请求头中添加一个自定义字段(例如 X-Trace-Id),用于传递 Trace ID。
  • 消息队列: 如果服务之间通过消息队列进行通信,可以将 Trace ID 放在消息的元数据中。

在 NestJS 中,我们可以使用拦截器(Interceptor)来统一处理 Trace ID 的传递:

// src/common/interceptors/trace-id.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class TraceIdInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// 尝试从请求头中获取 Trace ID
let traceId = request.headers['x-trace-id'];
// 如果请求头中没有 Trace ID,则生成一个新的
if (!traceId) {
traceId = uuidv4();
request.headers['x-trace-id'] = traceId;
}
// 将 Trace ID 添加到响应头中,以便后续服务使用
const response = context.switchToHttp().getResponse();
response.setHeader('X-Trace-Id', traceId);
return next.handle();
}
}

然后在 AppModule 中使用这个拦截器:

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { TraceIdInterceptor } from './common/interceptors/trace-id.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: TraceIdInterceptor,
},
],
})
export class AppModule {}

在日志中包含 Trace ID

有了 Trace ID,接下来就是在日志中包含它。无论是使用 Winston 还是 Pino,都可以通过自定义日志格式来实现。

Winston 示例

// src/common/logger/winston.logger.ts
import { Injectable, Scope } from '@nestjs/common';
import * as winston from 'winston';
@Injectable({ scope: Scope.REQUEST })
export class WinstonLogger {
private logger: winston.Logger;
constructor() {
this.logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(({ level, message, timestamp, traceId }) => {
return `${timestamp} [${traceId || 'N/A'}] ${level}: ${message}`;
})
),
transports: [
new winston.transports.Console(),
// 可以添加其他 transports,例如输出到文件
],
});
}
log(message: string, traceId?: string) {
this.logger.info(message, { traceId });
}
error(message: string, trace: string, traceId?: string) {
this.logger.error(message, { trace, traceId });
}
// 其他日志级别的方法...
}

在需要记录日志的地方,注入 WinstonLogger 并传入 traceId

import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { WinstonLogger } from '../common/logger/winston.logger';
@Injectable({ scope: Scope.REQUEST })
export class MyService {
constructor(
@Inject(REQUEST) private readonly request: any,
private readonly logger: WinstonLogger
) {}
async doSomething() {
const traceId = this.request.headers['x-trace-id'];
this.logger.log('Doing something...', traceId);
// ...
}
}

Pino 示例

// src/common/logger/pino.logger.ts
import { Injectable, Scope } from '@nestjs/common';
import * as pino from 'pino';
@Injectable({ scope: Scope.REQUEST })
export class PinoLogger {
private logger: pino.Logger;
constructor() {
this.logger = pino({
level: 'info',
base: null, // 不包含默认的 pid, hostname 等信息
timestamp: pino.stdTimeFunctions.isoTime, // 使用 ISO 8601 格式的时间
formatters: {
level(label) {
return { level: label };
},
},
});
}
log(message: string, traceId?: string) {
this.logger.info({ traceId }, message);
}
error(message: string, trace: string, traceId?: string) {
this.logger.error({ traceId, trace }, message);
}
//其他方法
}

在需要记录日志的地方,注入 PinoLogger 并传入 traceId

// 用法和WinstonLogger基本一样
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { PinoLogger } from '../common/logger/pino.logger';
@Injectable({ scope: Scope.REQUEST })
export class MyService {
constructor(
@Inject(REQUEST) private readonly request: any,
private readonly logger: PinoLogger
) {}
async doSomething() {
const traceId = this.request.headers['x-trace-id'];
this.logger.log('Doing something...', traceId);
// ...
}
}

日志收集与聚合

现在,每个服务的日志中都包含了 Trace ID。最后一步是将这些日志收集到一个集中的地方,并根据 Trace ID 进行关联。有很多工具可以帮助我们实现这一目标,例如:

  • ELK Stack (Elasticsearch, Logstash, Kibana): 这是目前非常流行的日志管理解决方案。Logstash 负责收集和处理日志,Elasticsearch 负责存储和索引日志,Kibana 负责可视化展示和查询日志。
  • Fluentd + Elasticsearch + Kibana: Fluentd 是一个开源的数据收集器,可以替代 Logstash。它的配置更灵活,插件更丰富。
  • Grafana Loki: Grafana Loki 是 Grafana Labs 推出的一个轻量级的日志聚合系统,它的设计灵感来自于 Prometheus。Loki 特别适合于 Kubernetes 环境。
  • Jaeger, Zipkin: 专门的分布式追踪系统,除了日志,还可以收集更详细的追踪信息。

由于篇幅所限,这里不再详细介绍这些工具的具体配置和使用方法。你可以根据自己的需求和技术栈选择合适的工具。

总结

分布式日志追踪是微服务架构中不可或缺的一环。通过本文的介绍,相信你已经了解了如何在 NestJS 中利用 Winston 或 Pino 实现分布式日志追踪的基本原理和方法。记住以下关键点:

  1. 为每个请求生成唯一的 Trace ID。
  2. 在整个请求链路中传递 Trace ID。
  3. 在每个服务的日志记录中包含 Trace ID。
  4. 选择合适的工具收集和聚合日志。

当然,分布式日志追踪还有很多高级特性和最佳实践,例如:

  • 日志采样: 对于高流量的系统,可以考虑对日志进行采样,以减少存储和性能开销。
  • 异常堆栈追踪: 在记录异常日志时,包含完整的堆栈信息,方便定位问题。
  • 日志上下文: 除了 Trace ID,还可以将其他有用的上下文信息添加到日志中,例如用户 ID、订单 ID 等。
  • 日志安全: 对于敏感信息,需要进行脱敏处理。

希望本文能帮助你更好地理解和应用分布式日志追踪,让你的微服务应用更加稳定可靠!

“老李,现在有了分布式日志追踪,再也不怕你的接口出问题了!有问题,直接甩 Trace ID,保证分分钟定位!”

全栈老王 NestJS微服务日志追踪

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/7892