Как мы логи собираем
Небольшой рассказ-руководство о том, как чуть лучше познать Elasticsearch, FluentD, себя и мир.
Привет, мир!
Сегодня я хочу рассказать вам о том, как я настраивал EFK-стек для nginx, чтобы парсить кастомные access- и errors- логи.
Сразу быстро пробежимся по составляющим стека, чтобы было общее понимание:
- Elasticsearch - поисковая база данных, которая хранит все собираемые нами метрики.
- FluentD и FluentBit - агрегаторы логов, которые занимаются их парсингом, маршрутизацией и прочими крутыми вещами.
- Kibana - веб-морда с возможность тонко настроить вывод информации из нашего Эластика в виде различных графиков.
Пару слов о структуре каталогов:
Для себя я определил достаточно удобную структуру каталогов для работы с docker-compose. В корне создается каталог docker, внутри которого находятся:
. ├── build # тут у нас лежит то, что билдится (спс, Кэп!) │ └── fluentd # для каждого сервиса - отдельный каталог ├── conf # тут находятся конфигурационные файлы │ ├── alertmanager │ ├── alertmanager_bot │ ├── apm │ ├── fluentd │ └── prometheus ├── data # в этом каталоге мы складываем бекапы, чтобы не грустить после "docker-compose down" │ ├── elasticsearch │ ├── grafana │ └── kibana └── docker-compose.yml # Сам композ-файл
Я не призываю всех и каждого соблюдать такую структуру, но мне кажется, хранить их в таком виде - вполне здравая мысль.
Цель: научится парсить access.log и errors.log nginx'a, узнавать и там, и там откуда наш клиент, получать информацию о его ОС, браузере... Короче, все то, что делает Filebeat или даже немного больше.
Разворачиваем стек
Создадим сеть в докере, а затем запилим вот такой docker-compose.yml:
version: '3'
services:
fluentd:
build: ./build/fluentd
container_name: fluentd
volumes:
- ./fluentd/conf:/fluentd/etc
expose:
- "24224"
- "24224/udp"
depends_on:
- elasticsearch
networks:
test:
ipv4_address: 192.168.1.111
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.0.0
container_name: elasticsearch
expose:
- "9200"
environment:
- node.name=es01
- cluster.initial_master_nodes=es01
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
networks:
test:
ipv4_address: 192.168.1.102
kibana:
image: docker.elastic.co/kibana/kibana:7.0.0
restart: always
container_name: kibana
expose:
- "5601"
environment:
SERVER_NAME: yourdomain.com
depends_on:
- elasticsearch
networks:
test:
ipv4_address: 192.168.1.103
networks:
test:
external: true
А для nginx на хосте была создана вот такая конфигурация /etc/nginx/conf.d/kibana.conf
upstream kibana {
server 192.168.1.103:5601;
}
# Редирректим на 443 порт, чтобы у нас был SSL и все было красиво
server {
listen 80;
server_name yourdomain.com;
location / {
return 301 https://yourdomain.com$request_uri;
}
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
auth_basic "Login to continue"; #Добавляем базовую аутентификацию, чтобы к нашей кибане имели доступ только мы
auth_basic_user_file /etc/nginx/.htpasswd;
satisfy any;
allow 122.222.111.252; #указываем доверенные IP, с которых не будем спрашивать логин/пароль
deny all;
# Указываем пути к ssl-сертификату
include /etc/nginx/ssl_params;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Пишем логи
access_log /var/log/nginx/elk-access.log;
error_log /var/log/nginx/elk-error.log;
client_max_body_size 20m;
ignore_invalid_headers off;
location / {
proxy_http_version 1.1;
proxy_read_timeout 300;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://kibana;
}
}
Так же создадим Dockerfile для Fluentd (в нашей структуре каталогов - в /docker/build/fluentd/), в котором добавим библиотеку GeoIP и навернем поверх пару плагинов:
FROM fluent/fluentd:v1.5-debian-1 USER root RUN apt-get update \ && apt-get install -y libgeoip-dev libcurl4-openssl-dev ruby-dev libc6 libtool autoconf automake build-essential RUN fluent-gem install fluent-plugin-elasticsearch \ && fluent-gem install fluent-plugin-geoip USER fluent
После чего проверим конфигурацию nginx (nginx -t), запустим наш маленьких стек (docker-compose up) и перезагрузим конфигурацию nginx'a (nginx reload).
Отлично! Теперь, если все было сделано правильно, у нас под адресу yourdomain.com доступна Kibana. Но вот незадача - стоит нам сделать docker-compose down, и все данные из контейнеров будут утеряны безвозвратно.
Для того, чтобы этого избежать, отредактируем docker-compose.yml и примонтируем внутрь контейнера область жесткого диска:
# Для Kibana:
volumes:
- ./data/kibana/export.json:/usr/share/kibana/export.json:ro
# Для elasticsearch:
volumes:
- ./data/elasticsearch:/usr/share/elasticsearch/data
Окей, так уже гораздо лучше!
Осталось создать конфигурационный файл для FluentD (docker/conf/fluentd/fluent.conf):
<source>
@type forward
format nginx
port 24224
bind 0.0.0.0
</source>
<match *.**>
@type copy
<store>
@type elasticsearch
host elasticsearch
port 9200
logstash_format true
logstash_prefix test
logstash_dateformat %Y%m%d
include_tag_key true
type_name access_log
tag_key @log_name
flush_interval 1s
</store>
<store>
@type stdout
</store>
</match>
Теперь наш FluentD будет просто слушать порт и перекидывать все поступившие записи в Stdout и в Elasticsearch.
Fluent Bit
Теперь поставим на хост FluentBit и настроим его.
По умолчанию Fluent Bit умеет работать только с access.log nginx'a, поэтому нам нужен парсер для errors.log. Откроем и изучим каталог встроенных парсеров (/etc/td-agent-bit/parsers.conf): как можно заметить, парсинг выполняется по регулярным выражениям.
Кстати, есть очень удобный инструмент для написания регулярных выражений для Ruby-приложений - Rubular.
Добавим наш парсер для errors.log:
[PARSER] Name nginx_error Format regex Regex ^(?<time>.*) \[(?<log_level>\w+)\] (?<pid>\d+).(?<tid>\d+): (?<message>.*), client: (?<remote>.*), server: (?<server>.*)$ Time_Key time Time_Format %Y/%m/%d %H:%M:%S
После чего отредактируем td-agent-bit.conf
[SERVICE] Flush 5 Daemon Off Log_Level info Parsers_File parsers.conf Plugins_File plugins.conf HTTP_Server Off HTTP_Listen 127.0.0.1 HTTP_Port 2020 [INPUT] Name tail Tag nginx.access Path /var/log/nginx/app-access.log Parser nginx Interval_Sec 1 [INPUT] Name tail Tag nginx.errors Path /var/log/nginx/app-bigbird-error.log Parser nginx_error Interval_Sec 1 [OUTPUT] Name forward Match * Host 192.168.1.111 Port 24224
Теперь можно перезапустить FluentBit и наслаждаться тем, как резво подтягиваются данные в интерфейсе Kibana. Но любой, кто хоть раз пользовался FileBeat'ом от Эластика заметит, что количество собираемых данных ничтожно мало - тут тебе ни удобоваримой инфы о браузере, ни о ОС, ни геолокации... Но все это можно легко исправить!
Я написал небольшой скриптик на lua, который на основе содержимого поля "agent" определяет, какой ОС и каким браузером пользуется клиент. Взять его можно на GitHub.
Подключим этот скрипт в наш td-agent-bit.conf, добавив пару фильтров в наш папйплайн обработки логов:
[FILTER]
Name lua
Match nginx.access
script agentparser.lua
call defineOS
[FILTER]
Name lua
Match nginx.access
script agentparser.lua
call defineBrowser
И после перезапуска FluentBit мы видим это:
Успех!
Но этого мне было мало, и я решил, что было бы неплохо, если б мы могли видеть request_time каждого пользователя. Благо, nginx позволяет кастомизировать формат своего access.log
Подправим nginx.conf, добавив туда новый формат для лога:
log_format timed_combined '$remote_addr $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' '$request_time $upstream_response_time';
И присвоим его access-логу нашего приложения.
Так же добавим еще один парсер в /etc/td-agent-bit/parsers.conf:
[PARSER] Name nginx-new Format regex Regex ^(?<remote>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)" (?<request_time>[^ ]*) (?<responce_time>[^ ]*))?$ Time_Key time Time_Format %d/%b/%Y:%H:%M:%S %z
И изменим парсер в td-agent-bit.conf
GeoIP. Узнаем, где наши пользователи.
На самом деле, FluentBit хорош всем, и особо любопытные могли уже задаться вопросом - "Так, погодите, а зачем вообще использовать FluentD, когда FluentBit может точно так же слать логи в Elasticsearch?". И действительно, зачем?..
На самом деле, причина в том, что плагин GeoIP доступен только для "старшего брата" (на момент написания заметки, 31.05.2019).
Именно поэтому вернемся к FluentD (а именно - к его конфигурационному файлу).
<source>
@type forward
port 24224
bind 0.0.0.0
</source>
<filter nginx.**>
@type geoip
geoip_lookup_keys remote
backend_library geoip2_c
<record>
city ${city.names.ru["remote"]} # можно заменить 'ru' на 'en', если в этом есть необходимость
country ${country.iso_code["remote"]}
country_name ${country.names.ru["remote"]}
region_code ${subdivisions.0.iso_code["remote"]}
region_name ${subdivisions.0.names.ru["remote"]}
location '[ ${location.longitude["remote"]},${location.latitude["remote"]} ]' #именно в таком порядке. См. документацию ElasticSearch для того, чтобы узнать подробности.
</record>
skip_adding_null_record true # если не удается установить геолокацию для какого-либо ip-адреса, поля не добавляются
</filter>
<match nginx.**> # Фильтруем записи по тэгу, чтобы в эластик не падал всякий мусор
@type copy
<store>
@type elasticsearch
host 192.168.1.102
port 9200
logstash_format true
logstash_prefix nginx
logstash_dateformat %Y%m%d
include_tag_key true
type_name access_log
flush_interval 0.1s
</store>
# все еще оставляем вывод в stdout для дебага
<store>
@type stdout
</store>
</match>
После чего рестартанем контейнер с FluentD и увидим, что у нас пишутся гео-координаты, выставляется город, страна и т.д.
Kibana и Elasticsearch. Настройка дашбордов и немного боли.
Я не буду тут сильно вдаваться в подробности о том, как настраивать визуализации и дашборды. Могу сказать лишь то, что если вы совсем ничего не понимаете - поставьте какой-нибудь Filebeat, экспортируйте из него дашборды и потыкайтесь в них.
Но рано или поздно вы столкнетесь с несоответствием типов данных (например, если в нашем примере попробуете создать на основе данных визуализацию в виде карты, на которой точками будут отмечены ваши клиенты). Проблема заключается в том, что Elastic занимается динамической типизацией индексов. Что еще хуже - их нельзя изменить. И ладно бы мы писали в один индекс, но ведь они у нас в logstash-формате (т.е. вместо одного жирного индекса nginx у нас куча индексов поменьше: nginx-20190529, nginx-20190530, nginx-20190531...) Что же тогда делать?
Тогда делать temlates. Если говорить простыми словами, template - это свод правил, который распространяется на все индексы, соответствующие его маске.
Идем в Kibana Dev Tools и пишем:
PUT _template/nginx
{
"index_patterns" : [
"nginx-*" #та самая маска
],
"mappings" : { # а как раз тут мы и задаем жесткое соотвествие между полем и его типом
"properties": {
"location": {
"type": "geo_point"
}
}
}
}
Итого:
- Распарсили кастомный access.log;
- Так же распарсили errors.log;
- Знаем информацию о ОС и браузере клиента;
- Знаем его приблизительную геолокацию;
- Имеем чуть большее представление о том, как работают индексы в Эластике и как изменить маппинг.
На этом сегодня все, удачных экспериментов!