日志监控,是一种外挂式的采集。通过读取进程打印的日志,来进行监控数据的采集与汇聚计算。汇聚成标准的时间序列数据之后,推送给统一的后端存储。日志监控是一种典型的应用、业务监控的手段,如果我们没法在应用程序里内嵌SDK埋点,使用日志监控不失为一种折中方案。
这么说好像还不太明白日志监控到底能够做什么,简单点就是说就是对程序的日志内容进行过滤,如果出现了我们设定的关键字,对其进行计数当达到一定数量时可以触发报警。
下面来看一下需要哪些步骤:
1、不断的监听日志文件,获取最新的日志内容
2、正则功能对日志内容进行过滤
3、计数功能,触发报警
第一步的实现其实很简单,我参考了 python实现tail -f 功能 这篇文章有兴趣的话可以看一下。
思路是什么呢
打开一个文件,把指针移到最后。
每隔1秒钟获取一下日志内容
看一下代码吧
import time
import sys
file = '/Users/cyt/work/my_python/script/logMonitoring/test.txt'
with open(file,'r') as f:
f.seek(0,2)
while True:
line = f.readline()
if line and line!= '\n': #空行不打印
sys.stdout.write(line)
time.sleep(1)
这样就可以实现一个不断监听日志文件并打印最新内容的功能了
代码中用到了seek , stdout.write。stdout.write
和 print
的区别就是 print
会打印换行符,stdout.write
打印换行符。
在上面的代码中
sys.stdout.write(line)
#等同
line.strip()
print(line)
这里有个问题,那就是 sleep(1)
当我们同时写入多行内容到一个文件时,会一行一行很慢的打印出来。你可以把时间调小一点,但下面会说一个更好的方法。(你会说直接不用sleep不就行了,你可以尝试一下,然后观察该进程在系统中的cpu使用率)
你哪怕设置为0.1 都要比不设置sleep好很多,下图是设置为0.1的效果
使用类实现上面的逻辑,并改进打印速度的问题。
class LogMonitoring():
def __init__(self,file_name):
self.__file_name = file_name
def source(self):
with open(self.__file_name) as f:
f.seek(0,2)
while True:
data = f.readlines()
# 可以添加日志量的监控,如果line列表越长说明单位时间产生的日志越多
# print(len(line))
for line in data:
if line != '\n': #空行不处理
sys.stdout.write(line)
time.sleep(1)
file = '/Users/cyt/work/my_python/script/logMonitoring/test.txt'
test = LogMonitoring(file)
test.source()
如何解决刚刚说过的那个问题呢,很简单把f.readline()
替换为 f.readlines()
区别想必很多人都知道,这里再说明一下。前者每次获取一行内容,后者是获取到当前指针到末尾所有的内容,返回一个列表每一个列表元素就是一行内容。然后我们遍历这个列表,输出列表元素就可以了。
这个速度是非常快的,相当于不加sleep的速度,其实就算你设置sleep为0.1 如果一下写入上千行内容,那么要近两分钟的时间才能打印完(0.1 * 1000 =100s)。
然后你还可以使用len(line) 获取列表长度,以此来获取单位时间内产生日志的数量,这个单位时间就是sleep的时间。这个单位时间内产生的日志数量,这个指标在某些需求中很有用。
你不用担心列表会占用大量内存的问题,即使是一个长度为1000的列表也不会占用大量的内存(几MB吧),况且应该很难遇到1秒中写入这么多日志的情况吧,即使有也很好解决,调短sleep的时间。或者你可以测试一下一个长度1万的列表会占用多大的内存。使用sys.getsizeof
可以自己玩一下。
到这里第一个问题应该算是搞定了
剩下的的两个我先把完整代码贴出来再一起说吧
class LogMonitoring():
def __init__(self,file_name,pattern,threshold):
self.__file_name = file_name
self.__pattern = pattern
self.__threshold = threshold
self.__count = 0
def source(self):
with open(self.__file_name) as f:
f.seek(0,2)
while True:
data = f.readlines()
# 可以添加日志量的监控,如果line列表越长说明单位时间产生的日志越多
# print(len(line))
for line in data:
if line != '\n': #空行不处理
self.check(line)
time.sleep(1)
#匹配规则
def check(self,data):
if re.search(f'{self.__pattern}.*?', data):
self.call()
return 0
else:
return 1
#报警规则
def call(self,):
self.__count += 1
if self.__count > self.__threshold:
print('***********')
调用逻辑就是,source获取日志内容,把内容给到check方法去过滤如果匹配设置的规则,那么调用call方法进行计数并判断计算器是否超过阀值。
这两个功能我没有测试(因为我突然发现我要做的不是这个东西),整个逻辑就是这样了。
正则的使用可以看这两篇文章: Python 中的正则表达式 和 正则表达式
最后说一些可以优化的点吧。
1、call 报警规则哪里可以完善一下,添加自己想要的告警规则,还可以在写一个告警途径,发挥自己的想象。或者你这里只需要有一个计数的功能,返回计数器的值,结合现成的监控系统去做监控(zabbix就OK)
2、可以建一个单独的配置文件 conf.py ,文件的路径、正则表达式的规则,匹配规则、报警规则等等都可以在这里配置
3、还有一个问题就是程序运行的过程中,日志被备份清空或者切割了,怎么处理。其实很简单,你维护一个指针位置,如果下次循环发现文件指针位置变了(tell()方法返回文件的当前位置,即文件指针当前位置。),从最新的指针位置开始读就行
4、可以考虑使用异步(协程)、线程来提高速度。
这是滴滴开源监控系统夜莺的日志监控,可以参考一下它的功能,使用go语言实现的。