日志功能扩展:优化日志收集与筛选
-
在项目打包上线之后,我们不可避免地需要了解服务器的运行状态以及服务器上的一些错误日志等信息
-
这些错误日志等信息本身存放在服务器里,以文件形式存放
-
在
src/common/logger
里面有一个叫logs.module.ts
,import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { WinstonModule } from 'nest-winston'; import { consoleTransports, createRotateTransport, } from './createRotateTransport'; @Module({ imports: [ WinstonModule.forRootAsync({ inject: [ConfigService], useFactory: (configService: ConfigService) => { const logOn = configService.get('LOG_ON') === 'true'; return { transports: [ consoleTransports, ...(logOn ? [ createRotateTransport('info', 'application'), createRotateTransport('warn', 'error'), ] : []), ], }; }, }), ], }) export class LogsModule {}
-
在这个模块里我们使用到了一个模块,叫
WisdomModule
,Wisdom
是一个日志模块,当我们开启本地的日志标签后LOG_ON
,它会把这些日志以文件形式存放到本地目录中。 -
具体会存放到
logs
目录下, 可参考createRotateTransport.ts
的文件里,并且按照日期的形式进行滚动存储import DailyRotateFile from 'winston-daily-rotate-file'; import { format } from 'winston'; import { Console } from 'winston/lib/winston/transports'; import { utilities } from 'nest-winston'; export const consoleTransports = new Console({ level: 'info', format: format.combine( format.timestamp(), format.ms(), utilities.format.nestLike('Winston'), ), }); export function createRotateTransport(level: string, fileName: string) { return new DailyRotateFile({ level, dirname: 'logs', filename: `${fileName}-%DATE%.log`, datePattern: 'YYYY-MM-DD-HH', zippedArchive: true, maxSize: '20m', maxFiles: '14d', format: format.combine(format.timestamp(), format.simple()), }); }
-
这里可能会有几个问题:
-
- 占用系统 IO:写磁盘会占用系统的 IO。
-
- 数据处理不便:这些数据不方便进行下载、浏览和筛选。
-
- 占用系统空间:数据量大了之后,会占用系统空间,需要定期进行清理
-
- 日志分散难汇总:
- 后续当服务端项目变得复杂,日志可能会变得分散,不方便浏览汇总
- 例如,服务端项目或某个服务进行多节点部署并做负载均衡时,既有服务端的日志,也可能有
nginx
上面的访问日志 - 在出现节点问题或功能性问题时,需要快速汇集这些日志进行查看、筛选和排错定位问题
- 线上系统运行时,要快速修复功能并回稳,还需通过日志收集来论证系统功能是否修复,这就需要快速响应并方便地汇总这些日志
-
-
到最后,这其实涉及到分布式的日志系统
- 分布式的日志系统有很多,如
ELK Stack
、Graylog
、Fluentd
、Prometheus
- 还有大家熟知的错误收集系统
Sentry
等,这些都是用于日志收集或系统监控的工具 - 我们把这个层级的扩展放到后面,现在重点看看前面这些问题如何优化
- 分布式的日志系统有很多,如
-
问题及优化方案分析
-
- 占用系统磁盘 IO:
- 无论哪种日志系统,要么往本地磁盘写,要么往数据库或其他服务器的文件写,都会占用磁盘 IO 或网络带宽。
- 对于单服务器、单
NestJS
的简单业务系统,如简单的小程序,使用文档类型的日志系统没问题,Wisdom
就提供了类似方案,它支持通过HTTP Transport
的方式将日志文件发送到远端的静态资源服务器,还支持一些简单的权限验证 - 但这种方式会占用一定的系统网络带宽,且发送日志频率不能太高,同时也无法解决多服务日志的筛选汇总问题
-
- 日志筛选汇总难题
- 为了解决这个问题,我们介绍
MongoDB Transport
方式,即往本地或其他网络互通且网络情况较好的数据库服务器写日志数据 - 选择往其他机器的数据库写日志,一是因为本地局域网网络没有带宽限制,网速有保障;二是不会占用业务系统本身的系统 IO;此外,数据库兼顾了筛选和存储功能,可以存储多个服务的日志
-
-
接下来,我们按照
MongoDB Transport
部分进行实操,看看常见的选项以及如何配置,同时修改原先的项目,为用户提供更多选择,比如将Wisdom Transport
配置到对应的数据库上,以解决日志数据集中和筛选的问题
Winston - MongoDB 日志数据库存储方案:配置、测试与优化全解析
- 看一下这个
winston - MongoDB
是如何进行配置的 winston-mongodb - 使用的是
winston 3.x
版本,对应需要集成的是2.0.6
版本的winston-mongodb
1 ) 配置动机与应用
这边我们先介绍一下它的动机(motivation),主要是为了扩展 winston
的日志功能,让其实现解耦,使 winston
专注于日志记录,而 winston
的插件则用于实现其他扩展功能
-
下面是一个具体应用示例
const winston = require('winston'); // Requiring `winston-mongodb` will expose winston.transports.MongoDB` require('winston-mongodb'); const log = winston.createLogger({ level: 'info', transports: [ // write errors to console too new winston.transports.Console({format: winston.format.simple(), level:'error'}) ], }); // logging to console so far log.info('Connecting to database...'); const MongoClient = require('mongodb').MongoClient; const url = "mongodb://localhost:27017/mydb"; const client = new MongoClient(url); await client.connect(); const transportOptions = { db: await Promise.resolve(client), collection: 'log' }; log.add(new winston.transports.MongoDB(transportOptions)); // following entry should appear in log collection and will contain // metadata JSON-property containing url field log.info('Connected to database.',{url});
-
这里 创建了一个
transport
new winston.transports.Console({format: winston.format.simple(), level:'error'})
- 在这个
transport
中,不仅可以在控制台(console)打印日志
-
还创建了另一个
transport
log.add(new winston.transports.MongoDB(transportOptions));
- 调用了其中名为
MongoDB
的方法,并引用了上面的transportOptions
- 这个
options
包含一个MongoDB
客户端,支持众多选项,如下
-
db
:这是一个MongoDB
的连接字符串,包含数据库名称、连接的用户名和密码等信息。不过,这些设置并非都必须配置。 -
其他
MongoDB
配置选项:如连接池、是否连接、是否使用新的 URL 转换工具(newUrlParser
)等,默认设置通常即可。若用户有定制需求,可在模块中传递相应参数。
2 ) 依赖安装
- 回到项目, 打开终端工具,使用
pnpm i winston-mongodb
安装依赖包,我的版本是5.1.1
- 模块配置
-
安装完成后,我们开始配置对应的模块。之前我们在某个地方添加了一个
LOG_ON
标志,写在env
配置文件中 -
同样,我们也可以将与日志和
MongoDB
相关的配置写在这里。常见选项如下:#是否开启日志 LOG_ON=true # file & mongo LOG_TYPE=mongo LOG_DB=mongodb: //root: example@localhost:27017/nest-logs LOG_STOREHOST=false LOG_COLLECTION=log LOG_LEVEL=error LOG_CAPPED=true LOG_CAPPED_SIZE=10000000 LOG_CAPPED_MAX=1000
-
LOG_CAPPED_SIZE
:用于设置滚动连接(connections)的大小,默认值是一千万字节(bytes),约为九点几兆。 -
LOG_TYPE
:默认有两种类型,file
和mongo
,我们将其设为mongo
以作区分。 -
LOG_DB
:这是MongoDB
的连接地址,我们复制之前使用过的MongoDB
地址,例如将数据库名改为messagesLogs
,这里的用户和数据库都需要重新创建。 -
其他选项
- 如
LOG_COLLECTION
(默认连接是否为log
) LOG_LEVEL
(默认是info
)LOG_CAPPED
(默认是false
,可设为true
)LOG_CAPPED_MAX
(最大连接数量,无默认值,可自行设置)LOG_STOREHOST
(将机器的主机名存入MongoDB
中,默认是false
,可调整位置)日志数据会多出来一条 hostname 的属性
- 如
配置完成这些选项后,我们就可以在 logs.modules.ts
中使用了
4 ) 创建 Transport 方法
- 在
createRotateTransport.ts
中创建一个createMongoTransport
方法:
import { MongoDB, MongoDBConnectionOptions } from 'winston-mongodb';
export function createMongoTransport(options: MongoDBConnectionOptions){
return new MongoDB(options);
}
- 同时,我们需要从配置文件中读取默认属性项并传递给它, 下面改造 log.module.ts
5 ) 定制 log.module.ts
- 在
logModules
中,我们根据lockType
的值选择不同的transport
:
@Module({
imports: [
WinstonModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const logOn = configService.get('LOG_ON') === 'true';
const logType = configService.get('LOG_TYPE');
let transportArr = [];
if (logOn) {
if (logType === 'mongo') {
const defaultOptions = {
// 从配置文件中读取的一系列与 MongoTransport 相关的配置
db: configService.get('LOG_DB'),
collection: configService.get('LOG_COLLECTION'),
level: configService.get('LOG_LEVEL'),
capped: configService.get('LOG_CAPPED')=='true',
cappedSize: parseInt(configService.get('LOG_CAPPED_SIZE', '10000000'), 10),
cappedMax: parseInt(configService.get('LOG_CAPPED_MAX', '10000'), 10),
storeHost: configService.get('LOG_STOREHOST')=='true',
options: {
useUnifiedTopology: true,
poolSize: 2,
// autoReconnect: true, // 与 上面 useUnifiedTopology 不兼容
useNewUrlParser: true,
},
};
transportArr = [createMongoTransport(defaultOptions)];
} else if (logType === 'file') {
// 处理 file 类型的 transport
transportArr = [
createRotateTransport('info', 'application'),
createRotateTransport('warn', 'error'),
]
}
}
transportArr.push(consoleTransports);
return {
transports: transportArr
};
},
}),
],
})
6 ) 测试与问题解决
-
创建数据库和用户:创建一个
NestJS
的MongoDB
数据库,并创建一个MongoDB
用户,设置其权限。可以使用命令行,如docker exec -it <容器名> mongo -u root -p
连接数据库docker exec -it nestjs-starter-mongo-1 mongo -u root -p
use nest-logs
- 创建用户
db.createUser({ user:"root", pwd:"example", roles: [ { role:"dbOwner", db:"nest-logs" } ] });
- 输出
Successfully added user: { "user":"root", "roles": [ { "role": "dbOwner", "db":"nest-logs" } ] }
-
测试配置:启动进程后可能会遇到各种问题,如认证失败、日志无数据等。我们需要逐步排查,例如检查密码拼写、添加或调整
MongoDB
客户端的额外选项(如autoReconnect
等)、确保logOn
在环境配置文件中设置为true
等。 -
生成日志数据:在登录或注册模块中使用
logger
记录日志,发起请求并检查log
中是否有数据。若没有数据,可继续调整配置,直到日志正常记录到MongoDB
中。
7 ) 日志筛选与优化
- 我们可以通过设置
level
属性来筛选日志,例如将level
设为error
,这样只有错误级别的日志会被记录 - 同时,设置
storeHost
为true
后,每条日志会多一个hostname
属性,方便在服务器端区分不同服务器产生的错误 - 通过以上步骤,我们完成了
MongoDB
配合winston
进行日志收集的功能,这种配置方式解决了磁盘 IO 占用、数据库下载筛选不便以及日志配置分散等问题
数据库日志存储与维护机制:问题、解决方案及实践探索
1 ) Wisdom与Mongo集成存在的问题
- 之前一节我们完成了Wisdom与Mongo级别的集成
- 但细想之下,仍存在一些不便之处:
-
- 日志设置局限性:存储的日志只能通过日志级别进行设置。
-
- 错误日志筛选不便:
- 对于错误日志,难以快速筛选,需要了解MongoDB的查询语句
- 在数据库操作界面进行查询时,即便能通过如
level
等条件查询,但时间节点可能不一致,还需补充查询条件
-
2 ) MongoDB数据库日志存储问题及应对
- 日志数据存储压力
- MongoDB数据库中若存放大量英文数据,可能会很快写爆日志服务器
- 因此,对于日志服务器及其存储,需要加入清理操作
- 定时清理任务
- 清理操作并非每天执行,需考虑定时任务
capped
、cappedSize
、cappedMax
选项的作用是滚动日志的属性项- 若将
capped
设置位true
后,会尝试创建新的 log cappedSize
会记录所有日志的大小cappedMax
是设置日志大小
- 若将
3 )日志存储测试与分析
-
测试过程
- 将
capped
设置为 10,cappedSize
设置为一千,LOG_LEVEL
设置为info
,删除数据库连接,重启项目,发现日志只有一条数据 - 原因是在
cappedMax
配置中设置了日志大小,达到上限便不再产生数据 - 增大该值后再次测试,删除
logconnection
,重启调试进程,发现日志数据不会超过十条,保存操作会使数据滚动
- 将
-
属性设置建议:建议保持这三个属性的默认值,即
capped
设为false
,不创建新的connection
;cappedSize
约为9.3兆的日志空间;cappedMax
不设置,可无限记录文件数量。查看源码可知,未设置时这三个值为空
4 ) 合理的日志存储方案
- 不同业务场景的选择
- 根据业务场景选择存储方案, 小型业务系统可使用文档或数据库存储
- 文档方案检索不便,数据库方案便于多台机器部署的服务端应用进行日志汇总、筛选和查询,集中式数据库存储方案较为理想
- 其他存储方案及扩展操作
- Wisdom还提供了如HTTP的方案,但仍离不开文档方式
- 使用数据库后,可创建简单界面进行数据查询和筛选
- 更进一步,需对MongoDB数据库进行滚动或备份操作,可编写服务器定时维护脚本,按时间(如月、三个月、七天等)滚动备份日志数据,此思路与配置Wisdom的
DailyRotateFile
类似