在上一篇文章基于 @vue/cli3
插件,集成日志系统中,我们为 ssr
插件中的服务器端逻辑接入了日志系统。
接下来让我们考虑为 ssr
插件中的服务器端逻辑接入基于 influxdb
的监控系统。我们按照下面的步骤逐步讲解:
1.什么是 influxdb
2.定义监控信息的内容3.搭建监控系统客户端4.官方提供的展示监控数据的工具
什么是 influxdb
influxDB
是一个由InfluxData
开发的开源时序型数据库。
它由Go
写成,着力于高性能地查询与存储时序型数据。InfluxDB
被广泛应用于存储系统的监控数据,IoT
行业的实时数据等场景。
------ 来自wikipedia InfluxDB[2]
我们收集的监控信息,最终会上报到 influxdb
中,关于 influxdb
,我们需要记住以下概念:
•influxDB
: 是一个时序数据库,它存储的数据由 Measurement
, tag组
以及 field组
以及一个 时间戳
组成。•Measurement
: 由一个字符串表示该条记录对应的含义。比如它可以是监控数据 cpu_load
,也可以是测量数据average_temperature
(我们可以先将其理解为 mysql
数据库中的表 table
)•tag组
: 由一组键值对组成,表示的是该条记录的一系列属性信息。同样的 measurement
数据所拥有的 tag组
不一定相同,它是无模式的(Schema-free)。tag
信息是默认被索引的。•field组
: 也是由一组键值对组成,表示的是该条记录具体的 value
信息(有名称)。field组
中可定义的 value
类型包括:64位整型,64位浮点型,字符串以及布尔型。Field
信息是无法被索引的。•时间戳: 就是该条记录的时间属性。如果插入数据时没有明确指定时间戳,则默认存储在数据库中的时间戳则为该条记录的入库时间。
定义监控信息的内容,以及数据来源
对于 influxdb
有了基本的了解后,我们来设计具体的监控信息内容。
我们首先需要考虑 ssr
服务端有哪些信息需要被监控,这里我们简单定义如下监控内容:
•请求信息(请求数量、请求耗时)•错误信息(错误数量、错误类型)•内存占用
请求数量,指的是服务端每接收到一次页面请求(这里可以不考虑非 GET
的请求),记录一次数据。
请求耗时,指的是服务端接收到请求,到开始返回响应之间的时间差。
错误数量,指的是服务端发生错误和异常的次数。
错误类型,指的是我们为错误定义的分类名称。
内存占用,指的是服务端进程占用的内存大小。(这里我们只记录服务端进程的 RSS
信息)。
那么数据源从哪里来呢?
对于 请求信息
、错误信息
这两个个监控信息的内容,我们可以借助于在上一篇文章基于 @vue/cli3
插件,集成日志系统中,设计的日志系统来采集。
这个系统基于 winston
这个日志工具,winston
支持我们在写入日志前,对日志进行一些处理,具体参考creating-custom-formats[4]
我们通过日志系统创建请求日志和错误日志,并在这两类日志的信息中,采集我们需要的数据。
为此,我们需要让我们的日志系统在初始化时支持一个函数类型的参数,在每次写入日志前,都调用这个函数。
打开 app/lib/logger.js
,添加此支持,最终代码如下:
const winston = require('winston')const { format } = winstonconst { combine, timestamp, json } = format// 我们声明一个什么都不做的 hook 函数let _hook = () => {}const _getToday = (now = new Date()) => `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`// 我们借助 winston 提供的日志格式化 api ,实现了一个采集上报函数const ReportInfluxDB = format((info) => {_hook(info)info.host = os.hostname()info.pid = process.pidreturn info})const rotateMap = {'hourly': 'YYYY-MM-DD-HH','daily': 'YYYY-MM-DD','monthly': 'YYYY-MM'}module.exports = (dirPath = './', rotateMode = '', hookFunc) => {// 当传递了自定义 hook 函数后,替换掉我们的默认 hook 函数if (hookFunc) _hook = hookFuncif (!~Object.keys(rotateMap).indexOf(rotateMode)) rotateMode = ''let accessTransportlet combineTransportif (rotateMode) {require('winston-daily-rotate-file')const pid = process.piddirPath += '/pid_' + pid + '_' + _getToday() + '/'const accessLogPath = dirPath + 'access-%DATE%.log'const combineLogPath = dirPath + 'combine-%DATE%.log'const datePattern = rotateMap[rotateMode] || 'YYYY-MM'accessTransport = new (winston.transports.DailyRotateFile)({filename: accessLogPath,datePattern: datePattern,zippedArchive: true,maxSize: '1g',maxFiles: '30d'})combineTransport = new (winston.transports.DailyRotateFile)({filename: combineLogPath,datePattern: datePattern,zippedArchive: true,maxSize: '500m',maxFiles: '30d'})}const options = {// 我们在这里定义日志的等级levels: { error: 0, warning: 1, notice: 2, info: 3, debug: 4 },format: combine(timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),// 为产品环境日志挂载我们的采集上报函数ReportInfluxDB()),transports: rotateMode ? [combineTransport] : []}// 开发环境,我们将日志也输出到终端,并设置上颜色if (process.env.NODE_ENV === 'development') {options.format = combine(timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),winston.format.colorize(),json(),// 为开发环境日志挂载我们的采集上报函数ReportInfluxDB())// 输出到终端的信息,我们调整为 simple 格式,方便看到颜色;// 并设置打印 debug 以上级别的日志(包含 debug)options.transports.push(new winston.transports.Console({format: format.simple(), level: 'debug'}))}winston.loggers.add('access', {levels: { access: 0 },level: 'access',format: combine(timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),json(),// 为请求日志挂载我们的采集上报函数ReportInfluxDB()),transports: rotateMode ? [accessTransport] : []})const logger = winston.createLogger(options)return {logger: logger,accessLogger: winston.loggers.get('access')}}
在 app/server.js
中引入 lib/logger.js
也需要调整为以下方式:
const LOG_HOOK = logInfo => {if (logInfo.level === 'access') return process.nextTick(() => {/* TODO: 采集请求数量和请求耗时,并上报 */})if (logInfo.level === 'error') return process.nextTick(() => {/* TODO: 采集错误数量和错误类型,并上报 */})}const { logger, accessLogger } = require('./lib/logger.js')('./', 'hourly', LOG_HOOK)
对于 内存占用
,我们只需要通过 Nodejs
提供的 process.memoryUsage()
方法来采集。
搭建监控系统客户端
确定好了监控信息内容、数据源。剩下的就是如何设计监控系统客户端。
我们借助工具库influxdb-nodejs[5]来实现。
首先,我们创建 app/lib/reporter.js
文件,内容如下:
'use strict'const Influx = require('influxdb-nodejs')class Reporter {constructor (protocol,appName,host,address,measurementName,fieldSchema,tagSchema,syncQueueLimit,intervalMilliseconds,syncSucceedHook = () => {},syncfailedHook = () => {}) {if (!protocol) throw new Error('[InfluxDB] miss the protocol')if (!appName) throw new Error('[InfluxDB] miss the app name')if (!host) throw new Error('[InfluxDB] miss the host')if (!address) throw new Error('[InfluxDB] miss the report address')if (!measurementName) throw new Error('[InfluxDB] miss the measurement name')this.protocol = protocolthis.appName = appNamethis.host = hostthis.measurementName = measurementNamethis.fieldSchema = fieldSchemathis.tagSchema = tagSchemathis.syncSucceedHook = syncSucceedHookthis.syncfailedHook = syncfailedHook// _counter between the last reported data and the next reported datathis.count = 0// default sync queue then it has over 100 recordsthis.syncQueueLimit = syncQueueLimit || 100// default check write queue per 60 secondsthis.intervalMilliseconds = intervalMilliseconds || 60000this.client = new Influx(address)this.client.schema(this.protocol,this.fieldSchema,this.tagSchema,{stripUnknown: true})this.inc = this.inc.bind(this)this.clear = this.clear.bind(this)this.syncQueue = this.syncQueue.bind(this)this.writeQueue = this.writeQueue.bind(this)// report data to influxdb by specified time intervalsetInterval(() => {this.syncQueue()}, this.intervalMilliseconds)}inc () {return ++this.count}clear () {this.count = 0}syncQueue () {if (!this.client.writeQueueLength) returnlet len = this.client.writeQueueLengththis.client.syncWrite().then(() => {this.clear()this.syncSucceedHook({ measurement_name: this.measurementName, queue_size: len })}).catch(err => {this.syncfailedHook(err)})}writeQueue (fields, tags) {fields.count = this.inc()tags.metric_type = 'counter'tags.app = this.appNametags.host = this.hostthis.client.write(this.measurementName).tag(tags).field(fields).queue()if (this.client.writeQueueLength >= this.syncQueueLimit) this.syncQueue()}}const createReporter = (option) => new Reporter(option.protocol || 'http',option.app,option.host,option.address,option.measurement,option.fieldSchema,option.tagSchema,option.syncQueueLimit,option.intervalMilliseconds,option.syncSucceedHook,option.syncfailedHook)module.exports = createReporter
通过上面的代码可以看到,我们基于 influxdb-nodejs
封装了一个叫做 createReporter
的类。
通过 createReporter
,我们可以创建:
•request reporter
(请求信息上报器)•error reporter
(错误信息上报器)•memory reporter
(内存信息上报器)
所有这些信息,都标配如下字段信息:
•app
应用的名称,可以将工程项目中 pacage.json
中的 name
值作为此参数值•host
所在服务器操作系统的 hostname
•address
监控信息上报的地址•measurement
influxdb
中 measurement
的名称•fieldSchema
field组
的定义,(具体请参考write-point[6])•tagSchema
tag组
的定义,(具体请参考write-point[7])•syncQueueLimit
缓存上报信息的最大个数,达到这个值,会触发一次监控信息上报,默认缓存 100
条记录•intervalMilliseconds
上报信息的时间间隔,默认 1
分钟•syncSucceedHook
上报信息成功后执行的函数,可以通过此函数打印一些日志,方便跟踪上报监控信息的情况•syncfailedHook
上报信息失败后执行的函数,可以通过此函数打印一些日志,方便跟踪上报监控信息的情况
下面,让我们来看如何使用 app/lib/reporter.js
来创建我们需要的监控信息上报器。
首选,创建 influxdb
配置文件 app/config/influxdb.js
,内容如下:
'use strict'const options = {app: '在这里填写您的应用名称',address: '在这里填写远程 influxdb 地址',access: {measurement: 'requests',fieldSchema: {count: 'i',process_time: 'i'},tagSchema: {app: '*',host: '*',request_method: '*',response_status: '*'}},error: {measurement: 'errors',fieldSchema: {count: 'i'},tagSchema: {app: '*',host: '*',exception_type: '*'}},memory: {measurement: 'memory',fieldSchema: {rss: 'i',heapTotal: 'i',heapUsed: 'i',external: 'i'},tagSchema: {app: '*',host: '*'}}}module.exports = options
对于请求信息,我们设置了:
•count
整型,方便统计请求数•process_time
整型,请求耗时(单位:毫秒)•request_method
任意类型,请求方法•response_status
任意类型,响应状态码
对于错误信息,我们设置了:
•count
整型,方便统计错误数•exception_type
任意类型,错误类型值(这需要我们在应用中定义)
对于内存信息,我们设置了:
•rss
后端服务进程实际占用内存•heapTotal
堆空间上限•heapUsed
已使用的堆空间•external
V8管理的 C++ 对象占用空间
接着创建 app/lib/monitor.js
,内容如下:
'use strict'const createReporter = require('./reporter.js')const os = require('os')const _ = require('lodash')const config = require('../config/influxdb.js')const protocol = 'http'const app = config.appconst host = os.hostname()const address = config.addressconst intervalMilliseconds = 60000const syncQueueLimit = 100const syncSucceedHook = info => {console.log(JSON.stringify({ title: '[InfluxDB] sync write queue success', info: info }))}const syncfailedHook = err => {console.log(JSON.stringify({ title: '[InfluxDB] sync write queue fail.', error: err.message }))}const accessReporter = createReporter({protocol,app,host,address,measurement: _.get(config, 'access.measurement'),fieldSchema: _.get(config, 'access.fieldSchema'),tagSchema: _.get(config, 'access.tagSchema'),syncQueueLimit,intervalMilliseconds,syncSucceedHook,syncfailedHook})const errorReporter = createReporter({protocol,app,host,address,measurement: _.get(config, 'error.measurement'),fieldSchema: _.get(config, 'error.fieldSchema'),tagSchema: _.get(config, 'error.tagSchema'),syncQueueLimit,intervalMilliseconds,syncSucceedHook,syncfailedHook})const memoryReporter = createReporter({protocol,app,host,address,measurement: _.get(config, 'memory.measurement'),fieldSchema: _.get(config, 'memory.fieldSchema'),tagSchema: _.get(config, 'memory.tagSchema'),syncQueueLimit,intervalMilliseconds,syncSucceedHook,syncfailedHook})function reportAccess (accessData) {accessReporter.writeQueue({process_time: accessData.process_time},{request_method: accessData.request_method,response_status: accessData.response_status})}function reportError (errorData) {errorReporter.writeQueue({},{exception_type: errorData.type || 0})}function reportMemory () {const memInfo = process.memoryUsage()memoryReporter.writeQueue({rss: memInfo.rss || 0,heapTotal: memInfo.heapTotal || 0,heapUsed: memInfo.heapUsed || 0,external: memInfo.external || 0},{})}global.reportAccess = reportAccessglobal.reportError = reportErrorglobal.reportMemory = reportMemory
最后,我们在 app/server.js
中添加具体的上报器调用代码,代码片段如下:
require('./lib/monitor.js')const reportMemoryStatInterval = 30 * 1000setInterval(() => {global.reportMemory()}, reportMemoryStatInterval)const LOG_HOOK = logInfo => {if (logInfo.level === 'access') return process.nextTick(() => {global.reportAccess(logInfo)})if (logInfo.level === 'error') return process.nextTick(() => {global.reportError(logInfo)})}const { logger, accessLogger } = require('./lib/logger.js')('./', 'hourly', LOG_HOOK)
至此,我们在应用中设计监控信息并创建监控系统客户端的步骤就算完成了。
最终,ssr
插件的目录结构如下所示:
├── app│ ├── config│ │ ├── influxdb.js│ ├── middlewares│ │ ├── dev.ssr.js│ │ ├── dev.static.js│ │ └── prod.ssr.js│ ├── lib│ │ ├── reporter.js│ │ ├── monitor.js│ │ └── logger.js│ └── server.js├── generator│ ├── index.js│ └── template│ ├── src│ │ ├── App.vue│ │ ├── assets│ │ │ └── logo.png│ │ ├── components│ │ │ └── HelloWorld.vue│ │ ├── entry-client.js│ │ ├── entry-server.js│ │ ├── main.js│ │ ├── router│ │ │ └── index.js│ │ ├── store│ │ │ ├── index.js│ │ │ └── modules│ │ │ └── book.js│ │ └── views│ │ ├── About.vue│ │ └── Home.vue│ └── vue.config.js├── index.js└── package.json
官方提供的展示监控数据的工具
展示监控数据的工具有很多,这里推荐一个官方 influxdata
提供的工具:chronograf[8]。
关于 chronograf
的知识,本文不再展开,有兴趣的同学可以查阅官方文档[9]学习相关细节。
References
[1]
wikipedia InfluxDB: https://zh.wikipedia.org/wiki/InfluxDB[2]
creating-custom-formats: https://github.com/winstonjs/winston#creating-custom-formats[3]
influxdb-nodejs: https://github.com/vicanso/influxdb-nodejs[4]
write-point: https://github.com/vicanso/influxdb-nodejs#write-point[5]
write-point: https://github.com/vicanso/influxdb-nodejs#write-point[6]
chronograf: https://www.influxdata.com/time-series-platform/chronograf/[7]
官方文档: https://docs.influxdata.com/chronograf/v1.7/
水滴前端团队招募伙伴,欢迎投递简历到邮箱:fed@shuidihuzhu.com





