Nest框架: 日志功能之收集,筛选,存储,维护

日志功能扩展:优化日志收集与筛选

  • 在项目打包上线之后,我们不可避免地需要了解服务器的运行状态以及服务器上的一些错误日志等信息

  • 这些错误日志等信息本身存放在服务器里,以文件形式存放

  • 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()),
      });
    }
    
  • 这里可能会有几个问题:

      1. 占用系统 IO:写磁盘会占用系统的 IO。
      1. 数据处理不便:这些数据不方便进行下载、浏览和筛选。
      1. 占用系统空间:数据量大了之后,会占用系统空间,需要定期进行清理
      1. 日志分散难汇总:
      • 后续当服务端项目变得复杂,日志可能会变得分散,不方便浏览汇总
      • 例如,服务端项目或某个服务进行多节点部署并做负载均衡时,既有服务端的日志,也可能有 nginx 上面的访问日志
      • 在出现节点问题或功能性问题时,需要快速汇集这些日志进行查看、筛选和排错定位问题
      • 线上系统运行时,要快速修复功能并回稳,还需通过日志收集来论证系统功能是否修复,这就需要快速响应并方便地汇总这些日志
  • 到最后,这其实涉及到分布式的日志系统

    • 分布式的日志系统有很多,如 ELK StackGraylogFluentdPrometheus
    • 还有大家熟知的错误收集系统 Sentry 等,这些都是用于日志收集或系统监控的工具
    • 我们把这个层级的扩展放到后面,现在重点看看前面这些问题如何优化
  • 问题及优化方案分析

      1. 占用系统磁盘 IO:
      • 无论哪种日志系统,要么往本地磁盘写,要么往数据库或其他服务器的文件写,都会占用磁盘 IO 或网络带宽。
      • 对于单服务器、单 NestJS 的简单业务系统,如简单的小程序,使用文档类型的日志系统没问题,Wisdom 就提供了类似方案,它支持通过 HTTP Transport 的方式将日志文件发送到远端的静态资源服务器,还支持一些简单的权限验证
      • 但这种方式会占用一定的系统网络带宽,且发送日志频率不能太高,同时也无法解决多服务日志的筛选汇总问题
      1. 日志筛选汇总难题
      • 为了解决这个问题,我们介绍 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
  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:默认有两种类型,filemongo,我们将其设为 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 ) 测试与问题解决

  • 创建数据库和用户:创建一个 NestJSMongoDB 数据库,并创建一个 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,这样只有错误级别的日志会被记录
  • 同时,设置 storeHosttrue 后,每条日志会多一个 hostname 属性,方便在服务器端区分不同服务器产生的错误
  • 通过以上步骤,我们完成了 MongoDB 配合 winston 进行日志收集的功能,这种配置方式解决了磁盘 IO 占用、数据库下载筛选不便以及日志配置分散等问题

数据库日志存储与维护机制:问题、解决方案及实践探索


1 ) Wisdom与Mongo集成存在的问题

  • 之前一节我们完成了Wisdom与Mongo级别的集成
  • 但细想之下,仍存在一些不便之处:
      1. 日志设置局限性:存储的日志只能通过日志级别进行设置。
      1. 错误日志筛选不便:
      • 对于错误日志,难以快速筛选,需要了解MongoDB的查询语句
      • 在数据库操作界面进行查询时,即便能通过如level等条件查询,但时间节点可能不一致,还需补充查询条件

2 ) MongoDB数据库日志存储问题及应对

  • 日志数据存储压力
    • MongoDB数据库中若存放大量英文数据,可能会很快写爆日志服务器
    • 因此,对于日志服务器及其存储,需要加入清理操作
  • 定时清理任务
    • 清理操作并非每天执行,需考虑定时任务
    • cappedcappedSizecappedMax选项的作用是滚动日志的属性项
      • 若将capped设置位 true 后,会尝试创建新的 log
      • cappedSize 会记录所有日志的大小
      • cappedMax 是设置日志大小

3 )日志存储测试与分析

  • 测试过程

    • capped设置为 10,cappedSize设置为一千,LOG_LEVEL设置为info,删除数据库连接,重启项目,发现日志只有一条数据
    • 原因是在cappedMax配置中设置了日志大小,达到上限便不再产生数据
    • 增大该值后再次测试,删除logconnection,重启调试进程,发现日志数据不会超过十条,保存操作会使数据滚动
  • 属性设置建议:建议保持这三个属性的默认值,即capped设为false,不创建新的connectioncappedSize约为9.3兆的日志空间;cappedMax不设置,可无限记录文件数量。查看源码可知,未设置时这三个值为空

4 ) 合理的日志存储方案

  • 不同业务场景的选择
    • 根据业务场景选择存储方案, 小型业务系统可使用文档或数据库存储
    • 文档方案检索不便,数据库方案便于多台机器部署的服务端应用进行日志汇总、筛选和查询,集中式数据库存储方案较为理想
  • 其他存储方案及扩展操作
    • Wisdom还提供了如HTTP的方案,但仍离不开文档方式
    • 使用数据库后,可创建简单界面进行数据查询和筛选
    • 更进一步,需对MongoDB数据库进行滚动或备份操作,可编写服务器定时维护脚本,按时间(如月、三个月、七天等)滚动备份日志数据,此思路与配置Wisdom的DailyRotateFile类似
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值