nestjs日志
目前方案:
- 内置日志模块(平常开发用)
pino
(简单)winston
(用于生产)
日志等级
- Log: 通用日志,按需记录
- Warning: 警告日志,如多次对数据库进行操作
- Error: 严重日志,如数据库异常
- Debug: 调试日志,如加载数据库日志
- Verbose: 详细日志,所有操作和详细信息,非必要不打印
功能分类
- 错误日志:方便定位问题,给用户友好提示
- 调试日志,方便开发
- 请求日志,记录敏感行为
日志记录位置
- 控制台日志:方便调试用
- 文件日志:方便回溯和追踪(24消失滚动)
- 数据库日志:敏感操作、敏感数据
内置日志
在全局配置main.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
中打印日志
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用法用
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
中配置
@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模块
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
中注入全局服务
import { Logger, } from '@nestjs/common'
@Global()
@Module({
providers: [Logger],
exports: [Logger],
})
在控制器中使用
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
中修改代码
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
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
中引入
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
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)
// 返回接口
const resData = {
code: status, msg: resBody.error,
// timestamp: new Date().toDateString(),
}
httpAdapter.reply(response, resData, status)
}
}
在main.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
文件中中增肌配置
# logs日志配置
LOG_ON=true
LOG_LEVEL=info
在src/enum/config.enum.ts
中增加配置
/** log日志枚举 */
export enum LogEnum {
LOG_ON = 'LOG_ON',
LOG_LEVEL = 'LOG_LEVEL',
}
创建src/logs/logs.module.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 '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
中引入文件
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
中引入模块
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
中使用日志模块
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 命令
"script" {
...
"typeorm": "typeorm-ts-node-commonjs"
}
创建文件ormconfig.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
@Module({
imports: [
...
+ TypeOrmModule.forRoot(ormconfig),
],
})
TypeORM项目
初始化一个新的TypeORM项目: npx typeorm init --database mysql2
上面的步骤会生成一些文件
./src/data-source.ts
,./src/entity/User.ts
,'./src/index.ts'
参考data-source.ts
修改文件ormconfig.ts
, 删除自动生成的data-source.ts
和User.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
import { connectionParams } from '../ormconfig'
@Module({
imports: [
...
+ TypeOrmModule.forRoot(connectionParams),
],
})
编辑自动生成./src/index.ts
测试, 运行npx ts-node .\src\index.ts
测试数据库后删除此文件
import AppDataSource from '../ormconfig'
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
运行项目,成功
读取不同的环境变量
修改文件ormconfig.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
中的打包命令
"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
"scripts": {
"start:prod": "cross-env NODE_ENV=production node dist/src/main",
}
现在存在一个问题, 无论怎么配置LOG_ON
环境变量都会生成log
日志文件夹, 编辑logs.module.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
中添加命令, 以后生产中用于数据库迁移
"scripts": {
"generate:models": "typeorm-model-generator -h 11.111.111.111 -p 3306 -d mysqlDbName -u root -x mysqlPassword -e mysql -o ./src/entites",
"typeorm": "typeorm-ts-node-commonjs -d ormconfig.ts",
"migration:generate": "f() { npm run typeorm migration:generate -p \"./src/migrations/$@\";}; f",
"migration:create": "typeorm-ts-node-commonjs migration:create",
"migration:run": "npm run typeorm migration:run",
"migration:revert": "npm run typeorm migration:revert",
"migration:drop": "npm run typeorm schema:drop"
}
数据库结构Entity变化迁移步骤
以下在执行命令时未设置环境变量,在实际过程中请设置正确环境变量,否则修改的数据库地址可能不对
第一步,执行migration:create
命令, 记录当前的版本
# 会创建一个文件`src/migrations/init/timestamp-init.ts`
$ npm run migration:create src/migrations/init
第二步,按需新建和修改Entity文件
创建文件src/menus/menus.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
enum TypeEnum {MENU = 'menu', PAGE = 'page', AUTH = 'auth',}
@Entity()
export class Menus {
@PrimaryGeneratedColumn()
id: number
@Column({ comment: '名称' })
name: string
@Column({ comment: '路径', default: '' })
path: string // 路径
@Column({ comment: '排序' })
order: number // 排序
@Column({ comment: '图标,"menu", "page"有图标,"auth"没有图标', default: '' })
icon: string // 图标
@Column({ comment: '菜单类型', type: 'enum', enum: TypeEnum, default: TypeEnum.MENU })
type: TypeEnum
@Column({ comment: '父级id', default: 0 })
pid: number // 父级id
@Column({ comment: '是否在菜单栏显示', default: true })
inMenu: boolean // 是否在菜单栏显示
}
修改文件src/roles/roles.entity.ts
import { User } from '../user/user.entity'
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'
@Entity()
export class Roles {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@Column({ type: 'longtext' })
auths: string // 保存menu的id的数组
@ManyToMany(() => User, (user) => user.roles)
users: User[]
}
在ormconfig.ts
配置文件中引入刚刚创建的src/menus/menus.entity.ts
+ import { Menus } from './src/menus/menus.entity'
...
+ entities: [User, Logs, Profile, Roles, Menus],
...
第三步,执行migration:generate
命令, 记录当前的版本对数据库的操作命令
会创建一个文件
src/migrations/init/timestamp-menus.ts
$ npm run typeorm migration:generate -p "./src/migrations/menus"
第四步,修改真实的数据库结构
当前表列表和角色表
执行命令对真实数据库进迁移:
npm run typeorm migration:run
, 再次查看表结构
如果线上数据库出现异常情况,执行命令退回上一个版本
# 执行完这条命令后,线上数据库又回到了之前未修改的状态
# 这条命令只能回退一个版本,要回退多个版本需要执行多次
$ npm run typeorm migration:revert