1. 程式人生 > >系統日誌處理——shell和Nodejs的實踐

系統日誌處理——shell和Nodejs的實踐

問題描述:

宣告:僅供分享探討,不喜勿噴,希望大神們多多指教!

我們的日誌系統為分散式日誌系統,記錄IOS或Android的APP請求後臺API的情況,有4臺伺服器進行日誌記錄工作。現在的工作就是每天定時得獲取分佈在伺服器的日誌然後處理,主要就是做統計工作,這裡有兩個指標。

PV:頁面訪問量,這裡指的是一個API被訪問的次數;

UV:使用者訪問量,這裡指的是一個API被多少個使用者不重複訪問。

這裡列出一條記錄案例:

10.130.92.149 - - [07/Aug/2016:03:00:14 +0800] "POST /api/superBike/gpsTrack/uploadGps?access_token=14069e1dc20LWykljPD3r8sbBNj8jFcgIvnXOyjVBE2IT4HY9pem15KUenN447DcKENNKIqpQ&letvId=172434715 HTTP/1.1" 200 49 "-" "lesports"

關鍵步驟:

這裡我將任務劃分成3個階段來實現:1)如何實現自動定時,自動部署觸發;2)如何實現PV和UV的統計;3)結果的儲存。

1)Jenkins構建定時任務

伺服器一般都是linux系統,所以可以使用linux下的crontab命令來部署定時任務,到設定時間自動跑指令碼等(實際上我也沒有用過,以後可以試試)。那我是如何實現定時任務的?答案就是——Jenkins(功能很強大,不過我只懂得皮毛,值得深度學習)。我們公司採用Jenkins工具來構建所有的專案,Jenkins整合了Git、Svn、Maven等主流工具,十分方便。具體使用過程: 1、新建Jenkins專案; 2、進入到設定勾選“Build periodically”; 3、編寫“Schedule”,“Schedule”有5項,分別代表:分、時、日、月、年,用空格隔開,如下如“50 02 * * *” 表示每天的凌晨02時50分要自動構建這個專案,其中“*”表示任何時間定。

至此,一個自動化的定時任務就不熟成功了,之後就可以開展真正的工作了。

2)shell指令碼處理文字,統計日誌

有人會問,我建好Jenkins專案後,設定完定時後該怎麼用,上一小節中的教程連結應該多少介紹一些,那我就說說我這裡的用處,僅僅是跑指令碼而已(應該是大材小用了)。
這就是我這個專案的全部內容了跑了兩段指令碼分別是Shell指令碼和Nodejs指令碼。當然Jenkins還支援很多指令碼語言的,這裡給個截圖不贅述。
值得注意是,這裡新增指令碼的順序就是指令碼的執行順序。好了該切入主題了,先上程式碼!
<pre name="code" class="javascript"><pre name="code" class="javascript">set -x
# rm * -rf

ip_list=(<strong>遠端伺服器的IP地址,用空格隔開</strong>)
for ip in ${ip_list[@]}
do
    scp ${ip}:/home/nginx/nginx/logs/access.log <strong>Jenkins的workspace目錄</strong>/${ip}.log
done

log_list=(<strong>拷貝過來的日誌檔案,檔名為ip地址</strong>)

# 獲取當前時間的前一天,切割日誌時間是凌晨3點
# 所以處理的日誌範圍為前一天的02:50:00——今天的02:50:00
today=`date +"%d/%b/%Y" -d "-1days"`
# today="08/Aug/2016"
dateTime=`date +"%Y-%m-%d %H:%M:%S" -d "-1day"`
# 轉化成unix時間戳,單位為秒不是毫秒
timestamp=`date -d "${dateTime}" +%s`

