Каждый сервис Versola (auth, central, edge) отдаёт health-пробы и метрики Prometheus на порту диагностики, пишет JSON-логи в стандартный вывод и умеет экспортировать трейсы по OTLP. Эта страница перечисляет, что есть, и какие сигналы стоит отслеживать алертами. Versola не поставляет стек мониторинга: Prometheus (или совместимый сборщик) и конвейер логов вы приносите сами.
Health-эндпоинты
Порт диагностики (DPORT) обслуживает три GET-эндпоинта и ничего больше. Это отдельный listener, не связанный с приложением, и ни заголовки безопасности, ни аутентификация приложения на него не распространяются, поэтому не выставляйте его в публичный интернет.
| Эндпоинт | Отвечает |
|---|---|
/liveness | 200, как только процесс запустил свой сервер диагностики. |
/readiness | 200, когда сервис готов обслуживать запросы, до этого 503. |
/metrics | Текстовый формат Prometheus. |
Сервер диагностики стартует до того, как сервис построит свои зависимости. Пока auth или edge ждёт первой синхронизации конфигурации с central (до двух минут), /liveness уже отвечает 200, а /readiness — 503, так что проба живости не убьёт сервис, который просто ждёт. Если central остаётся недоступным, старт завершается ошибкой, а процесс выходит, чтобы супервизор его перезапустил.
Порты диагностики по умолчанию на таргете vps: auth — 8081, central — 8091, edge — 8096; chart использует те же номера. Проверка сервиса с хоста:
curl -sf http://127.0.0.1:8091/readiness && echo central ready
В Kubernetes chart опрашивает /liveness (начальная задержка 10 с, период 10 с) и /readiness (5 с, 5 с) на порту диагностики. versola up ждёт те же эндпоинты готовности, прежде чем запустить следующий сервис.
Метрики
Собирайте /metrics с каждой реплики каждого сервиса. Серии приходят из трёх источников.
HTTP
| Метрика | Тип | Метки |
|---|---|---|
http_server_requests_total | counter | method, route, status, status_class |
http_server_request_duration_seconds | histogram | method, route, status_class |
http_server_active_requests | gauge | method, route |
http_client_requests_total | counter | method, peer, route, status, status_class |
http_client_request_duration_seconds | histogram | method, peer, route, status_class |
http_client_active_requests | gauge | peer |
status_class — 2xx, 4xx, 5xx и так далее. route — шаблон маршрута, а не сырой путь, так что кардинальность ограничена. Серии http_client_* покрывают вызовы, которые сервис делает другим сервисам, например auth к central. http_client_active_requests считает исходящие запросы в работе, включая стоящие в очереди, и первым растёт, когда исходящий пул слишком мал.
JVM и процесс
Метрики среды выполнения JVM публикуются с именами в соглашении Prometheus (например, jvm_memory_used_bytes) и покрывают пулы памяти, сборку мусора, потоки, загрузку классов и CPU процесса. Они снимаются по расписанию, а не при каждом сборе.
База данных
| Метрика | Тип | Значение |
|---|---|---|
db_client_connection_count, db_client_connection_max, db_client_connection_idle_min, db_client_connection_pending_requests | gauge | Заполненность пула. Публикуются, только если задан postgres.pool-metrics-interval. |
db_client_connection_wait_time_seconds | histogram | Время получения соединения из пула. То же условие. |
db_client_connection_timeouts_total | counter | Получения соединения, закончившиеся таймаутом. То же условие. |
db_client_operation_duration_seconds | histogram | Задержка операций с базой. |
db_wal_bytes_total, db_wal_records_total, db_wal_fpi_total | counter | Объём записанного WAL, число записей и full-page images, читаются из собственной базы сервиса. |
db_wal_max_size_bytes, db_checkpoint_timeout_seconds | gauge | Настройки max_wal_size и checkpoint_timeout этой базы. |
db_wal_retained_by_slots_bytes, db_replication_slot_retained_bytes, db_replication_slot_active | gauge | WAL, удерживаемый слотами репликации, в сумме и по slot, и активен ли каждый слот. |
db_table_rows_estimate, db_table_size_bytes | gauge | По table: оценка числа строк планировщиком (отсутствует у таблицы, которую ни разу не анализировали) и точный размер вместе с индексами. |
cleanup_rows_deleted_total | counter | Строки, удалённые очисткой, по table. |
cleanup_expired_rows, cleanup_oldest_expired_age_seconds | gauge | Строки с истёкшим expires_at, ещё лежащие в таблице (считаются до предела, значение на пределе означает «не меньше»), и как давно истекла самая старая. Оба значения падают к нулю, пока очистка успевает. |
cleanup_last_success_timestamp_seconds, cleanup_configured | gauge | Когда таблицу очищали в последний раз и настроена ли для неё очистка. |
Серии WAL и слотов каждый сервис читает из собственной базы раз в минуту (pg_stat_wal, pg_stat_checkpointer, pg_settings, pg_replication_slots), поэтому postgres_exporter не нужен, а роли базы не нужны дополнительные права; представление, которое она не может прочитать, логируется один раз и пропускается. Все реплики сервиса сообщают одни и те же числа, а когда три сервиса работают в одной базе, их сообщают все три: читайте их через max by (...), а не sum. Накопительные счётчики сохраняют смысл при сбросе статистики или перезапуске базы (сброс виден как перезапуск счётчика, а не отрицательная скорость).
| db_notification_listener_connected | gauge | 1, пока соединение LISTEN доказанно доставляет уведомления, а не просто открыто. |
| db_notifications_received_total | counter | Полученные уведомления. |
| db_notification_listener_reconnects_total | counter | Переподключения соединения LISTEN. |
| db_notification_listener_silent_total | counter | Сколько раз соединение было открыто, но перестало доставлять. |
| db_notification_listener_queue_overflow_total | counter | Уведомления, отброшенные, потому что подписчик отстал. |
Серии пула по умолчанию выключены; задайте postgres.pool-metrics-interval (например, 10 seconds), чтобы публиковать их. См. Конфигурацию.
Приложение
| Метрика | Сервис | Значение |
|---|---|---|
auth_authorize_total, auth_conversation_started_total, auth_conversation_completed_total, auth_conversation_step_total | auth | Счётчики авторизации и разговоров входа. |
dpop_edge_assertion_exemptions_total, dpop_edge_assertion_rejections_total | auth | Обработка assertion’ов edge при проверках DPoP. |
revocation_cache_entries | edge | Размер списка отзывов в памяти. |
revocation_cache_staleness_seconds | edge | Секунды с момента, когда эта реплика в последний раз сверилась с базой. Растёт, пока перезагрузки не удаются. |
revocation_cache_reload_failures_total | edge | Неудачные перезагрузки списка отзывов. |
dpop_shared_ring_fallbacks_total, dpop_local_ring_capacity_hits_total | edge | Работа кольца защиты DPoP от повторов. |
edge_proxy_overhead_seconds | edge | Гистограмма времени, которое сам edge добавляет к проксируемому запросу. |
argon2_hashes_waiting | auth | Хеши паролей, ждущие разрешения. Ненулевое значение значит, что спрос выше того, что пропускает этот под. |
argon2_hashes_in_flight, argon2_max_concurrent | auth | Хеши в работе сейчас и предел (argon2.max-concurrent). Отношение первого ко второму показывает, какую долю допуска использует под. |
argon2_hash_wait_seconds, argon2_hash_duration_seconds | auth | Гистограммы: сколько хеш ждал разрешения (та часть задержки входа, которую убрали бы дополнительные реплики) и сам хеш. |
Хеширование паролей идёт за семафором, поэтому CPU перестаёт следовать за спросом, как только все разрешения заняты. argon2_hashes_waiting и гистограмма ожидания — лучшие сигналы для масштабирования, чем загрузка CPU. central и edge тоже регистрируют серии Argon2, но не хешируют и сообщают нули.
Перечислены только метрики, найденные в исходниках. Метки прикладных счётчиков здесь не описаны.
На что ставить алерты
| Сигнал | Условие для алерта | Почему |
|---|---|---|
| Готовность | /readiness не 200 дольше, чем занимает перезапуск. | Сервис не обслуживает запросы. При первом старте или перезапуске central auth и edge ждут до двух минут. |
| Доля ошибок | Доля http_server_requests_total с status_class="5xx". | Ошибки сервера. |
| Задержка | Высокий квантиль http_server_request_duration_seconds по route. | Медленные эндпоинты. |
| Давление на пул | db_client_connection_pending_requests выше нуля в течение долгого времени или любой рост db_client_connection_timeouts_total. | Пул исчерпан относительно maximum-pool-size. Нужен pool-metrics-interval. |
| Свежесть отзывов (edge) | revocation_cache_staleness_seconds выше настроенного интервала перезагрузки (по умолчанию 600 s) или любой рост revocation_cache_reload_failures_total. | Пока список устарел, отозванный токен ещё может приниматься. Только так реплика edge может пропустить лишнее. |
| Доставка изменений | db_notification_listener_connected равен 0 либо растут db_notification_listener_silent_total или ..._queue_overflow_total. | Изменение конфигурации или отзыв может не дойти до этой реплики до её следующей периодической перезагрузки. За пулером проверьте postgres.notifications-url. |
| Очередь входов | argon2_hashes_waiting выше нуля в течение долгого времени. | Входы стоят в очереди за разрешением на хеширование; добавьте реплики auth или увеличьте argon2.max-concurrent вместе с памятью. |
| Очистка | Растёт cleanup_oldest_expired_age_seconds или cleanup_last_success_timestamp_seconds далеко в прошлом у таблицы с cleanup_configured. | Очистка проигрывает таблице или остановилась. |
| Удержание WAL | db_wal_retained_by_slots_bytes растёт к db_wal_max_size_bytes. | Неактивный слот репликации удерживает WAL на диске. |
| Куча JVM и GC | Использование кучи приближается к фиксированному -Xmx (по умолчанию 512 МиБ), долгие или частые паузы GC. | Куча фиксирована, поэтому её рост заканчивается выходом по нехватке памяти. См. настройки JVM. |
| Перезапуски | Перезапуски контейнера (RestartCount или restartCount в Kubernetes). | JVM выходит при ошибке нехватки памяти намеренно. |
Сбор метрик
Простая задача Prometheus для таргета vps, где порты диагностики на loopback (запускайте Prometheus на том же хосте или обращайтесь через туннель):
scrape_configs:
- job_name: versola
metrics_path: /metrics
static_configs:
- targets: ["127.0.0.1:8081"]
labels: { service: auth }
- targets: ["127.0.0.1:8091"]
labels: { service: central }
- targets: ["127.0.0.1:8096"]
labels: { service: edge }
Реплики, добавленные через versola replica, слушают на портах, сдвинутых на 100 на слот (слот 2 у auth — на 8181), поэтому добавьте каждую реплику как отдельную цель.
В Kubernetes chart аннотирует каждый под, когда metrics.prometheusAnnotations равно true (по умолчанию):
prometheus.io/scrape: "true"
prometheus.io/port: "8081" # порт диагностики сервиса
prometheus.io/path: /metrics
Используйте обнаружение подов по этим аннотациям, а не имена ClusterIP Service, которые скрыли бы, из какого пода пришла серия. Обоснование и рабочую конфигурацию vmagent см. в разделе Kubernetes.
Доска Grafana
В репозитории продукта есть сгенерированная доска Grafana для всей системы, dashboards/grafana/versola.json (uid versola). Импортируйте её в Grafana (Dashboards, New, Import) или подключите из ConfigMap с меткой, за которой следит sidecar вашей Grafana. В ней раздел Overview (две строки по четыре плитки на сервис: поды, запросы, доля 5xx, p99, две специфичные для сервиса плитки, пул базы, куча), затем раздел на каждый сервис с трафиком, ошибками и задержкой по эндпоинтам и собственными сигналами сервиса (auth: воронка входа и обновление токенов; edge: ресурсы, веб, мобильный /native и отзыв токенов; central: синхронизации конфигурации, административные записи и уведомления об изменениях). Строки про базу и рантайм свёрнуты. Есть также исходящий пул, таймауты соединений, раздел WAL, DPoP-assertion’ы edge, допуск Argon2 и очистка.
Единственные переменные — источник данных Prometheus и namespace. Доска выбирает серии по namespace и app_kubernetes_io_component (auth, central или edge) — метке пода, которую выставляет chart. Все три сервиса собираются под одним job, поэтому ваша конфигурация сбора должна копировать метки пода на серии (labelmap, как в k8s/monitoring/vmagent-values.yaml). Некоторые панели пусты, пока не появится их источник: панелям пула нужен postgres.pool-metrics-interval, а панели ошибок пусты, пока ничего не падает.
JSON генерируется: правьте dashboards/grafana/generate.py и запускайте python3 dashboards/grafana/generate.py, а не редактируйте файл. В том же каталоге лежит loadgen.json — доска для собственного нагрузочного генератора проекта, вам она не нужна.
Логи
Сервисы пишут по одному JSON-объекту на строку в стандартный вывод. На таргете vps читайте их через docker logs:
docker logs --tail=200 versola-auth 2>&1 | jq -r '"\(.timestamp) \(.level) \(.message)"'
В каждой строке эти поля, в таком порядке:
| Поле | Значение |
|---|---|
timestamp | Время события. |
system | Имя сервиса: auth, central или edge. |
env | Значение env из конфигурации. |
level | Уровень лога. |
thread | Идентификатор fiber. |
message | Сообщение. |
stack_trace | Есть только при наличии причины ошибки. |
span_id, trace_id | Контекст трейсинга, чтобы связать строку лога с трейсом. |
http, auth, error | Структурированные аннотации событий запроса, исходящего вызова, аутентификации и ошибки (когда есть). |
logger | Имя логгера. |
Иллюстративная строка (значения условные; пустые поля опускаются):
{"timestamp":"2026-10-01T10:15:30.123Z","system":"auth","env":"prod","level":"INFO","thread":"zio-fiber-123","message":"Set temporary bootstrap password for admin user 'admin'","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736"}
Заголовки авторизации маскируются (Bearer ***). Access-лог versola-proxy — обычный формат nginx, не JSON.
Трейсинг
Чтобы экспортировать трейсы, задайте otel-exporter на верхнем уровне конфигурации сервиса — OTLP gRPC-эндпоинт:
otel-exporter = "http://otel-collector:4317"
Имя сервиса в трейсах — <service>-<env>, например auth-prod. Контекст передаётся заголовком W3C traceparent. Без ключа экспортёр — заглушка, и из процесса ничего не уходит. В сгенерированных файлах ключ закомментирован. См. Конфигурацию.
Диагностика
Образы задают флаги JVM (см. настройки JVM), которые оставляют что исследовать после падения. /diagnostics — каталог внутри каждого контейнера, а не HTTP-эндпоинт:
heap.hprof: дамп кучи, который пишется при нехватке памяти JVM. Путь — фиксированное имя файла, поэтому каждое падение перезаписывает предыдущий дамп. Затем JVM завершается, а супервизор (политика перезапуска Docker или Kubernetes) перезапускает контейнер.gc.log: GC-лог, ротируемый по 5 файлов по 20 МБ.
Оба деплоя монтируют туда том: именованный том на сервис на vps (versola-auth-diagnostics-vps, versola-central-diagnostics-vps, versola-edge-diagnostics-vps) и emptyDir в chart размером diagnosticsSizeLimit (1Gi). Дамп занимает не больше размера кучи. Скопируйте его наружу:
docker cp versola-auth:/diagnostics/heap.hprof .
kubectl cp <namespace>/<pod>:/diagnostics/heap.hprof ./heap.hprof
В дампе кучи есть всё, что было в памяти, включая ключи и токены. Обращайтесь с ним как с секретом.
jcmd есть в образе. JVM запущена с -XX:NativeMemoryTracking=summary, так что память контейнера можно разложить на кучу, прямые буферы, metaspace, code cache и стеки потоков:
docker exec versola-auth jcmd 1 VM.native_memory summary
Дальше
- Конфигурация:
otel-exporter,postgres.pool-metrics-intervalи настройки JVM. - Kubernetes: сбор метрик по аннотациям chart.
- Справочник CLI:
versola statusдля вида контейнеров деплояvps.