Skip to main content
Начните с /readyz на тех процессах, которые его отдают, и с датчика omniflow_dependency_up. Большинство инцидентов — это одна зависимость, и остальная часть страницы предполагает, что вы это уже исключили. Про готовность стоит знать две вещи, прежде чем на неё полагаться:
  • Бот вообще не обслуживает HTTP, пока не задан APP_BOT_METRICS_ADDR, и этот адрес игнорируется, когда он совпадает с APP_BOT_HTTP_ADDR, — публиковать пробы и метрики на смотрящем в интернет хосте вебхука запрещено. Из коробки у бота нет эндпоинта проб, поэтому «бот не отвечает на /readyz» обычно означает, что он никогда и не слушал.
  • API, запущенный без APP_DATABASE_URL, не регистрирует ни одной пробы зависимостей и отвечает 200 {"status":"ready","checks":[]}. Это зелёный свет для процесса, который умеет обслуживать только пробы и /metrics.
Воркер регистрирует только postgres. API регистрирует postgres, valkey и remnawave, когда настроена коммерция. См. Наблюдаемость.

Клиент заплатил и не получил доступ

Это тот сбой, против которого Omniflow защищается упорнее всего, и почти всегда дело в выдаче доступа, а не в оплате.
  1. Найдите заказ. GET /v1/admin/orders/{orderID} показывает его состояние и оплаченную сумму.
  2. paid означает, что деньги записаны и право на услугу существует. fulfilled означает, что Remnawave это подтвердил.
  3. GET /v1/admin/jobs?state=dead_letter показывает запуски выдачи доступа, которые сами по себе повторяться не будут.
  4. GET /v1/admin/fulfillment/drifts показывает права на услугу, чьё удалённое состояние не совпадает.
Чтобы перезапустить застрявший прогон, вызовите POST /v1/admin/jobs/{jobID}/retry или воспользуйтесь панелью: /admin/system → Jobs. Лежащая под этим операция исполнения несёт собственный ключ идемпотентности, поэтому повтор не может применить дважды то, что уже удалось. Платёж здесь ничем не рискует. Расчёт и запись долговременной задачи фиксируются в одной транзакции, поэтому у оплаченного заказа всегда есть ожидающая его задача.

Воркер падает при старте, жалуясь на отсутствующую таблицу

Сервис migrate применяет зафиксированные миграции Atlas из database/migrations, и среди них есть внутренние таблицы очереди (river_job, river_leader, river_queue, river_notification, river_migration). Если воркер падает на client.Start, сообщая об отсутствии одного из этих отношений, значит миграции не доехали до конца на той базе, на которую смотрит воркер: проверьте лог сервиса migrate и сравните APP_DATABASE_URL у сервисов, прежде чем предполагать проблему со связью. Пока этих таблиц нет, воркер не может запуститься, а оформление заказа, которое вставляет задачу исполнения внутри своей расчётной транзакции, падает вместе с ним. См. Задачи и очереди.

Платёж прошёл у провайдера, но заказ в ожидании

Вебхук не пришёл либо был отклонён.
  1. GET /v1/admin/webhooks перечисляет недавние поступления с signatureValid и status.
  2. signatureValid: false означает, что подпись не сошлась. Проверьте учётные данные провайдера и убедитесь, что обратный прокси не изменяет тело запроса — перекодированное тело делает недействительной любую подпись, потому что подпись считается по точным байтам, которые прислал провайдер.
  3. status: failed с payment_not_found означает, что вебхук ссылался на платёжное намерение, которого в этой установке нет. Обычно это вебхук из другого окружения, направленный на эту.
  4. Если вебхук не пришёл вовсе, POST /v1/admin/payments/{paymentID}/reconcile опрашивает провайдера напрямую. Проход payment_reconcile на воркере также сам переопрашивает до 100 незавершённых намерений каждую минуту.
Оба пути идемпотентны. Вебхук, пришедший после ручной сверки, ничего не делает.
Собственные кнопки Reconcile и Refund на /admin/finance записывают событие аудита о том, кто попросил и почему; сами по себе они не переопрашивают провайдера и не двигают деньги. Когда нужно, чтобы к провайдеру обратились, используйте маршруты /v1/admin выше.

Учётные данные провайдера в панели выглядят правильными, а платежи всё равно не проходят

