如何用NodeJS讀取分析Nginx錯誤日誌

zpzxgcr發表於2019-05-14

網上很少看到有用NodeJS運維繫列文章,後續我會更新一些NodeJS運維相關的內容又或者說讓我們更加的深入瞭解一些伺服器的知識以及自動化運維方面的基礎知識 為什麼要做錯誤日誌分析,因為網上這方面的工具不多我找到一個goaccess但是都是分析成功日誌以及使用者訪問趨勢,找了半天沒找著自己想要的索性就自己利用Node造一個

錯誤日誌分析

首先我們要讀取Nginx日誌,我們可以看到Nginx的錯誤日誌格式一般都是這樣子,需要注意的是Nginx的錯誤日誌格式是差不多的因為無法設定日誌格式只能設定日誌錯誤等級所以我們分析的時候很方便

如何用NodeJS讀取分析Nginx錯誤日誌
這裡我們用到readline逐行讀取,簡單來說可以做

  • 檔案逐行讀取:比如說進行日誌分析。
  • 自動完成:比如輸入npm,自動提示"help init install"。
  • 命令列工具:比如npm init這種問答式的腳手架工具。 這裡我們主要做日誌分析其他的感興趣可以琢磨一下

實現方法

const readline = require('readline');
const fs = require('fs');
const path = require('path');
console.time('readline-time')
const rl = readline.createInterface({
  input: fs.createReadStream(path.join(__dirname, '../public/api.err.log'), {
    start: 0,
    end: Infinity
  }),

});
let count = 0; 
rl.on('line', (line) => {
  const arr = line.split(', ');
  const time = arr[0].split('*')[0].split('[')[0].replace(/\//g, '-');//獲取到時間
  const error = arr[0].split('*')[1].split(/\d\s/)[1];//錯誤原因
  const client = arr[1].split(' ')[1];//請求的客戶端
  const server = arr[2].split(' ')[1];//請求的網址
  const url = arr[3].match(/\s\/(\S*)\s/)[0].trim()//獲取請求連結
  const upstream = arr[4].match(/(?<=").*?(?=")/g)[0];//獲取上游
  const host = arr[5].match(/(?<=").*?(?=")/g)[0];//獲取host
  const referrer = arr[6] ? arr[6].match(/(?<=").*?(?=")/g)[0] : '';//來源
  console.log(`時間:${time}-原因:${error}-客戶端:${client}-網址:${server}-地址:${url}-上游:${upstream}-主機:${host}-來源:${referrer}`); 
  count++;
});
rl.on('close', () => {
  let size = fs.statSync(path.join(__dirname, '../public/api.err.log')).size;
  console.log(`讀取完畢:${count};檔案位置:${size % 2 === 0}`);
  console.timeEnd('readline-time')
});
複製程式碼

上面程式碼有幾點需要注意的是會建立一個檔案可讀流然後由於演示所以我是直接找的本地地址如果是生產環境的話大家可以直接填寫伺服器上的錯誤日誌地址,如果沒有Nginx錯誤日誌分割的話每天會產生很多日誌,createReadStream讀取幾十M的檔案還好如果讀取幾百M或者上G的容量日誌這會造成效能問題,所以我們需要在每次createReadStream沒必要每次從0位元組開始讀取,ceateReadStream提供了start和end

如何用NodeJS讀取分析Nginx錯誤日誌
所以我們每次可以在讀取完之後記錄一下當前檔案位元組大小下一次讀取檔案就是可以用該檔案上次的大小開始讀取

let size = fs.statSync(path.join(__dirname, '../public/api.err.log')).size;
複製程式碼

我們可以對比一下每次從0位元組開始讀取和從指定位元組讀取

如何用NodeJS讀取分析Nginx錯誤日誌

儲存資料進行分析

這裡我是用node-schedule這個庫進行定時儲存錯誤日誌和linux的cron差不多,用的mongodb儲存資料,這裡更推薦大家用elasticsearch來做日誌分析

  rl.on('close', async () => {
          let count = 0;
          for (let i of rlist) {
            count++;
            if (count % 500 === 0) {
              const res = await global.db.collection('logs').bulkWrite(rlist.slice(count, count + 500), { ordered: false, w: 1 }).catch(err => { console.error(`批量插入出錯${err}`) }); 
            } else if (count === rlist.length - 1) {
            //批量插入 資料
              const res = await global.db.collection('logs').bulkWrite(rlist.slice(rlist - (rlist % 500), rlist.length), { ordered: false, w: 1 });
              let size = fs.statSync(addres).size;
              size = size % 2 === 0 ? size : size + 1;//保證位元組大小是偶數 不然會出現讀取上行內容不完整的情況
              count = 0;
              rlist.length = [];
              //更新資料庫裡面檔案的size
              global.db.collection('tasks').updateOne({ _id: addre }, { $set: { _id: addre, size, date: +new Date() } }, { upsert: true }); 
            }
          }
          resolve(true);
        })
複製程式碼

上面主要是500條儲存一次,因為我用的是批量插入然後mongodb有限制一次性最多插入16M資料的限制,所以大家看自己清空決定一次性插入多少條 猶豫對readline的實現比較感興趣,就去翻閱了一下原始碼發現並不是我們想的那麼複雜,readline原始碼,下面貼一下line事件的原始碼,想繼續深入的同學可以看看全部的原始碼


  if (typeof s === 'string' && s) {
          var lines = s.split(/\r\n|\n|\r/);
          for (var i = 0, len = lines.length; i < len; i++) {
            if (i > 0) {
              this._line();
            }
            this._insertString(lines[i]);
          }
        }
...

Interface.prototype._line = function() {
  const line = this._addHistory();
  this.clearLine();
  this._onLine(line);
};

...
Interface.prototype._onLine = function(line) {
  if (this._questionCallback) {
    var cb = this._questionCallback;
    this._questionCallback = null;
    this.setPrompt(this._oldPrompt);
    cb(line);
  } else {
    this.emit('line', line);
  }
};
複製程式碼

儲存的資料需要進行分析比如哪個IP訪問最多哪條錯誤最多可以用聚合來進行分析貼出示例分析某個IP在某一天訪問出錯最多的原因

db.logs.aggregate(

	// Pipeline
	[
		// Stage 1
		{
			$group: {
			  '_id': { 'client': '114.112.163.28', 'server': '$server', 'error': '$error', 'url': '$url', 'upstream': '$upstream','date':'$date' ,'msg':'$msg' } ,
			      
			  'date':{'$addToSet':'$date'},
			   count: { '$sum': 1 } 
			}
		},

		// Stage 2
		{
			$match: { 
			      count: { $gte: 1 },
			      date: ['2019-05-10']
			     
			}
		},
 
		{
			$sort: {
			    count: -1
			}
		},
	],

	// Options
	{
		cursor: {
			batchSize: 50
		},

		allowDiskUse: true
	}
 

);
複製程式碼

通過這次日誌分析學習到很多東西,歡迎大家和我交流,有問題的同學可以在下面留言

相關文章