ELK的基础架构搭建相对来说还是比较容易的,架构搭建完成后一个重要任务就是如何把生产中的服务器日志给解析好后

存进Elasticsearch了,这个过程中间最关键的一步就是如何使用filter插件格式化日志数据的问题。笔者在此过程中

也是被grok的正规表达式磨到想哭的地步,所以大家也不要遇到一点困难就选择放弃了!


logstash格式化日志的插件比较多,比较常用的就是grok、date、geoip

如果日志本身没有做成json格式,我们就需要使用grok格式先把日志解析成json格式,才能方便ES存储

grok解析日志时实际使用的是正则表达式来匹配相应字段后并给其字段命名

但如果每次匹配字段时都要直接使用正则表达式的元字符写表达式,是一件很痛苦的事情,所以官方在开发grok插件时,

就已经为用户提前写好了很多的现成的模式(也即使用元字符写好的一个表达式模块),我们调用grok插件解析日志,

大多都可以直接使用,但如果我们的日志是自己订制过的格式的话,就需要自行写grok表达式的模式了。

grok表达式是需要边写边调试的,好在ELK官方在5.5版本以后直接把grok调试工具集成在了kibana的web页面上了。

所以我们完全可以把整个ELK的环境搭建起来之后再去写grok表达式,这样就可以使用kibana本身集成的grok调试工具了。


下面我们简单展示一下grok格式化nginx错误日志(以下操作都是使用kibana集成的grok debug toos进行的)


源日志内容正文如下:

2018/08/29 21:34:53 [error] 1195#1195: *11 open() "/usr/share/nginx/html/favicon.ico" failed (2: No such file or directory), client: 192.168.10.1, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "192.168.10.150", referrer: "http://192.168.10.150/index0.html"


粗略格式化grok表达式

(?\d{4}/\d{2}/\d{2}\s{1,}\d{2}:\d{2}:\d{2})\s{1,}\[%{DATA:log_level}\]\s{1,}%{GREEDYDATA:error_message}


格式化后的效果

{

  "error_message": "1195#1195: *11 open() \"/usr/share/nginx/html/favicon.ico\" failed (2: No such file or directory), client: 192.168.10.1, server: localhost, request: \"GET /favicon.ico HTTP/1.1\", host: \"192.168.10.150\", referrer: \"http://192.168.10.150/index0.html\"",

  "log_level": "error",

  "timestamp": "2018/08/29 21:34:53"

}


精细格式化grok表达式