Платёжные адаптеры, которые вызывают API, бот и воркер, строятся из переменных окружения при старте — APP_TELEGRAM_TOKEN, APP_CRYPTOBOT_TOKEN, APP_YOOKASSA_SHOP_ID, APP_YOOKASSA_SECRET. Учётные данные, сохранённые на карточках провайдеров в панели, — не то, чем пользуется работающий процесс. Измените переменную и перезапустите. См. Платежи и Настройки установки.

Пополнение зачислило неверную сумму

Смотрите топик topup или события платежа по заказу. Пополнение зачисляет ровно то, что провёл провайдер:
  • overpayment — пришло больше, чем запрашивалось, и всё это зачислено.
  • underpayment — пришло меньше, и зачислено именно это.
В обоих случаях заказ остаётся paid. Так и задумано: кошелёк не должен ничего сверх самих денег. Если расхождение нужно скорректировать, используйте корректировку кошелька с причиной, которая записывается компенсирующей проводкой, а не переписыванием.

Клиенту отказано в пополнении, которое он считает укладывающимся в лимиты

Лимиты пополнения берутся из APP_WALLET_TOPUP_* на API, боте и воркере, а не из карточки пополнения в панели. Сохранение новых цифр на этой карточке меняет то, что показывает карточка, а не то, что применяет магазин. Проверьте значения окружения и сумму в скользящем окне: отказ обычно даёт APP_WALLET_TOPUP_WINDOW_LIMIT_MINOR за APP_WALLET_TOPUP_WINDOW_HOURS, а не потолок одной операции.

Сохранённая корзина не выкупает себя

Рассчитанное пополнение пробует купить корзину сразу; любое другое начисление — реферальное вознаграждение, корректировка оператора — освобождает её через проход commerce_maintenance на воркере раз в минуту. Без работающего воркера такие корзины ждут, а неудачный проход логируется как periodic task failed с именем задачи. В остальных случаях проверьте carts.last_failure: Помните, что корзина переоценивается при каждой попытке. Корзина, сохранённая до повышения цены, по старой цене не купится — так и задумано.

Двойные списания

Они должны быть невозможны, и инварианты объясняют почему:
  • orders (user_id, idempotency_key) уникален, поэтому одно оформление заказа даёт один заказ.
  • ledger_transactions.idempotency_key уникален, и (reference_type, reference_id, type) уникален, поэтому один расчёт зачисляется один раз.
  • entitlement_addons (order_id, addon_version_id) уникален, поэтому одно дополнение применяется один раз.
  • fulfillment_operations.idempotency_key уникален, поэтому одна операция выполняется один раз.
Если вам кажется, что вы видите дубль, сравните ledger_entries по клиенту. Две проводки в рамках одной транзакции — это две стороны одного движения, а не два списания.

Операторская группа молчит

  1. Убедитесь, что APP_OPERATOR_CHAT_ID задан и бот был перезапущен. Если он не задан, бот пишет в лог operator notifications are disabled because no group is configured и никогда не разбирает очередь — уведомления при этом продолжают накапливаться строками pending и доставляются, как только группа будет настроена.
  2. Проверьте operator_topics на строку со status = 'failed' и прочитайте last_error_code. См. Операторские уведомления.
  3. Проверьте operator_notifications на строки со status = 'suppressed' и error_code = 'volume_cap' — это означает, что ограничитель делает свою работу.
  4. Если молчат только некоторые топики, это может быть правильно. У покупок, продлений, пополнений, возвратов, сбоев исполнения и админской безопасности есть источники событий. Топики 🚨 Инциденты и 💾 Резервные копии создаются, но сейчас в них ничто не ставится в очередь.

При входе появляется «Не удалось войти. Попробуйте ещё раз.» с верными данными

