nestjs:log记录api接口访问人信息本文简述了在NestJS项目中整合winston日志库与LoggingInt
在实际项目中,记录每个接口的访问日志是十分重要的。通过日志,我们可以追踪每个接口的访问情况,尤其是了解哪个用户触发了哪些操作。在本文中,我将介绍如何通过 NestJS 结合 winston 日志库,记录当前操作人的信息。具体来说,我们将通过 JWT 解析出用户信息,并将其包含在日志记录中。
需预先了解:
本次主要会使用到的接口之前文章提及的访问当前登录人的profile接口

由图中可以看到请求接口:localhost:3000/auth/profile 携带了token,服务也是通过解析token来识别当前访问人
步骤
1. 启动应用
由于是在之前文展基础上,所以第一步不需要更改任何代码,只需要顺序启动应用:
- 启动docker应用
- 项目根目录下运行:
docker-compose up --build -d
- 项目根目录下启动项目:
npm run start:dev
2. 配置winston
Winston 是一个强大的 Node.js 日志库,支持多种日志传输和格式化选项,首先安装对应依赖
npm install winston nest-winston --save
接下来,通过 WinstonModule 来配置日志记录。在 AppModule 中配置全局的日志记录器。
// src/app.module.ts
import { Module } from '@nestjs/common';
+ import { TypeOrmModule } from '@nestjs/typeorm';
+ import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
import { RedisModule } from './redis/redis.module';
@Module({
imports: [
RedisModule,
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3307,
username: 'root',
password: 'example',
database: 'testdb',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
+ WinstonModule.forRoot({
+ transports: [
+ new winston.transports.Console({
+ format: winston.format.combine(
+ winston.format.timestamp(),
+ winston.format.colorize(),
+ winston.format.simple()
+ ),
+ }),
+ new winston.transports.File({
+ filename: 'logs/combined.log',
+ format: winston.format.combine(
+ winston.format.timestamp(),
+ winston.format.json()
+ ),
+ }),
+ ],
+ }),
UserModule,
AuthModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
3. 访问profile打印日志
具体访问profile的方式是在:auth.service 中的 getProfile,需要再auth.service中修改代码调用winston ,打印日志:"fetch current user profile"
// src/auth/auth.service.ts
import {
Injectable,
+ Inject
} from '@nestjs/common';
import { UserService } from 'src/user/user.service';
import { User } from 'src/user/user.entity';
import * as argon2 from 'argon2';
import { InjectRedis, Redis } from '@nestjs-modules/ioredis';
+ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
+ import { Logger } from 'winston';
import { JwtService } from '@nestjs/jwt';
import { SignInDto } from './auth.dto'
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
@InjectRedis() private readonly redisClient: Redis,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
) { }
async getProfile(username: string): Promise<{ success: boolean, data?: User, message?: string }> {
try {
const existingUser = await this.userService.findOneByUsername(username);
if (!existingUser) {
return { success: false, message: 'User not found' };
}
+ this.logger.info('fetch current user profile');
return {
success: true,
data: existingUser
};
} catch (error) {
return { success: false, message: error.message };
}
}
...
}
Postman Get请求:localhost:3000/auth/profile 并携带token(为登录生成的token 包含了当前用户的信息),请求成功后可以看到根目录下 logs/combined.log生成内容:
生成一条记录:
{"level":"info","message":"fetch current user profile","timestamp":"2024-08-14T09:20:02.549Z"}
4. winston-daily-rotate-file
步骤3中日志都会生成到logs/combined.log显然不利于日志维护,这里使用winston-daily-rotate-file:为winston日志库设计的传输模块,专门用于日志文件的自动轮转。主要特点:
-
自动按日期轮转:根据配置的日期模式(如每天、每周等)自动创建新的日志文件,并将旧的日志文件进行归档。
-
文件大小管理:支持设置单个日志文件的最大大小,当文件达到指定大小时,会自动轮转到新的日志文件。
-
旧文件清理:可以配置保留的日志文件数量或天数,超出限制的最旧文件会被自动删除,以节省存储空间。
-
压缩归档:支持将旧的日志文件进行压缩,减少存储空间占用。
-
无缝集成Winston:作为Winston的传输模块,可以轻松地与Winston日志库集成,无需修改现有的日志记录逻辑。
-
灵活的配置选项:提供了丰富的配置选项,如自定义文件名、日期格式、时间戳格式等,以满足不同的日志管理需求。
首先安装对应依赖
npm i winston-daily-rotate-file --save
修改 WinstonModule.forRoot
// src/app.module.ts
...
+ import DailyRotateFile = require("winston-daily-rotate-file");
+ const format = winston.format;
@Module({
imports: [
...
WinstonModule.forRoot({
- transports: [
- new winston.transports.Console({
- format: winston.format.combine(
- winston.format.timestamp(),
- winston.format.colorize(),
- winston.format.simple()
- ),
- }),
- new winston.transports.File({
- filename: 'logs/combined.log',
- format: winston.format.combine(
- winston.format.timestamp(),
- winston.format.json()
- ),
- }),
- ],
+ exitOnError: false,
+ format: format.combine(
+ format.colorize(),
+ format.timestamp({
+ format: 'HH:mm:ss YY/MM/DD'
+ }),
+ format.label({
+ label: "fat"
+ }),
+ format.splat(),
+ format.printf(info => {
+ return `${info.timestamp} ${info.level}: [${info.label}]${info.message}`
+ }),
+ ),
+ transports: [
+ new winston.transports.Console({
+ level: 'info',
+ }),
+ new DailyRotateFile({
+ filename: 'logs/application-%DATE%.log',
+ datePattern: 'YYYY-MM-DD-HH',
+ zippedArchive: true,
+ maxSize: '20m',
+ maxFiles: '14d',
+ }),
+ ],
}),
],
...
})
通过format自定义了输出的日志格式,new DailyRotateFile 中配置:
- zippedArchive: true 当旧的日志文件被轮转出去时,它们会被压缩成.zip文件
- maxSize: '20m' 单个日志文件可以增长到的最大大小为20mb
- maxFiles: '14d' 最近14天的日志文件(包括压缩归档的)会被保留在文件系统中
生成的效果:
5. 配置logging.interceptor 拦截器
为了在所有接口访问时记录日志,我们可以创建一个拦截器,将每个请求的访问人信息、携带参数和操作结果记录到日志中。
创建全局创建拦截器 logging.interceptor.ts
// src/interceptors/logging.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Logger } from 'winston';
import { Inject } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, originalUrl, body, query, params } = request;
const username = request.user?.username || 'Guest';
const requestData = {
body,
query,
params,
};
const requestDataString = JSON.stringify(requestData);
return next.handle().pipe(
tap(() => {
const { statusCode } = context.switchToHttp().getResponse();
const message = `Method: ${method}, URL: ${originalUrl}, User: ${username}, Status: ${statusCode}, Data: ${requestDataString}`;
this.logger.info(message);
}),
);
}
}
这里的 request.user 是在 auth.guard.ts 中 把解析token 把user信息塞入request : request['user'] = payload
// src/guards/auth.guard.ts
...
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(
token,
{
secret: jwtConstants.secret
}
);
const tokenCache = await this.redis.get(payload.username)
if (tokenCache !== token) {
throw new UnauthorizedException('Invalid Token!!!');
}
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
...
request为 Express 对象代表了 HTTP 请求,Request声明中没有 user属性,需要添加全局Request声明支持user
// @types/express.d.ts
import { User } from 'src/user/user.entity';
declare global {
namespace Express {
interface Request {
user?: User;
}
}
}
app.module中引入LoggingInterceptor拦截器
...
+ import { APP_INTERCEPTOR } from '@nestjs/core';
+ import { LoggingInterceptor } from './interceptors/logging.interceptor';
@Module({
...
providers: [AppService, {
+ provide: APP_INTERCEPTOR,
+ useClass: LoggingInterceptor,
}],
})
6. 使用拦截器记录 profile 请求日志
通过拦截器记录访问 profile 接口的日志信息。拦截器会在守卫(AuthGuard)之后执行,因此可以访问到 req.user 中的用户信息。
发送 Postman 请求:
- GET 请求: localhost:3000/auth/profile
- 携带 token(登录生成的 token 包含了当前用户的信息)
请求成功后,日志文件生成了包括操作人,和请求参数等信息内容:
09:23:53 24/08/15 [32minfo[39m: [fat]Method: GET, URL: /auth/profile, User: NeoLuo, Status: 200, Data: {"body":{},"query":{},"params":{}}
总结
通过结合 LoggingInterceptor 和 winston,我们可以有效地记录每个接口的访问日志,包括访问者信息、请求参数以及接口的响应状态。这种方法在实际项目中非常有用,有助于问题的追踪与调试。
转载自:https://juejin.cn/post/7403244457262841868