likes
comments
collection
share

nestjs:log记录api接口访问人信息本文简述了在NestJS项目中整合winston日志库与LoggingInt

作者站长头像
站长
· 阅读数 60

在实际项目中,记录每个接口的访问日志是十分重要的。通过日志,我们可以追踪每个接口的访问情况,尤其是了解哪个用户触发了哪些操作。在本文中,我将介绍如何通过 NestJS 结合 winston 日志库,记录当前操作人的信息。具体来说,我们将通过 JWT 解析出用户信息,并将其包含在日志记录中。

需预先了解:

本次主要会使用到的接口之前文章提及的访问当前登录人的profile接口

nestjs:log记录api接口访问人信息本文简述了在NestJS项目中整合winston日志库与LoggingInt

由图中可以看到请求接口: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生成内容:

nestjs:log记录api接口访问人信息本文简述了在NestJS项目中整合winston日志库与LoggingInt

生成一条记录:

 {"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天的日志文件(包括压缩归档的)会被保留在文件系统中

生成的效果:

nestjs:log记录api接口访问人信息本文简述了在NestJS项目中整合winston日志库与LoggingInt

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 info: [fat]Method: GET, URL: /auth/profile, User: NeoLuo, Status: 200, Data: {"body":{},"query":{},"params":{}}

nestjs:log记录api接口访问人信息本文简述了在NestJS项目中整合winston日志库与LoggingInt

总结

通过结合 LoggingInterceptor 和 winston,我们可以有效地记录每个接口的访问日志,包括访问者信息、请求参数以及接口的响应状态。这种方法在实际项目中非常有用,有助于问题的追踪与调试。

转载自:https://juejin.cn/post/7403244457262841868
评论
请登录