前言
大家好,我是 elk。今天我们要给项目加上一个超级重要的功能------日志系统!日志系统就像是应用程序的 "黑匣子",记录着它的一举一动,比如控制台输出、文件记录、HTTP 请求、数据库操作等等。有了它,我们就能轻松地监控应用的运行状态,快速定位问题,让开发和运维变得更加高效。
日志系统选型
技术选型
- Winston:Node.js生态最主流的日志库,支持多种传输方式
- DailyRotateFile:实现日志文件按时间/大小自动轮转
- NestJS整合:通过拦截器+过滤器实现无侵入式日志记录
结构设计
bash
日志系统工作流:
客户端请求 -> 全局拦截器(记录入参)
-> 业务处理
-> 全局拦截器(记录响应)
-> 若异常 -> 异常过滤器(记录错误详情)
插件安装
bash
# 核心依赖
pnpm install --save nest-winston winston winston-daily-rotate-file
创建日志服务
新建公共的日志服务
目录:src/module/common/logger/logger.service.ts|logger.module.ts
kotlin
import { Module, Global } from '@nestjs/common';
import { LoggerService } from './logger.service';
@Global()
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}
typescript
// 从@nestjs/common导入Injectable装饰器
import { Injectable } from '@nestjs/common';
// 从winston导入日志相关功能
import { createLogger, format, transports, Logger } from 'winston';
// 导入winston-daily-rotate-file用于日志文件轮转
import 'winston-daily-rotate-file';
// 使用Injectable装饰器,将该类标记为可注入的服务
@Injectable()
export class LoggerService {
private logger: Logger; // 日志记录器实例
constructor() {
// 初始化日志记录器
this.logger = createLogger({
level: 'info', // 默认日志级别
format: format.combine(
// 日志格式组合
format.timestamp({
// 时间戳格式
format: 'YYYY-MM-DD HH:mm:ss',
}),
format.printf(({ message, timestamp, level, ...metadata }) => {
// 自定义日志格式
let LogMessage = `${timestamp} ${level}: ${message}`;
if (Object.keys(metadata).length > 0) {
if (metadata.message !== 'message') {
LogMessage += ` - ${JSON.stringify(metadata, null, 2)}`;
}
}
return LogMessage;
}),
),
transports: [
// 日志传输方式
new transports.Console({
// 控制台输出
format: format.combine(format.colorize(), format.simple()),
}),
new transports.DailyRotateFile({
// 每日轮转文件输出
filename: 'logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true, // 是否压缩归档
maxSize: '20m', // 单个文件最大大小
maxFiles: '14d', // 保留天数
level: 'info', // 日志级别
format: format.combine(
// 文件日志格式
format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
format.printf((info) => {
const paramsInfo = JSON.parse(JSON.stringify(info));
return `${info.timestamp} [${info.level}]: ${info.message} ${
Object.keys(info).length > 0
? JSON.stringify(paramsInfo)
: ''
}`;
}),
),
}),
],
});
}
// 记录日志信息,支持附加参数
log(message: string, params: object = {}) {
if (typeof params === 'object' && Object.keys(params).length > 0) {
// 如果有附加参数,将参数与消息合并
const logMessage = {
message,
...params,
level: 'info', // 日志级别为info
};
this.logger.log(logMessage); // 记录日志
} else {
// 如果没有附加参数,只记录消息
this.logger.log({ message, level: 'info' });
}
}
// 记录错误日志,支持附加参数和堆栈跟踪
error(message: string, params: object = {}, trace = '') {
if (typeof params === 'object' && Object.keys(params).length > 0) {
const logMessage = {
message,
...params,
level: 'error', // 日志级别为error
trace, // 堆栈跟踪信息
};
this.logger.error(logMessage); // 记录错误日志
} else {
this.logger.error({ message, level: 'error' });
}
}
// 记录警告日志,支持附加参数
warn(message: string, params: object = {}) {
if (typeof params === 'object' && Object.keys(params).length > 0) {
const logMessage = {
message,
...params,
level: 'warn', // 日志级别为warn
};
this.logger.warn(logMessage); // 记录警告日志
} else {
this.logger.warn({ message, level: 'warn' });
}
}
// 记录调试日志,支持附加参数
debug(message: string, params: object = {}) {
if (typeof params === 'object' && Object.keys(params).length > 0) {
const logMessage = {
message,
...params,
level: 'debug', // 日志级别为debug
};
this.logger.debug(logMessage); // 记录调试日志
} else {
this.logger.debug({ message, level: 'debug' });
}
}
// 记录信息日志,支持附加参数
info(message: string, params: object = {}) {
if (typeof params === 'object' && Object.keys(params).length > 0) {
const logMessage = {
message,
...params,
level: 'info', // 日志级别为info
};
this.logger.info(logMessage); // 记录信息日志
} else {
this.logger.info({ message, level: 'info' });
}
}
}
配置错误过滤和响应拦截中的日志记录
错误过滤器中请求和响应打印错误日志内容,进行记录
目录:src/common/filters/all-exceptions.filters.ts
typescript
// 从@nestjs/common导入所需的装饰器和接口
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
// 从express导入Response和Request类型
import { Response, Request } from 'express';
// 引入日志服务
import { LoggerService } from '@/module/common/logger/logger.service';
// 使用Catch装饰器,指定捕获HttpException类型的异常
@Catch(HttpException)
export class AllExceptionsFilter implements ExceptionFilter {
constructor(private readonly logger: LoggerService) {}
// 实现catch方法,处理捕获的异常
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取HTTP上下文
const response = ctx.getResponse<Response>(); // 获取响应对象
const request = ctx.getRequest<Request>(); // 获取请求对象
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR; // 获取状态码,默认为500
const message =
exception instanceof HttpException ? exception.message : '服务器错误'; // 获取错误信息,默认为'服务器错误'
// 返回统一的错误响应格式
response.status(status).json({
code: status, // 状态码
message, // 错误信息
timestamp: new Date().toISOString(), // 时间戳
path: request.url, // 请求路径
});
const { headers, url, params, query, body, method } = request;
this.logger.log('请求信息', {
headers,
url,
params,
query,
body,
method,
});
this.logger.log('响应信息', {
url,
method,
status,
message,
timestamp: new Date().toISOString(),
});
}
}
统一的请求响应拦截器进行请求和响应的日志打印,进行记录
目录:src/common/interceptors/response.interceptor.ts
typescript
// 从@nestjs/common导入所需的装饰器和接口
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
// 导入RxJS的Observable
import { Observable } from 'rxjs';
// 导入RxJS的map操作符
import { map, tap } from 'rxjs/operators';
// 导入自定义的ApiResponse接口
import { ApiResponse } from '@/common/interfaces/api-response';
// 引入日志服务
import { LoggerService } from '@/module/common/logger/logger.service';
import { Request } from 'express';
// 使用Injectable装饰器,将该类标记为可注入的拦截器
@Injectable()
export class ResponseInterceptor<T>
implements NestInterceptor<T, ApiResponse<T>>
{
// 构造函数,接收LoggerService实例
constructor(private readonly logger: LoggerService) {}
// 实现拦截器方法,用于处理响应数据
intercept(
context: ExecutionContext, // 执行上下文
next: CallHandler, // 下一个处理程序
): Observable<ApiResponse<T>> {
const request = context.switchToHttp().getRequest<Request>();
const { method, url, params, query, body, headers } = request;
this.logger.log('请求信息', {
method,
url,
params,
query,
body,
headers,
});
return next.handle().pipe(
tap((data) => {
this.logger.log('响应信息', {
url,
method,
status: 200,
msg: '请求成功',
data,
});
}),
// 使用map操作符转换响应数据
map((data) => ({
code: 200, // 响应状态码
message: '请求成功', // 响应消息
data, // 实际响应数据
})),
);
}
}
启动日志系统
在入口文件中进行日志服务的注册,将日志服务传递到错误过滤器和响应拦截器中
main.ts
csharp
// 引入logger日志服务
import { LoggerService } from '@/module/common/logger/logger.service';
// 全局日志服务
const loggerService = app.get(LoggerService);
// 全局响应拦截器
app.useGlobalInterceptors(new ResponseInterceptor(loggerService));
// 全局异常过滤器
app.useGlobalFilters(new AllExceptionsFilter(loggerService));
📍 下期预告
《从0搭建NestJS后端服务(六):JWT身份验证以及管道参数验证》
我们将探讨:
- JWT 身份验证的原理和实现步骤
- 如何创建自定义管道进行参数验证
- 如何处理验证失败的情况
🤝 互动时间
你在日志实践中遇到过哪些"坑"?欢迎留言讨论!
💬 如何平衡日志详尽性与性能损耗?
💬 日志脱敏有哪些最佳实践?
💬 是否遇到过日志文件权限问题?
一起交流成长,让开发更高效!🚀