WEBKT

NestJS 进阶:打造生产级日志系统与监控体系(集成 Winston、Sentry、Prometheus)

379 0 0 0

NestJS 进阶:打造生产级日志系统与监控体系(集成 Winston、Sentry、Prometheus)

大家好,我是你们的“老码农”朋友。今天咱们来聊聊 NestJS 应用在生产环境下的日志管理和监控这个“老大难”问题。很多开发者在本地开发时,可能就直接 console.log 大法一把梭,但到了生产环境,这可就万万不行了。一个稳定、可靠的生产级应用,必须具备完善的日志记录和监控机制,这样才能在出现问题时快速定位、及时止损。

这篇文章,我就手把手教你如何将 NestJS 的过滤器与第三方日志库(Winston、Bunyan)以及监控平台(Sentry、Prometheus)集成,构建一个全面、高效的日志管理与监控体系。别担心,咱们会一步步来,保证你能听懂、学会、用得上。

为什么需要完善的日志和监控?

在正式开始之前,咱们先来明确一下,为什么我们需要费这么大劲儿去搞日志和监控?这可不仅仅是为了“看起来专业”,而是实实在在的生产环境需求:

  1. 问题排查: 线上环境一旦出现问题,日志是定位问题的最重要线索。详细、清晰的日志记录,能帮你快速还原问题现场,找到根本原因。
  2. 性能监控: 通过监控系统,你可以实时了解应用的各项性能指标,比如 CPU 使用率、内存占用、请求响应时间等,及时发现潜在的性能瓶颈。
  3. 安全审计: 日志可以记录用户的操作行为,为安全审计提供依据,帮助你发现异常操作,防范安全风险。
  4. 业务分析: 通过对日志数据进行分析,你可以了解用户行为、业务趋势等,为产品优化和业务决策提供支持。

总而言之,完善的日志和监控是保障应用稳定运行、提升用户体验、保障业务安全的基石。

NestJS 过滤器:日志和监控的入口

NestJS 的过滤器(Filters)是处理异常的“守门员”。当应用程序中发生未捕获的异常时,过滤器会捕获这些异常,并允许你执行自定义的逻辑,比如记录错误日志、发送告警通知等。这正是我们集成日志和监控系统的绝佳入口。

1. 创建自定义异常过滤器

首先,我们需要创建一个自定义的异常过滤器。这个过滤器将捕获所有未处理的异常,并进行统一处理。

import { Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
 private readonly logger = new Logger(AllExceptionsFilter.name);

 catch(exception: unknown, host: ArgumentsHost) {
 const ctx = host.switchToHttp();
 const response = ctx.getResponse();
 const request = ctx.getRequest();

 let status = HttpStatus.INTERNAL_SERVER_ERROR;
 let message = 'Internal server error';

 if (exception instanceof HttpException) {
 status = exception.getStatus();
 message = exception.message;
 }

 this.logger.error(
 `[${request.method}] ${request.url} - ${status} - ${message}`, 
 exception,
 );

 response.status(status).json({
 statusCode: status,
 timestamp: new Date().toISOString(),
 path: request.url,
 message: message,
 });
 }
}

这个 AllExceptionsFilter 继承自 NestJS 内置的 BaseExceptionFilter,并重写了 catch 方法。在 catch 方法中,我们首先获取了 HTTP 请求的上下文信息,然后判断异常的类型:

  • 如果是 HttpException,则获取异常的状态码和消息。
  • 否则,默认设置为 500 内部服务器错误。

接着,我们使用 NestJS 内置的 Logger 记录错误日志。最后,向客户端返回一个统一的 JSON 响应。

2. 全局注册过滤器

创建好自定义过滤器后,我们需要在 NestJS 应用中全局注册它。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './all-exceptions.filter';

async function bootstrap() {
 const app = await NestFactory.create(AppModule);
 app.useGlobalFilters(new AllExceptionsFilter());
 await app.listen(3000);
}
bootstrap();

