1. 程式人生 > 實用技巧 >使用 tail 結合 grep 查詢日誌關鍵字並高亮及顯示所在行上下文

使用 tail 結合 grep 查詢日誌關鍵字並高亮及顯示所在行上下文

此文轉載自:https://my.oschina.net/goldenshaw/blog/4703734
大咖揭祕Java人都栽在了哪?點選免費領取《大廠面試清單》,攻克面試難關~>>>

對於一個開發或運維人員而言, 當系統出現故障時, 第一步常常就是檢視日誌. 檢視日誌經常碰到的一個需求就是按關鍵字去搜索, 在日常開發機子上的 IDE 上, 都集成了強大的搜尋功能, 但因為系統通常部署在 Linux 系統上, 一般只有命令列介面, 在其上應該怎麼去搜索呢? 恐怕有些同學就不是那麼清楚了.

有些人會用 ftp 之類的把日誌下載下來本地再搜尋, 如果是小一點的檔案還好, 但日誌檔案往往都比較大, 因此這樣的方式無疑是極為低效的.

下面就介紹一種相對快捷的方式, 也不需要用到特別高階的命令, 僅需要 tail 和 grep 兩個命令結合起來即可, 能達到這樣一個效果:

  1. 能按關鍵字搜尋;
  2. 在顯示關鍵字所在行時還能高亮關鍵字;
  3. 能把關鍵字所在行的上下文, 比如上下 10 行的內容也一起顯示出來.

下面是一個效果示意圖:

在這裡, 我用我雲主機的 nginx access log 做了個示範, 我搜索一篇文章 url 的關鍵字 "a-port", 然後顯示出搜尋的結果及上下文, 可以看到關鍵字被標紅顯示, 上下文也有顯示, 多個搜尋結果間以藍色的短橫間隔開來.

下面具體說說怎麼實現這樣的搜尋, 先具體講講各個命令及引數, 再說說怎麼結合起來, 最後還給出一個指令碼化的高階用法.

tail 命令

首先是 tail 命令. 因為檢視日誌通常從後面最新的日誌去看, tail 命令就是從後往前找.

比如下述命令會顯示 access.log 的最後 10 行的內容:

tail access.log

tail 指定行數

預設情況下, tail 只會顯示最後的 10 行, 對於一個日誌很多的應用來說, 這可能是不夠的, 為此我們需要搜尋更多的行.

如果想實時檢視日誌, 可以參考之前的這篇文章使用 tail -f 實時觀測伺服器日誌輸出

tail 可以結合 -n 引數指定一個行數, 比如下述命令會顯示最後的 30 行的日誌:

tail -n 30 access.log

注: 如果不太能記住引數, 還可以使用 -n 的完整命令引數 --lines:

 tail --lines 30 access.log

grep 命令

tail 僅能列印顯示日誌, 很多時候這是不夠的, 日誌通常非常多, 而且很多是沒有用, 我們還需要能過濾, 或者說搜尋篩選日誌的內容, 這時就可以使用 grep 命令.

grep 命令的基本用法是這樣的. 假如你有一個檔案 index.html, 你想在其中搜索一個關鍵詞 official, 你可以這樣用:

grep official index.html

結果如下:

它會把關鍵字所在行給你顯示出來, 並高亮關鍵字.

關於高亮問題, 如果預設沒有高亮, 則可以自行加入 --color 選項, 像這樣: grep --color official index.html

注意, 通常不要直接用 grep 命令去搜索整個日誌檔案, 因為日誌檔案通常很大, 而且 grep 也是從開頭開始搜尋的, 因此可能搜尋出一大堆你不感興趣的歷史記錄.

後面將介紹如何結合 tail 和 grep 命令以縮小搜尋範圍.

帶有空格的關鍵字

如果關鍵字有多個單詞並帶有空格, 可以使用 '' 單引號引起來, 例如:

grep 'english version' index.html

grep 顯示上下文

有時, 我們不但要找出關鍵子所在行, 而且還想顯示所在行上下的一些行.

這在查詢異常資訊時非常常見, 一方面異常棧會列印成非常多行, 另外我們通常需要前前後後都看一下到底發生了什麼.

這時可以使用 grep 的 -NUM 引數來實現, 如下:

grep -5 official index.html

或者是使用 -C

grep -C 5 official index.html

它表示, 不但要找出 official 關鍵字所在行, 還要把所在行前後的 5 行都顯示出來.

  • 後接的數字 5 就表示前後 5 行, 如果是 -10 就表示前後 10 行

