Thank you for reading this post, don't forget to subscribe!
Что такое 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 ошибок было вчера? Пытался ли кто-нибудь зайти в админку блога без спроса? Сколько раз? Где он живёт?