> ## Documentation Index
> Fetch the complete documentation index at: https://omniflow-vpn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Наблюдаемость

> Пробы, которые отдаёт каждый процесс, метрики, которые действительно пишутся, как устроены логирование и редактирование, и реальное состояние трассировки

Операционная поверхность Omniflow намеренно мала: три неаутентифицированных HTTP-эндпоинта, один реестр Prometheus на процесс и структурированные логи в формате JSON в stdout. Всё на ней рассчитано на ваш оркестратор и ваш мониторинг, и ничему из этого не место на публичном хосте API — оба поставляемых примера прокси отдают 404 на `/metrics` и `/readyz` в домене API, потому что эти эндпоинты отвечают на вопросы о внутреннем устройстве установки любому желающему. См. [Обратный прокси](https://omniflow-vpn.mintlify.site/ru/operations/reverse-proxy.md).

Эта страница описывает то, что существует. Там, где метрика объявлена, но никогда не пишется, об этом сказано прямо, потому что оповещение, построенное на ряде, который никогда не выдаётся, — это оповещение, которое никогда не срабатывает.

## Три эндпоинта

| Эндпоинт | Что отвечает | Для чего использовать |
| - | - | - |
| `/livez` | `200 {"status":"ok","version":"…"}`, как только процесс начал обслуживать запросы | Проба живости и политика перезапуска |
| `/healthz` | Тот же обработчик, байт в байт | Совместимость с существующими развёртываниями |
| `/readyz` | `200 {"status":"ready",…}` или `503 {"status":"degraded",…}` | Проба готовности и членство в балансировщике нагрузки |

Они разделены, потому что отвечают на разные вопросы. Процесс, который работает, но не может достучаться до PostgreSQL, надо вывести из пула, а не убивать и перезапускать: перезапуск не вернёт базу данных, а цикл перезапусков превращает одну отказавшую зависимость в полное отсутствие мощности.

> [!NOTE]
>
> `version` всегда равна `dev`. Все три бинарника объявляют `var version = "dev"`, и ни Dockerfile, ни релизный workflow не переопределяют её флагом компоновщика, поэтому выпущенные образы сообщают `dev` в `/livez`, в операторском диагностическом пакете и в анонимном телеметрическом сигнале. Опознавайте работающую сборку по тегу образа, а не по этому полю.

### Где слушает каждый процесс

| Процесс | Переменная | По умолчанию | Примечания |
| - | - | - | - |
| `api` | `APP_HTTP_ADDR` | `:8080` | Всегда отдаёт пробы, даже без настроенной базы данных |
| `worker` | `APP_WORKER_HTTP_ADDR` | `:8081` | Пустое значение полностью отключает слушателя |
| `bot` | `APP_BOT_METRICS_ADDR` | *(не задана)* | Не задана — значит, бот **не** обслуживает HTTP вообще |

Из коробки у бота нет эндпоинта проб. Если в вашем регламенте написано «проверить `/readyz` на каждом процессе», сначала задайте `APP_BOT_METRICS_ADDR`. Она игнорируется, когда совпадает с `APP_BOT_HTTP_ADDR`, адресом слушателя вебхуков: публиковать метрики на хосте, до которого достаёт Telegram, запрещено, а не тихо разрешено.

## Готовность

Готовность запускает по одной пробе на каждую зарегистрированную зависимость и отвечает 503, пока хоть одна из них не проходит.

| Процесс | Зарегистрированные пробы |
| - | - |
| `api`, с заданным `APP_DATABASE_URL` | `postgres` (`Ping`), `valkey` (`PING`), `remnawave` (аутентифицированный вызов) |
| `api`, без `APP_DATABASE_URL` | нет ни одной |
| `worker` | `postgres` |
| `bot` | `postgres`, и только когда существует пул |

```json theme={null}
{
  "status": "degraded",
  "checks": [
    {"name": "postgres", "healthy": true, "checkedAt": "2026-08-11T12:00:00Z", "latencyMs": 2},
    {"name": "valkey", "healthy": true, "checkedAt": "2026-08-11T12:00:00Z", "latencyMs": 1},
    {"name": "remnawave", "healthy": false, "error": "timeout", "checkedAt": "2026-08-11T12:00:00Z", "latencyMs": 3001, "consecutiveFailures": 4}
  ]
}
```

`error` — это стабильная классификация, и она бывает только `timeout` (контекст пробы истёк) или `unavailable`. Собственное сообщение драйвера отбрасывается намеренно: ошибка pgx или HTTP рутинно содержит DSN, имя хоста или учётные данные, а оркестратор логирует это тело дословно.

`consecutiveFailures` считает нездоровые замеры подряд и сбрасывается на первом успешном. Это тот же счётчик, серии которого отслеживает автоматический [режим обслуживания](https://omniflow-vpn.mintlify.site/ru/operations/maintenance-mode.md), так что тело ответа готовности говорит вам, насколько близка зависимость к приостановке покупок.

Результаты кешируются на 2 секунды, а каждая проба ограничена 3 секундами — обе величины вкомпилированы. Кеш существует для того, чтобы всплеск запросов готовности не превратился во всплеск обращений к базе данных.

Проба Remnawave — это **аутентифицированный** вызов (`ListUsers` за одной записью), а значит, истёкший или отозванный токен панели отражается как недоступность, а не проходит как здоровая панель. См. [Интеграция с Remnawave](https://omniflow-vpn.mintlify.site/ru/integrations/remnawave.md).

> [!WARNING]
>
> API, запущенный без `APP_DATABASE_URL`, не регистрирует ни одной пробы и отвечает `200 {"status":"ready","checks":[]}`. Это зелёный свет для процесса, который обслуживает только четыре операционных эндпоинта и ничего больше. Если ошибка развёртывания обнулит URL базы данных, готовность этого не поймает — проверяйте заодно, что отвечает `/v1/catalog`.

### Та же картина в панели

`GET /v1/panel/overview/health` (право доступа `system.read`) возвращает идентичный список `checks`, так что панель и балансировщик нагрузки не могут разойтись во мнении о том, работает ли PostgreSQL, плюс состояние платёжных провайдеров и провайдеров цифровых товаров, читаемое из базы данных. Вкладка **System → Health** отрисовывает это и обновляется каждые 30 секунд. См. [Панель администратора](https://omniflow-vpn.mintlify.site/ru/operations/admin-panel.md).

## Индикаторы уровня обслуживания

Каждый индикатор ниже опирается на метрику, реально существующую в таблице
далее на этой странице. Там, где для поверхности нет метрики, на основе
которой можно построить индикатор, это сказано прямо, а не прикрыто
выдуманным числом.

| Поверхность | Индикатор | Строится из |
| - | - | - |
| API | Доступность: доля запросов без `5xx` | `omniflow_http_requests_total{status="5xx"}` относительно общего числа того же ряда |
| API | Задержка: распределение длительности запроса | `omniflow_http_request_duration_seconds` |
| Бот | Пропускная способность обновлений | `rate(omniflow_job_duration_seconds_count{kind="telegram.update"}[5m])` — см. примечание ниже об этой паре |
| Задачи (выдача доступа) | Доля успеха | `omniflow_jobs_total{kind="fulfillment",outcome="succeeded"}` относительно общего числа того же ряда |
| Задачи (выдача доступа) | Распределение длительности | `omniflow_job_duration_seconds{kind="fulfillment"}` |
| Платежи | Здоровье расчётов | `omniflow_payments_total` по `classification` — сигнал это растущая доля `underpayment` или `wallet_unavailable`, а не единственный порог |
| Платежи | Доверие к webhook | `omniflow_provider_webhooks_total{outcome="signature_invalid"}` — заслуживает внимания любая ненулевая частота |
| Выдача доступа | Расхождение | Сегодня не ряд Prometheus; читается из экрана расхождений выдачи доступа в панели — см. [Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md#задачи-выдачи-доступа) |
| Уведомления | *(индикатора нет)* | Метрики доставки не существует. `omniflow_telegram_sends_total` объявлена и никогда не увеличивается — считаются только входящие обновления. До появления инструментирования наблюдайте за доставкой через собственную классификацию отказов и логи бота, а не через Prometheus |

> [!NOTE]
>
> У гистограммы бота `omniflow_job_duration_seconds{kind="telegram.update"}`
> нет соответствующего ряда счётчика `omniflow_jobs_total`, поэтому
> единственный способ получить из неё пропускную способность Telegram — это
> частота по собственному `_count` гистограммы — см.
> [Что объявлено, но никогда не пишется](#что-объявлено-но-никогда-не-пишется).

Машиночитаемые правила оповещений и стартовый дашборд Grafana, построенные
на этих же метриках, — в [Надёжность и эксплуатация](https://omniflow-vpn.mintlify.site/ru/operations/reliability.md#дашборды-и-оповещения).

## Метрики

Каждый процесс строит собственный изолированный реестр Prometheus, отдаваемый по `GET /metrics` в текстовом формате экспозиции вместе со стандартными коллекторами Go и процесса. Каждый ряд несёт постоянную метку `service` со значением `api`, `bot` или `worker`.

Кардинальность ограничена самой конструкцией. Маршруты записываются как **шаблоны** маршрутов chi, например `/v1/admin/orders/{orderID}`, и никогда как конкретные пути; HTTP-статус группируется в `2xx`, `4xx`, `5xx` или `unknown`; несопоставленный запрос записывается как `route="unmatched"`. Ни один идентификатор клиента, заказа или платежа никогда не становится меткой, потому что цель сбора хранит значения меток бессрочно, а эндпоинт метрик редко защищён так же хорошо, как API.

### Что пишется

| Метрика | Тип | Метки | Кем пишется |
| - | - | - | - |
| `omniflow_http_requests_total` | счётчик | `route`, `method`, `status` | Каждым HTTP-запросом в любом процессе, у которого есть метрики |
| `omniflow_http_request_duration_seconds` | гистограмма | `route`, `method` | Тем же middleware |
| `omniflow_telegram_updates_total` | счётчик | `kind`, `outcome` | Ботом, по разу на обновление. `outcome` всегда `handled` |
| `omniflow_job_duration_seconds` | гистограмма | `kind` | `kind="fulfillment"` — воркером, `kind="telegram.update"` — middleware обновлений бота |
| `omniflow_jobs_total` | счётчик | `kind`, `outcome` | Только `kind="fulfillment"`, с `outcome` `succeeded` или `failed` |
| `omniflow_provider_webhooks_total` | счётчик | `provider`, `outcome` | Приёмом платёжных вебхуков. `outcome` равен `signature_invalid` для отклонённого колбэка, иначе — классификация расчёта, приведённая ниже |
| `omniflow_payments_total` | счётчик | `provider`, `operation`, `classification` | Расчётом платежа. `classification` — одно из `paid`, `late`, `underpayment`, `overpayment`, `currency_mismatch`, `wallet_unavailable`, `duplicate` |
| `omniflow_maintenance_mode` | датчик | — | Контроллером режима обслуживания в API, на каждом замере |
| `omniflow_dependency_up` | датчик | `dependency` | Каждым вызовом `/readyz` и каждым замером режима обслуживания в API |

`omniflow_jobs_total` использует **вид** задачи, поэтому ряд выдачи доступа — это `kind="fulfillment"`. `fulfillment.operation` — это имя спана, а не значение метки, и запрос по нему не находит ничего.

### Что объявлено, но никогда не пишется

Четыре метрики появляются в выводе `/metrics` без замеров, потому что ничто в коде их никогда не выставляет:

| Метрика | Состояние |
| - | - |
| `omniflow_outbox_pending_events` | Никогда не вызывается `Set`. У outbox нет диспетчера — см. [Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md) |
| `omniflow_remnawave_requests_total` | Никогда не инкрементируется; клиент Remnawave не инструментирован |
| `omniflow_remnawave_errors_total` | Никогда не инкрементируется |
| `omniflow_telegram_sends_total` | Никогда не инкрементируется; считаются только входящие обновления |

Ещё два пробела стоит знать, когда вы пишете дашборд. Задачи доставки цифровых товаров не инструментированы — `omniflow_jobs_total{kind="goods_delivery"}` и ряд её длительности не появляются никогда, поэтому застрявшая доставка видна на экранах магазина в панели, но не в Prometheus. А у `omniflow_job_duration_seconds{kind="telegram.update"}` нет парного ряда-счётчика, так что скорость по `_count` гистограммы — единственный способ получить из этой пары пропускную способность Telegram.

### Защита `/metrics` на API

API отдаёт `/metrics` на том же слушателе, что и публичный API. Задайте
`APP_METRICS_TOKEN`, и маршрут будет отвечать только на
`Authorization: Bearer <token>` — укажите его как учётные данные `authorization`
в задании сбора Prometheus. Без токена маршрут остаётся открытым, как в прежних
выпусках, а API при старте пишет предупреждение; закройте путь на обратном
прокси, как это делают оба поставляемых примера. Пробы `/livez`, `/readyz` и
`/healthz` не закрываются никогда. Воркер и бот отдают метрики на собственных
слушателях, которые не предназначены для публикации, и токен не читают.

### Как выключить метрики

`APP_METRICS_ENABLED` по умолчанию равна `true` и читается **api** и **worker**: установка её в `false` оставляет объект метрик непостроенным, что убирает и маршрут `/metrics`, и middleware HTTP-инструментирования, тогда как пробы продолжают работать. Бот её игнорирует — он строит метрики безусловно и отдаёт `/metrics` всякий раз, когда `APP_BOT_METRICS_ADDR` задан и отличается от адреса вебхука.

Объект метрик повсюду безопасен к nil: каждый вспомогательный метод записи выходит сразу же на nil-получателе, поэтому отключение метрик убирает инструментирование, а не добавляет путь к падению.

### Оповещения, которые стоит настроить

Каждое из них построено на ряде, который действительно кто-то пишет:

* `omniflow_dependency_up == 0` дольше, чем `APP_MAINTENANCE_FAILURE_STREAK × APP_MAINTENANCE_PROBE_SECONDS`, — это тот момент, когда автоопределение приостановит покупки.
* `omniflow_maintenance_mode == 1` дольше запланированного окна. Сигнал — это автоопределение, открывшее окно, которого вы не планировали; инцидент — это то, что окно не закрывается.
* `rate(omniflow_jobs_total{kind="fulfillment",outcome="failed"}[5m])` выше нуля — заплативший клиент ждёт выдачи доступа, которая не доходит.
* Растущая скорость `omniflow_payments_total{classification="underpayment"}` или `{classification="wallet_unavailable"}` — деньги пришли, а заказ не рассчитался.
* Любое значение `omniflow_provider_webhooks_total{outcome="signature_invalid"}` — колбэк провайдера, который установка отклоняет, обычно из-за прокси, перекодирующего тело, или из-за смены секрета.
* `omniflow_http_requests_total{status="5xx"}` на сервисе `api`.

Поскольку датчик outbox никогда не пишется, за отставанием очереди приходится следить через `GET /v1/admin/outbox` или через вкладку Outbox в панели. Прочитайте [Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md), прежде чем решать, что означает растущий счётчик здесь.

## Структурированное логирование

Все три процесса логируют JSON через `log/slog` без заданных опций обработчика, а это значит — на уровне Info по умолчанию: API и бот пишут в **stdout**, воркер — в **stderr**. Переменной окружения для уровня, формата или назначения логов нет нигде в репозитории, поэтому отладочное логирование требует изменения кода. Собирайте потоки средствами вашего контейнерного рантайма.

### Редактирование

Всё, что может попасть в строку лога, в уведомление оператору или в диагностический пакет, сначала проходит через общий редактор. Он убирает:

* токены ботов Telegram, а также значения bearer, basic, token, API-key, secret, password и authorization;
* последовательности из 13–19 цифр, то есть похожие на карту;
* пары `hwid`, `device_id`, `charge_id` и `invoice_payload`;
* **любой** URL со схемой `http`, `https`, `vless`, `vmess`, `trojan` или `ss` — целиком, потому что даже хост идентифицирует установку, к которой подключается клиент.

Структурированные полезные нагрузки дополнительно редактируются по имени ключа, по совпадению с `token`, `secret`, `password`, `authorization`, `signature`, `api_key`, `hwid`, `subscription_url`, `checkout_url`, `raw_body`, `payload`, `card`, `pan`, `cvv`, `iban`, `email` и `phone`. Это применяется к полезным нагрузкам операторских уведомлений до записи строки и к последней ошибке задачи до того, как она покинет процесс.

Редактор намеренно агрессивен: перередактированная строка лога — небольшое неудобство, а утёкшая ссылка на подписку или токен бота — инцидент безопасности.

### Классификации вместо сообщений

Большинство операционных путей логируют стабильный код, а не строку ошибки, — именно это одновременно делает их безопасными для логирования и пригодными для поиска:

| Область | Коды |
| - | - |
| Пробы состояния | `timeout`, `unavailable` |
| Remnawave | `remote_user_not_found`, `remnawave_http_<status>`, `remnawave_unavailable` |
| Резервные копии | `command_exit_N`, `verification_failed`, `file_missing`, `cancelled`, `backup_failed` |
| Операторские топики | `missing_manage_topics_permission`, `chat_not_found`, `forum_topics_disabled`, `bot_not_in_group`, `telegram_error` |

> [!NOTE]
>
> `/readyz` логирует `readiness probe failed` с числом проверок и никогда — с тем, какая именно не прошла. Чтобы узнать это, читайте тело ответа или собирайте `omniflow_dependency_up`.

### Корреляция

`X-Request-ID` генерируется первым middleware в цепочке и возвращается в каждом ответе. Мутации в панели сохраняют его в событии аудита, которое пишут, так что разговор с поддержкой на тему «что именно изменил этот оператор» сводится к одному запросу. Бот логирует `correlation_id` на платёжных путях, а операции выдачи доступа и история их попыток несут тот же идентификатор, так что одно нажатие клиента можно проследить до вызова Remnawave, который оно вызвало.

## Трассировка

Код Omniflow инструментирован OpenTelemetry в пяти точках:

| Точка | Спан | Атрибуты |
| - | - | - |
| Входящий HTTP | `http.server` | Значения `otelhttp` по умолчанию |
| Обновление Telegram | `telegram.update` | `messaging.system=telegram`, вид обновления, идентификатор обновления |
| Задача выдачи доступа | `fulfillment.operation` | `messaging.system=river`, вид задачи, номер попытки |
| PostgreSQL | один спан на запрос | Текст запроса, урезанный в имя спана |
| Исходящий HTTP | `http.client` | Только метод и хост |

Атрибуты спанов подчиняются тому же правилу, что и метки метрик, и оно обеспечивается в единственной точке входа, так что ни одному вызывающему коду не приходится решать, что безопасно записывать. Спан Telegram несёт идентификатор обновления, который коррелирует с логами, но никогда не несёт аккаунт Telegram. Спаны запросов несут текст запроса и никогда — значения аргументов.

> [!WARNING]
>
> **Ни один трейс никогда не экспортируется.** Omniflow никогда не устанавливает провайдер трассировки, не импортирует ни одного экспортёра OTLP и не читает ни одной переменной `OTEL_*`. Глобальный провайдер является заглушкой, поэтому каждый описанный выше спан создаётся и выбрасывается. Установка `OTEL_EXPORTER_OTLP_ENDPOINT` не меняет ничего, и поддерживаемой конфигурации, включающей трассировку, сегодня не существует.

Если в форке это изменится, действуют ещё два ограничения: middleware Telegram выставляет спану статус `Ok` даже тогда, когда обработчик завершился ошибкой, поэтому неуспешное обновление не видно как ошибка спана, а воркер доставки цифровых товаров не открывает спан вообще.

До тех пор корреляция между процессами делается по идентификатору запроса и идентификатору корреляции выдачи доступа в логах, а также через операторские поверхности, описанные в [Операции администратора](https://omniflow-vpn.mintlify.site/ru/operations/admin-operations.md).

## Связанные страницы

> **[Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md)**
>
> Что на самом деле выполняет воркер и как читать представление недоставленных задач.

> **[Режим обслуживания](https://omniflow-vpn.mintlify.site/ru/operations/maintenance-mode.md)**
>
> К чему в итоге приводят серии отказов зависимостей в `/readyz`.

> **[Диагностика неисправностей](https://omniflow-vpn.mintlify.site/ru/operations/troubleshooting.md)**
>
> Разбор от симптома, когда проба или метрика указывают в бесполезную сторону.

> **[Анонимная телеметрия](https://omniflow-vpn.mintlify.site/ru/operations/telemetry.md)**
>
> Отдельный необязательный сигнал, который Omniflow отправляет о самом себе.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.