通过前面的章节,我们已经实现了一个相对完善的后端接口框架,也通过这个框架创建了一个简单的个人博客。
接下来,我们给接口框架添加 日志系统
。
日志
基本是每个后端框架都有的功能,用于记录系统发生的,包括请求、异常、错误、参数等内容。
这些内容全部存储到数据库,会造成数据库的大小剧集增加,而且大部分日志都是临时记录,主要用来做偶尔的问题追查。
所以,一般把这些记录存储到文件里,按照日志的时间和大小进行分割,避免日志文件过大,不方便查看。
经过对比,winston
比较符合我们的要求,而且更新也相对比较勤快。
D:\codes\chensuiyi\nodejs-full-stack>pnpm add winston-daily-rotate-file winston
Packages: +32
++++++++++++++++++++++++++++++++
Progress: resolved 93, reused 92, downloaded 0, added 32, done
dependencies:
+ winston 3.17.0
+ winston-daily-rotate-file 5.0.0
Done in 3.5s
以上已经安装了 winston
和 winston-daily-rotate-file
两个依赖,分别用来记录日志和分割日志。
import winston from 'winston';
const logger = winston.createLogger({
level: 'info', // 日志级别
format: winston.format.json(), // 以什么格式输出
defaultMeta: {},
transports: [
//
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/info.log' })
],
exitOnError: false, // 发生异常不要退出
silent: false // 正常打印日志
});
// 如果不是生产环境,则直接在控制台显示日志
if (process.env.NODE_ENV !== 'production') {
logger.add(
new winston.transports.Console({
format: winston.format.simple()
})
);
}
以上是一个 winston
日志的初始化,主要关注 format
和 transports
两个属性,一个是日志的格式,一个是用什么来存储日志。
我们先集成到我们的项目中试试效果。
// /logger.js 文件
import winston from 'winston';
const logger = winston.createLogger({
level: 'info', // 日志级别
format: winston.format.json(), // 以什么格式输出
defaultMeta: {
// 默认添加到日志的数据
service: 'user-service'
},
transports: [
//
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/info.log' })
],
exitOnError: false, // 发生异常不要退出
silent: false // 正常打印日志
});
// 如果不是生产环境,则直接在控制台显示日志
if (process.env.NODE_ENV !== 'production') {
logger.add(
new winston.transports.Console({
format: winston.format.simple()
})
);
}
export { logger };
在项目根目录创建一个 logger.js
文件,内容如上,最后导出 logger
方法。
// /server.js 文件
import { createServer } from 'node:http';
import { readFileSync, existsSync } from 'node:fs';
import { createVerifier } from 'fast-jwt';
import { lookup } from 'mime-types';
import { apiMaps } from './routes.js';
import { config } from './config.js';
import { logger } from './logger.js'; // 看这里
const verifySync = createVerifier({ key: config.jwt.key });
const server = createServer(async (req, res) => {
req.session = {};
if (req.url.startsWith('/api/') === true) {
res.setHeader('Content-Type', 'application/json');
if (['/api/admin/login', '/api/admin/register'].includes(req.url) === false) {
try {
req.session = verifySync(req.headers.authorization?.split(' ')?.[1] || 'none');
} catch (err) {
res.end(
JSON.stringify({
code: 1,
msg: 'token 错误或已过期'
})
);
return;
}
}
const apiName = req.url.replace('/api/', '');
if (apiName === 'tool/upload') {
// 文件上传
const result = await apiMaps[apiName].default(req);
res.end(JSON.stringify(result));
} else {
if (apiMaps[apiName]) {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', async () => {
try {
req.body = JSON.parse(body);
logger.info(JSON.stringify({ url: req.url, session: req.session, body: req.body })); // 看这里
const result = await apiMaps[apiName].default(req);
res.end(JSON.stringify(result));
} catch (err) {
console.log('🚀 ~ req.on ~ err:', err);
res.end(
JSON.stringify({
code: 1,
msg: '请求参数结构有误'
})
);
}
});
} else {
res.end(
JSON.stringify({
code: 1,
msg: '接口不存在'
})
);
}
}
} else {
// 如果是资源
let staticFile = req.url.split('?')?.[0] || '';
if (staticFile === '/') {
staticFile = '/index.html';
}
if (existsSync(`./public/${staticFile}`) === true) {
res.setHeader('Content-Type', lookup(staticFile));
res.end(readFileSync(`./public/${staticFile}`));
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
}
}
});
server.listen(3000, '127.0.0.1', () => {
console.log('服务已启动,监听端口为:3000');
});
然后编辑 server.js
文件,请看以上注释为 看这里
的两行,分别是导入 logger
函数和记录当前请求的路径、当前请求的用户和请求的参数。
启动项目,登录,添加文章,修改文章,所有的请求日志都在控制台可以看到了,这样我们就能知道,什么时间,什么人,请求了什么接口,发送了什么参数。
同时,在 /logs/info.log
文件中,就记录了我们所有的请求信息,当我们需要追踪某个问题的时候,就能从日志中去查找蛛丝马迹了。
光有 请求日志
肯定是不足的,当我们的接口处理逻辑出现错误的时候,也需要记录 错误日志
。
// /apis/article/insert.js 文件
import { mysqlPool } from '../../mysql.js';
import { ajvValid } from '../../utils.js';
import { logger } from '../../logger.js'; // 看这里
export const apiSchema = {
// 元数据
meta: {
tags: ['文章'],
summary: '添加文章'
},
// 请求协议
request: {
type: 'object',
properties: {
title: {
type: 'string',
title: '用户名',
minLength: 1,
maxLength: 100
},
content: {
type: 'string',
title: '密码',
minLength: 1,
maxLength: 50000
}
},
required: ['title', 'content'],
additionalProperties: false
},
// 返回协议
response: {
type: 'object',
properties: {
token: {
type: 'string',
description: '登录令牌'
}
}
}
};
export default async (req) => {
try {
await ajvValid(apiSchema.request, req.body);
// ------------------------------------------------------
// 从连接池中获取一个数据库连接
const db = await mysqlPool.getConnection();
// 查询数据库是否有对应的用户数据
const [result] = await db.query({
sql: 'INSERT INTO `article2` (`title`,`content`,`author`,`created_at`) VALUES (:title,:content,:author,:created_at)',
values: {
title: req.body.title,
content: req.body.content,
author: Number(req.session.id),
created_at: Date.now()
}
});
// 释放数据库连接
db.release();
// 返回成功信息
return {
code: 0,
msg: '添加文章成功',
data: {
insertId: result.insertId
}
};
} catch (err) {
logger.error(JSON.stringify(err)); // 看这里
return {
code: 1,
msg: '未知错误'
};
}
};
上面是添加文章的接口,同样引入日志函数,在 catch
分支里,通过 logger
打印错误日志。
请注意,我们的 sql
语句中,添加的表名称从 article
改成了 article2
,这个表名是不存在的。
现在我们再看控制台打印的报错信息。
可以看到,非常清晰地打印了,article2
表不存在的日志。
此时再看我们的 /logs/error.log
文件,错误日志信息也被永久记录在日志文件中,再也不怕找不到错误原因了。
这里,我们的 错误日志
跟 普通日志
是分开的,就是为了方便我们查看报错信息。
前面我们说过,日志需要安装时间和大小进行分割,不然全部日志都放到一个文件,日积月累,那日志就大了去了,很不方便查看。
所以,接下来,我们使用 winston-daily-rotate-file
来进行日志分割。
// /logger.js 文件
import winston from 'winston';
import 'winston-daily-rotate-file';
const transportConfig = {
dirname: 'logs', // 日志目录
datePattern: 'YYYY-MM-DD', // 日志文件名
maxSize: '1k', // 最大大小
maxFiles: '30d' // 最大有效期为30天
};
const logger = winston.createLogger({
level: 'info', // 日志级别
format: winston.format.json(), // 以什么格式输出
defaultMeta: {},
transports: [
//
new winston.transports.DailyRotateFile({
...transportConfig,
filename: 'error-%DATE%.log',
level: 'error'
}),
new winston.transports.DailyRotateFile({
...transportConfig,
filename: 'info-%DATE%.log',
level: 'info'
})
],
exitOnError: false, // 发生异常不要退出
silent: false // 正常打印日志
});
// 如果不是生产环境,则直接在控制台显示日志
if (process.env.NODE_ENV !== 'production') {
logger.add(
new winston.transports.Console({
format: winston.format.simple()
})
);
}
export { logger };
调整后的日志创建代码如上,我们用 winston.transports.DailyRotateFile
来替代之前的 winston.transports.File
。
同时,transportConfig
变量也做了相应的注释,日志文件最大为 1kb
大小,保存时间最久为 30天
。
接下来,我们不断发起请求,可以看到,日志就被以每个文件最大为 1kb
进行分割,保存在多个文件中。
这样就可以方便我们根据时间来查看日志,也不用一次性下载几十上百兆的文件来定位问题。
那么本文,或者说本小册的主要目标,就是让我们了解到一个后端框架可能会涉及到的方方面面。
通过自己动手实现一个后端接口框架,一个前后端对接了数据的博客系统,来体会全栈开发的感觉。
后续呢,大家可以关注我打磨了几年的 Node.js 后端接口开发框架 funpi
,可以用它来开发实际生产中的项目。
开源地址:https://github.com/chenbimo/funpi
所有购买本小册的小伙伴,均可直接在本小册交流群无偿咨询,交流关于 funpi
框架的使用问题。