成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

NestJS中如何優(yōu)雅的實(shí)現(xiàn)接口日志記錄

開發(fā) 前端
在我們系統(tǒng)開發(fā)中,通常會(huì)需要對(duì)接口的請(qǐng)求情況做一些日志記錄,通過詳細(xì)的日志記錄,我們可以獲取每個(gè)接口請(qǐng)求的關(guān)鍵信息。本篇文章將介紹如何在 NestJS 中優(yōu)雅的實(shí)現(xiàn)接口日志記錄。

在我們系統(tǒng)開發(fā)中,通常會(huì)需要對(duì)接口的請(qǐng)求情況做一些日志記錄,通過詳細(xì)的日志記錄,我們可以獲取每個(gè)接口請(qǐng)求的關(guān)鍵信息,包括請(qǐng)求時(shí)間、請(qǐng)求參數(shù)、請(qǐng)求主機(jī)、以及用戶身份等。這些信息將為后續(xù)的性能優(yōu)化、故障排查和用戶行為分析提供重要依據(jù)。本篇文章將介紹如何在 NestJS 中優(yōu)雅的實(shí)現(xiàn)接口日志記錄。

什么是 AOP

在開始之前,我們需要了解一下什么是 AOP 架構(gòu)?

我們首先了解一下 NestJS 對(duì)一個(gè)請(qǐng)求的處理過程。在 NestJS 中,一個(gè)請(qǐng)求首先會(huì)先經(jīng)過控制器(Controller),然后 Controller 調(diào)用服務(wù) (Service)中的方法,在 Service 中可能還會(huì)進(jìn)行數(shù)據(jù)庫的訪問(Repository)等操作,最后返回結(jié)果。但是如果我們想在這個(gè)過程中加入一些通用邏輯,比如日志記錄,權(quán)限控制等該如何做呢?

這時(shí)候就需要用到 AOP(Aspect-Oriented Programming,面向切面編程)了,它允許開發(fā)者通過定義切面(Aspects)來對(duì)應(yīng)用程序的各個(gè)部分添加橫切關(guān)注點(diǎn)(Cross-Cutting Concerns)。橫切關(guān)注點(diǎn)是那些不屬于應(yīng)用程序核心業(yè)務(wù)邏輯,但在整個(gè)應(yīng)用程序中多處重復(fù)出現(xiàn)的功能或行為。這樣可以讓我們?cè)诓磺秩霕I(yè)務(wù)邏輯的情況下來加入一些通用邏輯。也就是說 AOP 架構(gòu)允許我們?cè)谡?qǐng)求的不同階段插入代碼,而不需要修改業(yè)務(wù)邏輯的代碼。

NestJS 中的五種實(shí)現(xiàn) AOP 的方式有Middleware(中間件)、Guard(導(dǎo)航守衛(wèi))、Pipe(管道)、Interceptor(攔截器)、ExceptionFilter(異常過濾器),感興趣的可以查看相關(guān)資料了解這些AOP。本篇文章將介紹如何使用Interceptor(攔截器)來實(shí)現(xiàn)接口日志記錄。

然后看一下我們的需求,我們需要記錄每個(gè)接口的請(qǐng)求情況,包括請(qǐng)求時(shí)間、請(qǐng)求參數(shù)、請(qǐng)求主機(jī)、以及用戶身份等。我們肯定是不能在每個(gè)接口中都去手動(dòng)的去添加日志記錄的,這樣會(huì)非常的麻煩,而且也不優(yōu)雅。所以這時(shí)候我們就可以使用 AOP 架構(gòu)中的Interceptor(攔截器)來實(shí)現(xiàn)接口日志記錄。攔截器可以在請(qǐng)求到達(dá)控制器之前或之后執(zhí)行一些操作,我們可以在攔截器中記錄接口的請(qǐng)求情況,這樣就可以實(shí)現(xiàn)接口日志記錄了。

日志記錄模塊實(shí)現(xiàn)

首先我們需要生成一個(gè)日志記錄模塊,用于記錄接口的請(qǐng)求情況。在NestJS中執(zhí)行nest g res log就可以自動(dòng)生成一個(gè)模板。然后新建log/entities/operationLog.entity.ts文件,用于定義日志記錄的實(shí)體類。

import * as moment from "moment";
import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from "typeorm";
//操作日志表
@Entity("fs_operation_log")
export class OperationLog {
  @PrimaryGeneratedColumn()
  id: number; // 標(biāo)記為主鍵,值自動(dòng)生成
  @Column({ length: 100, nullable: true })
  title: string; //系統(tǒng)模塊
  @Column({ length: 20, nullable: true })
  operation_type: string; //操作類型
  @Column({ length: 20, nullable: true })
  method: string; //請(qǐng)求方式
  @Column({ type: "text", nullable: true })
  params: string; //參數(shù)
  @Column({ nullable: true })
  ip: string; //ip
  @Column({ type: "text", nullable: true })
  url: string; //地址
  @Column({ nullable: true })
  user_agent: string; //瀏覽器
  @Column({ nullable: true })
  username: string; //操作人員
  @CreateDateColumn({
    transformer: {
      to: (value) => {
        return value;
      },
      from: (value) => {
        return moment(value).format("YYYY-MM-DD HH:mm:ss");
      },
    },
  })
  create_time: Date;

