本次讨论的日志范围
日志在计算机系统中是一个非常广泛的概念,任何程序都有可能输出日志:操作系统内核、各种应用服务器等等。日志的格式也没有限定,有些为文本文件,有些为特殊的二进制格式如 Windows 日志。这里讨论的日志处理方法仅指文本文件格式的日志,如Web服务器 IIS、Tomcat等产生的用户访问日志,以及各种Web应用程序自己输出的日志等。这些日志还有一个共同的特点就是其为半结构化数据,可以使用模式匹配的方式将其转化为结构化数据。
为什么要解析日志
以Web日志为例,Web日志中包含了大量有价值的信息。最简单的,我们可以从中获取网站每类页面的PV值(PageView,页面访问量)、独立IP数(即去重之后的IP数量)等;稍微复杂一些的,可以计算得出用户所检索的关键词排行榜、用户停留时间最高的页面等;更复杂的,构建广告点击模型、分析用户行为特征等等。
另外还有一类产品,它们不分析直接日志,而是通过让用户在页面中嵌入js代码的方式来直接进行数据统计,或者说我们可以认为它是直接让日志输出到了它们的服务器。典型的代表产品——大名鼎鼎的Google Analytics,另外还有国内的cnzz、百度统计等。但是这些产品提供的功能都相对比较通用,一般只限于上面说到的PV, UV, 关键词排行之类的,当有特殊需求时还需要自己来完成日志的解析工作来实现。
LogStash
Logstash 是开源的服务器端数据处理管道,能够同时 从多个来源采集数据、转换数据,然后将数据发送到您最喜欢的 “存储库” 中。Logstash 能够动态地转换和解析数据,不受格式或复杂度的影响:
利用 Grok 从非结构化数据中派生出结构
从 IP 地址破译出地理坐标
将 PII 数据匿名化,完全排除敏感字段
整体处理不受数据源、格式或架构的影响
过滤器库丰富多样,拥有无限可能。
使用Docker来尝试一下:
首先,拉取开源版的logstash最新版:
--rm 表示容器退出后自动删除
-it 表示以终端交互式运行
-v 表示把本机上~/pipleline目录映射到容器内的/usr/share/logstash/pipeline目录,这个目录为logstash的运行配置文件所在的目录。程序运行时会自动从这个目录加载配置文件并执行。
docker.elastic.co/logstash/logstash-oss:6.3.0 为镜像名称
我们使用以下配置来运行命令
input { stdin { } } filter { grok { match => { "message" => "%{TIMESTAMP_ISO8601:datetime} %{IP:s-ip} %{WORD:cs-method} %{URIPATH:cs-uri-stem} %{NOTSPACE:cs-uri-query} %{NUMBER:s-port} %{NOTSPACE:cs-username} %{IP:c-ip} %{NOTSPACE:userAgent} %{NOTSPACE:cookie} %{NOTSPACE:referer} %{NUMBER:sc-status} %{NUMBER:sc_substatus} %{NUMBER:sc_winstatus} %{NUMBER:sc-bytes} %{NUMBER:cs-bytes} %{NUMBER:time-taken}" } } } output { stdout {} }
在input中使用stdin即键盘输出,在output中使用stdout即屏幕。在filter中使用grok并给定了表达式。这个表达式最终会变成正则表达式来匹配输入。
表过式的语法为%{正则或已定义好的模式:标识符},通过这样的转换我们就把难以维护的正则表达式都进行命名,再将匹配组进行标识化,大大提高了正则匹配可维护度。
在以下目录中可以找到系统定义好的pattern,目录中的版本号可能有所不同:/logstash-6.2.4/vendor/bundle/jruby/2.3.0/gems/logstash-patterns-core-4.1.2/patterns。如本例中使用的TIMESTAMP_ISO8601,IP等就定义在grok-patterns文件中。
在测试时可以使用以下这个日志行来进行:
Grok
如果我们想在自己的项目中也使用grok,来完成一些半结构化数据的处理。可以尝试这个开源项目:
https://github.com/thekrakken/java-grok
public static void main(String[] args) { /* Create a new grokCompiler instance */ GrokCompiler grokCompiler = GrokCompiler.newInstance(); grokCompiler.registerDefaultPatterns(); /* Grok pattern to compile, here httpd logs */ final Grok grok = grokCompiler.compile("%{TIMESTAMP_ISO8601:datetime} %{IP:s-ip} %{WORD:cs-method} %{URIPATH:cs-uri-stem} %{NOTSPACE:cs-uri-query} %{NUMBER:s-port} %{NOTSPACE:cs-username} %{IP:c-ip} %{NOTSPACE:userAgent} %{NOTSPACE:cookie} %{NOTSPACE:referer} %{NUMBER:sc-status} %{NUMBER:sc_substatus} %{NUMBER:sc_winstatus} %{NUMBER:sc-bytes} %{NUMBER:cs-bytes} %{NUMBER:time-taken}"); /* Line of log to match */ String log = "2013-03-21 15:59:59 27.54.194.244 GET /data-source/acc/3d/10026.swf - 80 - 120.83.65.98 Mozilla/4.0+(compatible;+MSIE+8.0;+Windows+NT+6.1;+WOW64;+Trident/4.0;+QQDownload+718;+SLCC2;+.NET+CLR+2.0.50727;+.NET+CLR+3.5.30729;+.NET+CLR+3.0.30729;+Media+Center+PC+6.0;+.NET4.0C;+.NET4.0E) Hm_lpvt_dd614ea78861a7710d8659f866d5c807=1363881587;+pgv_si=s9497496576;+Hm_lvt_dd614ea78861a7710d8659f866d5c807=1363763148,1363765488,1363765519,1363880532;+pgv_pvi=714221568 http://www.sdgundam.cn/unit/10026 200 0 0 34640 630 270"; Match gm = grok.match(log); /* Get the map with matches */ final Map capture = gm.capture(); capture.forEach((k, v) -> { System.out.println(String.format("%s = %s", k, v)); }); }