暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【第五期】基于 @vue/cli3 ssr 插件与 influxdb,接入监控系统【SSR第四篇】

水煮前端 2019-07-01
384

在上一篇文章基于 @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 } = winston
const { 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.pid


return 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 = hookFunc


if (!~Object.keys(rotateMap).indexOf(rotateMode)) rotateMode = ''


let accessTransport
let combineTransport


if (rotateMode) {
require('winston-daily-rotate-file')


const pid = process.pid


dirPath += '/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 = protocol
this.appName = appName
this.host = host
this.measurementName = measurementName
this.fieldSchema = fieldSchema
this.tagSchema = tagSchema
this.syncSucceedHook = syncSucceedHook
this.syncfailedHook = syncfailedHook


// _counter between the last reported data and the next reported data
this.count = 0
// default sync queue then it has over 100 records
this.syncQueueLimit = syncQueueLimit || 100
// default check write queue per 60 seconds
this.intervalMilliseconds = intervalMilliseconds || 60000


this.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 interval
setInterval(() => {
this.syncQueue()
}, this.intervalMilliseconds)
}


inc () {
return ++this.count
}


clear () {
this.count = 0
}


syncQueue () {
if (!this.client.writeQueueLength) return


let len = this.client.writeQueueLength


this.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.appName
tags.host = this.host


this.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.app
const host = os.hostname()
const address = config.address
const intervalMilliseconds = 60000
const syncQueueLimit = 100


const 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 = reportAccess
global.reportError = reportError
global.reportMemory = reportMemory

最后,我们在 app/server.js
 中添加具体的上报器调用代码,代码片段如下:

require('./lib/monitor.js')


const reportMemoryStatInterval = 30 * 1000


setInterval(() => {
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



文章转载自水煮前端,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论