问题场景
测试Burrow的评估功能,加了自定义打印的日志,然后每一条自己打印的日志格式是这样的
{"level":"warn","ts":1532414931.7416606,"msg":"[XYZ]Update Offset: 2931 Lag: 248 Timestamp: 1532414619051",
"type":"module","coordinator":"storage","class":"inmemory","name":"mystorage","worker":0,
"cluster":"mycluster","consumer":"myconsumers","topic":"xyz-test","partition":0,"topic_partition_count":0,
"offset":2931,"timestamp":1532414619051,"owner":"","request":"StorageSetConsumerOffset"}
原本就是一行,为了方便阅读,我手动换行,我感兴趣的是msg
对应的3个字段:Offset
、Lag
、Timestamp
。很自然地,可以用正则表达式去做。由于对其他的信息不感兴趣,所以也不必大费周章用个JSON解析库。
本来想着趁机复习下C++的regex
,然并卵,脚本语言才是王道,虽然现在我看文档写C++可能会比搜资料写脚本快,但是在熟悉的前提下,写脚本肯定是快些。Python是非常棒的脚本语言,但是对于这种简单的任务而言,一条命令就能搞定,即使是Python也得打开文件之类,一旦写了代码,也会忍不住去错误处理。最后选择用grep+sed+awk这个文本处理三剑客。
1. grep提取目标行
grep "\[XYZ\].\+Offset" burrow.log
grep
是查找文件burrow.log中包含的字符串,相当于regex_search
,而非regex_match
。grep
采用的是标准正则表达式(Basic Regex Expression, BRE),因此+
必须加上转义符号\+
。
使用扩展正则表达式(Extend Regex Expression, ERE)可以加上-e
选项,也可以干脆用egrep
egrep "\[XYZ\].+Offset" logs/burrow.log | less
2. sed分析关键词
这一步等价于正则替换regex_replace
,我们实际需要的是这一段
Offset: 2931 Lag: 248 Timestamp: 1532414619051
于是只用简单地解析数字,注意sed
只支持标准正则表达式,甚至连\d
这种扩展是不识别的,只能用[[:digit:]]
,之前我一直以为vim
的替换是用sed
,实际上只是形式相近,两者并没什么关系。
参考链接search-and-replace-inconsistency-between-vim-and-sed
如果把第1步grep
的结果> res
重定向到文件res
中,vim
打开该文件后只要切换到命令模式下输入
:%s/.*Offset: \(\d\+\) Lag: \(\d\+\) Timestamp: \(\d\+\).*/\1 \2 \3/g
就可以批量替换文件,但是这种方式不便于把整个过程合成一步,因此还是要用sed
。
sed -i 's/.*Offset: \([[:digit:]]\+\) Lag: \([[:digit:]]\+\) Timestamp: \([[:digit:]]\+\).*/\1 \2 \3/' res
-i
选项是修改文件,不加该选项就是将结果打印到标准输出。sed
可以像这样指定文件路径作为命令行参数,如果没有指定,则从标准输入中读取内容,因此可以用管道连接grep
和sed
。
3. awk格式化输出
对这个问题而言,上一步其实已经足够了,但是打印格式会像这样
6 3173 1532344763332
9 3170 1532344768326
11 3168 1532344773326
// ...
28 3151 1532344828327
29 3150 1532344832624
2 3177 1532344848326
3 3176 1532344853314
// ...
338 2841 1532347936968
339 2840 1532347956968
2925 254 1532400463159
2917 262 1532402348131
2917 262 1532402509929
awk
能读取以分隔符(默认是空格)分隔的字符串,然后print
打印出来,更强大的是awk
也可以用C风格的printf
,因此命令如下
awk '{printf "%5d %4d %d\n", $1, $2, $3}' res
格式化参数就不说了,$1
、$2
、$3
即该行分隔后的第1个、第2个、第3个参数。这里得强调了,$0
代表一整行(不包含换行符)。
4. 封装成python脚本
#!/your-python-dir/python3
# analysis_log.py
import os
import sys
def usage(filename):
print('usage: %s log-file [window-size]' % filename)
os._exit(1)
if __name__ == '__main__':
if len(sys.argv) < 2:
usage(sys.argv[0])
if sys.argv[1] == '-h' or sys.argv[1] == '--help':
usage(sys.argv[0])
command = 'cat ' + sys.argv[1] + ' | grep "\[XYZ\].\+Offset" | sed ' \
+ "'s/.*Offset: \([[:digit:]]\+\) Lag: \([[:digit:]]\+\) Timestamp: \([[:digit:]]\+\).*/\\1 \\2 \\3/'" \
+ \
''' \
| awk '{printf "%5d %4d %d\\n", $1, $2, $3}' \
''' \
if len(sys.argv) > 2:
command = command + " | tail -n " + sys.argv[2]
# print("# " + command)
os.system(command)
其实用Python本身也能做,这里Python仅仅是简单解析命令行参数,因为模板字符串既带有单引号又带有双引号,用Python这种。最初是想采用str.format()
方法,但是发现模板字符串中带%d
解析起来有点坑,于是干脆算了,直接拼接字符串。
执行方式
python3 analysis_log.py
看起来比shell
和可执行程序麻烦不少,毕竟每次都要敲python3这几个字符,而不能直接./analysis_log.py
允许。实际上是可以的,但是这样的话第1行#!/your-python-dir/python3
就必不可少,这一行必须以#!
(注意不能有空格)开始。
这里我为了不透露自己的HOME目录(包含我的私密信息),所以用了your-python-dir
代替,实际上是要指明解释器的路径,参考我机器上python解释器和shell解释器的信息
$ file /bin/bash
/bin/bash: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, stripped
$ file ~/local/python3.6.0/bin/python3.6
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, not strippe
这是我自己编译安装的Python3.6,所以不像大多数代码指定的默认安装目录#!/usr/bin/python3
。
更详细的内容参考《Unix环境高级编程》8.12 解释器文件。
总之这就具备了直接执行的准备条件,只要给该脚本加上可执行权限即可
$ ./analysis_log.py --help
-bash: ./analysis_log.py: Permission denied
$ chmod u+x analysis_log.py
$ ./analysis_log.py --help
usage: ./analysis_log.py log-file [window-size]
此外,grep
、sed
、awk
的选项也非常多,实际使用时再参考Linux手册然后积累经验,不必死记硬背。看英文累的也可以直接搜索用法,结果应该是比较多的。