Итак, с Elasticsearch, гибридом гугла и NoSQL базы данных, мы разобрались, самое время перейти к следующей букве ELK стэка от Elastic — Logstash.
Что такое Logstash
Logstash — это конвейер обработки данных, который получает сырые данные (например, логи) из одного или нескольких источников, обрабатывает их и улучшает фильтрами, а затем отправляет результат одному или нескольким получателям. Elastic рекомендует в качестве получателя использовать Elasticsearch, но на самом деле можно использовать всё, что душе угодно: STDOUT, WebSocket, обычные сокеты, очереди сообщений — выбор огромный.
Установка
Если на хосте установлена Java, то можно просто скачать архив, распаковать его и запустить bin/logstash -f logstash.conf
. Для запуска, правда, понадобится файл конфигурации, но для простейших примеров сойдёт что-нибудь вроде input {STDIN {}} output {STDOUT {}}
.
Да, любителям контейнеров можно не утруждать себя скачкой архивов и использовать заводской Docker образ:
1 |
docker run -it logstash -e 'input { stdin { } } output { stdout { } }' |
Конфигурация, кодеки и «Hello World!»
Итак, архив скачан, что дальше? Как я упомянул выше, для запуска Logstash нужен файл конфигурации. Например, такой:
1 2 3 4 5 6 7 |
input { stdin { } } output { stdout { } } |
С ним Logstash будет брать данные из консоли, делать свою магию по-умолчанию, и выдавать результат назад в консоль.
Hello world
Я нашёл у себя на хосте немного логов от Apache2, так что почему бы не скормить их Logstash и не посмотреть, что из этого получится?
1 2 3 4 5 |
$ bin/logstash -e 'input { stdin { } } output { stdout {} }' #.... #05:38:59.948 [Api Webserver] INFO logstash.agent - Successfully started Logstash API endpoint {:port=>9600} $ 172.17.0.1 - - [11/Feb/2017:04:41:22 +0000] "GET / HTTP/1.1" 200 3525 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" #2017-02-13T05:39:12.684Z 269a27a16415 172.17.0.1 - - [11/Feb/2017:04:41:22 +0000] "GET / HTTP/1.1" 200 3525 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" |
А получится много скучного текста. Logstash запустился, ему на вход вошла строка (4) с логом от Apache2, и она тут же пошла на выход с двумя новыми полями: временной меткой 2017-02-13T05:39:12.684Z
и именем хоста 269a27a16415
.
Кодеки
Но мы можем сделать вывод более читабельным. Logstash разрешает подключать ко вводу и выводу различные кодеки, которые форматируют поток текста, но не меняют его значение: добавляют отступы, архивируют, переводят в JSON, и т. п. В нашем случае мы можем использовать rubydebug
для того, чтобы он отформатировал результат и показал, как именно Logstash «видит» данные, с которыми работает.
Конфигурация изменится совсем чуть-чуть, но результат станет намного человечнее:
1 2 3 4 5 6 |
... output { stdout { codec => rubydebug } } |
1 2 3 4 5 6 7 8 9 |
$ bin/logstash -e 'input { stdin { } } output { stdout {codec => rubydebug} }' #.... $ 172.17.0.1 - - [11/Feb/2017:04:41:22 +0000] "GET / HTTP/1.1" 200 3525 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" # { # "@timestamp" => 2017-02-12T05:24:21.270Z, # "@version" => "1", # "host" => "31190306c1eb", # "message" => "172.17.0.1 - - [11/Feb/2017:04:41:22 +0000] \"GET / HTTP/1.1\" 200 3525 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36\"" #} |
Выводим результаты в Elasticsearch
Прежде чем двинуться дальше, стоит попробовать кое-что еще. В начале поста я сказал, что получателей данных может быть больше одного, и вообще мы можем писать результаты прямо в Elasticsearch. Это как раз тот случай, когда можно одним телодвижением подтвердить оба пункта.
Так уж оказалось, что в соседнем с Logstash контейнером у меня обосновался чистый Elasticsearch с айпишкой 172.19.0.2 и портом 9200. Чтобы подключить их друг к другу, нужно добавить всего пару строк в конфигурацию Logstash:
1 2 3 4 5 6 7 8 9 |
... output { stdout { codec => rubydebug } elasticsearch { hosts => ["172.19.0.2:9200"] } } |
Теперь, если перезапустить Logstash и снова скормить ему Apache2 логов, в Elasticsearch появится кое что относительно интересное:
1 2 |
$ curl 172.19.0.1:9200/_cat/indices #yellow open logstash-2017.02.12 rgQub7hsS0qq-FBj3HA2Rg 5 1 5 0 20.1kb 20.1kb |
Во-первых, там появился новый индекс — logstash-2017.02.12
. Во-вторых, запустив по этому индексу поиск, мы получим те же данные, что Logstash писал в STDOUT:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ curl 172.19.0.2:9200/logstash-2017.02.12/_search?pretty #{ #... # "hits" : [ # { # "_index" : "logstash-2017.02.12", # "_type" : "logs", #... # "_source" : { # "@timestamp" : "2017-02-12T05:24:21.272Z", # "@version" : "1", # "host" : "31190306c1eb", # "message" : "172.17.0.1 - - [11/Feb/2017:04:41:22 +0000] \"GET /icons/ubuntu-logo.png HTTP/1.1\" 200 3623 \"http://localhost/\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36\"" #... #} |
Обрабатываем данные фильтрами
Пока Logstash справлялся с переправкой логов из точки А в точку Б, но не делал ничего для того, чтобы это данные стали хоть немного полезнее. Пришло время перемен. Кроме INPUT и OUTPUT в конфигурацию можно добавить FILTER, и вот там-то основная магия и начнётся.
Фильтры есть практически для всего: для агрегации метрик, маскировки чувствительных данных (вроде имён и номеров кредитных карт), добавления и удаления полей, нахождения IP по доменному имени, нахождения адреса по IP (держитесь, комментаторы!), и т.п. Но для нас хорошо бы начать с разбора строки Apache2 лога на её компоненты: IP, путь, user agent, и так далее. Фильтр, который занимается подобной работой, называется grok.
Grok
Grok — это основной инструмент для придания формы и структуры сполшному массиву текста. Работает это так: из предустановленных паттернов (IP, Number, Word, …) мы собираем строку-шаблон, которая по структуре повторяет ту, которую мы собираемся парсить. Например, если бы мои логи выглядели так:
1 2 |
0 127.0.0.1 /default.html 1 172.1.0.9 / |
То я бы использовал такой шаблон для их разбора:
1 |
%{NUMBER:sequence} %{IP:client} ${URIPATHPARAM:target} |
После обработки фильтром в логах появились бы новые поля: sequence
, client
и target
. Те самые, которые указали в шаблоне.
Кроме элементарных паттернов вроде Number есть более сложные, которые описывают известные форматы логов целиком. Например, COMBINEDAPACHELOG
описывает строку лога от Apache2. Вот её-то и можно попробовать в действии.
Новая конфигурация:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
input { stdin {} } filter { grok { match => { "message" => "%{COMBINEDAPACHELOG}" } } } output { stdout { codec => rubydebug } elasticsearch { hosts => ["172.19.0.2:9200"] } } |
И реакция Logstash на неё:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "request" => "/", "agent" => "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36\"", "auth" => "-", "ident" => "-", "verb" => "GET", "message" => "172.17.0.1 - - [11/Feb/2017:04:41:22 +0000] \"GET / HTTP/1.1\" 200 3525 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36\"", "referrer" => "\"-\"", "@timestamp" => 2017-02-12T05:41:18.687Z, "response" => "200", "bytes" => "3525", "clientip" => "172.17.0.1", "@version" => "1", "host" => "31190306c1eb", "httpversion" => "1.1", "timestamp" => "11/Feb/2017:04:41:22 +0000" } |
Сработало!
Дополнительные настройки GROK
Но обработку можно сделать еще лучше. В середине распаршеного лога затесалось поле message
, которое дублирует всё, что мы только что распарсили. Хорошо бы его убрать. А в grok как раз есть дополнитальные настройки, одна из которых называется remove_field
— как раз то, что нам нужно:
1 2 3 4 5 6 7 8 9 10 |
... filter { grok { match => { "message" => "%{COMBINEDAPACHELOG}" } remove_field => [ "message" ] } } ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "request" => "/icons/ubuntu-logo.png", "agent" => "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36\"", "auth" => "-", "ident" => "-", "verb" => "GET", "referrer" => "\"http://localhost/\"", "@timestamp" => 2017-02-12T05:42:55.999Z, "response" => "200", "bytes" => "3623", "clientip" => "172.17.0.1", "@version" => "1", "host" => "31190306c1eb", "httpversion" => "1.1", "timestamp" => "11/Feb/2017:04:41:22 +0000" } |
Теперь результат просто прекрасен. Но мы можем сделать его ещё лучше.
geoip фильтр
Как следует из названия, geoip конвертирует IP адрес в координаты на карте и сопутствующий уличный адрес. Я заменил докеровскую айпишку одного из логов на свою внешнюю, включил geoip, и пропустил лог через Logstash еще раз:
1 2 3 4 5 6 7 8 9 10 |
... filter { grok { ... } geoip { source => "clientip" } } ... |
Ну разве это не прекрасно? Разве что телефона там моего нет:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ "request" => "/", "agent" => "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8\"", "geoip" => { "timezone" => "America/Toronto", "latitude" => 43.4464, "continent_code" => "NA", "city_name" => "Oakville", "country_code2" => "CA", "country_name" => "Canada", "country_code3" => "CA", "region_name" => "Ontario", "location" => [ [0] -79.7593, [1] 43.4464 ], "postal_code" => "L6M", "longitude" => -79.7593, "region_code" => "ON" }, "auth" => "-", "ident" => "-", ... |
geoip взял clientip
поле, которое создал grok, и потом добавил несколько собственных полей. Мне даже не пришлось что-то выкачивать из ваших интернетов, чтобы это заработало.
А теперь, когда обработанные данные стекаются в Elasticsearch, мы можем использовать его поиск на полную катушку и начать задавать вопросы. Были ли посетители из Oakville?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$curl -s 172.19.0.2:9200/logstash-2017.02.12/_search?q=oakville | json_pp #{ # ... # "hits" : [ # { # "_id" : "AVow3zOW6fU5oFCNC7kH", # "_score" : 1.5404451, # "_type" : "logs", # "_index" : "logstash-2017.02.12", # "_source" : { # "geoip" : { # ... # "city_name" : "Oakville", # }, # "response" : "200", # "@timestamp" : "2017-02-12T05:50:18.333Z", # "agent" : "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8\"", # "request" : "/", #... # } # } # ], #... |
Конечно были. А сколько HTTP ошибок было вчера? Пытался ли кто-нибудь зайти в админку блога без спроса? Сколько раз? Где он живёт?
Можно задавать много, много вопросов.
Итог
Logstash принадлежит к классу инструментов, которые не создают особого впечатления своим описанием, но немного влюбляют в себя после того, как с ними поиграешься. Стоит немного покомбинировать входы, выходы и фильтры, как становится дико интересно, что же ещё можно выцедить из своих данных.
Да, Logstash прекрасно работает с Elasticsearch, но и источниками и получателями данных может быть огромный набор сервисов, начиная с очередей сообщений и заканчивая TCP сокетами.
Но в этой картине вселенского счастья пропущен один элемент — пользовательский интерфейс. Что ни говори, анализировать логи из командной строки — не самое продуктивное занятие. Чтобы это исправить, в следующий раз мы посмотрим в сторону последнего компонента ELK стека — Kibana.
Очень хорошая статья. Особенно респект за разьяснение что и в каких полях лежит, и где это искать в индексе. Благодарочка.