Выпускная квалификационная работа "Сервис сбора и анализа ошибок информационных систем". 2-й семестр 2-го курса МИФИ. ИИКС РПО (2025-2026 уч. г.).
errlog - сервис для централизованного сбора и анализа ошибок информационных систем.
Проект полезен в ситуациях, когда системы уже пишут логи, содержащие события ошибок, и хочется работать с ними как с отдельными проблемами: видеть группы однотипных ошибок, смотреть детали событий и анализировать динамику их возникновения во времени.
Идея проекта состоит в том, чтобы не заставлять источник специально отправлять ошибки через SDK. Источник продолжает писать события журналирования, внешний агент сбора забирает события нужных уровней, а ядро сервиса нормализует их, группирует по fingerprint, сохраняет в аналитическое хранилище и предоставляет REST API для поиска и анализа.
Проект настроен на работу с логами Java Spring Boot + Logback, но архитектура позволяет подключать другие источники при доставке их событий в Kafka и добавлении нормализатора под нужный sourceType.
- Сбор событий ошибок уровней
WARN,ERRORиFATALиз логов источников. - Фильтрация и обогащение событий на стороне Vector.
- Асинхронная доставка событий через Kafka.
- Нормализация входных событий к единой модели ошибки.
- Группировка однотипных ошибок на основе детерминированного
fingerprint. - Хранение событий ошибок в ClickHouse.
- REST API для получения групп ошибок, отдельных событий, карточки события, временных рядов и списка доступных фильтров.
- Регистрация, аутентификация, JWT и роли пользователей.
- Swagger/OpenAPI-документация API.
- Воспроизводимый запуск через Docker Compose.
Проект разделен на 2 Docker Compose-контура:
errlog-core- ядро сервиса: Kafka, Ingestor, ClickHouse, PostgreSQL, Errapi.- Этот контур представляет собой сам сервис сбора и анализа ошибок информационных систем.
errlog-demo- демонстрационный контур: Jerrgen и Vector.- Этот контур нужен для демонстрации работы сервиса и имитации внешних микросервисов, генерирующих ошибки в процессе своей работы.
- При подключении своих систем предлагается ориентироваться на реализацию данного контура.
Совместная работа обоих контуров называется стендом. При подключении реальных систем demo-контур может быть заменен на собственные источники событий и агент сбора логов.
jerrgen- генератор WARN/ERROR логов, написанный на Java (Spring Boot + Logback).ingestor- Java Spring Boot сервис, читает Kafka, нормализует события, вычисляетfingerprintи пишет события в ClickHouse.errapi- REST API с JWT и ролями для взаимодействия пользователей с сервисом.docker/docker-compose.core.yml- core-контур.docker/docker-compose.demo.yml- demo-контур.docker/vector/vector.yaml- конфигурация Vector.docker/clickhouse/init.sql- SQL-скрипт инициализации ClickHouse.docker/kafka/init.sh- скрипт инициализации Kafka и создания топикаerrors-raw.scripts/- вспомогательные скрипты запуска, остановки и перезапуска стенда.
- Jerrgen пишет логи в
stdoutв формате JSON Lines. - Vector в demo-контуре читает Docker-логи контейнеров с лейблом
errlog.collect=true. - Vector отбрасывает не-JSON строки, оставляет только события уровней
WARN,ERROR,FATAL, добавляет метаданные источника и отправляет события в Kafkaerrlog-core-контура. - Ingestor читает события из Kafka, нормализует их, вычисляет
fingerprintи записывает результат в ClickHouse. - Errapi осуществляет аутентификацию пользователя, читает данные из ClickHouse и предоставляет API для поиска и аналитики по ошибкам.
Ingestor подтверждает Kafka offset только после успешной записи в ClickHouse, что обеспечивает семантику доставки сообщений at-least-once.
В текущей версии поддерживается формат java-spring-logback - JSON-строки Java Spring Boot + Logback логов с выводом шаблона сообщения.
Чтобы подключить другой источник, например приложение на Python, Go или другом стеке, нужно выполнить два шага:
- Доставить события в Kafka
errlog-core. - Научить Ingestor обрабатывать новый
sourceType.
errlog-coreпринимает входные события из Kafka-топикаerrors-raw.- Для внешних источников необходимо использовать внешний listener Kafka:
${ERRLOG_KAFKA_EXTERNAL_HOST}:9094(см.docker/.env). - Сообщение в Kafka должно быть в формате JSON: одна запись Kafka - одно событие.
- Сообщение должно содержать поле
sourceTypeна верхнем уровне.
Минимальный пример JSON:
{
"sourceType": "my-app-source-type",
"timestamp": 1770000000000,
"service": "billing",
"level": "ERROR",
"message": "Something failed"
}Поля можно расширять. Способ доставки сообщений в errlog-core может быть произвольным, но в проекте предлагается использовать Vector как удобный агент сбора и отправки логов.
Важно: если sourceType неизвестен Ingestor, событие будет пропущено.
Ingestor выбирает нормализатор по полю sourceType и преобразует raw JSON в каноническую модель NormalizedErrorEvent.
Чтобы добавить новый формат, необходимо:
- Создать класс, реализующий
RawEventNormalizer. - Вернуть нужный
sourceType()- строго то же значение, которое приходит в JSON-сообщениях. - В
normalize(JsonNode rawEvent)распарсить поля события и вернутьNormalizedErrorEvent. - Пометить класс
@Component, чтобы он автоматически попал вRawEventNormalizerRegistry.
Скелет:
@Component
public class MyAppRawEventNormalizer implements RawEventNormalizer {
@Override
public String sourceType() {
return "my-app-source-type";
}
@Override
public Optional<NormalizedErrorEvent> normalize(JsonNode rawEvent) {
// Распарсить timestamp/service/level/message и опциональные поля.
// Вернуть new NormalizedErrorEvent(...).
}
}После этого сообщения с sourceType = "my-app-source-type" начнут приниматься и обрабатываться.
Для группировки ошибок у каждого обрабатываемого события в Ingestor вычисляется fingerprint.
Основа всегда начинается с:
service|logger|level
Далее используется один из следующих вариантов:
- Если у события есть
stacktrace:service|logger|level|stacktraceWithoutDigits,fingerprintSource=STACKTRACE. - Иначе если у события одновременно есть
exceptionClassиexceptionMessage:service|logger|level|exceptionClass|exceptionMessage,fingerprintSource=EXCEPTION. - Иначе если у события есть
messageTemplate:service|logger|level|messageTemplate,fingerprintSource=MESSAGE_TEMPLATE. - Иначе:
service|logger|level,fingerprintSource=MINIMAL.
Для уменьшения влияния шума из stacktrace (адреса, идентификаторы, номера строк) перед включением в основу из самого stacktrace удаляются все цифры. Остальные части основы при этом не изменяются.
Хэш вычисляется в ClickHouse как xxh3(fingerprintBase) и хранится как UInt64.
Так как logger является опциональным полем, в случае его отсутствия при вычислении fingerprint используется пустая строка.
В папке docker лежит .env - файл переменных окружения для Docker Compose.
Основной параметр сейчас один:
ERRLOG_KAFKA_EXTERNAL_HOST=host.docker.internalЕсли оба контура подняты на одной машине, этого достаточно для корректной работы.
Если core- и demo-контуры находятся на разных машинах, значением данного параметра следует поставить IP или DNS машины, где поднят errlog-core. Например:
ERRLOG_KAFKA_EXTERNAL_HOST=192.168.1.50Все команды ниже предполагают запуск из корня репозитория.
docker compose -f docker/docker-compose.core.yml up -d --builddocker compose -f docker/docker-compose.demo.yml up -d --buildВ папке scripts представлены вспомогательные скрипты. Их также следует запускать из корня репозитория.
Перезапустить core-контур:
./scripts/restart-core.shПерезапустить demo-контур:
./scripts/restart-demo.shПерезапустить весь стенд:
./scripts/restart-stand.shОстановить весь стенд:
./scripts/stop-stand.shОстановить весь стенд с удалением Docker volumes проекта:
./scripts/stop-stand-remove-volumes.shСтатус контейнеров:
docker compose -f docker/docker-compose.core.yml ps -aОжидается, что:
- сервисы
kafka-initиclickhouse-initбудут завершены сExited (0); - остальные сервисы будут находиться в состоянии
Up.
Проверка ClickHouse:
curl "http://localhost:8123/?user=errlog_ch_user&password=errlog_ch_password&query=SELECT%201"Здесь и далее %20 - экранирование пробела в HTTP-запросе.
Ожидается вывод:
1
Количество событий:
curl "http://localhost:8123/?user=errlog_ch_user&password=errlog_ch_password&query=SELECT%20count()%20FROM%20errlog_ch.error_events"Ожидается вывод количества записанных событий. Значение должно стать ненулевым после запуска demo-контура и поступления событий.
Логи Ingestor:
docker logs -f errlog-core-ingestor-1Ожидается поток логов обработки событий, если уже поднят demo-контур.
Статус контейнеров:
docker compose -f docker/docker-compose.demo.yml ps -aОжидается, что все сервисы будут находиться в состоянии Up.
Логи Vector:
docker logs -f errlog-demo-vector-1Ожидается успешное подключение к Kafka и начало обработки логов.
Swagger доступен по адресу:
http://localhost:8080/swagger-ui/index.html
Пользователь с ролью OWNER создается при старте Errapi из переменных окружения ERRLOG_OWNER_* (см. docker/docker-compose.core.yml), если такого пользователя еще нет в PostgreSQL.
curl -X POST "http://localhost:8080/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"login":"owner","password":"owner_password"}'Для удобства можно добавить полученный токен в переменную окружения:
export ERRLOG_OWNER_JWT="<token>"Проверка корректности токена:
curl -sS \
-H "Authorization: Bearer $ERRLOG_OWNER_JWT" \
"http://localhost:8080/api/users"Ожидается вывод информации о зарегистрированных пользователях.
curl -sS \
-H "Authorization: Bearer $ERRLOG_OWNER_JWT" \
"http://localhost:8080/api/errors/filters" | jqПо умолчанию возвращаются события за последние 24 часа.
curl -sS \
-H "Authorization: Bearer $ERRLOG_OWNER_JWT" \
-H "Content-Type: application/json" \
-X POST "http://localhost:8080/api/errors/events?limit=10&offset=0" \
-d '{}' | jqПример с границами времени:
curl -sS \
-H "Authorization: Bearer $ERRLOG_OWNER_JWT" \
-H "Content-Type: application/json" \
-X POST "http://localhost:8080/api/errors/events?limit=20&offset=0" \
-d '{"from":"2026-02-24T00:00:00Z","to":"2027-02-25T00:00:00Z"}' | jqПример с фильтрами:
curl -sS \
-H "Authorization: Bearer $ERRLOG_OWNER_JWT" \
-H "Content-Type: application/json" \
-X POST "http://localhost:8080/api/errors/events?limit=20&offset=0" \
-d '{"filters":[{"field":"service","operation":"in","values":["jerrgen-alpha","jerrgen-gamma"]},{"field":"level","operation":"eq","values":["ERROR"]}]}' | jqcurl -sS \
-H "Authorization: Bearer $ERRLOG_OWNER_JWT" \
"http://localhost:8080/api/errors/events/<eventId>" | jqcurl -sS \
-H "Authorization: Bearer $ERRLOG_OWNER_JWT" \
-H "Content-Type: application/json" \
-X POST "http://localhost:8080/api/errors/groups?limit=10&offset=0" \
-d '{}' | jqАвтоматический выбор размера бакета:
curl -sS \
-H "Authorization: Bearer $ERRLOG_OWNER_JWT" \
-H "Content-Type: application/json" \
-X POST "http://localhost:8080/api/errors/timeseries" \
-d '{}' | jqРучной выбор размера бакета:
curl -sS \
-H "Authorization: Bearer $ERRLOG_OWNER_JWT" \
-H "Content-Type: application/json" \
-X POST "http://localhost:8080/api/errors/timeseries?bucket=1m" \
-d '{}' | jq- Список событий сортируется по
timestamp DESC, eventId DESC. - Список групп событий сортируется по
groupCount DESC, groupLastSeen DESC, groupFingerprint DESC.
Если поток живой и в систему продолжают приходить новые события, для стабильной offset-пагинации следует зафиксировать to на момент первого запроса и переиспользовать его на следующих страницах, меняя только offset.
Проект покрыт автоматическими тестами (JUnit 5 + Mockito + Testcontainers). Тесты спроектированы, написаны и отлажены с помощью LLM (Claude Code): каждое покрытие сверялось с исходным кодом и прогонялось реальными ./mvnw test / verify до зелёного результата. В процессе был обнаружен и исправлен баг в production-коде (ErrapiException возвращал null из getMessage()).
| Модуль | Юнит-тесты | Интеграционные | Всего |
|---|---|---|---|
| Ingestor | 79 | 10 | 89 |
| Errapi | 129 | 13 | 142 |
| Jerrgen | 1 | - | 1 |
| Итого | 209 | 23 | 232 |
В таблице учтены только активные тесты. Дополнительно есть 2 отключённых smoke-теста (*ApplicationTests, требуют полный Spring-контекст + живую инфраструктуру) - итого 234 теста в проекте.
Юнит-тесты проверяют бизнес-логику без внешних зависимостей: нормализацию событий, вычисление fingerprint (все 4 ветки: STACKTRACE, EXCEPTION, MESSAGE_TEMPLATE, MINIMAL), форматирование стектрейса, парсеры, валидаторы, иерархию ролей, JWT-сервис, обработку исключений, семантику at-least-once (ack - только после успешной записи).
Интеграционные тесты поднимают реальные контейнеры через Testcontainers (ClickHouse и PostgreSQL) и проверяют: корректность SQL-запросов к ClickHouse (включая xxh3-хэш), миграции Flyway и constraint'ы PostgreSQL.
Тестовая стратегия, нюансы покрытия (включая недостижимую ветку EXCEPTION, семантику at-least-once, smoke-тесты) и список непокрытых компонентов - в TESTING.md.
# Все тесты одной командой
./scripts/run-all-tests.sh
# По модулям (только юниты, быстро, без Docker)
cd ingestor && ./mvnw test
cd errapi && ./mvnw test
# По модулям (юниты + интеграционные, нужен Docker)
cd ingestor && ./mvnw verify
cd errapi && ./mvnw verifyТребуется JDK 21. Интеграционные тесты требуют работающий Docker.