  @UpdateDateColumn({
    transformer: {
      to: (value) => {
        return value;
      },
      from: (value) => {
        return moment(value).format("YYYY-MM-DD HH:mm:ss");
      },
    },
  })
  update_time: Date;
}

啟動(dòng)項(xiàng)目后,在數(shù)據(jù)庫中就會(huì)自動(dòng)生成fs_operation_log表了。

然后在log/log.module.ts文件中通過@Global將這個(gè)模塊注冊(cè)為全局模塊,并導(dǎo)入這個(gè)實(shí)體類,同時(shí)將LogService導(dǎo)出,這樣就可以在其它模塊中使用了。

import { Global, Module } from "@nestjs/common";
import { LogService } from "./log.service";
import { LogController } from "./log.controller";
import { OperationLog } from "./entities/operationLog.entity";
import { TypeOrmModule } from "@nestjs/typeorm";

//全局模塊
@Global()
@Module({
  controllers: [LogController],
  providers: [LogService],
  imports: [TypeOrmModule.forFeature([OperationLog])],
  exports: [LogService],
})
export class LogModule {}

最后在log/log.service.ts文件中定義一個(gè)saveLog方法,用于保存日志記錄。

import { Injectable } from '@nestjs/common';
import { OperationLog } from './entities/operationLog.entity';
import {Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { ApiException } from 'src/common/filter/http-exception/api.exception';
import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';

@Injectable()
export class LogService {
    constructor(
        @InjectRepository(OperationLog)
        private readonly operationLog: Repository<OperationLog>
    ) { }
    // 保存操作日志
    async saveOperationLog(operationLog: OperationLog) {
        await this.operationLog.save(operationLog);
    }

}

這樣我們就完成了日志記錄模塊的實(shí)現(xiàn)了。后面我們會(huì)在攔截器中調(diào)用這個(gè)方法來實(shí)現(xiàn)接口日志的記錄。

攔截器實(shí)現(xiàn)

新建src/common/interceptor/log.interceptor.ts文件,用于實(shí)現(xiàn)攔截器。在攔截器中可以通過context.switchToHttp().getRequest()獲取到請(qǐng)求相關(guān)信息。同時(shí)我們可以通過context.getHandler()獲取到當(dāng)前控制器的元數(shù)據(jù),從而獲取到控制器中自定義裝飾器定義的模塊名。

首先看一下自定義裝飾器@LogOperationTitle。

src/common/decorator/oprertionlog.decorator.ts文件中定義了一個(gè)@LogOperationTitle裝飾器,用于標(biāo)記當(dāng)前控制器的模塊名。

import { SetMetadata } from "@nestjs/common";

// 操作日志裝飾器,設(shè)置操作日志模塊名
export const LogOperationTitle = (title: string) =>
  SetMetadata("logOperationTitle", title);

簡(jiǎn)單來說就是使用@LogOperationTitle裝飾器可以定義模塊名稱(logOperationTitle),然后在攔截器中獲取到這個(gè)模塊名稱。然后看下自定義攔截器的實(shí)現(xiàn)。

//操作日志攔截器
import {
    Injectable,
    NestInterceptor,
    ExecutionContext,
    CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { LogService } from 'src/log/log.service';
import { OperationLog } from 'src/log/entities/operationLog.entity';
import { Reflector } from '@nestjs/core';
export interface Response<T> {
    data: T;
}

@Injectable()
export class OperationLogInterceptor<T>
    implements NestInterceptor<T, Response<T>>
{
    constructor(
        private readonly logService: LogService,
        private readonly reflactor: Reflector,
    ) { }
    intercept(
        context: ExecutionContext,
        next: CallHandler,
    ): Observable<Response<T>> {
        //獲取請(qǐng)求對(duì)象
        const request = context.switchToHttp().getRequest();
        //獲取當(dāng)前控制器元數(shù)據(jù)中的日志logOperationTitle
        const title = this.reflactor.get<string>('logOperationTitle', context.getHandler());
        return next
            .handle().pipe(tap(() => {
                const log = new OperationLog();
                log.title = title;
                log.method = request.method;
                log.url = request.url;
                log.ip = request.ip;
                //請(qǐng)求參數(shù)
                log.params = JSON.stringify({ ...request.query, ...request.params, ...request.body });
                //瀏覽器信息
                log.user_agent = request.headers['user-agent'];
                log.username = request.user?.username;
                this.logService.saveOperationLog(log).catch((err) => {
                    console.log(err);
                });

            }
            ));
    }
}

這樣我們就完成了攔截器的實(shí)現(xiàn)了。

使用攔截器

因?yàn)槲覀冃枰诿總€(gè)請(qǐng)求中都用到這個(gè)攔截器,所以我們可將其定義為全局?jǐn)r截器。前面文章中我們介紹過可以在main.ts文件中通過app.useGlobalInterceptors(new OperationLogInterceptor())將攔截器注冊(cè)為全局?jǐn)r截器,但是這樣會(huì)出現(xiàn)一個(gè)問題,就是我們?cè)?/span>log/log.module.ts文件中定義的LogService服務(wù)無法在攔截器中使用,因?yàn)閿r截器是沒有依賴注入的,所以我們需要在app.module.ts文件中通過APP_INTERCEPTOR提供者將攔截器注冊(cè)為全局?jǐn)r截器,這樣才可以在攔截器中使用LogService服務(wù)了。

