跳到主要内容

29-Nest记录请求日志

请求日志

Nest 服务会不断处理用户用户的请求,如果我们想记录下每次请求的日志呢?

可以通过 interceptor 来做。

我们写一下:nest new request-log

进入项目,创建个 interceptor:nest g interceptor request-log --no-spec --flat

打印下日志:

import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { Response } from 'express';
import { Request } from 'express';
import { Observable, tap } from 'rxjs';

@Injectable()
export class RequestLogInterceptor implements NestInterceptor {
private readonly logger = new Logger(RequestLogInterceptor.name);

intercept(
context: ExecutionContext,
next: CallHandler<any>,
) {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();

const userAgent = request.headers['user-agent'];

const { ip, method, path } = request;

this.logger.debug(
`${method} ${path} ${ip} ${userAgent}: ${
context.getClass().name
} ${
context.getHandler().name
} invoked...`,
);

const now = Date.now();

return next.handle().pipe(
tap((res) => {
this.logger.debug(
`${method} ${path} ${ip} ${userAgent}: ${response.statusCode}: ${Date.now() - now}ms`,
);
this.logger.debug(`Response: ${JSON.stringify(res)}`);
}),
);
}
}

这里用 nest 的 Logger 来打印日志,可以打印一样的格式。

打印下 method、path、ip、user agent,调用的目标 class、handler 等信息。

然后记录下响应的状态码和请求时间还有响应内容。

全局启用这个 interceptor:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { RequestLogInterceptor } from './request-log.interceptor';

@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_INTERCEPTOR,
useClass: RequestLogInterceptor
}
],
})
export class AppModule {}

把服务跑起来:npm run start:dev

浏览器访问下:

可以看到,打印了请求的信息,目标 class、handler,响应的内容。

但其实这个 ip 是有问题的:

如果客户端直接请求 Nest 服务,那这个 ip 是准的,但如果中间经过了 nginx 等服务器的转发,那拿到的 ip 就是 nginx 服务器的 ip 了。

这时候要取 X-Forwarded-For 这个 header,它记录着转发的客户端 ip。

当然,这种事情不用自己做,有专门的库 request-ip:

安装下:npm install --save request-ip

然后把打印的 ip 换一下:

换成 X-Forwarded-For 的客户端 ip 或者是 request.ip。

import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { Response } from 'express';
import { Request } from 'express';
import { Observable, tap } from 'rxjs';
import * as requestIp from 'request-ip';

@Injectable()
export class RequestLogInterceptor implements NestInterceptor {
private readonly logger = new Logger(RequestLogInterceptor.name);

intercept(
context: ExecutionContext,
next: CallHandler<any>,
) {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();

const userAgent = request.headers['user-agent'];

const { ip, method, path } = request;

const clientIp = requestIp.getClientIp(ip) || ip;

this.logger.debug(
`${method} ${path} ${clientIp} ${userAgent}: ${
context.getClass().name
} ${
context.getHandler().name
} invoked...`,
);

const now = Date.now();

return next.handle().pipe(
tap((res) => {
this.logger.debug(
`${method} ${path} ${clientIp} ${userAgent}: ${response.statusCode}: ${Date.now() - now}ms`,
);
this.logger.debug(`Response: ${JSON.stringify(res)}`);
}),
);
}
}

访问下:

因为我们本地访问用 localhost 是拿不到真实 ip 的。

你可以查一下本地 ip,用 ip 访问:

这里的 ::ffff 是 ipv6 地址的意思:

这样部署到线上之后就能拿到真实地址了。

那如果想拿到 ip 地址对应的城市呢?

很多系统会做登录日志,每次登录的时候记录登录时的 ip 和对应的城市信息到数据库里。

如何根据 ip 拿到城市信息呢?

其实可以通过一些在线的免费接口:

https://whois.pconline.com.cn/ipJson.jsp?ip=221.237.121.165&json=true

这个就是用于查询 IP 对应的城市的。

请求三方服务用 axios 的包,

安装下:npm install --save @nestjs/axios axios

在 AppModule 里引入下:

然后在 interceptor 里注入 HttpService 来发请求:

注入 HttpService,封装个 ipToCity 方法来查询,在 intercept 方法里调用下:

import { CallHandler, ExecutionContext, Inject, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { Response } from 'express';
import { Request } from 'express';
import { Observable, tap } from 'rxjs';
import * as requestIp from 'request-ip';
import { HttpService } from '@nestjs/axios';

@Injectable()
export class RequestLogInterceptor implements NestInterceptor {
private readonly logger = new Logger(RequestLogInterceptor.name);

@Inject(HttpService)
private httpService: HttpService;

async ipToCity(ip: string) {
const response = await this.httpService.axiosRef(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`);
return response.data.addr;
}

async intercept(
context: ExecutionContext,
next: CallHandler<any>,
) {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();

console.log(await this.ipToCity('221.237.121.165'))

const userAgent = request.headers['user-agent'];

const { ip, method, path } = request;

const clientIp = requestIp.getClientIp(ip) || ip;

this.logger.debug(
`${method} ${path} ${clientIp} ${userAgent}: ${
context.getClass().name
} ${
context.getHandler().name
} invoked...`,
);

const now = Date.now();

return next.handle().pipe(
tap((res) => {
this.logger.debug(
`${method} ${path} ${clientIp} ${userAgent}: ${response.statusCode}: ${Date.now() - now}ms`,
);
this.logger.debug(`Response: ${JSON.stringify(res)}`);
}),
);
}
}

直接用 httpService 的方法是被包装过后的,返回值是 rxjs 的 Observable,需要用 firstValueFrom 的操作符转为 promise:

如果想用原生 axios 对象,可以直接调用 this.httpService.axiosRef.xxx,这样返回的就是 promise。

可以看到,返回的数据是没问题的,但是字符集不对:

接口返回的字符集是 gbk,而我们用的是 utf-8,所以需要转换一下。

用 iconv-lite 这个包:

它就是用来转换字符集的。

npm install --save iconv

指定 responseType 为 arraybuffer,也就是二进制的数组,然后用 gbk 的字符集来解码。

async ipToCity(ip: string) {
const response = await this.httpService.axiosRef(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
responseType: 'arraybuffer',
transformResponse: [
function (data) {
const str = iconv.decode(data, 'gbk');
return JSON.parse(str);
}
]
});
return response.data.addr;
}

现在,就能拿到 utf-8 编码的城市信息了:

当然,这个不建议放到请求日志里,不然每次请求都调用一次接口太浪费性能了。

登录日志里可以加这个。

案例代码上传了小册仓库

总结

我们通过 interceptor 实现了记录请求日志的功能。

其中 ip 地址如果被 nginx 转发过,需要取 X-Forwarded-For 的 header 的值,我们直接用 request-ip 这个包来做。

如果想拿到 ip 对应的城市信息,可以用一些免费接口来查询,用 @nestjs/axios 来发送请求。当然,这个不建议放到请求日志里。

这样,就可以记录下每次请求响应的信息了。