В данном руководстве мы рассмотрим подключение и настройку системы логирования в Spring Boot проекте и отправку логов в ELK с помощью Filebeat. Руководство предназначено для разработчиков начального уровня.
Логирование и зачем оно нужно
Когда я только начинал работать программистом один мой старший коллега любил повторять: "Если у тебя нет логов, то у тебя нет ничего". Действительно, столкнувшись с первым же багом на тестовых стендах или хуже того в промышленной среде, первое что нам будет нужно это логи приложения и удобный доступ к ним. За сами логи отвечают разработчики приложения, которые должны обеспечить логирование поведения системы таким образом, чтобы в любой момент можно было понять что происходит с системой и главное что с ней не так.
Следующий вопрос, это удобство доступа к логам. Обычно при локальном тестировании мы видим лог в консоли приложения, а на тестовым стенде — в специальных лог файлах на сервере. Удобно ли и безопасно ли каждый раз подключаться к стенду, искать нужную директорию и читать файлы логов оттуда? Практика показывает что нет и это вторая проблема которую призван решить ряд продуктов, обеспечивающих удобный доступ к логам и поиск в них важной информации. Сегодня мы очень кратко поговорим о одной из групп таких продуктов, так называемом стеке ELK (Elasticsearch — Logstash — Kibana) и более подробно о Filebeat — Open source продукте, обеспечивающем удобный механизм доставки логов до ELK.
Три строчки о ELK
- Logstash — получение, модификация логов
- Elasticsearch — хранение и поиск
- Kibana — отображение
Причем здесь Filebeat?
Filebeat обеспечит доставку данных до ELK и будет разворачиваться рядом с нашим приложением, зачастую это просто удобнее чем настраивать Logstash для того чтобы читать данные из файлов логов или иных каналов.
Хорошим подходом будет развертывание общего ELK для группы микросервисов, а для каждого микросервиса разворачивать свой Filebeat (тем более что это проще некуда), который будет читать логи микросервиса и отдавать в общий ELK.
Именно такое решение мы попробуем реализовать позже в локальной среде.
Практика
Java 8
ApacheMaven3.6
Spring Boot 2.3.4.RELEASE
Docker
Spring Boot App
Начнем с создания простого Spring Boot приложения, единственной задачей которого будет генерировать логи по запросу. Полный код приложения можно посмотреть/скачать здесь либо повторить по шагам
Создадим проект Spring Boot самостоятельно или используя Spring Initalizr
Дополнительно нам понадобятся зависимости
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>6.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
- spring-boot-starter-web — т.к. мы будем обращаться к приложению по сети
- logstash-logback-encoder — позволит сформировать записи в лог файле в правильном формате
- lombok — просто для удобства, чтобы писать меньше кода
Здесь полный pom.xml
Главный класс нашего приложения как и в любом простейшем Spring Boot проекте:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Класс генератор который будет создавать логи:
@Slf4j
@Service
public class LogGenerator {
public void generate(int count) {
log.info("Start generating logs");
LongStream.range(0, count)
.forEach(i -> log.info("Log {}", i));
}
}
Здесь мы просто создаем от 0 до count записей в логе
Контроллер, который мы будем вызывать для обращения к генератору:
@Slf4j
@RestController
@RequiredArgsConstructor
public class LogController {
private final LogGenerator generator;
@GetMapping("/generate")
public ResponseEntity test(@RequestParam(name = "count", defaultValue = "0") Integer count) {
log.info("Test request received with count: {}", count);
generator.generate(count);
return ResponseEntity.ok("Success!");
}
}
Контроллер обрабатывает GET запросы вида:
http://localhost:8080/generate?count=10
и запускает генератор передавая ему количество записей
Теперь настроим систему логирования. Для этого добавим в resources файл logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d [%thread] %-5level %logger{35} - [%mdc] - %msg%n</pattern>
</encoder>
</appender>
<appender name="filebeatAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./log/application.log</file>
<append>true</append>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>./log/application.%d.%i.log.gz</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="consoleAppender" />
<appender-ref ref="filebeatAppender" />
</root>
</configuration>
Здесь мы обьявили два аппендера:
- consoleAppender — пишет записи согласно указанному нами паттерну в привычную нам консоль
- filebeatAppender — пишет записи в файл, причем в качестве енкодера используется LogstashEncoder из той самой библиотеки logstash-logback-encoder
Назначения данного енкодера — кодировать файлы логов в JSON формат, который легко будет понимать Logstash. В противном случае нужно будет конфигурировать сам Logstash что снижает универсальность нашего решения и просто добавит лишней работы по настройке.
Как видно из кода, файл лога будет писаться согласно пути ./log/application.log что соответствует поддиректории log в папке с проектом. Разумеется можно указать любой другой путь. Также при желании можно скорректировать максимальный размер файла maxFileSize
На самом деле на этом можно было бы закончить и перейти к установки и настройке Filebeat но мы сделаем еще кое что.
Добавим в наш проект класс фильтр для запросов:
@Slf4j
@Component
public class LogFilter extends OncePerRequestFilter {
private static final String REQUEST_ID = "requestId";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestId = request.getHeader(REQUEST_ID);
if (requestId == null) {
requestId = UUID.randomUUID().toString();
}
MDC.put(REQUEST_ID, requestId);
try {
log.info("Started process request with {} : {}", REQUEST_ID, requestId);
filterChain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
Назначения этого класса, перехватывать все запросы к контроллеру и генерировать уникальный идентификатор запроса (если таковой не был отправлен в виде хедера requestId), после чего помещает его в MDC (Mapped Diagnostic Context)
MDC.put(REQUEST_ID, requestId);
А finally блоке MDC будет очищаться
MDC.clear();
Ценность такого подхода в том, что теперь все логи записанные в рамках конкретного запроса объединены единым уникальным идентификатором, что может облегчить анализ логов при разборе инцидентов. Дальше мы увидим как просто в интерфейсе Kibana можно будет отфильтровать только логи для каждого конкретного запроса.
После того как ваше приложение будет закончено, запустите его удобным для вас способом, например:
mvn spring-boot:run
После того как приложение стартует, можно вызвать его и убедиться что логи пишутся в файла application.log
curl "localhost:8080/generate?count=10"
Сервис должен отвечать Success!, а в application.log появлятся записи вида:
{
"@timestamp":"2020-10-17T22:39:45.595+03:00",
"@version":"1",
"message":"Writing [\"Success!\"]",
"logger_name":"org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor",
"thread_name":"http-nio-8080-exec-3",
"level":"INFO",
"level_value":10000,
"requestId":"77abe5ac-4458-4dc3-9f4e-a7320979e3ae"
}
Filebeat
Скачать можно по ссылке Filebeat
В рамках данной статья тестирование проводилось на версии 7.9.2 на macOS
Далее следует распаковать дистрибутив в необходимую директорию.
Для настройки Filebeat вам понадобится всего лишь скорректировать содержимое файла filebeat.xml
Конфигурация уже почти готова, нам нужно только скорректировать блоки inputs и output:
inputs:
- enabled: true
encoding: utf-8
exclude_files: ['\.gz$']
json:
add_error_key: true
keys_under_root: true
overwrite_keys: true
paths:
- {ваш путь до папки с логами}/*.log
scan_frequency: 10s
type: log
Здесь самое главное, мы указываем Filebeat где лежат файлы логов и как их следует интерпретировать. Для этого мы добавим следующие параметры:
- keys_under_root — наши логи в формате json будут встраивались в существующий json, который Filebeat будет отправлять в Logstash
- overwrite_keys — позволит перезаписывать ключи полей и корректно разрешать конфликты
- add_error_key — Filebeat будет добавлять поля error.message и error.type: json в случае если сформированный нами json оказался некорректным.
output:
logstash:
hosts:
- localhost:5044
ssl:
certificate_authorities:
- {путь до папки с сертификатами}/logstash-beats.crt
Этот блок отвечает за то, куда Filebeat будет отправлять логи. В данном случае указан дефолтный порт с которым будет запущен Logstash (если вы умышленно не будете запускать его на другом порту)
блок ssl.certificate_authorities не обязательный и понадобится в случае если вы будете использовать Logstash защищенный сертификатами (именно такой будем рассматривать мы), в противном случае он вам не нужен.
Этих настроек достаточно для локальной работы Filebeat, за тем лишь исключением что пока ему некуда отдавать данные, т.к. ELK еще не запущен.
Вопрос полного развертывания и конфигурирования всех компонентов ELK выходит за рамки данной статьи. Для упрощения, предлагается воспользоваться готовым docker образом ELK sebp/elk и также предлагается воспользоваться logstash-beats.crt. Его следует скачать и прописать как certificate_authorities в filebeat.xml
Автор запускает посредством docker-compose с пробросом портов:
version: '3.7'
services:
elk:
image: sebp/elk
ports:
- "5601:5601" #kibana
- "9200:9200" #elastic
- "5044:5044" #logstash
После запуска ELK можно запустить и Filebeat соответствующей командой в зависимости от вашей операционной систему, например для macOS это:
./filebeat -e run
Что происходит? Наше приложение по запросу генерирует логи, они с помощью LogstashEncoder преобразуются к виду JSON и записываются в файл application.log, а Filebeat в свою очереди регулярно забирает новые записи, преобразует их в соответствии со своей конфигурацией и отправляет в Logstash. Далее с этими данными мы можем работать в интерфейсе Kibana.
Зайдем в интерфейс Kibana по адресу:
http://localhost:5601/
Далее перейдем на вкладку Discover:
Нужно будет создать индекс паттерн:
Затем Kibana покажет вам index от которого ELK уже получил данные. Важно! Если ваше приложение еще ничего не залогировало или Filebeat не успел ничего отправить, то и индекса не появится на этой вкладки. Потому прежде сделайте несколько вызовов нашего сервиса:
curl "localhost:8080/generate?count=100"
Немного подождем и можно будет определить нужный паттерн:
Выберем поля для дефолтной сортировки:
И увидим поля для каждой лог записи нашего приложения. Можно увидеть тот самый requestId который был нами добавлен в MDC ранее:
Теперь во вкладке Discover для нашего индекса можно настроить отображения полей и увидеть, что все логи в рамках одного запроса объединены одним и тем же requestId. Можно развернуть поле JSON и посмотреть полный текст сообщения полученного из Filebeat:
ivanovdev
Filebeat надо прикручивать к старому легаси, которое уже все боятся ворошить.
К спрингу лучше прикрутить что-то типа этого github.com/logstash/logstash-logback-encoder и избавиться от зависимости в виде диска.А в мире к8s решение с записью логов на диск может привести вообще к их частичной потере.
Throwable
Я могу ошибаться, но без диска в случае, если упадет ELK, все трейсы за период простоя будут потеряны, тогда как в дисковом варианте Filebeat корректно сможет их залить после восстановления работы.