Troubleshooting Guide
Operational runbook: от жалобы пользователя до root cause.
Пользователь написал "не работает" / "не засчиталось" / "не пришло" — открой эту страницу и следуй шагам.
Для изучения архитектуры observability → Observability Для стандартов логирования → Logging Guidelines
1. Quick Start (90% случаев)
Пользователь сообщил о проблеме
↓
Шаг 1: Admin Panel → карточка юзера → кнопка "Логи"
↓
Шаг 2: В Grafana отфильтровать по времени инцидента
↓
Шаг 3: Найти лог с level=error или reason != ""
↓
Шаг 4: Скопировать requestId → найти ВСЕ логи этого запроса
↓
Шаг 5: Прочитать цепочку: вход → бизнес-логика → решение → выход
↓
Root cause найден
2. Decision Tree: Какой инструмент использовать
Когда какой инструмент
| Ситуация | Инструмент | Что делать |
|---|---|---|
| Пользователь жалуется "не засчиталось" | /diagnostics | Автоматический поиск в Grafana по userId + domain |
| Ошибка 500 в production | /diagnostics --errors | Поиск последних ошибок + цепочка requestId |
| Системные проблемы | /diagnostics --health | Health check: targets, cache, error rate |
| Поле не приходит на frontend | /debug | 7-layer data flow trace |
| Медленный ответ | Tempo (через Grafana) | Trace waterfall |
| Баг не воспроизводится | /trace | Добавить диагностические логи |
| Непонятно как работает фича | /investigate | Deep research по слоям |
| Проверить данные в БД | /db | SQL запрос к БД |
3. Grafana: Пошаговый flow
Шаг 1: Найти пользователя
Через Admin Panel (рекомендуется):
- Открыть админку → найти пользователя по telegramId или имени
- Нажать кнопку "Логи" → Grafana откроется с pre-filled запросом
Вручную в Grafana:
{container_name=~".*backend.*"} | json | userId = "USER_UUID_HERE"
Если известен только telegramId — сначала найди пользователя в админке, скопируй его UUID.
Шаг 2: Отфильтровать по времени
- В Grafana → установи time range на период инцидента
- Пользователь обычно помнит "примерно час назад" → ставь ±1 час
Шаг 3: Найти проблемный лог
Ищи ошибки:
{container_name=~".*backend.*"} | json | userId = "UUID" | level >= 40
Ищи отказы по бизнес-правилам:
{container_name=~".*backend.*"} | json | userId = "UUID" | reason != ""
Ищи конкретный endpoint:
{container_name=~".*backend.*"} | json | userId = "UUID" | path =~ "/api/quests/.*/claim"
Шаг 4: Развернуть контекст запроса
Нашёл подозрительный лог → скопируй requestId:
{container_name=~".*backend.*"} | json | requestId = "abc-123-def"
Это покажет полную цепочку одного HTTP-запроса:
[Domain] ...— бизнес-логика, decision points[Domain] .../logBusinessEvent(...)— решениеHTTP Request— onResponse лог с statusCode, duration, path
Шаг 5: Определить root cause
| Что видишь в логах | Root cause | Действие |
|---|---|---|
reason: "NOT_COMPLETED" | Прогресс не достигнут | Проверить progress vs targetValue |
reason: "ALREADY_CLAIMED" | Повторный claim | Пользователь уже получил награду |
reason: "COOLDOWN" | Cooldown не прошёл | Показать когда будет доступно |
reason: "INSUFFICIENT_BALANCE" | Не хватает ресурсов | Проверить баланс |
level: 50 (error) | Exception в коде | Смотреть error.message, stacktrace |
statusCode: 500 | Серверная ошибка | Искать error в цепочке requestId |
| Нет логов domain-уровня | Запрос не дошёл до сервиса | Проверить auth, middleware, route |
Шаг 6 (опционально): Tempo trace
Для анализа где тормозит запрос:
- Из лога скопируй
trace_id(не путать сrequestId) - Grafana → Explore → Tempo datasource
- Вставь
trace_id→ waterfall диаграмма
Tempo покажет:
- Время каждой операции (Prisma запросы, HTTP вызовы)
- Вложенность операций (span hierarchy)
- Где именно произошла задержка
В Grafana настроена связь: клик на span в Tempo → переход к логам с этим requestId.
4. LogQL Cheat Sheet
По типу проблемы
Квест не засчитался
# Все quest-related логи пользователя (controller + services)
{container_name=~".*backend.*"} |= "Quest" | json | userId = "UUID"
# Отказы по квестам (thrown as QuestRewardError, caught by controller)
{container_name=~".*backend.*"} |= "QuestRewardError" | json | userId = "UUID"
# Прогресс квестов
{container_name=~".*backend.*"} |= "[QuestProgressService]" | json | userId = "UUID"
Награда не пришла (XP, Scrap)
# Бизнес-события экономики пользователя
{container_name=~".*backend.*"} |= "[BUSINESS]" | json | userId = "UUID"
# Все economy события
{container_name=~".*backend.*"} | json | eventType =~ "economy.*" | userId = "UUID"
# Конкретно scrap
{container_name=~".*backend.*"} | json | eventType = "economy.scrap_earned" | userId = "UUID"
Кейс не открылся
# Case opening логи
{container_name=~".*backend.*"} |= "[Case]" | json | userId = "UUID"
# Ошибки на endpoint
{container_name=~".*backend.*"} | json | path =~ "/api/cases.*" | userId = "UUID" | level >= 40
Referral проблемы
# Referral логи пользователя
{container_name=~".*backend.*"} |= "[Referral]" | json | userId = "UUID"
# Passive income
{container_name=~".*backend.*"} |= "passive" | json | userId = "UUID"
# Активация реферала
{container_name=~".*backend.*"} |= "activate" |= "[Referral]" | json
Webhook от Rust сервера
# Все webhook логи
{container_name=~".*backend.*"} | json | path =~ "/webhook.*"
# Webhook ошибки
{container_name=~".*backend.*"} | json | path =~ "/webhook.*" | level >= 50
# Конкретный тип события
{container_name=~".*backend.*"} |= "KILL_ANIMAL" | json
Промокод не сработал
# Promo code логи
{container_name=~".*backend.*"} |= "[CodeActivation]" | json | userId = "UUID"
# Отказы
{container_name=~".*backend.*"} | json | path = "/api/promo-codes/redeem" | userId = "UUID"
Общие запросы
# Все ошибки 500 за последний час
{container_name=~".*backend.*"} | json | statusCode = 500
# Медленные запросы (>2 секунды)
{container_name=~".*backend.*"} | json | duration > 2000
# Все отказы по бизнес-правилам
{container_name=~".*backend.*"} | json | reason != ""
# Бизнес-события определённого домена
{container_name=~".*backend.*"} |= "[BUSINESS]" | json | eventType =~ "quests.*"
# Поиск по тексту ошибки
{container_name=~".*backend.*"} |= "PrismaClientKnownRequestError"
# Rate of errors (для dashboards)
rate({container_name=~".*backend.*"} | json | level >= 50 [5m])
Reason Codes — что означает каждый
| Reason Code | Что произошло | Это баг? |
|---|---|---|
NOT_COMPLETED | Прогресс не достигнут (5/10) | Нет — пользователь не выполнил условие |
ALREADY_CLAIMED | Награда уже получена | Нет — повторный запрос |
COOLDOWN_ACTIVE | Не прошёл cooldown (spin, passive income) | Нет — нужно подождать |
NOT_FOUND | Сущность не найдена | Возможно — проверить ID |
INSUFFICIENT_BALANCE | Не хватает scrap/SP | Нет — недостаточно средств |
SELF_REFERRAL / self_referral | Попытка саморефа | Нет — антифрод |
EXPIRED | Истёк срок (промокод, сессия) | Нет — время вышло |
Большинство reason codes — это нормальное поведение системы. Баг — когда reason не соответствует реальности (прогресс 10/10, а reason = NOT_COMPLETED).
5. Escalation: когда логов недостаточно
Уровень 1: /debug — Data Flow Trace
Когда: Поле = null / 0 / не приходит на frontend, но в БД данные есть.
/debug "баффы не показывают tier"
/debug проверяет 7 слоёв в порядке частоты багов:
| # | Слой | % багов | Что проверяет |
|---|---|---|---|
| 1 | Fastify Schema | 45% | Поле есть в response schema? |
| 2 | Controller | 20% | Поле передаётся в response? |
| 3 | Prisma Query | 15% | Поле в select/include? |
| 4 | Shared Types | 8% | Тип синхронизирован? |
| 5 | Service Return | 5% | Return включает поле? |
| 6 | Frontend | 4% | Правильный путь к данным? |
| 7 | Zod Validation | 3% | .strict() не отбрасывает? |
Результат: Точный файл:строка где данные "теряются".
Уровень 2: /trace — Диагностические логи
Когда: /debug не нашёл проблему в коде, нужно видеть runtime данные.
/trace "quest reward не начисляется"
/trace добавляет маркированные logger.debug('[TRACE:ID]') в критичные точки.
После деплоя — воспроизвести проблему и найти в Grafana:
{container_name=~".*backend.*"} |= "TRACE:a1b2c3"
После решения: обязательно удалить trace-логи:
/trace --clean
Уровень 3: /investigate — Deep Research
Когда: Нужно понять архитектуру прежде чем искать баг.
/investigate --flow "webhook → quest progress → frontend"
/investigate --impact "удалить поле X"
Уровень 4: /db — Прямой запрос к БД
Когда: Нужно проверить состояние данных конкретного пользователя.
/db "квесты пользователя UUID"
/db "passive income balance для UUID"
6. Типичные проблемы и их сигнатуры
"Квест не засчитывается"
Сигнатура в логах:
[QuestController] QuestRewardError caught { code: "NOT_COMPLETED", ... }
Checklist:
- Прогресс действительно не достигнут? → Проверить
currentvsrequired - Progress обновляется? → Искать
[QuestProgressService] Progress updatedдля этого userId - Webhook приходит? → Искать
path =~ "/webhook.*"с нужным eventType - Правильный serverId? → Проверить что сервер привязан к пользователю
"Награда не пришла"
Сигнатура в логах:
[BUSINESS] QUEST_REWARD_CLAIMED { userId, questId, questTitle, rewardType, rewardAmount }
Если [BUSINESS] лог есть, но баланс не изменился:
- Проверить транзакцию → была ли ошибка после event?
- Проверить Prisma query →
$transactionзавершился?
Если [BUSINESS] лога нет:
- Искать
reasonв warn логах - Искать
errorв цепочке requestId
"Ошибка 500"
Сигнатура в логах:
HTTP Request { statusCode: 500, duration: 45, path: "/api/...", requestId: "..." }
Всегда:
- Скопировать
requestId - Найти все логи этого request — среди них будет
level: 50сerror.message - Типичные причины:
PrismaClientKnownRequestError→ проблема с БД запросомTypeError: Cannot read properties of null→ данные не найдены, нет проверкиETIMEDOUT→ внешний сервис не отвечает
"Баффы / предметы не отображаются"
Скорее всего /debug проблема, а не лог-проблема:
- 45% случаев — поле не описано в Fastify response schema
- 20% — controller не передаёт поле
- 15% — Prisma select/include не включает relation
/debug "баффы не показывают tier"
"Поле есть в БД, но не приходит на frontend"
Сигнатура: Данные корректно сохранены в БД, API endpoint отвечает 200, но поле отсутствует в JSON response.
Root cause в 95% случаев — один из трёх слоёв:
| # | Слой | Что проверить | Пример |
|---|---|---|---|
| 1 | Fastify Response Schema (45%) | CaseSchema.properties содержит поле? | seasonCases отсутствовал в case.schemas.ts |
| 2 | Mapper (30%) | toPlainObject() включает поле? | seasonCases не маппился в case.mapper.ts |
| 3 | Prisma include/select (20%) | Repository загружает relation? | seasonCases в findAll() include |
Fastify использует response schema как whitelist. Если поле не объявлено в schema.response[200].properties — оно будет молча удалено из JSON ответа. Ошибки не будет — просто поле исчезнет.
Это самая коварная ловушка: mapper возвращает данные, console.log показывает их, но клиент получает объект без этого поля.
Checklist:
grep "fieldName" backend/src/domains/{domain}/schemas/*.ts— есть в response schema?grep "fieldName" backend/src/domains/{domain}/mappers/*.ts— есть в mapper?grep "fieldName" backend/src/domains/{domain}/repositories/*.ts— есть в Prisma include?
/debug "поле X не приходит на frontend"
"Webhook не обрабатывается"
Сигнатура в логах:
{container_name=~".*backend.*"} | json | path =~ "/webhook.*" | level >= 40
Checklist:
- Webhook вообще приходит? → Искать
HTTP Requestсpath =~ "/webhook.*" - Валидация прошла? → Искать
[Rust]логи - Идемпотентность → Повторный webhook? Проверить
RustWebhookLog
7. Инфраструктурные проблемы
Docker Swarm не подхватывает новый image после деплоя
Это задокументированный баг Docker Swarm с локальными images. Также воспроизводится в Dokploy.
Симптом: Dokploy показывает "Done ✅", но контейнер работает на старом image.
Диагностика:
# Image ID контейнера (текущий):
docker inspect $(docker ps -q -f name=backend) --format='{{.Image}}'
# Image ID последнего билда:
docker images | grep backend
# Если ID разные — контейнер не обновился
Причина: Docker Swarm кэширует image ID при создании/обновлении сервиса. Когда Dokploy пересобирает image с тегом :latest, Swarm не переразрешает тег — продолжает использовать старый image ID. Это происходит потому что Swarm спроектирован для работы с registry, а не с локальными images.
Решение (от быстрого к радикальному):
- Deploy через Dokploy UI — обычно работает, но не всегда
- Force update через CLI:
docker service update --image golootstaging-backend-z9c2jc:latest --force golootstaging-backend-z9c2jc - Если задачи зависают в "New" state (moby#35849):
# Проверить статус задач:
docker service ps golootstaging-backend-z9c2jc --no-trunc
# Если NODE пустой и CURRENT STATE = "New" — Swarm не может назначить задачу
# Перезагрузить Docker (поднимет все сервисы с актуальными images):
systemctl restart docker
Верификация после исправления:
# Проверить что контейнер использует новый image:
docker inspect $(docker ps -q -f name=backend) --format='{{.Image}}'
docker images | grep backend
# Image ID должны совпадать
# Проверить конкретный файл внутри контейнера:
docker exec $(docker ps -q -f name=backend) grep "ожидаемый_контент" /app/backend/dist/path/to/file.js
Профилактика:
- После Dokploy Deploy/Rebuild всегда проверять image ID контейнера vs image ID в
docker images - Если не совпадают —
systemctl restart docker
Логи не появляются в Grafana
-
Приложение не выводит логи?
- Проверить
LOG_LEVELв Dokploy → Environment Variables LOG_LEVEL=infoне покажет debug логи
- Проверить
-
Promtail не работает?
docker logs promtail -
Loki не принимает?
curl -s http://localhost:3100/ready -
Docker socket?
docker exec promtail ls -la /var/run/docker.sock
Подробнее: Log Architecture → Troubleshooting
Нужно временно включить debug логи
- В Dokploy: изменить
LOG_LEVEL=debug - Перезапустить контейнер
- Воспроизвести проблему
- Не забыть вернуть
LOG_LEVEL=infoпосле диагностики
Логи старше 7 дней
Loki хранит логи 7 дней. Если проблема произошла раньше — логов уже нет.
Альтернативы:
- Docker logs на сервере (если не ротированы)
git logдля анализа что менялось в коде в тот период
8. Reference
Grafana Access
| Ресурс | URL | Доступ |
|---|---|---|
| Grafana UI | https://grafana.goloot.online | Логин/пароль (Traefik + SSL) |
| Grafana API | https://grafana.goloot.online/api/ | Service Account Token (Bearer) |
| Prometheus | Только внутри Docker (prometheus:9090) | Через Grafana datasource proxy |
| Loki | Только внутри Docker (loki:3100) | Через Grafana datasource proxy |
| Tempo | Только внутри Docker (tempo:3200) | Через Grafana datasource proxy |
Prometheus, Loki, Tempo не имеют внешних портов. Все запросы — через Grafana UI или Grafana API (datasource proxy).
Grafana API (для автоматизации)
Grafana Service Account с ролью Viewer позволяет запрашивать все datasource через API без UI:
# Prometheus метрики
curl -H "Authorization: Bearer glsa_xxx" \
"https://grafana.goloot.online/api/datasources/proxy/uid/prometheus/api/v1/query?query=cache_hit_total"
# Loki логи (последний час)
curl -G -H "Authorization: Bearer glsa_xxx" \
"https://grafana.goloot.online/api/datasources/proxy/uid/loki/loki/api/v1/query_range" \
--data-urlencode 'query={container=~".*backend.*"} | json | level >= 50' \
--data-urlencode "start=$(date -d '1 hour ago' +%s)" \
--data-urlencode "end=$(date +%s)" --data-urlencode "limit=10"
# Статус всех Prometheus targets
curl -H "Authorization: Bearer glsa_xxx" \
"https://grafana.goloot.online/api/datasources/proxy/uid/prometheus/api/v1/targets"
Создание токена: Grafana → Administration → Service Accounts → New (Viewer) → Generate Token. Подробнее: Monitoring Setup → Grafana API.
Datasources в Grafana
| Source | UID | Назначение | Когда использовать |
|---|---|---|---|
| Loki | loki | Логи | Поиск по userId, requestId, reason |
| Prometheus | prometheus | Метрики | Dashboards, rate of errors, latency, cache stats |
| Tempo | tempo | Traces | Waterfall запроса, bottleneck analysis |
Связанные документы
| Документ | Что покрывает |
|---|---|
| Observability | Архитектура: Logger API, Metrics, Tracing, Business Events |
| Log Architecture | Пайплайн: Pino → Docker → Promtail → Loki → Grafana |
| Logging Guidelines | Стандарты: формат, domain tags, reason codes |
| Monitoring README | Quick start мониторинг-стека |
Claude Skills для troubleshooting
| Skill | Когда использовать |
|---|---|
/diagnostics "проблема" | Основной инструмент — автоматический health check + поиск в Grafana (Loki, Prometheus, Tempo) по userId, domain, reason codes. Поддерживает --user=UUID, --errors, --health |
/debug "симптом" | Поле не приходит / null / 0 — проверка 7 слоёв |
/trace "проблема" | Нужны runtime-данные — добавление диагностических логов |
/investigate "вопрос" | Нужно понять архитектуру — deep research |
/db "запрос" | Проверить данные в БД напрямую |
При любой проблеме пользователя начинай с /diagnostics. Он автоматически:
- Проверит health всех систем (Prometheus targets, cache, errors)
- Определит домен по описанию проблемы
- Найдёт логи пользователя в Loki (reason codes, errors, business events)
- При необходимости — запросит полную цепочку по requestId
Только если /diagnostics не дал ответа — переходи к /debug, /trace, /db.