/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 защищается упорнее всего, и почти всегда дело в выдаче доступа, а не в оплате.- Найдите заказ.
GET /v1/admin/orders/{orderID}показывает его состояние и оплаченную сумму. paidозначает, что деньги записаны и право на услугу существует.fulfilledозначает, что Remnawave это подтвердил.GET /v1/admin/jobs?state=dead_letterпоказывает запуски выдачи доступа, которые сами по себе повторяться не будут.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
у сервисов, прежде чем предполагать проблему со связью.
Пока этих таблиц нет, воркер не может запуститься, а оформление заказа, которое вставляет
задачу исполнения внутри своей расчётной транзакции, падает вместе с ним. См.
Задачи и очереди.
Платёж прошёл у провайдера, но заказ в ожидании
Вебхук не пришёл либо был отклонён.GET /v1/admin/webhooksперечисляет недавние поступления сsignatureValidиstatus.signatureValid: falseозначает, что подпись не сошлась. Проверьте учётные данные провайдера и убедитесь, что обратный прокси не изменяет тело запроса — перекодированное тело делает недействительной любую подпись, потому что подпись считается по точным байтам, которые прислал провайдер.status: failedсpayment_not_foundозначает, что вебхук ссылался на платёжное намерение, которого в этой установке нет. Обычно это вебхук из другого окружения, направленный на эту.- Если вебхук не пришёл вовсе,
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 по клиенту. Две проводки в
рамках одной транзакции — это две стороны одного движения, а не два списания.
Операторская группа молчит
- Убедитесь, что
APP_OPERATOR_CHAT_IDзадан и бот был перезапущен. Если он не задан, бот пишет в логoperator notifications are disabled because no group is configuredи никогда не разбирает очередь — уведомления при этом продолжают накапливаться строкамиpendingи доставляются, как только группа будет настроена. - Проверьте
operator_topicsна строку соstatus = 'failed'и прочитайтеlast_error_code. См. Операторские уведомления. - Проверьте
operator_notificationsна строки соstatus = 'suppressed'иerror_code = 'volume_cap'— это означает, что ограничитель делает свою работу. - Если молчат только некоторые топики, это может быть правильно. У покупок, продлений, пополнений, возвратов, сбоев исполнения и админской безопасности есть источники событий. Топики 🚨 Инциденты и 💾 Резервные копии создаются, но сейчас в них ничто не ставится в очередь.
При входе появляется «Не удалось войти. Попробуйте ещё раз.» с верными данными
Это сообщение панель показывает, когда запрос вернулся без сессии, и есть две причины, не имеющие отношения к паролю. На экране они выглядят одинаково, и обе относятся к конфигурации, а не к учётным данным. Откройте вкладку сети в браузере: она покажет, какая из них у вас. Запрос вернул 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 работает. В режиме вебхука:- Убедитесь, что прокси направляет
POST /telegram/webhookна слушатель бота (APP_BOT_HTTP_ADDR, по умолчанию:8082) и не переписывает тело. Эндпоинт ограничивает тело 1 MiB. - Убедитесь, что
APP_TELEGRAM_WEBHOOK_SECRETсовпадает с тем, что было зарегистрировано. Обновление без заголовка с секретным токеном отбрасывается до разбора, и выглядит это ровно как отсутствие трафика. Секрет должен соответствовать^[A-Za-z0-9_-]{32,256}$, иначе бот откажется стартовать. APP_TELEGRAM_WEBHOOK_URLдолжен быть HTTPS, иначе бот откажется стартовать.- Спросите у Telegram, что думает он:
getWebhookInfoсообщает URL, на который он доставляет, и последнюю увиденную ошибку.
APP_TELEGRAM_WEBHOOK_URL и перезапустите.
/ops ничего не делает в Telegram
Команда зарегистрирована всегда и отвечает вообще ничем аккаунту, которому ею пользоваться
нельзя, поэтому факт существования команды клиенту не подтверждается. Прежде чем вы получите
хоть какой-то ответ, должны выполняться оба условия:
APP_OPERATOR_TELEGRAM_IDSсодержит числовой ID аккаунта Telegram, и бот с тех пор был перезапущен. Пустой список полностью отключает эту поверхность, и это значение по умолчанию.- Сообщение отправлено в приватном чате с ботом. В группе оно игнорируется.
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, срок хранения удаляет строки и оставляет байты на диске навсегда. Задайте одинаковый путь в обоих местах.
Запущено больше одной реплики
Поддерживается сколько угодно реплик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_*. Без установленного провайдера глобальный является пустышкой, и
ни один спан не экспортируется, поэтому одна лишь настройка переменных коллектора ничего не
меняет. Метрики и логи — та инструментальная база, на которую сегодня можно полагаться.