NestJS 进阶:打造生产级日志系统与监控体系(集成 Winston、Sentry、Prometheus)
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 的日志和监控,并在实际项目中应用起来。如果你有任何问题或建议,欢迎在评论区留言,咱们一起交流学习!记住,生产环境无小事,日志和监控是保障应用稳定运行的“左膀右臂”,一定要重视起来!