Это сообщение панель показывает, когда запрос вернулся без сессии, и есть две причины, не имеющие отношения к паролю. На экране они выглядят одинаково, и обе относятся к конфигурации, а не к учётным данным. Откройте вкладку сети в браузере: она покажет, какая из них у вас. Запрос вернул 404. Он дошёл до веб-приложения, а не до API. Обе панели обращаются к API на своём собственном origin — у браузерного клиента нет базового URL, и CORS нигде нет, — поэтому кто-то должен обслуживать /v1 на веб-хосте. За обратным прокси это его работа, и примеры в deploy/proxies её выполняют; конфигурация, написанная вручную и направляющая только приложение, даст ровно такую картину. Без прокси веб-сервису нужна переменная API_INTERNAL_URL; если она не задана, он отвечает 502 с проблемным документом, который её называет. См. Обратный прокси. Запрос вернул 200, а следующий уже анонимный. Браузер отбросил cookie. По обычному HTTP это значит, что Secure всё ещё включён: браузер не возвращает cookie с флагом Secure по незашифрованному соединению. Поставьте APP_ADMIN_COOKIE_SECURE=false и APP_CUSTOMER_COOKIE_SECURE=false для локального стека и оставьте их включёнными везде, где есть TLS. Проверьте заодно имя cookie в ответе: с Secure это __Host-omniflow_admin, без него — omniflow_admin, а имя с префиксом __Host-, пришедшее без Secure, отвергается целиком, а не просто считается более слабым.

Оператор не может войти, и страница пишет «rate limited»

Ограничение частоты запросов при входе опирается на Valkey и отказывает по умолчанию: когда Valkey недоступен, ограничитель отвечает «не разрешено», а не пропускает запросы, поэтому сбой Valkey полностью блокирует вход операторов. Сначала проверьте запись valkey в выводе /readyz у API. Настоящее ограничение отвечает 429 rate_limited; заблокированная учётная запись отвечает 429 account_locked после пяти неудач подряд, причём ожидание удваивается вплоть до часа. Учтите, что сбой Valkey не открывает режим обслуживания. Автоматическое обнаружение следит только за postgres и remnawave, поэтому valkey появляется в /readyz и сам по себе никогда не запускает окно.

Бот не получает обновлений

В режиме длинных опросов проверьте логи бота на ошибки Telegram API и убедитесь, что исходящий HTTPS работает. В режиме вебхука:
  1. Убедитесь, что прокси направляет POST /telegram/webhook на слушатель бота (APP_BOT_HTTP_ADDR, по умолчанию :8082) и не переписывает тело. Эндпоинт ограничивает тело 1 MiB.
  2. Убедитесь, что APP_TELEGRAM_WEBHOOK_SECRET совпадает с тем, что было зарегистрировано. Обновление без заголовка с секретным токеном отбрасывается до разбора, и выглядит это ровно как отсутствие трафика. Секрет должен соответствовать ^[A-Za-z0-9_-]{32,256}$, иначе бот откажется стартовать.
  3. APP_TELEGRAM_WEBHOOK_URL должен быть HTTPS, иначе бот откажется стартовать.
  4. Спросите у Telegram, что думает он: getWebhookInfo сообщает URL, на который он доставляет, и последнюю увиденную ошибку.
Возврат к длинным опросам — поддерживаемый запасной вариант: очистите APP_TELEGRAM_WEBHOOK_URL и перезапустите.

/ops ничего не делает в Telegram

Команда зарегистрирована всегда и отвечает вообще ничем аккаунту, которому ею пользоваться нельзя, поэтому факт существования команды клиенту не подтверждается. Прежде чем вы получите хоть какой-то ответ, должны выполняться оба условия:
  1. APP_OPERATOR_TELEGRAM_IDS содержит числовой ID аккаунта Telegram, и бот с тех пор был перезапущен. Пустой список полностью отключает эту поверхность, и это значение по умолчанию.
  2. Сообщение отправлено в приватном чате с ботом. В группе оно игнорируется.
Если ответ всё же приходит и в нём написано «Backups are disabled», поверхность работает, а отсутствует APP_BACKUP_ENABLED или APP_BACKUP_ENCRYPTION_KEY. Некорректная запись в APP_OPERATOR_TELEGRAM_IDS — фатальная ошибка конфигурации, поэтому, если процесс вместо этого сразу завершился, проверьте стартовый лог бота.

Покупки отклоняются с уведомлением об обслуживании

Проверьте GET /v1/admin/maintenance или вкладку Health на /admin/system в панели. source говорит, было ли окно ручным (manual) или автоматическим (database, remnawave). Автоматическое окно закрывается само, как только зависимость была здорова APP_MAINTENANCE_RECOVERY_STREAK замеров подряд; ручное само не закрывается никогда. Задача исполнения, попавшая в активное окно, откладывается на минуту, а не проваливается, и её счётчик попыток возвращается, поэтому пауза на обслуживание не тратит бюджет повторов. См. Режим обслуживания.