main.ts 文件中,我们通过 app.useGlobalFilters() 方法将 AllExceptionsFilter 注册为全局过滤器。这样,所有未处理的异常都会被这个过滤器捕获。

集成 Winston 日志库

Winston 是 Node.js 社区中最流行的日志库之一,它提供了丰富的功能和灵活的配置选项。接下来,我们将 Winston 集成到 NestJS 应用中。

1. 安装 Winston

npm install winston

2. 创建 Winston Logger Service

为了更好地在 NestJS 中使用 Winston,我们创建一个 WinstonLoggerService

// winston-logger.service.ts
import { Injectable, LoggerService } from '@nestjs/common';
import * as winston from 'winston';

@Injectable()
export class WinstonLoggerService implements LoggerService {
 private readonly logger: winston.Logger;

 constructor() {
 this.logger = winston.createLogger({
 level: 'info', // 设置日志级别
 format: winston.format.combine(
 winston.format.timestamp(),
 winston.format.json()
 ),
 transports: [
 new winston.transports.Console(), // 输出到控制台
 new winston.transports.File({ filename: 'error.log', level: 'error' }), // 输出到文件
 new winston.transports.File({ filename: 'combined.log' }),
 ],
 });
 }

 log(message: string, context?: string) {
 this.logger.info(message, { context });
 }

 error(message: string, trace?: string, context?: string) {
 this.logger.error(message, { trace, context });
 }

 warn(message: string, context?: string) {
 this.logger.warn(message, { context });
 }

 debug(message: string, context?: string) {
 this.logger.debug(message, { context });
 }

 verbose(message: string, context?: string) {
 this.logger.verbose(message, { context });
 }
}

这个 WinstonLoggerService 实现了 NestJS 的 LoggerService 接口,并封装了 Winston 的 API。在构造函数中,我们创建了一个 Winston Logger 实例,并配置了日志级别、格式和输出方式(控制台和文件)。

3. 替换 NestJS 默认 Logger

接下来,我们需要在 AllExceptionsFilter 中使用 WinstonLoggerService 替换 NestJS 默认的 Logger。

import { Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { WinstonLoggerService } from './winston-logger.service';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
 constructor(private readonly logger: WinstonLoggerService) {
 super();
 }

 catch(exception: unknown, host: ArgumentsHost) {
 // ... 其他代码 ...

 this.logger.error(
 `[${request.method}] ${request.url} - ${status} - ${message}`, 
 exception,
 );

 // ... 其他代码 ...
 }
}

这里要注意,需要在AppModule中providers数组中加入WinstonLoggerService

@Module({
 imports: [],
 controllers: [AppController],
 providers: [AppService, WinstonLoggerService],
})
export class AppModule {}

同时, 在main.ts中注册AllExceptionsFilter时, 也要传入WinstonLoggerService的实例。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './all-exceptions.filter';
import { WinstonLoggerService } from './winston-logger.service';

async function bootstrap() {
 const app = await NestFactory.create(AppModule);
 const winstonLogger = app.get(WinstonLoggerService);
 app.useGlobalFilters(new AllExceptionsFilter(winstonLogger));
 await app.listen(3000);
}
bootstrap();

现在,所有的异常日志都会通过 Winston 输出到控制台和文件中。

4. 自定义 Winston 格式和传输

Winston 的强大之处在于其高度可定制性。你可以根据自己的需求,自定义日志的格式和输出方式。

例如,你可以添加更详细的上下文信息:

// winston-logger.service.ts
// ... 其他代码 ...

format: winston.format.combine(
 winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
 winston.format.printf(({ level, message, timestamp, context, trace }) => {
 return `${timestamp} [${context}] ${level}: ${message} ${trace ? `\n${trace}` : ''}`;
 }),
),

// ... 其他代码 ...

这里,我们使用了 winston.format.printf 自定义了日志的输出格式,添加了时间戳、上下文和堆栈跟踪信息。你还可以根据需要添加更多信息,比如用户 ID、请求 ID 等。

