在上一篇文章《基於 @vue/cli3
插件,集成日誌系統》中,咱們爲 ssr
插件中的服務器端邏輯接入了日誌系統。javascript
接下來讓咱們考慮爲 ssr
插件中的服務器端邏輯接入基於 influxdb
的監控系統。咱們按照下面的步驟逐步講解:前端
influxdb
influxdb
influxDB
是一個由InfluxData
開發的開源時序型數據庫。
它由Go
寫成,着力於高性能地查詢與存儲時序型數據。
InfluxDB
被普遍應用於存儲系統的監控數據,IoT
行業的實時數據等場景。
------ 來自wikipedia InfluxDBvue
咱們收集的監控信息,最終會上報到 influxdb
中,關於 influxdb
,咱們須要記住如下概念:java
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
有了基本的瞭解後,咱們來設計具體的監控信息內容。node
咱們首先須要考慮 ssr
服務端有哪些信息須要被監控,這裏咱們簡單定義以下監控內容:mysql
請求數量,指的是服務端每接收到一次頁面請求(這裏能夠不考慮非 GET
的請求),記錄一次數據。git
請求耗時,指的是服務端接收到請求,到開始返回響應之間的時間差。github
錯誤數量,指的是服務端發生錯誤和異常的次數。sql
錯誤類型,指的是咱們爲錯誤定義的分類名稱。數據庫
內存佔用,指的是服務端進程佔用的內存大小。(這裏咱們只記錄服務端進程的 RSS
信息)。
那麼數據源從哪裏來呢?
對於 請求信息
、錯誤信息
這兩個個監控信息的內容,咱們能夠藉助於在上一篇文章《基於 @vue/cli3
插件,集成日誌系統》中,設計的日誌系統來採集。
這個系統基於 winston
這個日誌工具,winston
支持咱們在寫入日誌前,對日誌進行一些處理,具體參考creating-custom-formats
咱們經過日誌系統建立請求日誌和錯誤日誌,並在這兩類日誌的信息中,採集咱們須要的數據。
爲此,咱們須要讓咱們的日誌系統在初始化時支持一個函數類型的參數,在每次寫入日誌前,都調用這個函數。
打開 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來實現。
首先,咱們建立 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)tagSchema
tag組
的定義,(具體請參考write-point)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。
關於 chronograf
的知識,本文再也不展開,有興趣的同窗能夠查閱官方文檔學習相關細節。
水滴前端團隊招募夥伴,歡迎投遞簡歷到郵箱:fed@shuidihuzhu.com