nestjs日志
目前方案:
- 内置日志模块(平常开发用)
pino
(简单)winston
(用于生产)
日志等级
- Log: 通用日志,按需记录
- Warning: 警告日志,如多次对数据库进行操作
- Error: 严重日志,如数据库异常
- Debug: 调试日志,如加载数据库日志
- Verbose: 详细日志,所有操作和详细信息,非必要不打印
功能分类
- 错误日志:方便定位问题,给用户友好提示
- 调试日志,方便开发
- 请求日志,记录敏感行为
日志记录位置
- 控制台日志:方便调试用
- 文件日志:方便回溯和追踪(24消失滚动)
- 数据库日志:敏感操作、敏感数据

内置日志
在全局配置main.ts
ts
async function bootstrap() {
const logger = new Logger()
const app = await NestFactory.create(AppModule, {
// logger的默认配置是true, 默认全部打印
// logger: false, // 关闭整个应用程序的日志
// logger: ['error', 'warn'], // 植打印'error'和'warn'等级的日志
})
app.setGlobalPrefix('api') // 给每一个接口添加前缀'/api'
await app.listen(3000)
logger.log(`App 运行在3000端口`) // 这个warn的日志会被打印出来
logger.warn(`App 运行在3000端口`) // 这个warn的日志会被打印出来
logger.error(`App 运行在3000端口`) // 这个warn的日志会被打印出来
}

在控制器user.controller.ts
中打印日志
ts
import { Controller, Get, Logger } from '@nestjs/common'
import { UserService } from './user.service'
@Controller('user')
export class UserController {
private logger = new Logger(UserController.name)
constructor(private userService: UserService) {
this.logger.log('------UserController init')
}
@Get()
getUsers() {
this.logger.log('------请求用户列表成功')
return {}
}
}
打印如下
第三方日志pino
安装插件npm install nestjs-pino
,文档地址,pino用法用
ts
import { NestFactory } from '@nestjs/core'
import { Controller, Get, Module } from '@nestjs/common'
import { LoggerModule, Logger } from 'nestjs-pino'
@Module({
controllers: [AppController],
imports: [LoggerModule.forRoot()]
})
class MyModule {}
@Controller()
export class AppController {
constructor(private readonly logger: Logger) {
this.logger.log('------UserController init')
}
@Get()
getHello() {
this.logger.log('something')
return `Hello world`
}
}
async function bootstrap() {
const app = await NestFactory.create(MyModule)
await app.listen(3000)
}
bootstrap()
打印如下(请求接口的时候会自动打印请求信息)
安装中间件,
- 美化打印
npm i pino-pretty
, 用于开发 - 保存日志到文件的中间件
npm i pino-roll
,用于生产
在根模块app.module.ts
中配置
ts
@Module({
controllers: [AppController],
imports: [
LoggerModule.forRoot({
pinoHttp: {
transport:
process.env.NODE_ENV === 'development'
? {
target: 'pino-pretty',
options: { colorize: true },
}
: {
target: 'pino-roll',
options: {
file: join('log', 'log.txt'), // 会输出文件到指定路径
frequency: 'daily', // hourly
size: '10m', // 文件大小,超过10m就会生成新的文件
mkdir: true,
},
},
},
}),
]
})
第三方日志winston
安装插件npm install -S nest-winston winston
,winston用法,nest-winston用法
在入口main.ts
中引入wiston模块
ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
+ import { createLogger } from 'winston'
+ import * as winston from 'winston'
+ import { WinstonModule, utilities } from 'nest-winston'
async function bootstrap() {
+ const instance = createLogger({
+ transports: [
+ new winston.transports.Console({
+ level: 'info',
+ format: winston.format.combine(winston.format.timestamp(),
+ utilities.format.nestLike()),
+ }),
+ ],
+ })
+ const logger = WinstonModule.createLogger({ instance })
+ const app = await NestFactory.create(AppModule, { logger })
await app.listen(3000)
}
bootstrap()
在app.module.ts
中注入全局服务
ts
import { Logger, } from '@nestjs/common'
@Global()
@Module({
providers: [Logger],
exports: [Logger],
})
在控制器中使用
ts
import { Controller, Get, Logger} from '@nestjs/common'
import { UserService } from './user.service'
@Controller('user')
export class UserController {
constructor(
private userService: UserService,
private readonly logger: Logger, // private logger: Logger,
) {
this.logger.log('UserController 初始化')
}
@Get()
getUsers() {
this.logger.log('请求用户列表-ok')
this.logger.warn('请求用户列表-ok')
this.logger.error('请求用户列表-ok')
return {}
}
}