import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { OperationLogInterceptor } from './common/interceptor/log/log.interceptor';
//此處省略其它代碼
@Module({
    providers: [AppService,
    // 注冊(cè)全局?jǐn)r截器
    {
      provide: APP_INTERCEPTOR,
      useClass: OperationLogInterceptor,
    }
  ],
})

此時(shí)啟動(dòng)項(xiàng)目我們的攔截器就已經(jīng)生效了。比如隨便訪問幾次菜單查詢的接口,就可以在數(shù)據(jù)庫看到日志記錄已經(jīng)成功了。

圖片

但是你會(huì)發(fā)現(xiàn)模塊名還是空的,因?yàn)槲覀冞€沒有在控制器中使用@LogOperationTitle裝飾器來定義模塊名。所以我們需要在控制器中使用@LogOperationTitle裝飾器來定義模塊名。比如在menu/menu.controller.ts文件中定義菜單查詢模塊名。

//菜單查詢
@Get()
@LogOperationTitle('菜單查詢')
async findAll() {
    return await this.menuService.findAll();
}

再次請(qǐng)求接口,就可以看到模塊名已經(jīng)記錄成功了。

圖片

提供查詢?nèi)罩窘涌?/span>

我們還需要提供一個(gè)查詢和導(dǎo)出日志接口給前端使用,用于查詢?nèi)罩居涗?。?/span>log/log.controller.ts文件中定義一個(gè)查詢和導(dǎo)出日志接口。(導(dǎo)出功能前面文章已經(jīng)介紹過了,這里就不詳細(xì)介紹了,感興趣的可以查看前面文章)

import { Controller, Get, Query, Res } from '@nestjs/common';
import { LogService } from './log.service';
import { FindListDto } from './dto/find-list.dto';
import { LogOperationTitle } from 'src/common/decorators/oprertionlog.decorator';
import { ApiOperation } from '@nestjs/swagger';
import { Permissions } from 'src/common/decorators/permissions.decorator';
import { Response } from 'express';
@Controller('log')
export class LogController {
  constructor(private readonly logService: LogService) { }

  //日志查詢
  @LogOperationTitle('日志查詢')
  @ApiOperation({ summary: '日志管理-查詢' })
  @Permissions('system:log:list')
  @Get('list')
  findLogList(@Query() findListDto: FindListDto) {
    return this.logService.findList(findListDto);
  }

  //日志導(dǎo)出
  @LogOperationTitle('日志導(dǎo)出')
  @ApiOperation({ summary: '日志管理-導(dǎo)出' })
  @Get('export')
  async export(@Query() findListDto: FindListDto, @Res() res: Response) {
    const data = await this.logService.export(findListDto);
    res.send(data);
  }
}

其中FindListDto類型為:

import { ApiProperty } from "@nestjs/swagger";
import { IsOptional } from "class-validator";

export class FindListDto {
    @ApiProperty({
        example: '模塊名稱',
        required: false,
    })
    @IsOptional()
    title?: string;

    @ApiProperty({
        example: '操作人',
        required: false,
    })
    @IsOptional()
    username?: string;
    @ApiProperty({
        example: '請(qǐng)求地址',
        required: false,
    })
    @IsOptional()
    url?: string;

    @ApiProperty({
        example: '結(jié)束時(shí)間',
        required: false,
    })
    end_time: string;

    @ApiProperty({
        example: '開始時(shí)間',
        required: false,
    })
    begin_time: string;
    @ApiProperty({
        example: '當(dāng)前頁',
        required: false,
    })
    page_num: number;
    @ApiProperty({
        example: '每頁條數(shù)',
        required: false,
    })
    page_size: number;
}

前端可以通過這些參數(shù)來查詢?nèi)罩居涗洝?/span>

