全局拦截器使用说明
概述
GlobalInterceptor 是 NestJS 应用的全局请求拦截器,负责处理请求日志记录、响应数据格式化、错误处理和超时控制等功能。
核心功能
1. 请求日志记录
- 自动记录所有请求的 URL、方法、参数信息
- 将请求日志保存到每日日志文件中
- 支持查询参数、请求体、路径参数的完整记录
2. 响应数据格式化
- 统一响应数据格式
- 自动包装业务数据
- 支持特殊标记数据的处理
3. 错误日志记录
- 记录请求处理过程中的错误信息
- 将错误日志保存到日志文件
- 保持错误的原始传播
4. 超时控制(可选)
- 支持请求超时控制(默认注释状态)
- 超时自动抛出 RequestTimeoutException
基础使用
注册全局拦截器
在 main.ts 中注册全局拦截器:
typescript
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { GlobalInterceptor } from './common/interceptor/GlobalInterceptor'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
// 注册全局拦截器
app.useGlobalInterceptors(new GlobalInterceptor())
await app.listen(3000)
}
bootstrap()模块级别注册
在特定模块中注册:
typescript
import { Module } from '@nestjs/common'
import { APP_INTERCEPTOR } from '@nestjs/core'
import { GlobalInterceptor } from './common/interceptor/GlobalInterceptor'
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: GlobalInterceptor,
},
],
})
export class AppModule {}API 参考
Interceptor 方法
intercept(context, next)
处理请求和响应的拦截逻辑
参数:
context: ExecutionContext 对象,包含请求上下文信息next: CallHandler 对象,用于继续执行请求处理链
返回值: Observable
日志文件格式
日志文件保存在 log/ 目录下,按日期命名:
log/
├── 2024-01-01.log
├── 2024-01-02.log
└── 2024-01-03.log日志内容格式:
/api/users --- {"query":{"page":"1"},"body":{"name":"John"},"params":{"id":"123"}}
/api/users/1 --- {"message":"User not found","statusCode":404}完整示例
基础控制器示例
typescript
// users.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common'
@Controller('users')
export class UsersController {
@Get()
findAll() {
return [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]
}
@Get(':id')
findOne(@Param('id') id: string) {
return { id: parseInt(id), name: 'Alice' }
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return { id: 3, ...createUserDto }
}
}带特殊标记的响应
typescript
// 返回特殊格式数据(不包装)
@Get('special')
getSpecialData() {
return {
_flag: true,
customData: 'some data',
additionalInfo: 'more info'
}
}
// 返回普通数据(自动包装)
@Get('normal')
getNormalData() {
return {
users: [{ id: 1, name: 'Alice' }],
totalCount: 1
}
}高级用法
自定义日志目录
typescript
// 修改日志保存路径
@Injectable()
export class GlobalInterceptor implements NestInterceptor {
private readonly logDir = process.env.LOG_DIR || 'logs'
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest()
mkdir(join(this.logDir), { recursive: true }).then(() => {
fs.appendFileSync(
`${this.logDir}/${dayjs().format('YYYY-MM-DD')}.log`,
`${req.url} --- ${JSON.stringify({ query: req.query, body: req.body, params: req.params })}\n`,
)
})
// ... 其余逻辑
}
}启用超时控制
typescript
// 启用 8 秒超时控制
return next.handle().pipe(
timeout(8000), // 启用这行
catchError((err) => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException())
}
// ... 错误处理
}),
// ... 其他操作符
)条件性日志记录
typescript
@Injectable()
export class GlobalInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest()
// 只记录特定路由的日志
const shouldLog = !req.url.includes('/health') && !req.url.includes('/metrics')
if (shouldLog) {
console.log('--> 请求数据:', req.url, req.method, {
query: req.query,
body: req.body,
params: req.params,
})
mkdir(join('log'), { recursive: true }).then(() => {
fs.appendFileSync(
`log/${dayjs().format('YYYY-MM-DD')}.log`,
`${req.url} --- ${JSON.stringify({ query: req.query, body: req.body, params: req.params })}\n`,
)
})
}
return next.handle().pipe(
catchError((err) => {
if (shouldLog) {
fs.appendFileSync(`log/${dayjs().format('YYYY-MM-DD')}.log`, `${req.url} --- ${JSON.stringify(err)}\n`)
}
return throwError(() => err)
}),
map((data) => {
let res = { code: 200, msg: 'success', ...(data?._flag ? data : { data }) }
delete res._flag
return res
}),
)
}
}响应时间统计
typescript
@Injectable()
export class GlobalInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest()
const startTime = Date.now()
console.log('--> 请求开始:', req.url, req.method)
return next.handle().pipe(
tap(() => {
const duration = Date.now() - startTime
console.log(`<-- 响应完成:${req.url} (${duration}ms)`)
}),
catchError((err) => {
const duration = Date.now() - startTime
console.log(`<-- 请求失败:${req.url} (${duration}ms)`, err.message)
return throwError(() => err)
}),
map((data) => {
const res = {
code: 200,
msg: 'success',
...(data?._flag ? data : { data }),
timestamp: new Date().toISOString(),
duration: Date.now() - startTime,
}
delete res._flag
return res
}),
)
}
}最佳实践
1. 日志级别管理
typescript
@Injectable()
export class GlobalInterceptor implements NestInterceptor {
private readonly isDevelopment = process.env.NODE_ENV === 'development'
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest()
// 开发环境记录详细日志
if (this.isDevelopment) {
console.log('--> 请求详情:', {
url: req.url,
method: req.method,
headers: req.headers,
query: req.query,
body: req.body,
params: req.params,
timestamp: new Date().toISOString(),
})
}
// 生产环境只记录基本信息
mkdir(join('log'), { recursive: true }).then(() => {
const logData = this.isDevelopment
? { query: req.query, body: req.body, params: req.params }
: { query: req.query, params: req.params } // 不记录 body 敏感信息
fs.appendFileSync(
`log/${dayjs().format('YYYY-MM-DD')}.log`,
`${new Date().toISOString()} - ${req.method} ${req.url} - ${JSON.stringify(logData)}\n`,
)
})
// ... 其余逻辑
}
}2. 响应数据标准化
typescript
// 统一的成功响应格式
map((data) => {
// 处理特殊标记数据
if (data?._flag) {
delete data._flag
return {
code: 200,
msg: 'success',
...data
}
}
// 处理分页数据
if (data?.data && data?.total !== undefined) {
return {
code: 200,
msg: 'success',
data: data.data,
pagination: {
total: data.total,
page: data.page,
pageSize: data.pageSize
}
}
}
// 普通数据包装
return {
code: 200,
msg: 'success',
data: data
}
}),3. 错误分类处理
typescript
catchError((err) => {
const req = context.switchToHttp().getRequest()
// 记录错误日志
const errorLog = {
timestamp: new Date().toISOString(),
url: req.url,
method: req.method,
error: {
name: err.name,
message: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
}
}
fs.appendFileSync(
`log/${dayjs().format('YYYY-MM-DD')}-error.log`,
JSON.stringify(errorLog) + '\n'
)
// 根据错误类型返回不同的响应
if (err instanceof ValidationError) {
return throwError(() => new HttpException({
code: 400,
msg: '参数验证失败',
details: err.message
}, HttpStatus.BAD_REQUEST))
}
return throwError(() => err)
}),注意事项
- 性能考虑:日志写入是同步操作,大量请求时可能影响性能
- 磁盘空间:定期清理旧日志文件,避免占用过多磁盘空间
- 敏感信息:生产环境中避免记录敏感的请求体数据
- 并发安全:文件写入需要考虑并发安全问题
- 目录权限:确保应用有创建日志目录和写入文件的权限
常见问题
Q: 如何排除健康检查等特定路由的日志记录?
A: 在拦截器中添加路由过滤逻辑:
typescript
const excludePaths = ['/health', '/metrics', '/favicon.ico']
const shouldLog = !excludePaths.some((path) => req.url.startsWith(path))
if (shouldLog) {
// 执行日志记录逻辑
}Q: 如何实现异步日志记录以提高性能?
A: 使用异步文件写入:
typescript
// 使用异步写入
await fs.promises.appendFile(`log/${dayjs().format('YYYY-MM-DD')}.log`, `${req.url} --- ${JSON.stringify(logData)}\n`)Q: 如何按不同级别记录日志?
A: 实现多级别日志记录:
typescript
private log(level: 'info' | 'error' | 'debug', message: string, data?: any) {
const logEntry = {
timestamp: new Date().toISOString(),
level,
message,
data
}
const filename = level === 'error'
? `log/${dayjs().format('YYYY-MM-DD')}-error.log`
: `log/${dayjs().format('YYYY-MM-DD')}.log`
fs.appendFileSync(filename, JSON.stringify(logEntry) + '\n')
}Q: 如何集成第三方日志服务?
A: 可以集成 Winston、Bunyan 等日志框架:
typescript
import { createLogger, transports, format } from 'winston'
@Injectable()
export class GlobalInterceptor implements NestInterceptor {
private readonly logger = createLogger({
level: 'info',
format: format.combine(format.timestamp(), format.json()),
transports: [
new transports.File({ filename: 'log/combined.log' }),
new transports.File({ filename: 'log/error.log', level: 'error' }),
],
})
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest()
this.logger.info('Incoming request', {
url: req.url,
method: req.method,
query: req.query,
})
// ... 其余逻辑
}
}Q: 如何处理大文件上传的日志记录?
A: 对于文件上传请求,可以选择性记录:
typescript
// 检测是否为文件上传
const isFileUpload = req.headers['content-type']?.includes('multipart/form-data')
if (!isFileUpload) {
// 正常记录日志
fs.appendFileSync(logFile, logData)
} else {
// 只记录基本信息,避免记录文件内容
const basicInfo = {
url: req.url,
method: req.method,
fileSize: req.headers['content-length'],
}
fs.appendFileSync(logFile, JSON.stringify(basicInfo))
}配置示例
完整的 main.ts 配置
typescript
import { NestFactory } from '@nestjs/core'
import { ValidationPipe } from '@nestjs/common'
import { AppModule } from './app.module'
import { GlobalInterceptor } from './common/interceptor/GlobalInterceptor'
import { GlobalExceptionsFilter } from './common/filters/GlobalExceptionsFilter'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
// 全局管道配置
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
)
// 全局拦截器
app.useGlobalInterceptors(new GlobalInterceptor())
// 全局异常过滤器
app.useGlobalFilters(new GlobalExceptionsFilter())
// CORS 配置
app.enableCors()
const port = process.env.PORT || 3000
await app.listen(port)
console.log(`Application is running on: http://localhost:${port}`)
}
bootstrap()