(?\d{4}/\d{2}/\d{2}\s{1,}\d{2}:\d{2}:\d{2})\s{1,}\[%{DATA:log_level}\]\s{1,}(%{NUMBER:pid:int}#%{NUMBER}:\s{1,}\*%{NUMBER}|\*%{NUMBER}) %{DATA:error_message}(?:,\s{1,}client:\s{1,}(?%{IP}|%{HOSTNAME}))(?:,\s{1,}server:\s{1,}%{IPORHOST:server})(?:, request: %{QS:request})?(?:, upstream: %{QS:upstream})?(?:, host: %{QS:host})?(?:, referrer: \"%{URI:referrer})?


{

  "error_message": "open() \"/usr/share/nginx/html/favicon.ico\" failed (2: No such file or directory)",

  "server": "localhost",

  "request": "\"GET /favicon.ico HTTP/1.1\"",

  "log_level": "error",

  "pid": 1195,

  "referrer": "http://192.168.10.150/index0.html",

  "host": "\"192.168.10.150\"",

  "client": "192.168.10.1",

  "timestamp": "2018/08/29 21:34:53"

}

由此可以对比看出,日志可以格式化的比较粗略,也可以格式化的比较精细,这个需要根据后期日后分析的需求来决定了,如果格式的比较粗略,

后期如果想对比较长的字段里面的一些内容做过滤分析的话,就会比较麻烦一些。


logstash关于nginx访问日志解析的具体配置

/etc/logstash/conf.d/nginx-access.conf

input {

  kafka {

    bootstrap_servers => "172.16.100.7:9092"

    topics => ["nginx-access"]

    consumer_threads => 5

    type => "nginx-access-log"

  }

}


filter {

  if [type] == "nginx-access-log" {

    grok {

      match => { "message" => "%{HOSTNAME:logserver} %{PATH:logpath} %{NGINX_ACCESS_LOG}" }

      remove_field  => "message"

    }

    date {

      match => [ "timestamp", "dd/MMM/yyyy:HH:mm:ss Z", "yyyy/MM/dd HH:mm:ss" ]

    }

    geoip {

      source => "remote_addr"

    }

  }

}


output {

  if [type] == "nginx-access-log" {

    elasticsearch {

      hosts => ["localhost:9200"]

      index => "nginx-%{+YYYY.MM.dd}"

      template => "/etc/logstash/conf.d/nginx.index"

      template_name => "nginx"

      template_overwrite => true

    }

  }

}


input部分我们配置logstash从kafka中指定的队列中去相应日志

consumer_threads配置用几个线程去kafka队列中去取日志

type字段是我们为了让filter能够进行提取对应队列中的数据自己添加了一个新的字段,

自定义的字段的值为string格式,所以需要用双引号括起来


filter部分在使用grok插件进行解析时必须匹配指定队列中的日志数据,

所以需要通过if语句进行判断,只有符合指定条件日志数据才会使用下方的grok表达式进行解析。

message正文内容使用了三串模式就写完了整条表达式,%{HOSTNAME:logserver}和%{PATH:logpath}这两段

是调用了默认pattern文件中预先定义好的pattern模块,logstash默认pattern文件路径:

/usr/share/logstash/vendor/bundle/jruby/2.3.0/gems/logstash-patterns-core-4.1.2/patterns/grok-patterns

%{NGINX_ACCESS_LOG}是用默认patter模块自行拼装的一个pattern的表达式,在我这里,我把和nginx相关的pattern都写在了

一个独立产pattern配置文件里面了,需要放置在/usr/share/logstash/vendor/bundle/jruby/2.3.0/gems/logstash-patterns-core-4.1.2/patterns此路径下,如果新增或修改了pattern文件内容,需要重启logstash服务


output插件也需要首先判断自定义的type类型后再进行输出处理,这里输出到ES,并且使用了自定义的映射模版。


自己编写的nginx模式集合文件

cat /usr/share/logstash/vendor/bundle/jruby/2.3.0/gems/logstash-patterns-core-4.1.2/patterns/nginx

NGINX_ALL %{IP:remote_addr} %{DATA:remote_user} \[%{HTTPDATE:timestamp}\] %{QS:request} %{NUMBER:status_code} %{NUMBER:body_bytes} %{QS:request_body} %{QS:http_referer} %{QS:user_agent} %{QS:x_forward_for}%{DATA:upstream_addr} %{DATA:upstream_response_time} %{NUMBER:request_time}


NGINX_NO_FORWARD_NO_UPSTREAM_ADDR %{IP:remote_addr} %{DATA:remote_user} \[%{HTTPDATE:timestamp}\] %{QS:request} %{NUMBER:status_code} %{NUMBER:body_bytes} %{QS:request_body} %{QS:http_referer} %{QS:user_agent} %{DATA:upstream_response_time} %{NUMBER:request_time}


NGINX_NO_UPSTREAM_ADDR %{IP:remote_addr} %{DATA:remote_user} \[%{HTTPDATE:timestamp}\] %{QS:request} %{NUMBER:status_code} %{NUMBER:body_bytes} %{QS:request_body} %{QS:http_referer} %{QS:user_agent} %{QS:x_forward_for} %{DATA:upstream_response_time} %{NUMBER:request_time}


NGINX_NO_REQUEST_BODY %{IP:remote_addr} %{DATA:remote_user} \[%{HTTPDATE:timestamp}\] %{QS:request} %{NUMBER:status_code} %{NUMBER:body_bytes} %{QS:http_referer} %{QS:user_agent} %{QS:x_forward_for}%{DATA:upstream_addr} %{DATA:upstream_response_time} %{NUMBER:request_time}


NGINX_NO HTTP_REFERER %{IP:remote_addr} %{DATA:remote_user} \[%{HTTPDATE:timestamp}\] %{QS:request} %{NUMBER:status_code} %{NUMBER:body_bytes} %{QS:request_body} %{QS:user_agent} %{QS:x_forward_for}%{DATA:upstream_addr} %{DATA:upstream_response_time} %{NUMBER:request_time}


NGINX_SIMPLE %{IP:remote_addr} %{DATA:remote_user} \[%{HTTPDATE:timestamp}\] %{QS:request} %{NUMBER:status_code} %{NUMBER:body_bytes} %{QS:http_referer} %{QS:user_agent} %{QS:x_forward_for}


NGINX_ACCESS_LOG %{NGINX_ALL}|%{NGINX_NO_FORWARD_NO_UPSTREAM_ADDR}|%{NGINX_NO_UPSTREAM_ADDR}|%{NGINX_NO_REQUEST_BODY}|%{NGINX_NO HTTP_REFERER}|%{NGINX_SIMPLE}


NGINX_ERROR_ALL (?\d{4}/\d{2}/\d{2}\s{1,}\d{2}:\d{2}:\d{2})\s{1,}\[%{DATA:log_level}\]\s{1,}(%{NUMBER:pid:int}#%{NUMBER}:\s{1,}\*%{NUMBER}|\*%{NUMBER}) %{DATA:error_message}(?:,\s{1,}client:\s{1,}(?%{IP}|%{HOSTNAME}))(?:,\s{1,}server:\s{1,}%{IPORHOST:server})(?:, request: %{QS:request})?(?:, upstream: %{QS:upstream})?(?:, host: %{QS:host})?(?:, referrer: \"%{URI:referrer})?


NGINX_ERROR_SIMPLE (?\d{4}/\d{2}/\d{2}\s{1,}\d{2}:\d{2}:\d{2})\s{1,}\[%{DATA:log_level}\]\s{1,}%{GREEDYDATA:error_message}


NGINX_ERROR_LOG %{NGINX_ERROR_ALL}|%{NGINX_ERROR_SIMPLE}


自己编写的php模式集合文件

cat /usr/share/logstash/vendor/bundle/jruby/2.3.0/gems/logstash-patterns-core-4.1.2/patterns/php 

DATETIME_PHP %{MONTHDAY}[./-]%{MONTH}[./-]%{YEAR} %{TIME}


PHP_ERROR_ALL \[%{DATETIME_PHP:timestamp}\]\s+%{LOGLEVEL:loglevel}:\s+%{GREEDYDATA:error_message}


#PHP_POOL_ERROR_ALL \[%{DATETIME_PHP:timestamp}\s+Asia/Shanghai\]\s+PHP\s+%{LOGLEVEL:loglevel}:\s+%{GREEDYDATA:error_message}

PHP_POOL_ERROR_ALL \[%{DATETIME_PHP:timestamp} %{DATA:timezone}\] %{WORD:mode} %{DATA:error_type}\: %{GREEDYDATA:log_content}\n(?m)%{GREEDYDATA:stack_trace}


PHP_ERROR_SIMPLE \[%{DATETIME_PHP:timestamp}\s+Asia/Shanghai\]\s+PHP\s+%{GREEDYDATA:error_message}


PHP_ERROR_LOG %{PHP_POOL_ERROR_ALL}|%{PHP_ERROR_ALL}|%{PHP_ERROR_SIMPLE}


#PHP_SLOW_LOG (?m)^\[%{DATETIME_PHP:timestamp}\]\s+\[pool\s+%{USERNAME:poolname}\]\s+pid\s+%{NUMBER:pid}\n%{USERNAME} = %{PATH:script_filename}\n%{GREEDYDATA:detail}^$


#PHP_SLOW_LOG \[%{DATETIME_PHP:timestamp}\]\s+\[pool\s+%{USERNAME:poolname}\]\s+pid\s+%{NUMBER:pid}\n%{USERNAME:source_name} = %{PATH:script_filename}\n%{GREEDYDATA:detail}$


PHP_SLOW_LOG \[%{DATETIME_PHP:timestamp}\]\s+\[pool\s+%{USERNAME}\]\s+pid\s+%{NUMBER}\n%{USERNAME} = %{PATH:script_filename}\n%{GREEDYDATA:slow_detail}$



注意、注意、请注意:

首先是把nginx日志的各种组合形式都用基础pattern或者正则元字符组成的单条pattern,

然后把各种组合好的单条pattern语句使用“或者”的逻辑判断组成一个能够解析多种日志组

合格式的完整pattern,在这里需要提醒大家的是,使用“或者”进行拼装时,需要把匹配条

件较精准的放在前面,否则很容易出现使用调试工具时完全正确,但放进logstash服务下

正式运行时就会报出许多解析失败或者解析超时的错误信息并将logstash卡死。


解析规则开启多行匹配模式时,如果日志的结尾没有明确的标识符,会把后续的行也匹配成当前

这一条日志的内容。或许有人会说,我的多行日志是以空行做分隔的,没错,使用filebeat从源

上收集日志传送到kafka队列中时,是没有问题的,但是logstash从kafka队列中拉取日志进行

解析时就会出现麻烦了,因为原来以空行做分隔的消息进入队列后空行被清除了,所以此时解析

使用多行匹配就需要有明确的结束符,才能正确的匹配到原来属于一条日志的内容。


logstash完整的小配置文件,解析nginx错误日志


/etc/logstash/conf.d/nginx-error.conf

input {

  kafka {

    bootstrap_servers => "172.16.100.7:9092"

    topics => ["nginx-error"]

    consumer_threads => 3

    type => "nginx-error-log"

  }

}


filter {

  if [type] == "nginx-error-log" {

    grok {

      match => { "message" => "%{HOSTNAME:logserver} %{PATH:logpath} %{NGINX_ERROR_LOG}" }

      remove_field  => "message"

    }

    date {

      match => [ "timestamp", "dd/MMM/yyyy:HH:mm:ss Z", "yyyy/MM/dd HH:mm:ss" ]

    }

    geoip {

      source => "remote_addr"

    }

  }

}


output {

  if [type] == "nginx-error-log" {

    elasticsearch {

      hosts => ["localhost:9200"]

      index => "nginx-%{+YYYY.MM.dd}"

      template => "/etc/logstash/conf.d/nginx.index"

      template_name => "nginx"

      template_overwrite => true

    }

  }

}


logstash完整的小配置文件,解析nginx访问日志

cat /etc/logstash/conf.d/nginx-access.conf 

input {

  kafka {

    bootstrap_servers => "172.16.100.7:9092"

    topics => ["nginx-access"]

    consumer_threads => 5

    type => "nginx-access-log"

  }

}


filter {

  if [type] == "nginx-access-log" {

    grok {

      match => { "message" => "%{HOSTNAME:logserver} %{PATH:logpath} %{NGINX_ACCESS_LOG}" }

      remove_field  => "message"

    }

    date {

      match => [ "timestamp", "dd/MMM/yyyy:HH:mm:ss Z", "yyyy/MM/dd HH:mm:ss" ]

    }

    geoip {

      source => "remote_addr"

    }

  }

}


output {

  if [type] == "nginx-access-log" {

    elasticsearch {

      hosts => ["localhost:9200"]

      index => "nginx-%{+YYYY.MM.dd}"

      template => "/etc/logstash/conf.d/nginx.index"

      template_name => "nginx"

      template_overwrite => true

    }

  }

}


logstash完整的小配置文件,解析php慢日志

cat /etc/logstash/conf.d/php-slow.conf 

input {

  kafka {

    bootstrap_servers => "172.16.100.7:9092"

    topics => ["php-slow"]

    consumer_threads => 5

    type => "php-slow-log"

  }

}


filter {

  if [type] == "php-slow-log" {

    grok {

      match => { "message" => "^%{HOSTNAME:logserver} %{PATH:logpath} %{PHP_SLOW_LOG}" }

      remove_field => "message"

    }

    date {

      match => [ "timestamp", "dd-MMM-yyyy HH:mm:ss" ]

    }

  }

}


output {

  if [type] == "php-slow-log" {

    elasticsearch {

      hosts => ["localhost:9200"]

      index => "php-%{+YYYY.MM.dd}"

    }

  }

}


logstash完整的小配置文件,解析php错误日志

cat /etc/logstash/conf.d/php-error.conf 

input {

  kafka {

    bootstrap_servers => "172.16.100.7:9092"

    topics => ["php-error"]

    consumer_threads => 5

    type => "php-error-log"

  }

}


filter {

  if [type] == "php-error-log" {

    grok {

      match => { "message" => "^%{HOSTNAME:logserver} %{PATH:logpath} %{PHP_ERROR_LOG}" }

      remove_field => "message"

    }

    date {

      match => [ "timestamp", "dd-MMM-yyyy HH:mm:ss" ]

    }

  }

}


output {

  if [type] == "php-error-log" {

    elasticsearch {

      hosts => ["localhost:9200"]

      index => "php-%{+YYYY.MM.dd}"

    }

  }

}


对了,关于filter插件中使用到的date插件和geoip插件,以及nginx配置文件中使用的自定义的映射模版文件

都会放在后续的文章中进行单独的介绍。本文分享的核心在于帮助大家理解如何使用grok的模式式写出解析自己

生产环境中的服务器日志的表达式。笔者在搭好ELK架构之后在写这个表达式时也头疼的好一阵子,查阅了好多相关

的文章,所以大家也不要觉得太痛苦。只要你深信技术都是这样的过程中沉淀下来的。