logstash-grok解析nginx+php日誌

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表達式

(?<timestamp>\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表達式

(?<timestamp>\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,}(?<client>%{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 (?<timestamp>\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,}(?<client>%{IP}|%{HOSTNAME}))(?:,\s{1,}server:\s{1,}%{IPORHOST:server})(?:, request: %{QS:request})?(?:, upstream: %{QS:upstream})?(?:, host: %{QS:host})?(?:, referrer: \"%{URI:referrer})?


NGINX_ERROR_SIMPLE (?<timestamp>\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架構之後在寫這個表達式時也頭疼的好一陣子,查閱了好多相關

的文章,所以大家也不要覺得太痛苦。只要你深信技術都是這樣的過程中沉澱下來的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章