NestJS 进阶:打造生产级日志系统与监控体系(集成 Winston、Sentry、Prometheus)
NestJS 进阶:打造生产级日志系统与监控体系(集成 Winston、Sentry、Prometheus)
为什么需要完善的日志和监控?
NestJS 过滤器:日志和监控的入口
1. 创建自定义异常过滤器
2. 全局注册过滤器
集成 Winston 日志库
1. 安装 Winston
2. 创建 Winston Logger Service
3. 替换 NestJS 默认 Logger
4. 自定义 Winston 格式和传输
集成 Sentry 错误跟踪
1. 安装 Sentry SDK
2. 初始化 Sentry
3. 在过滤器中捕获异常并发送到 Sentry
集成 Prometheus 监控指标
1. 安装 prom-client
2. 创建 PrometheusService
3. 创建 PrometheusController
4. 注册 PrometheusService 和 PrometheusController
5. 添加自定义指标
总结
NestJS 进阶:打造生产级日志系统与监控体系(集成 Winston、Sentry、Prometheus)
大家好,我是你们的“老码农”朋友。今天咱们来聊聊 NestJS 应用在生产环境下的日志管理和监控这个“老大难”问题。很多开发者在本地开发时,可能就直接 console.log
大法一把梭,但到了生产环境,这可就万万不行了。一个稳定、可靠的生产级应用,必须具备完善的日志记录和监控机制,这样才能在出现问题时快速定位、及时止损。
这篇文章,我就手把手教你如何将 NestJS 的过滤器与第三方日志库(Winston、Bunyan)以及监控平台(Sentry、Prometheus)集成,构建一个全面、高效的日志管理与监控体系。别担心,咱们会一步步来,保证你能听懂、学会、用得上。
为什么需要完善的日志和监控?
在正式开始之前,咱们先来明确一下,为什么我们需要费这么大劲儿去搞日志和监控?这可不仅仅是为了“看起来专业”,而是实实在在的生产环境需求:
- 问题排查: 线上环境一旦出现问题,日志是定位问题的最重要线索。详细、清晰的日志记录,能帮你快速还原问题现场,找到根本原因。
- 性能监控: 通过监控系统,你可以实时了解应用的各项性能指标,比如 CPU 使用率、内存占用、请求响应时间等,及时发现潜在的性能瓶颈。
- 安全审计: 日志可以记录用户的操作行为,为安全审计提供依据,帮助你发现异常操作,防范安全风险。
- 业务分析: 通过对日志数据进行分析,你可以了解用户行为、业务趋势等,为产品优化和业务决策提供支持。
总而言之,完善的日志和监控是保障应用稳定运行、提升用户体验、保障业务安全的基石。
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中导入PrometheusService
和 PrometheusController
。
4. 注册 PrometheusService 和 PrometheusController
最后,我们需要在 AppModule
中注册 PrometheusService
和 PrometheusController
。
// 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 指标,并指定了 method
、path
和 status
三个标签。然后,在处理 HTTP 请求的方法中,调用 httpRequestCounter.inc()
方法增加计数。
总结
好了,到这里,我们已经成功地将 NestJS 的过滤器与 Winston、Sentry 和 Prometheus 集成,构建了一个比较完善的日志管理和监控体系。现在,你的 NestJS 应用已经具备了生产级别的可观测性。
当然,这只是一个基础的集成方案,你还可以根据自己的需求进行更深入的定制和扩展。比如:
- 更细粒度的日志记录: 在关键业务逻辑中添加更详细的日志记录,方便问题排查。
- 更丰富的监控指标: 添加更多自定义指标,监控应用的各个方面。
- 告警系统集成: 将 Prometheus 与 Alertmanager 集成,实现告警通知。
- 分布式追踪: 集成 Jaeger 或 Zipkin 等分布式追踪系统,跟踪跨多个服务的请求。
希望这篇文章能帮助你更好地理解 NestJS 的日志和监控,并在实际项目中应用起来。如果你有任何问题或建议,欢迎在评论区留言,咱们一起交流学习!记住,生产环境无小事,日志和监控是保障应用稳定运行的“左膀右臂”,一定要重视起来!