Наблюдаемость — Versola Docs
VersolaVersola/docs
До 1.00.7.1versola.kzGitHub

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

Health-эндпоинты, метрики Prometheus и на что ставить алерты, формат JSON-логов, трейсинг OpenTelemetry и диагностика (дамп кучи и GC-лог) auth, central и edge.

Каждый сервис Versola (auth, central, edge) отдаёт health-пробы и метрики Prometheus на порту диагностики, пишет JSON-логи в стандартный вывод и умеет экспортировать трейсы по OTLP. Эта страница перечисляет, что есть, и какие сигналы стоит отслеживать алертами. Versola не поставляет стек мониторинга: Prometheus (или совместимый сборщик) и конвейер логов вы приносите сами.

Health-эндпоинты

Порт диагностики (DPORT) обслуживает три GET-эндпоинта и ничего больше. Это отдельный listener, не связанный с приложением, и ни заголовки безопасности, ни аутентификация приложения на него не распространяются, поэтому не выставляйте его в публичный интернет.

ЭндпоинтОтвечает
/liveness200, как только процесс запустил свой сервер диагностики.
/readiness200, когда сервис готов обслуживать запросы, до этого 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_totalcountermethod, route, status, status_class
http_server_request_duration_secondshistogrammethod, route, status_class
http_server_active_requestsgaugemethod, route
http_client_requests_totalcountermethod, peer, route, status, status_class
http_client_request_duration_secondshistogrammethod, peer, route, status_class
http_client_active_requestsgaugepeer

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_requestsgaugeЗаполненность пула. Публикуются, только если задан postgres.pool-metrics-interval.
db_client_connection_wait_time_secondshistogramВремя получения соединения из пула. То же условие.
db_client_connection_timeouts_totalcounterПолучения соединения, закончившиеся таймаутом. То же условие.
db_client_operation_duration_secondshistogramЗадержка операций с базой.
db_wal_bytes_total, db_wal_records_total, db_wal_fpi_totalcounterОбъём записанного WAL, число записей и full-page images, читаются из собственной базы сервиса.
db_wal_max_size_bytes, db_checkpoint_timeout_secondsgaugeНастройки max_wal_size и checkpoint_timeout этой базы.
db_wal_retained_by_slots_bytes, db_replication_slot_retained_bytes, db_replication_slot_activegaugeWAL, удерживаемый слотами репликации, в сумме и по slot, и активен ли каждый слот.
db_table_rows_estimate, db_table_size_bytesgaugeПо table: оценка числа строк планировщиком (отсутствует у таблицы, которую ни разу не анализировали) и точный размер вместе с индексами.
cleanup_rows_deleted_totalcounterСтроки, удалённые очисткой, по table.
cleanup_expired_rows, cleanup_oldest_expired_age_secondsgaugeСтроки с истёкшим expires_at, ещё лежащие в таблице (считаются до предела, значение на пределе означает «не меньше»), и как давно истекла самая старая. Оба значения падают к нулю, пока очистка успевает.
cleanup_last_success_timestamp_seconds, cleanup_configuredgaugeКогда таблицу очищали в последний раз и настроена ли для неё очистка.

Серии 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_totalauthСчётчики авторизации и разговоров входа.
dpop_edge_assertion_exemptions_total, dpop_edge_assertion_rejections_totalauthОбработка assertion’ов edge при проверках DPoP.
revocation_cache_entriesedgeРазмер списка отзывов в памяти.
revocation_cache_staleness_secondsedgeСекунды с момента, когда эта реплика в последний раз сверилась с базой. Растёт, пока перезагрузки не удаются.
revocation_cache_reload_failures_totaledgeНеудачные перезагрузки списка отзывов.
dpop_shared_ring_fallbacks_total, dpop_local_ring_capacity_hits_totaledgeРабота кольца защиты DPoP от повторов.
edge_proxy_overhead_secondsedgeГистограмма времени, которое сам edge добавляет к проксируемому запросу.
argon2_hashes_waitingauthХеши паролей, ждущие разрешения. Ненулевое значение значит, что спрос выше того, что пропускает этот под.
argon2_hashes_in_flight, argon2_max_concurrentauthХеши в работе сейчас и предел (argon2.max-concurrent). Отношение первого ко второму показывает, какую долю допуска использует под.
argon2_hash_wait_seconds, argon2_hash_duration_secondsauthГистограммы: сколько хеш ждал разрешения (та часть задержки входа, которую убрали бы дополнительные реплики) и сам хеш.

Хеширование паролей идёт за семафором, поэтому 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.Очистка проигрывает таблице или остановилась.
Удержание WALdb_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

Дальше