Тут коротко описаны мои изыскания касательно сбора информацией из текстовых логов компонентов кластера Twitter Storm средствами Rsyslog с последующей передачей оной через Logstash в базу Elasticsearch c возможностью анализировать её через Kibana.
Содержание
Отмазка
Данная статья не является точной инструкцией, следуя которой можно получить заранее известный результат. То что получилось «завести» у автора, возможно, в силу различных обстоятельств (отличия версий ПО), не получится у читателей. Цель статьи: описать идею, коротко — её реализацию, чуть подробнее — моменты, вызвавших некие трудности у автора. В общем, не удивляйтесь, что про Elasticsearch практически ничего в статье нет.
Проблема. Формулировка задачи.
Имеем вычислительный кластер CentOS 6 серверов на основе Storm (v.0.9.2), установленный из пакетов acromusashi / storm-installer. Состоящий из:
- Сервера с установленным на него Нимбусом
- Трёх серверов с супервизорами
Проблема заключается в том, что по умолчанию компоненты Twitter Storm пишут свои логи в файлы на тех серверах, на которых установлены. И это совсем неудобно. Хотелось бы работать с логами в режиме «одно окно» и, желательно, с красивым графическим интерфейсом.
Краткое описание технического решения
- Сервер с Супервизором (тут два)
- Службы Storm пишут свои логи в один файл (aggregate.log)
- rsyslog посредством модуля imfile читает aggregate.log и пересылает его содержимое на сервер с Нимбусом
- Сервер с Нимбусом
- rsyslog:
- получает логи с внешних серверов на local6, пишет их в storm.log — лог-файл всего кластера
- читает через модуль imfile локальный лог Storm’а в local5, пишет его в общий storm.log
- logstash:
- вычитывает содержимое storm.log
- отправляет его на внешний сервер с elasticsearch
- rsyslog:
- Сервер с Логами
- Elasticsearch + Kibana (Web GUI к Elasticsearch)
Пояснения:
- «Лить» ли логи из приложения напрямую на сборщик логов на Нимбусе или вначале собирать локально? Решено вначале складывать логи локально, это позволит совсем не остаться без логов, если в какой-то момент связь с rsyslog на сборщике будет нарушена — логи не потеряются.
- Записи логов многострочные (исключения Java), где-то после перевода строки есть отступ, где-то нет. Как с этим быть? Решено использовать в качестве разделителя отдельных записей логов пустую строку. С пустой строкой, как разделителем, хорошо работает logback, imfile модуль rsyslog’а и multiline фильтр logstash’a.
Конфигурация Storm
В рассматриваемой версии Storm используется logback в качестве системы логирования. И её конфигурацию можно обнаружить в файле:
/opt/storm-0.9.2incubating/logback/cluster.xml
Перенаправим все потоки логов в один файл, для этого опишем новый appender:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<appender name="AGGREGATED" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${storm.home}/logs/aggregated.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy"> <fileNamePattern>${storm.home}/logs/aggregated.log.%i</fileNamePattern> <minIndex>1</minIndex> <maxIndex>9</maxIndex> </rollingPolicy> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <maxFileSize>100MB</maxFileSize> </triggeringPolicy> <encoder> <pattern>%d{"yyyy-MM-dd'T'HH:mm:ss,SSSXXX"} ${HOSTNAME} %c{1} %p %m%n%xEx%n%n</pattern> </encoder> </appender> |
добавляем только что описанный appender в корневой элемент:
1 2 3 |
<root level="INFO"> <appender-ref ref="AGGREGATED"/> </root> |
Пояснения:
- %n%n — мы хотим, чтобы каждую запись в логе отделяла от другой пустая строка, поэтому в конце добавляем лишний перевод строки (%n). Подобный разделитель потребуется нам для того, чтобы в дальнейшем Rsyslog мог связать между собой строки одной записи лога.
- %xEx — явно указав исключение (exception) в шаблоне, мы гарантируем, что оно отобразится в логах именно так, как нам надо, а именно: между сообщением об ошибке (%m) и исключением (%xEx) будет ровно один перевод строки — исключение будет на следующей строке после сообщения об ошибке. В противном случае logback отобразит исключение как отдельную запись, отделив её от остальных пустыми строками.
Подробнее про шаблоны logback’а тут.
Конфигурация Rsyslog
Для импорта логов Storm’а в rsyslog используется модуль imfile. Флаг (escapeLF) для отмены экранирования переводов строк и пр. табов в этом модуле стал доступен только с версии Rsyslog 7+. В CentOS 6 же доступен только Rsyslog 5.8, потому поставили последнюю стабильную версию (8.4) из репозитария вендора.
Для простоты будем считать, что модифицируются файлы:
/etc/rsyslog.conf
В ходе отладки целесообразно запустить Rsyslog-сервер в консоли:
1 |
rsyslogd -nd |
На серверах с супервизорами
Добавим в конец файла следующие строки:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Collect read Storm's logs and forward them to collector node module(load="imfile" mode="inotify") input(type="imfile" File="/opt/storm/logs/aggregated.log" PersistStateInterval="100" Tag="storm.log" escapeLF="off" ReadMode="1" StateFile="storm.log.state" Facility="local6") template(name="simple" type="string" string="<%pri%> %msg%\n\n") local6.* @<log collector node IP>:514;simple |
Пояснения:
- escapeLF=»off» — отключаем маппирование переводов строк в #012 и пр., ибо хотим работать с многострочными логами
- Facility=»local6″ — определяем источник, из которого будем принимать логи Storm’а
- ReadMode=»1″ — инструктируем Rsyslog считать всё что находится между двумя пустыми строками одной записью лога
- string=»<%pri%> %msg%\n\n» — тут определяем формат строк, которые полетят на сборщик логов (сервер с Нимбусом)
- %pri% — тут подставится магическая цифра, благодаря которой внешний Rsyslog-сервер узнает про установленный нами local6
- %msg% — строка лога Storm’а именно в том формате, в каком мы его определили ранее в файле cluster.xml
- \n\n — добавляем лишний перевод строки, чтобы между соседними записями лога была пустая строка. Подобный разделитель потребуется нам для того, чтобы в дальнейшем Logstash мог связать между собой строки одной записи лога.
На сервере с нимбусом
В первую очередь раскомментируем строки, разрешающие приём логов с внешних Rsyslog-серверов
1 2 3 |
# Provides UDP syslog reception $ModLoad imudp $UDPServerRun 514 |
Направляем логи с внешних серверов в общий файл логов для Storm-кластера посредством local6:
1 2 |
template(name="remoteSimple" type="string" string="%msg:2:$%\n") local6.* /var/log/storm.log;remoteSimple |
Направляем логи от локальных сервисов Storm в общий файл логов для Storm-кластера посредством local5:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Collect read Storm's logs and forward them to collector node module(load="imfile" mode="inotify") input(type="imfile" File="/opt/storm/logs/aggregated.log" PersistStateInterval="100" Tag="storm.log" escapeLF="off" ReadMode="1" StateFile="storm.log.state" Facility="local5") template(name="simple" type="string" string="%msg%\n\n") local5.* /var/log/storm.log;simple |
Пояснения:
- local5, local6 — поскольку к логам с внешних серверов необходимо применить шаблон («%msg:2:$%\n») отличный от шаблона для логов из локального файла («%msg%\n»), пришлось воспользоваться двумя источниками:
- local5 — источник для локального файла
- local6 — источник для внешних серверов
- «%msg:2:$%\n» — по сложно объяснимым на пальцах причинам при поступлении записей логов с внешнего сервера строке «%msg%» соответствует исходная запись лога плюс лишний пробел вначале. Собственно предложенной конструкцией мы этот пробел обрезаем.
Конфигурация Logstash
Logstash устанавливается и запускается на том же сервере, куда собираются все логи Rsyslog’а. В нашем случае это сервер с Nimbus’ом.
Конфигурация Logstash
/etc/logstash/conf.d/nimbus.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
input { file { path => "/var/log/storm.log" sincedb_path => "/opt/logstash/.sincedb-storm.log" start_position => beginning } } filter { multiline { pattern => "^$" what => "previous" negate => true } grok { match => [ "message", "%{TIMESTAMP_ISO8601:date} %{HOST:hostname} %{NOTSPACE:class} %{NOTSPACE:prio} %{GREEDYDATA:str}" ] } } output { elasticsearch_http { host => "<Elasticsearch IP>" index => "logs" index_type => "storm" } } |
К примеру, указанным выше образом сконфигурированный сервис преобразует строку вида:
1 |
2014-09-06T19:00:35,662+02:00 example.com b.s.d.nimbus INFO Starting Nimbus server... |
перед отправкой в Elasticsearch во что-то подобное:
1 2 3 4 5 6 7 |
"host"=>"<name of collector host>", "path"=>"/var/log/storm.log", "date"=>"2014-09-06T19:00:35,662+02:00", "hostname"=>"example.com", "class"=>"b.s.d.nimbus", "prio"=>"INFO", "str"=>"Starting Nimbus server..." |
Отдельно стоит пояснить фильтр multiline. Настройки
1 2 3 |
pattern => "^$" what => "previous" negate => true |
инструктируют Logstash считать всё, что идёт после пустой строки и до следующей, одной записью лога.
Для отладки работы Logstash можно сначала запустить сервер из консоли:
1 |
java -jar /opt/logstash/logstash.jar agent -f /etc/logstash/conf.d/nimbus.conf -vv |
Конфигурация Elasticsearch
Тут требуется минимальная конфигурация, а именно: убедится в существовании индекса эквивалентного, указанному в настройках Logstash — logs.
Если у нас где-то есть развёрнутая Kibana, в итоге получаем все логи кластера в «одном окне»
Замечания
- В ходе изысканий SELinux был выключен, ибо препятствовал чтению файла модулем imfile rsyslog’а