一、安装
npm i winston
npm i winston-mysql
二、 配置 winston
2.1、封装
const config = require(__dirname + ‘/…/config/config.json’)[env];
- 先判断当前是什么环境,如果
.env
中没有配置,就是开发环境。- 接着去
config/config.json
中读取对应的配置。- 取到值后,填充到
options
中
const {createLogger, format, transports} = require('winston');
const MySQLTransport = require('winston-mysql');
// 读取 config/config.json 数据库配置文件
// 根据环境变量 NODE_ENV 来选择对应数据库配置
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const options = {
host: config.host,
user: config.username,
password: config.password,
database: config.database,
table: 'Logs'
};
const logger = createLogger({
// 日志级别,只输出 info 及以上级别的日志
level: 'info',
// 日志格式为 JSON
format: format.combine(
format.errors({stack: true}), // 添加错误堆栈信息
format.json()
),
// 添加元数据,这里添加了服务名称
defaultMeta: {service: 'xw-api'},
// 日志输出位置
transports: [
// 将 error 或更高级别的错误写入 error.log 文件
new transports.File({filename: 'error.log', level: 'error'}),
// 将 info 或更高级别的日志写入 combined.log 文件
new transports.File({filename: 'combined.log'}),
// 添加 MySQL 传输,将日志存储到数据库
new MySQLTransport(options)
]
});
// 在非生产环境下,将日志输出到控制台
if (process.env.NODE_ENV !== 'production') {
logger.add(new transports.Console({
format: format.combine(
format.colorize(), // 终端中输出彩色的日志信息
format.simple()
)
}));
}
module.exports = logger;
config文件
2.2、测试
const logger = require('../utils/logger');
/**
* 查询首页数据
* GET /
*/
router.get('/', async function (req, res, next) {
try {
logger.info('这是一个 info 信息');
logger.warn('这是一个 warn 信息');
logger.error('这是一个 error 信息');
// ...
} catch (error) {
failure(res, error);
}
});
三、修改封装的responses.js
const createError = require('http-errors');
const multer = require('multer');
const logger = require("./logger");
/**
* 请求成功
* @param res
* @param message
* @param data
* @param code
*/
function success(res, message, data = {}, code = 200) {
res.status(code).json({
status: true,
message,
data,
code
});
}
/**
* 请求失败
* @param res
* @param error
*/
function failure(res, error) {
// 初始化状态码和错误信息
let statusCode;
let errors;
if (error.name === 'SequelizeValidationError') { // Sequelize 验证错误
statusCode = 400;
errors = error.errors.map(e => e.message);
} else if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') { // Token 验证错误
statusCode = 401;
errors = '您提交的 token 错误或已过期。';
} else if (error instanceof createError.HttpError) { // http-errors 库创建的错误
statusCode = error.status;
errors = error.message;
} else if (error instanceof multer.MulterError) { // multer 上传错误
if (error.code === 'LIMIT_FILE_SIZE') {
statusCode = 413;
errors = '文件大小超出限制。';
} else {
statusCode = 400;
errors = error.message;
}
} else { // 其他未知错误
statusCode = 500;
errors = '服务器错误。';
logger.error('服务器错误:', error);
}
res.status(statusCode).json({
status: false,
message: `请求失败: ${error.name}`,
errors: Array.isArray(errors) ? errors : [errors]
});
}
module.exports = {
success,
failure
}
四、封装日志接口
4.1、创建Log表
sequelize model:generate --name Log --attributes level:string,message:string,meta:string,timestamp:date
4.2、修改迁移
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Logs', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER.UNSIGNED
},
level: {
allowNull: false,
type: Sequelize.STRING(16)
},
message: {
allowNull: false,
type: Sequelize.STRING(2048)
},
meta: {
allowNull: false,
type: Sequelize.STRING(2048)
},
timestamp: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Logs');
}
};
4.3、运行迁移
sequelize db:migrate
4.4、修改模型
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Log extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
Log.init({
level: DataTypes.STRING,
message: DataTypes.STRING,
meta: {
type: DataTypes.STRING,
get() {
return JSON.parse(this.getDataValue("meta"));
}
},
timestamp: DataTypes.DATE,
}, {
sequelize,
modelName: 'Log',
timestamps: false, // 没有 createdAt 与 updatedAt
});
return Log;
};
meta
里存储的是详细的错误信息。在读取的时候,需要使用JSON.parse转回json
格式。- 加上了
timestamps: false
。因为已经有专门的timestamp
字段记录时间了,所以并不需要createdAt
和updatedAt
。
4.5、 接口封装
const express = require('express');
const router = express.Router();
const { Log } = require('../../models');
const { NotFound } = require('http-errors');
const { success, failure } = require('../../utils/responses');
/**
* 查询日志列表
* GET /admin/logs
*/
router.get('/', async function (req, res) {
try {
const logs = await Log.findAll({
order: [['id', 'DESC']],
});
success(res, '查询日志列表成功。', { logs: logs });
} catch (error) {
failure(res, error);
}
});
/**
* 查询日志详情
* GET /admin/logs/:id
*/
router.get('/:id', async function (req, res) {
try {
const log = await getLog(req);
success(res, '查询日志成功。', { log });
} catch (error) {
failure(res, error);
}
});
/**
* 清空全部日志
* DELETE /admin/logs/clear
*/
router.delete('/clear', async function (req, res) {
try {
await Log.destroy({ truncate: true });
success(res, '清空日志成功。');
} catch (error) {
failure(res, error);
}
});
/**
* 删除日志
* DELETE /admin/logs/:id
*/
router.delete('/:id', async function (req, res) {
try {
const log = await getLog(req);
await log.destroy();
success(res, '删除日志成功。');
} catch (error) {
failure(res, error);
}
});
/**
* 公共方法:查询当前日志
*/
async function getLog(req) {
const { id } = req.params;
const log = await Log.findByPk(id);
if (!log) {
throw new NotFound(`ID: ${id}的日志未找到。`)
}
return log;
}
module.exports = router;
- 在清空日志里,我用了
truncate: true
,它的意思是截断表
。作用是将表中的数据清空后,自增的id恢复从1开始
。 - 还有要注意,
清空全部日志路由,必须在删除日志路由的上面
。不然就会先匹配到/:id,导致无法匹配到/clear的
4.6、app.js引入 参考自己的项目
const adminLogsRouter = require('./routes/admin/logs');
app.use('/admin/logs', adminAuth, adminLogsRouter);