除了控制台和文件,Winston 还支持多种传输方式,比如:

  • winston-daily-rotate-file:按日期轮转日志文件。
  • winston-syslog:将日志发送到 syslog 服务器。
  • winston-mongodb:将日志存储到 MongoDB 数据库。
  • @google-cloud/logging-winston: 将日志发送到谷歌云

你可以根据自己的需求,选择合适的传输方式。

集成 Sentry 错误跟踪

Sentry 是一个流行的错误跟踪平台,它可以帮助你实时捕获、分析和解决应用程序中的错误。接下来,我们将 Sentry 集成到 NestJS 应用中。

1. 安装 Sentry SDK

npm install @sentry/node @sentry/tracing

2. 初始化 Sentry

main.ts 文件中,初始化 Sentry SDK:

import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './all-exceptions.filter';
import { WinstonLoggerService } from './winston-logger.service';

async function bootstrap() {
 Sentry.init({
 dsn: 'YOUR_SENTRY_DSN', // 替换为你的 Sentry DSN
 integrations: [
 new Sentry.Integrations.Http({ tracing: true }),
 new Tracing.Integrations.Express(),
 ],
 tracesSampleRate: 1.0, // 调整采样率
 });

 const app = await NestFactory.create(AppModule);

 // Sentry 请求处理程序必须是第一个中间件
 app.use(Sentry.Handlers.requestHandler());
 // TracingHandler 创建一个跨多个服务的跨度
 app.use(Sentry.Handlers.tracingHandler());

 const winstonLogger = app.get(WinstonLoggerService);
 app.useGlobalFilters(new AllExceptionsFilter(winstonLogger));

 // Sentry 错误处理程序必须在所有控制器之后
 app.use(Sentry.Handlers.errorHandler());

 await app.listen(3000);
}
bootstrap();

这里,我们使用 Sentry.init() 初始化 Sentry SDK,并配置了 DSN、集成和采样率。dsn需要替换成你自己的。

然后,我们分别使用了 Sentry.Handlers.requestHandler()Sentry.Handlers.tracingHandler()Sentry.Handlers.errorHandler() 三个中间件。注意,这些中间件的顺序非常重要,必须按照上述顺序使用。

3. 在过滤器中捕获异常并发送到 Sentry

接下来,我们需要在 AllExceptionsFilter 中捕获异常,并将异常信息发送到 Sentry。

import { Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { WinstonLoggerService } from './winston-logger.service';
import * as Sentry from '@sentry/node';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
 constructor(private readonly logger: WinstonLoggerService) {
 super();
 }

 catch(exception: unknown, host: ArgumentsHost) {
 // ... 其他代码 ...

 // 将异常发送到 Sentry
 Sentry.captureException(exception);

 // ... 其他代码 ...
 }
}

catch 方法中,我们添加了 Sentry.captureException(exception),将捕获到的异常发送到 Sentry。

现在,当应用程序发生未处理的异常时,Sentry 会自动捕获这些异常,并发送到你的 Sentry 项目中。你可以在 Sentry 的仪表盘中查看异常的详细信息、堆栈跟踪、上下文信息等。

集成 Prometheus 监控指标

Prometheus 是一个开源的监控和告警系统,它可以收集应用程序的各种指标,并提供强大的查询和可视化功能。接下来,我们将 Prometheus 集成到 NestJS 应用中。

1. 安装 prom-client

npm install prom-client

prom-client 是 Prometheus 的 Node.js 客户端库。

2. 创建 PrometheusService

为了更好地在 NestJS 中使用 Prometheus,我们创建一个 PrometheusService

// prometheus.service.ts
import { Injectable } from '@nestjs/common';
import * as client from 'prom-client';

@Injectable()
export class PrometheusService {
 private readonly register: client.Registry;

 constructor() {
 this.register = new client.Registry();
 client.collectDefaultMetrics({ register: this.register });
 }

 getMetrics(): Promise<string> {
 return this.register.metrics();
 }

 // 添加自定义指标
 createCounter(name: string, help: string, labelNames?: string[]): client.Counter {
 return new client.Counter({
 name,
 help,
 labelNames,
 registers: [this.register],
 });
 }
}