滚动日志,安装插件
npm i winston-daily-rotate-file
在入口main.ts
中修改代码
ts
import 'winston-daily-rotate-file'
async function bootstrap() {
const instance = createLogger({
// options of Winston
transports: [
new winston.transports.Console({
level: 'info', // 打印所有info、warn、error的信息
format: winston.format.combine(winston.format.timestamp(),
utilities.format.nestLike()),
}),
+ new winston.transports.DailyRotateFile({
+ level: 'warn', // 只打印warn、error信息
+ dirname: 'log', // 输出目录
+ filename: 'application-%DATE%.log', // 文件命名
+ datePattern: 'YYYY-MM-DD-HH',
+ zippedArchive: true,
+ maxSize: '20m',
+ maxFiles: '14d',
+ format: winston.format.combine(winston.format.timestamp(), winston.format.simple()),
+ }),
+ new winston.transports.DailyRotateFile({
+ level: 'info', // 打印所有info、warn、error的信息
+ dirname: 'log', // 输出目录
+ filename: 'info-%DATE%.log', // 文件命名
+ datePattern: 'YYYY-MM-DD-HH',
+ zippedArchive: true,
+ maxSize: '20m',
+ maxFiles: '14d',
+ format: winston.format.combine(winston.format.timestamp(), winston.format.simple()),
+ }),
],
})
const logger = WinstonModule.createLogger({ instance })
const app = await NestFactory.create(AppModule, { logger })
await app.listen(3000)
}
bootstrap()
全局过滤器输出日志
拦截http异常并输出到日志
创建http过滤器./filters/http-exception.filter.ts
ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, LoggerService } from '@nestjs/common'
import { Request, Response } from 'express'
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
constructor(private logger: LoggerService) {}
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const req: Request = ctx.getRequest() // 请求对象
const res: Response = ctx.getResponse() // 响应对象
const status = exception.getStatus() // http状态码
// 打印日志
this.logger.error(exception.message, exception.stack)
// 返回接口
res.status(status).json({
code: status,
timestamp: new Date().toDateString(),
path: req.url,
method: req.method,
msg: exception.message || HttpException.name,
})
}
}
在main.ts
中引入
ts
import { HttpExceptionFilter } from './filters/http-exception.filter'
async function bootstrap() {
const logger = WinstonModule.createLogger({ instance })
const app = await NestFactory.create(AppModule, { logger })
app.useGlobalFilters(new HttpExceptionFilter(logger))
}
拦截全局异常并输出到日志
创建http过滤器./filters/all-exception.filter.ts
ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, LoggerService } from '@nestjs/common'
import { HttpAdapterHost } from '@nestjs/core'
import { Request, Response } from 'express'
import * as requestIp from 'request-ip'
@Catch()
export class AllExceptionFilter implements ExceptionFilter {
constructor(
private logger: LoggerService,
private httpAdapterHost: HttpAdapterHost,
) {}
catch(exception: unknown, host: ArgumentsHost) {
const { httpAdapter } = this.httpAdapterHost
const ctx = host.switchToHttp()
const req: Request = ctx.getRequest() // 请求对象
const res: Response = ctx.getResponse() // 响应对象
// http状态码
const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR
const resBody = {
Headers: req.headers,
query: req.query,
body: req.body,
params: req.params,
timestamp: new Date().toISOString(),
ip: requestIp.getClientIp(req), // 需要安装`request-ip`插件
exception: exception['name'],
error: exception['response'] || 'Internal Server Error',
}
// 打印日志
this.logger.error('[toimic]', resBody)
httpAdapter.reply(res, resBody, status)
}
}
在main.ts
中引入
ts
import { AllExceptionFilter } from './filters/all-exception.filter'
async function bootstrap() {
...
const logger = WinstonModule.createLogger({ instance })
const app = await NestFactory.create(AppModule, { logger })
const httpAdapter = app.get(HttpAdapterHost)
app.useGlobalFilters(new AllExceptionFilter(logger, httpAdapter))
await app.listen(3000)
}
日志模块使用优化
利用nestcli生成一个logs模块:nest g mo logs
在.env
文件中中增肌配置
ini
# logs日志配置
LOG_ON=true
LOG_LEVEL=info
在src/enum/config.enum.ts
中增加配置
ts
/** log日志枚举 */
export enum LogEnum {
LOG_ON = 'LOG_ON',
LOG_LEVEL = 'LOG_LEVEL',
}
创建src/logs/logs.module.ts
文件
python
import { Module } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { WinstonModule, WinstonModuleOptions, utilities } from 'nest-winston'
import * as winston from 'winston'
import { Console } from 'winston/lib/winston/transports'
import { LogEnum } from 'src/enum/config.enum'
const DailyRotateFile = require('winston-daily-rotate-file')
@Module({
imports: [
WinstonModule.forRootAsync({
inject: [ConfigService],
useFactory(configService: ConfigService) {
const consoleTransport = new Console({
level: 'info', // 打印所有info、warn、error的信息
format: winston.format.combine(winston.format.timestamp(), utilities.format.nestLike()),
})
// 下面2个是会输出文件的
const dailyTransport = new DailyRotateFile({
level: 'warn', // 只打印warn、error信息
filename: 'application-%DATE%.log', // 文件命名
dirname: 'log', // 输出目录
datePattern: 'YYYY-MM-DD-HH',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(winston.format.timestamp(), winston.format.simple()),
})
const dailyInfoTransport = new DailyRotateFile({
level: configService.get(LogEnum.LOG_LEVEL), // 打印所有info、warn、error的信息
filename: 'info-%DATE%.log', // 文件命名
dirname: 'log', // 输出目录
datePattern: 'YYYY-MM-DD-HH',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(winston.format.timestamp(), winston.format.simple()),
})
return {
transports: [consoleTransport, ...(configService.get(LogEnum.LOG_ON) ? [dailyTransport, dailyInfoTransport] : [])],
} as WinstonModuleOptions
},
}),
],
})
export class LogsModule {}
在main.ts
中引入文件
ts
import { HttpAdapterHost, NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'
import { AllExceptionFilter } from './filters/all-exception.filter'
async function bootstrap() {
const app = await NestFactory.create(AppModule, {})
+ const logger = app.get(WINSTON_MODULE_NEST_PROVIDER)
+ app.useLogger(logger)
// 把异常写入日志文件
app.useGlobalFilters(new AllExceptionFilter(logger, app.get(HttpAdapterHost)))
await app.listen(3000)
}
bootstrap()
在app.module.ts
中引入模块
ts
import { Global, Logger, Module } from '@nestjs/common'
import { LogsModule } from './logs/logs.module'
@Global()
@Module({
imports: [
...
LogsModule
],
providers: [Logger],
exports: [Logger],
})
在src/user/user.module.ts
中使用日志模块
ts
import { Controller, Get, LoggerService } from '@nestjs/common'
+ import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'
@Controller('user')
export class UserController {
constructor(
private userService: UserService,
+ @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService,
) {
+ this.logger.log('UserController初始化')
}
}
typeorm cli数据库代码重构
提取typeorm配置
全局安装 ts-node:npm install ts-node --save-dev
在 package.json 中的 scripts 下添加 typeorm 命令
json
"script" {
...
"typeorm": "typeorm-ts-node-commonjs"
}
创建文件ormcomfig.ts
ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm'
import { Logs } from 'src/logs/logs.entity'
import { Roles } from 'src/roles/roles.entity'
import { Profile } from 'src/user/profile.entity'
import { User } from 'src/user/user.entity'
export default {
type: 'mysql',
host: '11.111.111.111',
port: 3306,
username: 'root',
password: 'you_mysql_password',
database: 'testdb',
// 定义数据库表结构与实体类字段同步(这里一旦数据库少了字段就会自动加入,根据需要来使用), 一般用于数据库初始化
synchronize: true,
// 扫描本项目中.entity.ts或者.entity.js的文件: [__dirname + '/**/*.entity{.ts,.js}']
entities: [User, Profile, Logs, Roles],
// logging: process.env.NODE_ENV === 'development' ? true : ['error'], // 开发环境打印所有日志
logging: false, // 开发环境打印所有日志
} as TypeOrmModuleOptions
修改app.module.ts
ts
@Module({
imports: [
...
+ TypeOrmModule.forRoot(ormcomfig),
],
})
TypeORM项目
初始化一个新的TypeORM项目: npx typeorm init --database mysql2
上面的步骤会生成一些文件
./src/data-source.ts
,./src/entity/User.ts
,'./src/index.ts'
参考data-source.ts
修改文件ormcomfig.ts
, 删除自动生成的data-source.ts
和User.ts
文件
ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm'
import { Logs } from './src/logs/logs.entity'
import { Roles } from './src/roles/roles.entity'
import { Profile } from './src/user/profile.entity'
import { User } from './src/user/user.entity'
import { DataSource, DataSourceOptions } from 'typeorm'
export const connectionParams = {
type: 'mysql',
host: '11.111.111.111',
port: 3306,
username: 'root',
password: 'you_mysql_password',
database: 'testdb',
// 定义数据库表结构与实体类字段同步(这里一旦数据库少了字段就会自动加入,根据需要来使用), 一般用于数据库初始化
synchronize: true,
// 扫描本项目中.entity.ts或者.entity.js的文件: [__dirname + '/**/*.entity{.ts,.js}']
entities: [User, Profile, Logs, Roles],
// logging: process.env.NODE_ENV === 'development' ? true : ['error'], // 开发环境打印所有日志
logging: false, // 开发环境打印所有日志
} as TypeOrmModuleOptions
export default new DataSource({
...connectionParams,
migrations: ['src/migrations/**'],
subscribers: [],
} as DataSourceOptions)
修改app.module.ts
ts
import { connectionParams } from '../ormcomfig'
@Module({
imports: [
...
+ TypeOrmModule.forRoot(connectionParams),
],
})
编辑自动生成./src/index.ts
测试, 运行npx ts-node .\src\index.ts
测试数据库后删除此文件
ts
import AppDataSource from '../ormcomfig'
import { User } from './user/user.entity'
AppDataSource.initialize()
.then(async () => {
const res = await AppDataSource.manager.find(User)
console.log('Here you can setup and run express / fastify / any other framework.', res)
})
.catch((error) => console.log(error))

执行
npm run start:dev
运行项目,成功
读取不同的环境变量
修改文件ormcomfig.ts
ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm'
import { DataSource, DataSourceOptions } from 'typeorm'
import * as fs from 'fs'
import * as dotenv from 'dotenv'
import { ConfigEnum } from './src/enum/config.enum'
// 通过环境变量读取不同的.env文件
function getEnv(env: string): Record<string, unknown> {
if (fs.existsSync(env)) {
return dotenv.parse(fs.readFileSync(env))
}
return {}
}
// 通过dotenv解析不同的配置
function buildConnectionOptions() {
const defaultConfig = getEnv('.env')
const envConfig = getEnv(`.env.${process.env.NODE_ENV || 'development'}`)
const config = { ...defaultConfig, envConfig } // 合并配置
const entitiesDir = process.env.NODE_ENV === 'development' ? [__dirname + '/**/*.entity.ts'] : [__dirname + '/**/*.entity{.ts,.js}']
return {
type: config[ConfigEnum.DB_TYPE],
host: config[ConfigEnum.DB_HOST],
port: config[ConfigEnum.DB_PORT],
username: config[ConfigEnum.DB_USERNAME],
password: config[ConfigEnum.DB_PASSWORD],
database: config[ConfigEnum.DB_DATABASE],
// 扫描本项目中.entity.ts或者.entity.js的文件: [__dirname + '/**/*.entity{.ts,.js}']
entities: entitiesDir,
// 定义数据库表结构与实体类字段同步(这里一旦数据库少了字段就会自动加入,根据需要来使用), 一般用于数据库初始化
synchronize: true,
// logging: process.env.NODE_ENV === 'development' ? true : ['error'], // 开发环境打印所有日志
logging: false, // 开发环境打印所有日志
} as TypeOrmModuleOptions
}
export const connectionParams = buildConnectionOptions()
export default new DataSource({
...connectionParams,
migrations: ['src/migrations/**'],
subscribers: [],
} as DataSourceOptions)
修改package.json
中的打包命令
json
"scripts": {
"build": "cross-env NODE_ENV=production nest build",
}
执行项目打包
npm run build
,我们发现打包目录变成了build
.解决方案是删除因为typeorm项目初始化生成的tsconfig.json
配置,恢复为最原始的默认配置
执行npm run start:prod
运行生产环境,又报错了:Error: Cannot find module 'D:\demo\movie-cms\01-nestjs-demo\dist\main'
.
解决方案, 修改package.json
中的命令:node dist/main
->node dist/src/main
json
"scripts": {
"start:prod": "cross-env NODE_ENV=production node dist/src/main",
}
现在存在一个问题, 无论怎么配置LOG_ON
环境变量都会生成log
日志文件夹, 编辑logs.module.ts
ts
import { Module } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { WinstonModule, WinstonModuleOptions, utilities } from 'nest-winston'
import * as winston from 'winston'
import { Console } from 'winston/lib/winston/transports'
import { LogEnum } from '../enum/config.enum'
const DailyRotateFile = require('winston-daily-rotate-file')
function createTransport(level: string, filename: string) {
return new DailyRotateFile({
level, // 只打印什么级别的信息, info、warn、error信息
filename: `${filename}-%DATE%.log`, // 文件命名
dirname: 'log', // 输出目录
datePattern: 'YYYY-MM-DD-HH',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(winston.format.timestamp(), winston.format.simple()),
})
}
@Module({
imports: [
WinstonModule.forRootAsync({
inject: [ConfigService],
useFactory(configService: ConfigService) {
const consoleTransport = new Console({
level: 'info', // 打印所有info、warn、error的信息
format: winston.format.combine(winston.format.timestamp(), utilities.format.nestLike()),
})
const LOG_ON = configService.get(LogEnum.LOG_ON) === 'true'
const writeTransport = LOG_ON ? [createTransport('info', 'application'), createTransport('warn', 'error')] : []
return {
transports: [consoleTransport, ...writeTransport],
} as WinstonModuleOptions
},
}),
],
})
export class LogsModule {}
添加命令
在package.json
中添加命令, 以后生产中用于数据库迁移
json
"scripts": {
"typeorm": "typeorm-ts-node-commonjs -d ormconfig.ts",
"migration:typeorm": "f(){npm run typeorm migration:generate -p \"./src/migrations/$@\";};f",
"migration:run": "npm run typeorm migration:run",
"migration:revert": "npm run typeorm migration:revert",
"migration:drop": "npm run typeorm schema:drop"
}