日志分析
业务中会生成大量的系统日志、应用程序日志、安全日志等,通过对日志的分析可以了解服务器的负载、健康状况,可以分析客户的分布情况、客户的行为,甚至基于这些分析可以做出预测
一般采集流程
日志产出->采集(Logstash、Flume、Scribe)->存储->分析->存储(数据库、NoSQL)->可视化
开源实时日志分析ELK平台
Logstash收集日志,并存放到ElasticSearch集群中,Kibana则从ES集群中查询数据生成图表,返回浏览器端
分析日志à正则表达式提取数据à异常处理(抛异常或者返回特殊值None)
数据载入à窗口函数à数据分发(实现分发器)
(读文件) (时间窗口:数据求值,丢掉过期数据) (多线程,队列queue)
à文件加载及分析器实现
(整合前面功能) (状态码分析)
分析的前提
半结构化数据
日志是半结构化数据,是有组织的,有格式的数据。可以分割成行和列,就可以当作表理解和处理,分析里面的数据
文本分析
日志是文本文件,需要依赖文件IO、字符串操作、正则表达式等技术
通过这些技术就能够把日志中需要的数据提取出来
目标数据形如:
line ='''220.181.108.107
- - [18/Apr/2017:01:37:39
+0800]\"GET
/ HTTP/1.1" 200 8416 "-"\"Mozilla/5.0
(compatible; Baiduspider/2.0;
+http://www.baidu.com/search/spider.html)"'''
nginx、tomcat等WEB
Server都会产生这样的日志
提取数据
一、空格分割
importdatetime
defmakekey(line:str,chars=set('''
[]'"''')):
start
=0length
=len(line)
flag
=False#假设没有碰到开关fori,cinenumerate(line):ifcinchars:ifc
=='[':#开始,直到右括号start
= i +1#跳过[flag
=True
elifc
==']':
flag
=False#结束elifc
=='"':
flag
=notflag#双引号ifflag:#第一次碰到双引号start
= i +1ifflag:continue
ifstart
== i:
start
= i +1continue
yieldline[start:i]
start
= i +1else:ifstart
< length:yieldline[start:]
line ='''220.181.108.107
- - [18/Apr/2017:01:37:39
+0800]\"GET
/ HTTP/1.1" 200 8416 "-"\"Mozilla/5.0
(compatible; Baiduspider/2.0;
+http://www.baidu.com/search/spider.html)"'''names
= ('remote','','','datetime','request','status','size','','useragent')
ops = (None,
None,
None,#时间类型转换lambdadstr:
datetime.datetime.strptime(dstr,'%d/%b/%Y:%H:%M:%S
%z'),
lambdarequest:dict(zip(['method','url','protocol'],request.split())),#添加字段名int,int,
None, None) defextract(line:str):returndict(map(#映射字段名lambdatriple:
(triple[0],triple[1](triple[2])iftriple[1]elsetriple[2]),zip(names,ops,makekey(line))
)
) print(extract(line))#
print(list(makekey(line)))
二、正则表达式提取
importdatetimeimportre
line ='''220.181.108.107
- - [18/Apr/2017:01:37:39
+0800]\"GET
/ HTTP/1.1" 200 8416 "-"\"Mozilla/5.0
(compatible; Baiduspider/2.0;
+http://www.baidu.com/search/spider.html)"'''
ops
= {'datetime':lambdatimestr:
datetime.datetime.strptime(timestr,'%d/%b/%Y:%H:%M:%S
%z'),'status':int,'length':int}
pattern ='''(?P[\d.]{7,})
- - \[(?P[/\w +:]+)\]\"(?P\w+)
(?P\S+) (?P[\w/\d.]+)"\(?P\d+)
(?P\d+) .+ "(?P.+)"'''regex
= re.compile(pattern)defextract(line:str)
->dict:
matcher
= regex.match(line)return{k:
ops.get(k,
lambdax:x)(v)fork,vinmatcher.groupdict().items()}print(extract(line))
异常处理
日志中不免会出现一些不匹配的行,需要处理
这里使用re.match方法,有可能匹配不上。所以要增加一个判断
采用抛出异常的方式,让调用者获得异常并自行处理
defextract(line:str)
->dict:"""返回字段的字典,抛出异常说明匹配失败"""matcher
= regex.match(line)ifmatcher:return{k:
ops.get(k,
lambdax:
x)(v)fork,vinmatcher.groupdict().items()}else:raiseException('No
match. {}'.format(line))#或输出日志记录
或者返回一个特殊值,告知调用者没有匹配
defextract(line:str)
->dict:"""返回字段的字典,如果返回None说明匹配失败"""matcher
= regex.match(line)ifmatcher:return{k:
ops.get(k,
lambdax:
x)(v)fork,vinmatcher.groupdict().items()}else:raise
None#或输出日志记录
滑动窗口
数据载入
defload(path):"""装载日志文件"""withopen(path)asf:forlineinf:
fields
= extract(line)iffields:yieldfieldselse:continue#TODO解析失败就抛弃,或者打印日志
时间窗口分析
概念
很多数据,例如日志,都是和时间相关的,都是按照时间顺序产生的
产生的数据分析的时候,要按照时间求值
interval表示每一次求值的时间间隔
width时间窗接口宽度,指一次求值的时间窗口宽度
当width
> interval
数据求值时会有重叠
当width
= interval
数据求值没有重叠
时序数据
运维环境中,日志、监控等产生的数据都是与时间相关的数据,按照时间先后产生并记录下来的数据,所以一般按照时间对数据进行分析
数据分析基本程序结构
无限的生成随机数函数,产生时间相关的数据,返回时间和随机数字典
每次取3个数据,求平均值
importrandomimportdatetimeimporttime
defsource():while
True:yield{'value':
random.randint(1,100),'datetime':datetime.datetime.now()}
time.sleep(1)#攒一批数据s
= source()
items = [next(s)for_inrange(3)]#处理函数,送入一批数据计算一个结果defhandler(iterable):returnsum(map(lambdaitem:
item['value'],iterable))
/len(iterable)print(items)print("{:2f}".format(handler(items)))
窗口函数实现
将上面的获取数据的程序扩展为window函数,使用重叠的方案
importrandomimportdatetimeimporttimedefsource(second=1):"""生成数据"""while
True:yield{'value':
random.randint(1,100),'datetime':
datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8)))}
time.sleep(second)defwindow(iterator,handler,width:int,interval:int):"""窗口函数:paramiterator:数据源,生成器,用来拿数据:paramhandler:数据处理函数:paramwidth:时间窗口宽度,秒:paraminterval:处理时间间隔,秒"""start
= datetime.datetime.strptime('20170101
000000 +0800','%Y%m%d
%H%M%S %z')
current
= datetime.datetime.strptime('20170101
010000 +0800','%Y%m%d
%H%M%S %z')
buffer
= []#窗口的待计算数据delta
= datetime.timedelta(seconds=width
- interval)while
True:#从数据源获取数据data
=next(iterator)ifdata:
buffer.append(data)#存入临时缓冲等待计算current
= data['datetime']#每隔interval计算buffer中的数据一次if(current
- start).total_seconds() >= interval:
ret
= handler(buffer)print('{:.2f}'.format(ret))
start
= current#清除超出width的数据buffer
= [xforxinbufferifx['datetime']
> current - delta]#处理函数,送入一批数据计算一个结果defhandler(iterable):returnsum(map(lambdaitem:
item['value'],iterable))
/len(iterable)
window(source(),handler,10,5)
分发
生产者消费者模型
对于一个监控系统,需要处理很多数据,包括日志。对其中已有数据的采集、分析。
被监控对象就是数据的生产者producer,数据的处理程序就是数据的消费者consumer
生产者消费者传统模型
传统的生产者消费者模型,生产者生产,消费者消费。但这种模型有问题
开发的代码耦合高,如果生成规模扩大,不易扩展,生产和消费的速度很难匹配等。
解决办法:队列
作用:解耦、缓冲
日志生产者往往会部署好几个程序,日志产生的也很多,而消费者也会有很多个程序,去提取日志分析处理
数据生产是不稳定的。会造成段时间数据的潮涌,需要缓冲
消费者消费能力不一样,有快有慢,消费者可以自己决定消费缓冲区中的数据
单机可以使用queue内建的模块构建进程内的队列,满足多个线程间的生产消费需要
大型系统可以使用第三方消息中间件:RabbitMQ、RocketMQ、Kafka
queue模块——队列
queue.Queue(maxsize=0)
创建FIFO队列,返回Queue对象
maxsize小于等于0,队列长度没有限制
Queue.get(block=True,timeout=None)
从队列中移除元素并返回这个元素
block阻塞,timeout超时
如果block为True,是阻塞,timeout为None就是一直阻塞
如果block为True但是timeout有值,就阻塞到一定秒数抛出异常
block为False,是非阻塞,timeout将被忽略,要么成功返回一个元素,要么抛出empty异常
Queue.get_nowait()
等价于get(False)
Queue.put(item,block=True,timeout=None)
把一个元素加入到队列中去
block=True,timeout=None,一直阻塞直至有空位放元素
block=True,timeout=5,阻塞5秒就抛出Full异常
block=True,timeout失效,立刻返回,,一直阻塞直至有空位放元素
Queue.put_nowait(item)
等价于put(item,False)
分发器实现
实现多线程t
= threading.Thread(target=函数名(add),args=参数(4,5))方法
t.start()启动线程
生产者(数据源)生产数据,缓冲到消息队列中
数据处理流程:
数据加载->提取->分析(滑动窗口函数)
处理大量数据的时候,可能需要多个消费者处理
需要一个分发器(调度器),把数据分发给不同的消费者处理
每一个消费者拿到数据后,有自己的处理函数。所以要有一种注册机制
数据加载->提取->分发->分析函数1&分析函数2
分析1和分析2可以是不同的handler、窗口宽度、间隔时间
暂时采用轮询策略,一对多的副本发送,一个数据通过分发器、发送到多个消费者
消息队列
在生产者和消费者之间使用消息队列,那么所有消费者可以共有一个消息队列,或各自拥有一个消息队列
公用一个消息队列需要解决争抢问题。每个消费者拥有一个队列较易实现
注册
在调度器内部记录消费者,每一个消费者拥有自己的队列
线程
由于一条数据会被多个不同的注册过的handler处理,所以最好的方式就是多线程
分发器代码实现
importrandomimportdatetimeimporttimefromqueueimportQueueimportthreading
defsource(second=1):"""生成数据"""while
True:yield{'datetime':
datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))),'value':
random.randint(1,100),}
time.sleep(second)#平均数defavg_handler(iterable):returnsum(map(lambdaitem:
item['value'],iterable))
/len(iterable) defwindow(q:
Queue,handler,width:int,interval:int):
start
= datetime.datetime.strptime('20170101
00:00:01 +0800','%Y%m%d
%H:%M:%S %z')
current
= datetime.datetime.strptime('20170101
00:00:01 +0800','%Y%m%d
%H:%M:%S %z')
buf
= []#窗口的待计算数据delta
= datetime.timedelta(seconds=width
- interval)while
True:#从数据源获取数据data
= q.get()#阻塞的ifdata:
buf.append(data)#存入临时缓冲等待计算current
= data['datetime']print(current,start)#每隔interval计算buffer中的数据一次if(current
- start).total_seconds() > interval:print('---------------')
ret
= handler(buf)print('{:.2f}'.format(ret))print(threading.current_thread())
start
= current#清除超出width的数据buf
= [xforxinbufifx['datetime']
> current - delta]defdispatcher(src):
handlers
= []#分发器中记录handler,同时保存各自的队列queues
= []defreg(handler,width:int,interval:int):
q
= Queue()
t
= threading.Thread(target=window,args=(q,handler,width,interval))
queues.append(q)
handlers.append(t)defrun():fortinhandlers:
t.start()#启动线程处理数据while
True:
date
=next(src)#改这里数据源不同forqinqueues:
q.put(date)returnreg,run
reg,run
= dispatcher(source())
reg(avg_handler,10,5)print(threading.current_thread())
run()
整合代码
load函数就是从日志中提取合格的数据生成函数
它可以作为dispatcher函数的数据源
importrandomimporttimefromqueueimportQueueimportthreadingimportdatetimeimportrefrompathlibimportPathfromuser_agentsimportparse
pattern ='''(?P[\d.]{7,}) - - \[(?P[/\w +:]+)\]\"(?P\w+) (?P\S+) (?P[\w/\d.]+)"\(?P\d+) (?P\d+) .+ "(?P.+)"'''regex = re.compile(pattern)
ops = {'datetime':lambdadstr: datetime.datetime.strptime(dstr,'%d/%b/%Y:%H:%M:%S %z'),'status':int,'size':int}defextract(line:str) ->dict:"""返回字段的字典,如果返回None说明匹配失败"""matcher = regex.match(line)ifmatcher:return{k: ops.get(k, lambdax: x)(v)fork,vinmatcher.groupdict().items()}else:#输出日志, 记录不合格returnline# None
# raise Exception{
# 'No match. {}'.format(line)
# }
#数据源defloadfile(filename:str,encoding='utf-8'):"""装载日志文件"""withopen(filename,encoding=encoding)asf:forlineinf:
fields = extract(line)ifisinstance(fields,(dict,)):yieldfieldselse:# print('No match. {}'.format(fields)) #TODO解析失败就抛弃,或者打印日志pass
defload(*paths,encoding='utf-8',ext='*.log',r=False):forpinpaths:
path = Path(p)ifpath.is_dir():ifisinstance(ext,str):
ext = [ext]foreinext:
logs = path.glob(e)ifrelsepath.glob(e)#遍历当前目录forloginlogs:# Path对象yield fromloadfile(str(log.absolute()),encoding=encoding)# for x in loadfile(str(log.absolute())):
# yield xelifpath.is_file():yield fromloadfile(str(path.absolute()),encoding=encoding)#模拟用的数据源defsource(second=1):"""生成数据"""while True:yield{'datetime': datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))),'value': random.randint(1,100)}
time.sleep(second)# s = source()
#分析函数, 处理函数#平均数defavg_handler(iterable):returnsum(map(lambdaitem: item['value'],iterable)) /len(iterable)#状态码分析defstatus_handler(iterable:list):#指的是一个时间段内的数据list(dicts)status = {}foriteminiterable:# item => fieldskey = item['status']
status[key] = status.get(key,0) +1total =len(iterable)return{k: v/totalfork,vinstatus.items()}
allbrowser = {}#浏览器分析defbrowser_handler(iterable):
browser = {}foriteminiterable:# item => fieldsuastr = item['useragent']
ua = parse(uastr)
key = ua.browser.family,ua.browser.version_string
browser[key] = browser.get(key,0) +1allbrowser[key] = allbrowser.get(key,0) +1returnbrowser#窗口函数defwindow(q: Queue,handler,width:int,interval:int):
start = datetime.datetime.strptime('20170101 00:00:01 +0800','%Y%m%d %H:%M:%S %z')
current = datetime.datetime.strptime('20170101 00:00:01 +0800','%Y%m%d %H:%M:%S %z')
buf = []#窗口的待计算数据delta = datetime.timedelta(seconds=width - interval)while True:#从数据源获取数据data = q.get()#阻塞的消息队列ifdata:
buf.append(data)#存入临时缓冲等待计算current = data['datetime']print(current,start)#每隔interval计算buffer中的数据一次if(current - start).total_seconds() > interval:print('---------------')
ret = handler(buf)print('{}'.format(ret))print(threading.current_thread())
start = current#清除超出width的数据buf = [xforxinbufifx['datetime'] > current - delta]#还有持久化、可视化#分发器,觉定者数据的调度defdispatcher(src):
handlers = []#分发器中记录handler,同时保存各自的队列queues = []defreg(handler,width:int,interval:int):
q = Queue()
t = threading.Thread(target=window,args=(q,handler,width,interval))
queues.append(q)
handlers.append(t)defrun():fortinhandlers:
t.start()#启动线程处理数据while True:# date = next(src) #改这里数据源不同fordatainsrc:forqinqueues:
q.put(data)returnreg,run
if__name__ =='__main__':
src = load('.')#生成器reg,run = dispatcher(src)
reg(status_handler,10,5)
reg(browser_handler,10,10)# print(threading.current_thread())run()
完成分析功能
分析日志很重要,通过海量数据分析就能够知道是否遭受了***,是否被爬取及爬取高峰期,是否有盗链等
状态码分析
状态码中包含了很多信息。例如
304,服务器收到客户端提交的请求参数,发现资源未变化,要求浏览器使用静态资源的缓存
404,服务器找不到大请求的资源
304占比大,说明静态缓存效果明显。404占比大,说明网站出现了错误链接,或者尝试嗅探网站资源
如果400、500占比突然增大,网站一定出了问题。
defstatus_handle(iterable):
status
= {}foriteminiterable:
key
= item['status']
status[key]
= status.get(key,0)
+1total
=len(iterable)return{k:
v / totalfork,vinstatus.items()}
浏览器分析
useragent
这里指的是,软件按照一定的格式向远端的服务器提供一个表示自己的字符串
在HTTP协议中,使用useragent字段传送这个字符串
浏览器选项中可以修改此设置
信息提取
安装
pipinstallpyyaml
ua-parseruser-agents
数据分析
defbrowser_handler(iterable):
browser
= {}foriteminiterable:
uaster
= item['useragent']
ua
= parse(uaster)
key
= ua.browser.family,ua.browser.version_string
browser[key]
= browser.get(key,0)
+1returnbrowser
谢谢观看!!!