【第五期】基於 @vue/cli3 ssr 插件與 influxdb,接入監控系統【SSR第四篇】

在上一篇文章《基於 @vue/cli3 插件,集成日誌系統》中,咱們爲 ssr 插件中的服務器端邏輯接入了日誌系統。javascript

接下來讓咱們考慮爲 ssr 插件中的服務器端邏輯接入基於 influxdb 的監控系統。咱們按照下面的步驟逐步講解:前端

  1. 什麼是 influxdb
  2. 定義監控信息的內容
  3. 搭建監控系統客戶端
  4. 官方提供的展現監控數據的工具

什麼是 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 influxdbmeasurement 的名稱
  • 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

相關文章
相關標籤/搜索