# 獲取昨天和今天的訪問日誌
# 即昨天的00:00:00——今天的02:50:00
today=${today//\//\\/}
for file in ${log_list[@]}
do
    sed -n '/'${today}'/,$p' ${file} >> access.log
done

# 提取ip和api
# transform:返回使用者訪問的時間戳
# 然後與昨天的當前時間比較,保留昨天02:50:00後的日誌
awk '
function transform(date){
    day=substr(date,1,2);
    month=substr(date,4,3);
    if(month=="Jan"){
        month=01
    }
    if(month=="Feb"){
        month=02
    }
    if(month=="Mar"){
        month=03
    }
    if(month=="Apr"){
        month=04
    }
    if(month=="May"){
        month=05
    }
    if(month=="Jun"){
        month=06
    }
    if(month=="Jul"){
        month=07
    }
    if(month=="Aug"){
        month=08
    }
    if(month=="Sep"){
        month=09
    }
    if(month=="Oct"){
        month=10
    }
    if(month=="Nov"){
        month=11
    }
    if(month=="Dec"){
        month=12
    }
    year=substr(date,8,4);
    hour=substr(date,13,2);
    min=substr(date,16,2);
    second=substr(date,19,2);
    time=year" "month" "day" "hour" "min" "second;
    return mktime(time)
}
{
    dateTime=substr($4,2);
    split($7,a,"?");
    api=a[1];
    time=transform(dateTime)
}
{
    if (time > '$timestamp')
    print $1,api,time
}
' access.log > temp

awk --posix '
$2 ~ /\/[0-9]{15}\//{
    gsub(/\/[0-9]{15}\//,"/{id}/",$2)
}
$2 ~ /\/[0-9]{15}$/{
    gsub(/\/[0-9]{15}$/,"/{id}",$2)
}
$2 ~ /\/[a-zA-Z0-9]{24}\//{
    gsub(/\/[a-zA-Z0-9]{24}\//,"/{id}/",$2)
}
$2 ~ /\/[a-zA-Z0-9]{24}$/{
    gsub(/\/[a-zA-Z0-9]{24}$/,"/{id}",$2)
}
$2 ~ /\/[a-zA-Z0-9]{10}\//{
    if($2 !~ /\/[a-zA-Z]{10}\//)
    gsub(/\/[a-zA-Z0-9]{10}\//,"/{id}/",$2)
}
$2 ~ /\/[a-zA-Z0-9]{10}$/{
    if($2 !~ /\/[a-zA-Z]{10}$/)
    gsub(/\/[a-zA-Z0-9]{10}$/,"/{id}",$2)
}
{
    print $1,$2,$3
}' temp > ip_api

# 計算pu
cat ip_api | sort -t " " -k2 | awk 'a[$2]!=$1{a[$2]=$1;b[$2]++}END{for (i in b)print i" "b[i]}' | sort -t " " -k1 > uv_

# 計算uv
cut -d" " -f2 ip_api | sort | uniq -c | awk '{print $2,$1}' > pv_

# 合併pv和uv,切過濾掉以js,css,png,jpg,gif,cgi,ico,gch結尾,http開頭,包含"\"的API
paste -d " " pv_ uv_ | awk '$0 ~ /^\/api/ && $1 !~ /css$|js$|png$|jpg$|igf$|cgi$|ico$|gch|^http|.*\\.*|400/{print $1,$2,$4}' | sort -t " " -k2 -n -r > result.txt

# # 刪除中轉檔案
rm temp ip_api uv_ pv_ -rf
是不是感覺很亂,但我只能說Shell太強大了,以上的程式碼可以總結以下幾個linux關鍵工具,相關提供連結: 4、合併檔案paste Linux paste命令使用PS:awk的版本分很多種,不同的版本支援的正則表示式可能會不一樣,gawk就不支援/[0-9]{15}/的匹配,這時需要在awk 命令後 加上--posix,這貌似是某個標準,不過還是不太瞭解,以後有機會在研究一下。 這裡再推薦一個linux命令列學習網站菜鳥教程

3)Nodejs實現連線資料庫,儲存結果

首先還是上程式碼;
var fs = require('fs');
var MongoClient = require('mongodb').MongoClient;

//建立ApiLog陣列
var apiLogs = new Array();
//獲取當前時間戳
var timestamp = new Date().getTime() - 18000000;
//日誌檔名
var filename = 'result.txt';
var data = fs.readFileSync(filename).toString();
var lines = data.split('\n');
for (var i = 0; i < lines.length; i++){
    var line = lines[i];
    if(line != null && line != ''){
        var api = lines[i].split(' ');
        console.log(api);
        //封裝成Json物件
        var url = api[0];
        var pv = api[1];
        var uv = api[2];
        apiLogs.push({"url":url, "pv": pv, "uv":uv, "visitTime":timestamp});
    }
}
var DB_CONN_STR = 'mongodb://使用者名稱:密碼@IP地址:埠號/資料庫名';
var selectData = function(db, apiLogs, callback) {
    //連線到表  
    var collection = db.collection("apiLog");
    collection.insert(apiLogs, function(err, result) {
        if(err){
            console.log('Error:'+ err);
            return;
        }
        callback(result);
    });
}

MongoClient.connect(DB_CONN_STR, apiLogs, function(err, db) {
    console.log("連線成功!");
    selectData(db, apiLogs, function(result) {
        // console.log(result.length);
        db.close();
    });
});
上面的程式碼相對簡單一下,大致的過程就是:1)讀取shell指令碼處理後的結果檔案(result.txt),因為處理過後的檔案不大所以一次性讀取,之後判斷換行遍歷每行內容;2)切割每行內容分成3個欄位:url、PV、UV,組合成json物件,然後在放到一個數組變數中;3)連線mongo資料庫,把陣列變數批量插入資料庫中。這個過程需要注意以下幾點: 1)nodejs是非同步的,所以效率高,但是程式很多時候需要同步處理所以有時需要非同步轉化成同步,這個網上也有很多教程,這裡不贅述。我想說的是在讀取檔案的時候一定要同步讀取,如果不是同步讀取的話,那你最後得到的陣列變數會為空,等同於你沒有處理文字(實際上處理了,但是在連線資料庫時還沒有處理完)。 2)在nodejs程式碼的最上面可以看到require了兩個模組:fs和mongodb,fs是檔案系統模組,nodejs本身自帶的。而mongodb模組是第三方模組,需要安裝,安裝命令如下: npm install mongodb -g//npm是nodejs的一個模組管理工具,可以直接從網上下載模組,-g 是可選項表示是否全域性,即在當前計算機環境中的任何位置都可以引用。有人會問我在本地寫程式碼然後拷貝到Jenkins上,但是我沒有許可權進入伺服器怎麼安裝辦?答案就是先儲存Jenkins上shell指令碼(處理日誌的程式碼),然後清空換上 npm install mongodb -g,然後構建一下專案就可以安裝,之後再把shell改回去就行了。 PS:附上Nodejs的入門教程。Node.js 教程