by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权
原文链接: blog.devops.dev
大家好我是雪隐,请叫我雪宝,欢迎来到NodeJS框架NestJS的第一个提示和技巧系列。我想分享一下我对如何为您的服务改进错误处理的想法。我们希望实现什么?
- 轻松搜索和引用发生的错误
- 丰富和分类服务错误
- 将我们的错误作为度量标准公开
1. 引导项目
首先,像往常一样,让我们为我们引导一个示例服务。我建议您使用强大的nest-cli为我们的应用程序生成骨架:
npx nest new tip07
*# ...
# ? Which package manager would you ❤️ to use?
# > npm*
2. 注入错误
让我们直接抛出一些错误!在app.controller.ts中将代码更改为:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
throw new Error('Oh no!');;
}
}
是的,让我们从基础开始:首先抛出一个错误。我将使用Postman作为测试工具。让我们看看我们得到了什么回应:
{
"statusCode": 500,
"message": "Internal server error"
}
哇!我们甚至没有看到相应的错误消息。深入NestJS文档,您会发现一些绑定到HTTP响应状态的HTTP异常的良好层次结构。
3. 使用NestJS HttpExceptions
让我们使用@nestjs/common包中的HttpException:
import { Controller, Get,HttpException,HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
throw new HttpException(
'Oh no!',
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
测试一下:
{
"statusCode": 500,
"message": "Oh no!"
}
好吧,现在我们至少有一条错误消息。
还要注意,常见HTTP状态有异常别名,例如:InternalServerErrorException、BadRequestException等。在此处查找内置异常的完整列表。
如果你查看日志,你不会发现任何与错误有关的信息。如果它在生产中,那将是多么遗憾啊!让我们把它修好!
4. 日志记录错误
记录错误的过程在官方文档中有很好的描述。我们可以使用BaseExceptionFilter来捕获所有或特定的错误。
让我们创建一个新的文件exception.filter.ts:
import { Catch, ArgumentsHost, Logger } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
@Catch()
export class CustomExceptionFilter extends BaseExceptionFilter {
private readonly logger = new Logger(CustomExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
this.logger.error(JSON.stringify(exception));
super.catch(exception, host);
}
}
并告诉NestJS将其用作main.ts中的全局筛选器:
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CustomExceptionFilter } from './exception/exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 我们稍后会处理掉
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new CustomExceptionFilter(httpAdapter));
await app.listen(3000);
}
bootstrap();
让我们试试看:
{
"statusCode": 500,
"message": "Oh no!"
}
查看日志:
[Nest] 1888 - 2023/03/17 19:27:51 LOG [NestApplication] Nest application successfully started +2ms
[Nest] 1888 - 2023/03/17 19:28:29 ERROR [CustomExceptionFilter] {"response":"Oh no!","status":500,"message":"Oh no!","name":"HttpException"}
对这正是我们想要的。现在,至少我们可以观察到我们的例外情况。
为什么我们在这种情况下没有使用ExceptionFilter接口?简单地说,因为在这一步中,我们将失去NestJS提供的序列化机制。进一步阅读如何制作带有丰富异常的自己的序列化程序。
但是,简单的错误消息并不能为我们提供所发生错误的任何细节。此外,它还不够“机器可读”。让我们来解决它。
5. 丰富异常
典型异常应该包含哪些额外的财产?就我个人而言,这至少是身份证、域名和时间戳。但是,如何在不失去NestJS的力量的情况下实现它呢?
首先,让我们为所需的异常business.exception.ts定义一个类:
import { HttpStatus } from '@nestjs/common';
export type ErrorDomain = 'users' | 'orders' | 'generic';
export class BusinessException extends Error {
public readonly id: string;
public readonly timestamp: Date;
constructor(
public readonly domain: ErrorDomain,
public readonly message: string,
public readonly apiMessage: string,
public readonly status: HttpStatus,
) {
super(message);
this.id = BusinessException.genId();
this.timestamp = new Date();
}
private static genId(length = 16): string {
const p = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return [...Array(length)].reduce(
(a) => a + p[~~(Math.random() * p.length)],
'',
);
}
}
让我们按属性进行细分:
id包含在初始化异常时生成的唯一标识符;timestamp存储初始化异常时的时间戳;domain指定此错误属于哪个业务域或发生错误的位置;message包含用于日志记录的内部消息(可能包含私有数据,例如标识符、异常消息、堆栈跟踪等);apiMessage包含要在对用户的响应中返回的消息。这个是公开曝光的;status指定发生此错误时服务必须响应的HTTP状态。
6. 使用丰富的异常
为了集成这些异常,我们需要两件事:实际开始使用它们,并调整ExceptionFilter以正确处理这些错误。
- 让我们在app.controller.ts中抛出自定义错误,而不是内置异常:
import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
import { AppService } from './app.service';
import { BusinessException } from './business.exception';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
const userId = 1;
throw new BusinessException(
'users', // Error domain
`User with id=${userId} was not found.`, // Internal message
'User not found', // API message
HttpStatus.NOT_FOUND, // HTTP status
);
}
}
- 调整我们的
ExceptionFilter以正确处理我们的自定义异常,在exception.filter.ts中将BaseExceptionFilter替换为ExceptionFilter:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { BusinessException, ErrorDomain } from '../business.exception';
export interface ApiError {
id: string;
domain: ErrorDomain;
message: string;
timestamp: Date;
}
@Catch(Error)
export class CustomExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(CustomExceptionFilter.name);
catch(exception: Error, host: ArgumentsHost) {
let body: ApiError;
let status: HttpStatus;
if (exception instanceof BusinessException) {
// 直接处理我们自己的异常
body = {
id: exception.id,
message: exception.apiMessage,
domain: exception.domain,
timestamp: exception.timestamp,
};
status = exception.status;
} else if (exception instanceof HttpException) {
// 我们可以从NestJS错误中提取内部消息和状态对类验证器(class-validator)有用
body = new BusinessException(
'generic',
exception.message,
exception.message, // 如果你喜欢,也可以选择普通消息
exception.getStatus(),
);
status = exception.getStatus();
} else {
// 对于所有其他异常,只需返回500错误
body = new BusinessException(
'generic',
`Internal error occurred: ${exception.message}`,
'Internal error occurred',
HttpStatus.INTERNAL_SERVER_ERROR,
);
status = HttpStatus.INTERNAL_SERVER_ERROR;
}
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
// 日志将包含错误标识符以及发生错误的请求路径
this.logger.error(
`Got an exception: ${JSON.stringify({
path: request.url,
...body,
})}`,
);
response.status(status).json(body);
}
}
main.ts:
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而不是BaseExceptionFilterr;
app.useGlobalFilters(new CustomExceptionFilter());
await app.listen(3000);
}
bootstrap();
让我们测试一下!
{
"id": "fRaKKy8IBKf4v2K1",
"message": "User not found",
"domain": "users",
"timestamp": "2023-03-17T11:35:54.628Z"
}
查看日志:
[Nest] 2118 - 2023/03/17 19:35:54 ERROR [CustomExceptionFilter] Got an exception: {"path":"/","id":"fRaKKy8IBKf4v2K1","message":"User not found","domain":"users","timestamp":"2023-03-17T11:35:54.628Z"}
不错!
总结
在本文中,我简要地(实际上不是)描述了如何在几行代码中丰富和记录NestJS错误。下一个技巧部分描述如何使用Prometheus将您的错误暴露为度量,以提高您的服务可观察性。