結果如下:

grep 顯示行號

為了更清晰地呈現, 還可以選擇顯示行號, 用 -n 引數, 如下:

grep -5 -n official index.html

結果如下圖:

可以看到, official 關鍵字在 21 行, 行頭的行號還特別以冒號":" 標出; 此外, 關鍵行的前 5 行(16~20)和後 5 行(22~26)也一併顯示了出來.

如果我們是在跟蹤一個異常, 這些上下文的資訊可能會提供很多幫助.

管道符的使用

現在已經介紹完了 tail 和 grep 命令, 但還有一個問題, 如果直接在日誌檔案中去 grep 的話, 因為檔案通常特別大, 而且很多歷史資料可能不是我們想要的, 因此最好的方式是先用 tail 得到後面的那些行, 然後把 tail 出來的結果再交給 grep 命令去過濾, 而管道符可以實現這個目的, 管道符在命令列中就是一個"豎槓": |.

它可以把兩個命令結合起來, 把請一個命令的輸出當作後一個命令的輸入.

用管道符 | 結合 tail 和 grep 命令

用管道符結合 tail 和 grep 命令可以這樣去寫:

tail error.log | grep stream

注意: grep 之前的豎槓 |.

上述命令會把 tail 出來的最後 10 行的內容交給 grep 去搜索過濾, 並找出其中含有 stream 關鍵字的行, 結果如下:

結合前面所講, 如果想在更大範圍搜尋並顯示關鍵字的上下文, 最終可以這樣去寫:

tail -n 20 error.log | grep -3 stream

以上命令在最後 20 行中去搜索 stream 關鍵字並顯示關鍵字所在行及上下各 3 行的內容, 結果如下:

高階用法

有了以上命令, 要搜尋異常資訊就簡單了不少, 而且更容易觀察, 不過還是有一個問題, 就是整個命令還是太長了些, 如果想進一步簡化, 則可以考慮將整個命令做成一個指令碼, 並將部分引數值引數化, 這就帶有一定的程式設計的味道了, 好在這對於我們程式設計師來說, 不算太難的事, 甚至是我們的日常, 下面說說怎麼去實現.

先說下效果, 我們會編寫一個指令碼叫 search.sh

當然這個名字你可以自己去取

然後這樣去用:

./search.sh stream 20 3

然後其效果就像執行下述命令一樣:

tail -n 20 error.log | grep -3 stream

如果不打算傳入行數及上下文的數目, 而使用指令碼中定義的預設值, 整個命令還可以簡化成:

./search.sh stream

僅需要傳入要搜尋的關鍵字即可, 其它引數保持預設.

自定義命令

就以搜尋我本機上的 nginx 的 error.log 為例吧, 首先建立一個指令碼檔案 search.sh

touch search.sh

檔案的內容如下:

#!/bin/bash
cd /usr/local/nginx/logs
tail -n 20 error.log | grep --color -3 stream

注意: 放入指令碼檔案時, 如果沒有高亮, 需要自行加上 --color 選項

邏輯也比較簡單, 就是先進入 error.log 所在資料夾, 然後執行查詢.

有了 cd 命令, 就可以直接把指令碼放在遠端登入後的使用者目錄下, 比如 /root 下, 這樣進去了就可以直接執行, 連進入資料夾的動作也省略了.

另外, 如果不想用 cd 命令, 也可以在 tail 中寫上完整路徑名.

當然, 現在指令碼還是比較死的, 搜尋的關鍵字被寫死了. 不過目前來說, 我們先測試其它方面, 先把檔案改成可執行的:

chmod 755 search.sh

然後可以先執行一遍看看是否 ok, 如果 ok 了, 再下一步準備把關鍵字引數化.

./search.sh

引數傳遞

現在需要把搜尋的關鍵字給引數化, 不然執行指令碼時, 始終只能搜尋 'stream' 這個關鍵字, 這顯然不是我們希望的.

如果是用我們熟悉的語言, 比如 java, javascript, 寫一個可以接收引數的函式是很簡單的, 其實對於 bash 這種指令碼語言來說, 主要的問題是我們不熟悉其語法, 這個只要稍微查下它的手冊或是在網上搜索下即不難知道.

過程就不提了, 具體而言是這樣的:

#!/bin/bash
cd /usr/local/nginx/logs
tail -n 20 error.log | grep --color -3 $1

就是把 stream 這個寫死的關鍵字變成一個變數 $1, 自然 $ 符號就是 bash 跟定義變數有關的.