Число ожидающих событий в outbox только растёт

Так будет всегда. Строки outbox_events пишутся на order.paid, wallet.credited и addon.purchased, и ничто в продукте не отмечает их опубликованными. Поэтому GET /v1/admin/outbox и вкладка outbox в панели показывают число, которое растёт всю жизнь установки, а APP_RETENTION_OUTBOX_DAYS — который удаляет только опубликованные строки — не удаляет ничего. Это не отставание, это не застрявший диспетчер, и никакое действие это не очищает. Не настраивайте на это алерты. Объявленный датчик omniflow_outbox_pending_events тоже никогда не пишется, поэтому собирать нечего.

Резервные копии не создаются

Причина сбоя записана в строке резервной копии и в логе процесса, который её делал, в виде классифицированного кода: Плановые резервные копии выполняются в воркере; копии и восстановления из /ops — в боте, поэтому смотрите лог именно этого процесса, даже если сообщение на экране упоминает воркер. pg_dump должен соответствовать мажорной версии сервера. Образы бота и воркера поставляются с клиентскими утилитами PostgreSQL 18 именно по этой причине; если вы запускаете их где-то ещё, направьте APP_PG_DUMP_PATH и APP_PG_RESTORE_PATH на подходящие бинарники. См. Резервные копии и восстановление.

Вложение к обращению, загруженное из веба, пропало

APP_SUPPORT_ATTACHMENT_DIR (по умолчанию /var/lib/omniflow/support-attachments) читают и API, который пишет вложения, и воркер, который освобождает их файлы при истечении срока хранения. Отсюда два вида сбоев:
  • Compose-стек не монтирует том по этому пути ни на одном из сервисов, поэтому вложение, записанное API, живёт в записываемом слое контейнера и теряется при его пересборке. Смонтируйте том на API, если вложения, загруженные из браузера, должны переживать повторное развёртывание.
  • Если значение у воркера отличается от значения у API, срок хранения удаляет строки и оставляет байты на диске навсегда. Задайте одинаковый путь в обоих местах.
Вложения в установках, работающих только через Telegram, — это ссылки на файлы, которые хранит Telegram, и их это не затрагивает.

Запущено больше одной реплики

Поддерживается сколько угодно реплик api и worker: периодические проходы — обслуживание заказов и корзин, сверку платежей, продления, обходы, рассылки, очистку по срокам хранения, резервные копии — ставит в очередь выбранный River лидер, и они выполняются один раз за интервал, сколько бы воркеров ни работало. Исключение — bot: Telegram доставляет обновления одному потребителю, и long polling отказывает второму getUpdates. Запускайте ровно один бот. Больше реплик — это и больше подключений к базе. Каждая открывает пул размером APP_DATABASE_MAX_CONNS (по умолчанию api 20, bot 10, worker 40), и сумма должна оставаться ниже max_connections PostgreSQL. Воркер, которому дали меньше 32, отказывается запускаться и сообщает, сколько ему нужно. См. Развёртывание.

Процесс упал, и его никто не поднял

Поставляемый compose.yaml не объявляет политику restart: ни для postgres, valkey, api, bot, worker, ни для web — её задаёт только оверлей Traefik. Упавший процесс остаётся лежать, пока кто-нибудь не заметит. Для развёртывания без присмотра добавьте политику перезапуска. См. Развёртывание.

У клиента две подписки, а ожидается одна

Одновременные подписки по умолчанию отключены. Если у клиента их больше одной, значит либо переключатель включён, либо они были приняты от разных пользователей Remnawave при импорте. См. Подписки — там описано, как свести клиента к одной, не осиротив доступ.

Трассировки пустые

Omniflow открывает спаны на входящем HTTP, обновлениях Telegram, задачах исполнения, запросах к PostgreSQL и исходящем HTTP, но никогда не устанавливает провайдер трассировки и не читает ни одной переменной OTEL_*. Без установленного провайдера глобальный является пустышкой, и ни один спан не экспортируется, поэтому одна лишь настройка переменных коллектора ничего не меняет. Метрики и логи — та инструментальная база, на которую сегодня можно полагаться.