这个 PrometheusService 封装了 prom-client 的 API。在构造函数中,我们创建了一个 Registry 实例,并收集了默认的指标。getMetrics() 方法用于获取所有指标的文本格式数据。createCounter 方法可以创建自定义的 Counter类型指标。

3. 创建 PrometheusController

为了暴露 Prometheus 指标,我们创建一个 PrometheusController

// prometheus.controller.ts
import { Controller, Get, Res } from '@nestjs/common';
import { PrometheusService } from './prometheus.service';
import { Response } from 'express';

@Controller('metrics')
export class PrometheusController {
 constructor(private readonly prometheusService: PrometheusService) {}

 @Get()
 async getMetrics(@Res() res: Response) {
 res.setHeader('Content-Type', this.prometheusService.getMetricsContentType());
 res.send(await this.prometheusService.getMetrics());
 }
}

这个 PrometheusController 定义了一个 /metrics 路由,用于获取所有指标的文本格式数据。注意, 这里需要通过@Res装饰器来手动设置响应头。

记得在对应的Module中导入PrometheusServicePrometheusController

4. 注册 PrometheusService 和 PrometheusController

最后,我们需要在 AppModule 中注册 PrometheusServicePrometheusController

// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { WinstonLoggerService } from './winston-logger.service';
import { PrometheusService } from './prometheus.service';
import { PrometheusController } from './prometheus.controller';

@Module({
 imports: [],
 controllers: [AppController, PrometheusController],
 providers: [AppService, WinstonLoggerService, PrometheusService],
})
export class AppModule {}

现在,你可以通过访问 /metrics 路由来获取应用程序的指标数据了。Prometheus 可以定期抓取这个路由,收集指标数据,并进行可视化和告警。

5. 添加自定义指标

除了默认指标,你还可以根据自己的需求,添加自定义指标。例如,你可以添加一个用于统计 HTTP 请求次数的 Counter 指标:

// app.service.ts
import { Injectable } from '@nestjs/common';
import { PrometheusService } from './prometheus.service';

@Injectable()
export class AppService {
 private readonly httpRequestCounter;

 constructor(private readonly prometheusService: PrometheusService) {
 this.httpRequestCounter = this.prometheusService.createCounter(
 'http_requests_total',
 'Total number of HTTP requests',
 ['method', 'path', 'status'],
 );
 }

 // 在处理 HTTP 请求的方法中增加计数
 async handleRequest(method: string, path: string, status: number) {
 this.httpRequestCounter.inc({ method, path, status });
 // ... 其他业务逻辑 ...
 }
}

AppService 中,我们使用 PrometheusService 创建了一个名为 http_requests_total 的 Counter 指标,并指定了 methodpathstatus 三个标签。然后,在处理 HTTP 请求的方法中,调用 httpRequestCounter.inc() 方法增加计数。

总结

好了,到这里,我们已经成功地将 NestJS 的过滤器与 Winston、Sentry 和 Prometheus 集成,构建了一个比较完善的日志管理和监控体系。现在,你的 NestJS 应用已经具备了生产级别的可观测性。

当然,这只是一个基础的集成方案,你还可以根据自己的需求进行更深入的定制和扩展。比如:

  • 更细粒度的日志记录: 在关键业务逻辑中添加更详细的日志记录,方便问题排查。
  • 更丰富的监控指标: 添加更多自定义指标,监控应用的各个方面。
  • 告警系统集成: 将 Prometheus 与 Alertmanager 集成,实现告警通知。
  • 分布式追踪: 集成 Jaeger 或 Zipkin 等分布式追踪系统,跟踪跨多个服务的请求。

希望这篇文章能帮助你更好地理解 NestJS 的日志和监控,并在实际项目中应用起来。如果你有任何问题或建议,欢迎在评论区留言,咱们一起交流学习!记住,生产环境无小事,日志和监控是保障应用稳定运行的“左膀右臂”,一定要重视起来!

全栈老王 NestJS日志监控

评论点评