log/log.service.ts文件中實(shí)現(xiàn)findList方法和export方法。

import { Injectable } from '@nestjs/common';
import { OperationLog } from './entities/operationLog.entity';
import { Between, Like, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { FindListDto } from './dto/find-list.dto';
import { ApiException } from 'src/common/filter/http-exception/api.exception';
import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';
import { exportExcel } from 'src/utils/common';
import { mapLogZh } from 'src/config/excelHeader';
@Injectable()
export class LogService {
    constructor(
        @InjectRepository(OperationLog)
        private readonly operationLog: Repository<OperationLog>
    ) { }
    // 保存操作日志
    async saveOperationLog(operationLog: OperationLog) {
        await this.operationLog.save(operationLog);
    }
    // 分頁查詢操作日志
    async findList(findList: FindListDto) {
        const condition = {};
        if (findList.title) {
            condition['title'] = Like(`%${findList.title}%`);
        }
        if (findList.username) {
            condition['username'] = Like(`%${findList.username}%`);
        }
        if (findList.url) {
            condition['url'] = Like(`%${findList.url}%`);
        }
        if (findList.begin_time && findList.end_time) {
            condition['create_time'] = Between(findList.begin_time, findList.end_time);
        }
        try {
            const [list, total] = await this.operationLog.findAndCount({
                skip: (findList.page_num - 1) * findList.page_size,
                take: findList.page_size,
                order: {
                    create_time: 'DESC'
                },
                where: condition
            });
            return {
                list,
                total
            };
        } catch (error) {
            throw new ApiException('查詢失敗', ApiErrorCode.FAIL);
        }

    }
    //日志導(dǎo)出
    async export(findList: FindListDto) {

        try {
            const { list } = await this.findList(findList)
            const excelBuffer = await exportExcel(list, mapLogZh);
            return excelBuffer;
        } catch (error) {
            throw new ApiException('導(dǎo)出失敗', ApiErrorCode.FAIL);
        }
    }
}

這樣我們就完成了日志的查詢與導(dǎo)出接口。

前端實(shí)現(xiàn)

最后在前端調(diào)用接口實(shí)現(xiàn)日志的查詢與導(dǎo)出功能。最終實(shí)現(xiàn)的頁面如下:

圖片

感興趣的可以直接去源碼地址(https://github.com/qddidi/fs-admin)查看相關(guān)代碼實(shí)現(xiàn)。

責(zé)任編輯:龐桂玉 來源: web前端進(jìn)階
相關(guān)推薦

2023-03-06 11:36:13

SpingBoot注解

2020-12-08 08:08:51

Java接口數(shù)據(jù)

2021-03-09 13:18:53

加密解密參數(shù)

2022-06-04 12:25:10

解密加密過濾器

2022-02-15 17:56:19

SpringBoot日志

2020-08-26 07:17:19

通信

2022-02-18 17:34:47

數(shù)組多維五維數(shù)組

2021-02-14 20:41:56

API日志web

2025-06-17 07:37:53

2022-06-21 14:44:38

接口數(shù)據(jù)脫敏

2024-11-07 10:55:26

2024-11-08 15:56:36

2024-09-27 12:27:31

2021-11-17 10:25:28

loguru日志Python

2022-01-10 09:35:50

日志語言解析器

2020-03-27 15:10:23

SpringJava框架

2023-03-23 22:46:38

Spring限流機(jī)制

2021-10-26 10:28:41

開發(fā)架構(gòu)Kubernetes

2020-08-24 13:35:59

trycatchJava

2024-01-17 10:16:22

前端國(guó)際化消息鍵
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 91一区二区三区 | 亚洲成人蜜桃 | 在线精品一区 | 一区二区视频在线观看 | 日本成人片在线观看 | 国产视频综合 | 国产精品视频综合 | 国产女人第一次做爰毛片 | 日韩精品一区二区三区中文字幕 | 羞羞视频网站免费看 | 免费在线观看黄色av | 三级黄色大片网站 | 久久精品免费一区二区 | 国内精品久久久久久 | 久久精品国产一区二区三区不卡 | 玖玖久久| 久久99视频 | 亚洲电影第三页 | 久热精品免费 | 一级毛片免费 | 久久久www成人免费无遮挡大片 | 成人久久18免费网站图片 | 欧美成视频 | 国产精品久久久久久妇女6080 | 人人澡人人射 | 国产欧美精品一区二区 | 免费国产网站 | 一区二区三区日本 | 中文字幕日韩欧美一区二区三区 | 国产精品黄色 | 久久网一区二区 | 久久与欧美| 两性午夜视频 | 欧美4p| 欧洲精品视频一区 | 久久影音先锋 | www精品美女久久久tv | 国产精品无码专区在线观看 | 欧美日韩国产一区二区三区 | 中文字幕国产精品 | 欧美在线视频一区二区 |