自然, 你應該能猜到, 如果想傳遞更多的引數, 就用 $2, $3, 以此類推.

然後你這樣

./search.sh hello

那麼指令碼檔名後面跟的字串'hello'就會傳遞給 $1 這個變數, 於是就相當於執行了:

tail -n 20 error.log | grep --color -3 hello

同理, 可以把 tail 的行數和 grep 的上下文的行數也引數化:

#!/bin/bash
cd /usr/local/nginx/logs
tail -n $2 error.log | grep --color -$3 $1

如此一來, 當執行下述命令時:

./search.sh hello 1000 10

就相當於:

tail -n 1000 error.log | grep --color -10 hello

也即在日誌檔案的最後 1000 行裡搜尋, 並顯示關鍵行上下各 10 行的內容.

預設值及判斷邏輯

自然, 很多時候可能只想傳遞關鍵字即可, 當把 tail 的行數和 grep 的上下文的行數也引數化後, 每次呼叫也要傳遞它們是不方便的, 當如果把它們寫死的話, 有時我們可能又需要適當變化, 這個矛盾怎麼解決呢? 答案是利用預設值和邏輯判斷.

如果是常用的語言, 如 java, javascript, 寫個這種判斷相信對你來說是個再簡單不過的事, 對於 bash 這種指令碼語言, 最大的問題還是我們不熟悉其語法, 那麼這個還是跟之前說的那樣, 查查手冊, 或搜尋下, 過程就省略了, 具體來說, 可以這樣:

#!/bin/bash
lineCount=1000
if [ $2 ]; then
	lineCount=$2
fi

contextCount=10
if [ $3 ]; then
	contextCount=$3
fi

cd /usr/local/nginx/logs
tail -n $lineCount error.log | grep --color -$contextCount $1

簡單說就是定義兩個變數lineCountcontextCount, 分別具有 1000 和 10 兩個預設值, 然後利用 if 判斷使用者是否輸入了第二和第三個引數, 如果有, 就用它們的值取代預設值, 沒有的話就使用預設值, 這樣一來就比較靈活了.

如果只輸入了關鍵字:

./search.sh hi

結果就是這樣:

tail -n 1000 error.log | grep --color -10 hi

輸入兩個引數:

./search.sh hello 300

結果就是這樣:

tail -n 300 error.log | grep --color -10 hello

輸入三個引數:

./search.sh hey 500 8

結果就是這樣:

tail -n 500 error.log | grep --color -8 hey

當然還是有個問題, 當你想只調整第三個引數時, 你還是必須得傳入第二個引數, 否則傳入的值只會被第二個引數優先獲得.

命令輸出

最後, 如果你想在執行前回顯一下將要執行的命令, 還可以利用 echo 這個命令來實現, 它同樣支援變數:

#!/bin/bash
lineCount=1000
if [ $2 ]; then
	lineCount=$2
fi

contextCount=10
if [ $3 ]; then
	contextCount=$3
fi

cd /usr/local/nginx/logs
echo "========= tail -n $lineCount error.log | grep --color -$contextCount $1"
tail -n $lineCount error.log | grep --color -$contextCount $1

這樣一來, 執行前就會先打印出將要執行的命令.

總結

綜上所述, 從單個命令到複合命令, 再到指令碼化和引數化, 其實是用了程式設計中的抽象這一手法, 這是我們解決重複性以及解決複雜性的一種重要手段.

當一個命令或幾個的複合命令比較繁瑣時, 我們就用一個指令碼檔案去做抽象, 保留不變的東西, 把變化的東西引數化, 外部化, 通過這樣的方式, 就簡化了執行(呼叫)的過程, 減少了重複.

畢竟, 如果你經常需要查詢日誌的話, 輸入簡單的 ./search.sh foo 比反覆輸入如此之長的一串 tail -n 1000 error.log | grep --color -10 foo 要方便快捷的多.

作為一名程式設計師, 減少重複是我們的天職, 我們應該是怕重複, 怕麻煩的, 某種意義上, 我們應該是"懶惰"的:

還記得 Perl 語言的發明人 Larry Wall 的話嗎: "優秀程式設計師應該有三大美德:懶惰、急躁和傲慢(laziness, impatience and hubris)"

更多關於抽象及重複的話題, 可以參考之前 電腦科學及重複性管理 專題, 關於使用 tail 結合 grep 查詢日誌關鍵字並高亮及顯示所在行上下文就介紹到這裡.