by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权
在上一章节中,我描述了如何使NestJS中的错误更具信息性和统一性。在这个章节中,我将向您展示如何将错误不仅转换为日志行,还转换为普罗米修斯Prometheus用来监控和度量。
如果你还没有读过上一章,我强烈鼓励你读。它将为你提供结构合理的例外情况。
1. 将普罗米修斯添加到我们的项目中
pnpm i --save @willsoto/nestjs-prometheus prom-client
接下来,我们需要在app.module.ts中导入PrometheusModule:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrometheusModule} from '@willsoto/nestjs-prometheus'
@Module({
/**** 重点 ****/
imports: [PrometheusModule.register()],
/**** 重点end ****/
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
检查暴露的指标:
## 浏览器访问下面的地址
http://localhost:3000/metrics
## 得出的结果
# HELP process_cpu_user_seconds_total Total user CPU time spent in seconds.
# TYPE process_cpu_user_seconds_total counter
process_cpu_user_seconds_total 0.292606
# HELP process_cpu_system_seconds_total Total system CPU time spent in seconds.
# TYPE process_cpu_system_seconds_total counter
process_cpu_system_seconds_total 0.110539
# ...
# ...
您在这里看到的度量是由nestjs-prometheus公开的默认Node.js度量。签出有关如何禁用它们的文档。
创建错误计数器
为了导出发生的异常,我们需要创建一个单独的计数器。计数器可以具有用于对收集的度量进行分类的标签。所有例外都有哪些共同的资源?
- 错误域(例如用户、订单等)
- 响应状态代码
- (可选)错误代码(不是本文的一部分)
不要使用标签来存储基数较高的维度,例如错误消息。这将大大增加存储的数据量。
所以,首先让我们为我们的错误创建一个计数器。在app.module.ts中添加新的提供程序:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import {
PrometheusModule,
makeCounterProvider
} from '@willsoto/nestjs-prometheus'
@Module({
/**** 重点 ****/
imports: [PrometheusModule.register()],
/**** 重点end ****/
controllers: [AppController],
providers: [
/**** 重点 ****/
makeCounterProvider({
name: 'nestjs_errors',
help: 'nestjs_errors', // 我知道你可以做的更好
labelNames: ["domain","status"] // 我们要跟踪的标签名称
}),
/**** 重点end ****/
AppService],
})
export class AppModule {}
将错误计数器添加到ExceptionFilter
为了使用我们的计数器,我们需要将它注入到处理所有异常的地方。这是exception.filter.ts:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
/** 重点 */
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { Counter } from 'prom-client';
import { BusinessException, ErrorDomain } from 'src/business.exception';
/** 重点end */
@Catch(Error)
export class CustomExceptionFilter implements ExceptionFilter {
...
constructor(
/** 重点 */
@InjectMetric('nestjs_errors')
private readonly counter: Counter<string> /** 重点end */,
) {}
catch(exception: Error, host: ArgumentsHost) {
...
}
}
这就是事情变得有趣的地方。之前,我们在main.ts中手动初始化了CustomExceptionFilter:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CustomExceptionFilter } from './exception/exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 手动初始化我们的ExceptionFilter
app.useGlobalFilters(new CustomExceptionFilter());
await app.listen(3000);
}
bootstrap();
但是,因为现在我们的CustomExceptionFilter需要成为依赖注入的一部分,所以它必须作为app.module.ts中的全局筛选器添加:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import {
PrometheusModule,
makeCounterProvider,
} from '@willsoto/nestjs-prometheus';
import { CustomExceptionFilter } from './exception/exception.filter';
/**** 重点 ****/
import { APP_FILTER } from '@nestjs/core';
/**** 重点end ****/
@Module({
/**** 重点 ****/
imports: [PrometheusModule.register()],
/**** 重点end ****/
controllers: [AppController],
providers: [
/**** 重点 ****/
makeCounterProvider({
name: 'nestjs_errors',
help: 'nestjs_errors', // 我知道你可以做的更好
labelNames: ['domain', 'status'], // 我们要跟踪的标签名称
}),
{
provide: APP_FILTER,
useClass: CustomExceptionFilter,
},
/**** 重点end ****/
AppService,
],
})
export class AppModule {}
使用错误计数器
作为上一章的结果,我们有了一个统一的错误类型,作为应用程序中发生的所有错误的结果。因此,我们可以简单地将其财产用作标签值,并在exception.filter.ts中增加错误计数器:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Injectable,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { BusinessException, ErrorDomain } from './business.exception';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { Counter } from 'prom-client';
// ...
@Catch(Error)
export class CustomExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(CustomExceptionFilter.name);
constructor(
@InjectMetric('nestjs_errors') private readonly counter: Counter<string>,
) {}
catch(exception: Error, host: ArgumentsHost) {
let error: BusinessException;
let status: HttpStatus;
//一些代码然后触发this.counter.labels(error.domain,error.status.toString()).inc();
response.status(status).json(error.toApiError());
}
}
好吧,我们现在的情况是,每次发生错误,我们的计数器都会增加,并作为一个度量暴露出来。我们将在接下来的步骤中看到数据。
生成一些错误
在app.controller.ts中,让我们使端点在每个请求上抛出所有类型的随机错误:
import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
import { BusinessException, ErrorDomainEnum } from './business.exception';
@Controller()
export class AppController {
@Get()
public getHello() {
throw AppController.genError();
}
private static genError(): Error {
const getRandom = <T>(...values: T[]): T =>
values[Math.floor(Math.random() * values.length)];
const domain = getRandom(
ErrorDomainEnum.Generic,
ErrorDomainEnum.Orders,
ErrorDomainEnum.Users,
);
const status = getRandom(
HttpStatus.BAD_REQUEST,
HttpStatus.NOT_FOUND,
HttpStatus.CONFLICT,
HttpStatus.FORBIDDEN,
HttpStatus.UNAUTHORIZED,
HttpStatus.BAD_GATEWAY,
HttpStatus.GATEWAY_TIMEOUT,
);
switch (Math.floor(Math.random() * 3)) {
case 0:
return new HttpException('nestjs-exception', status);
case 1:
return new Error('unknown-error');
case 2:
return new BusinessException(
domain,
'business-exception',
'business-exception',
status,
);
}
}
}
设置普罗米修斯
我将使用docker-compose来挂载一个普罗米修斯的实例。此外,我将把我们的服务作为一个码头集装箱启动,以简化普罗米修斯和我们的服务之间的连接。我有一个关于如何对接NestJS应用程序的分步指南。
version: '3.9'
services:
nestjs:
image: 'node:19-bullseye-slim'
user: 'node'
working_dir: /home/app
container_name: instructor-nodejs
ports:
- '3000:3000'
volumes:
# 挂载日志
- ./logs:/home/app/logs
# 挂载各种第三方插件
- ./node_modules:/home/app/node_modules
# 挂载主要程序
- ./dist:/home/app/dist
command: node /home/app/dist/main.js
environment:
RUNNING_ENV: 'prod'
prometheus:
image: prom/prometheus:v2.36.2
ports:
- '9090:9090'
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
# - prometheus-data:/prometheus # TODO
command: --config.file=/etc/prometheus/prometheus.yml
告诉Prometheus从我们的服务中获取指标,docker compose/Prometheus/Prometheus.yml:
global:
scrape_interval: 5s # Scrape every 5 seconds
scrape_timeout: 1s
scrape_configs:
- job_name: services
metrics_path: /metrics
static_configs:
- targets:
- 'nestjs:3000' # Name of our service in docker-compose
这个文件稍后被装载到普罗米修斯容器中。
生成度量标准
好吧,我们现在做得很好。让我们把所有东西都跑一遍,看看普罗米修斯身上有什么。在项目目录中运行:
docker-compose up
[+] Running 3/3
Attaching to docker-prometheus-1, instructor-nodejs
docker-prometheus-1 | ts=2023-03-28T06:19:25.956Z caller=main.go:491 level=info msg="No time or size retention was set so using the default time retention" duration=15d
...
nestjs | [Nest] 1 - 03/28/2023, 6:21:30 AM LOG [NestFactory] Starting Nest application...
让我们打开prometheus的仪表板http://localhost:9090/graph.
对我们的错误度量的查询只是nestjs_errors,还记得我们是如何命名计数器的吗?
我们得到空结果的原因是,自从我们推出服务以来,没有流量。让我们向我们的服务发出一些请求:
while true; do curl http://localhost:3000/ ; sleep 0.2; done;
{"domain":"generic","apiMessage":"nestjs-exception","status":404,"id":"nPjXH4gi08zfuQEc","timestamp":"2023-03-28T06:32:02.531Z"}{"domain":"generic","apiMessage":"nestjs-exception","status":502,"id":"eYzO0XinFsXH1Ckd","timestamp":"2023-03-28T06:32:02.754Z"}{"id":"fZEM3HgoaBWWXogV","message":"business-exception","domain":"users","timestamp":"2023-03-28T06:32:02.977Z"}
...
好吧,我们已经生成了一些流量(如果你想持续生成指标,你可以让它继续运行)。重新运行prometheus查询:
令人惊叹的现在我们有一些指标可以使用。让我们为他们制作一个合适的图表
可视化指标
还记得我们的第一部分要求吗?其中一个目标是能够对我们的错误进行分类。此外,我们肯定希望有错误每秒图表。我们将使用rate prometheus函数,并按域和HTTP状态对指标进行分组:
sum by (domain, status)(rate(nestjs_errors[30s])
就是这样!我们实现了目标。我们有结构化、统一、丰富的错误,可以通过id引用(例如由用户引用),并轻松地在日志中搜索。此外,所有代码都可以提取到一个单独的库中,并在您的公司内共享。
总结
在本文中,我详细描述了如何收集NestJS错误,并将它们作为度量导出到监控系统(在我们的案例中是Prometheus)。