Добавление телеметрии и диагностика
Счётчик
private val resultCounter by lazy {
ServerContext.telemetry.counter("catalog_sync_results")
}
fun recordResult(status: String) {
resultCounter.increment(
1,
mapOf("status" to status)
)
}
В Prometheus этот инструмент будет называться
catalog_sync_results_total.
Если алерт строится через increase(), нужные комбинации тегов следует создать
нулевым увеличением при инициализации. Иначе первая ошибка создаст только первую
точку серии и рост может быть не виден до следующего scrape:
listOf("success", "failure").forEach { status ->
resultCounter.increment(0, mapOf("status" to status))
}
Таймер
Для измерения блока без тегов:
private val refreshTimer by lazy {
ServerContext.telemetry.timer("catalog_refresh_duration")
}
val result = refreshTimer.record {
refreshCatalog()
}
Время записывается и при исключении, после чего исключение проходит дальше.
Если длительность уже измерена или нужны теги:
refreshTimer.recordUnit(
duration,
TimeUnit.MILLISECONDS,
mapOf("status" to status)
)
Единица экспортируемой гистограммы всегда миллисекунды.
Gauge
private val queued = AtomicLong()
init {
ServerContext.telemetry.gauge("catalog_sync_queued") {
queued.get().toDouble()
}
}
Gauge нужно зарегистрировать один раз. Повторная регистрация того же имени не
обновит callback. Значение преобразуется в Long, поэтому дробная часть будет
потеряна.
Span
return ServerContext.telemetry.span(
"catalog.sync",
mapOf(
"catalog.kind" to kind,
"catalog.manual" to manual.toString()
)
) {
refreshCatalog()
}
При пустом server.telemetry.trace.endpoint этот код не создаёт span, но блок
выполняется как обычно. При исключении span получает статус ERROR, записывает
исключение и завершает работу; исходное исключение не подменяется.
Требования к именам и тегам
-
Используйте стабильное имя в
snake_caseи не формируйте его из данных запроса. -
В тегах допустим небольшой заранее известный набор значений: тип операции, класс, итоговый статус.
-
Не добавляйте ID процесса или пользователя, UUID, номер договора, полный текст ошибки, query string или произвольный URL.
-
Не передавайте секреты, токены и персональные данные.
-
Для счётчика учитывайте Prometheus-суффикс
_total, для таймера —_milliseconds. -
null-значение атрибута OpenTelemetry пропускает; серия будет отличаться набором labels от серии с непустым значением.
Каждая новая комбинация labels создаёт отдельный временной ряд. Высокая кардинальность увеличивает память ERP, объём scrape и нагрузку на Prometheus.
Не влиять на прикладной сценарий
Сбой телеметрии обычно не должен отменять бизнес-операцию. TelemetryListener
перехватывает ошибку записи и пишет её в лог, EventProcessor и часть прямых
счётчиков также защищены. Однако контракт ITelemetry сам исключения не гасит.
Новый прямой вызов следует изолировать там, где потеря метрики допустима:
runCatching {
resultCounter.increment(1, mapOf("status" to status))
}.onFailure {
log.error("Не удалось записать метрику синхронизации: ${it.message}", it)
}
Не добавляйте отдельный try/catch, если метрика специально является частью
контракта операции и ошибка должна остановить выполнение.
Проверка метрик
Проверить, что exporter запущен:
curl -fsS http://localhost:9090/metrics
Найти конкретное семейство:
curl -fsS http://localhost:9090/metrics | rg '^catalog_sync_'
Примеры PromQL:
# запросы в секунду по статусу
sum by (status) (rate(http_request_count_total[5m]))
# p95 времени Action по гистограмме
histogram_quantile(
0.95,
sum by (le, action) (
rate(http_request_duration_milliseconds_bucket[5m])
)
)
# среднее время запроса Retrofit
sum(rate(http_retrofit_request_duration_milliseconds_sum[5m]))
/
sum(rate(http_retrofit_request_duration_milliseconds_count[5m]))
Метрика появляется только после регистрации инструмента. Счётчик без
предварительного нулевого значения появляется после первого increment, таймер —
после первого измерения, а ленивый singleton запускает endpoint при первом
обращении к телеметрии.
Диагностика
| Симптом | Что проверить |
|---|---|
ERP не запускается, порт занят |
Свободен ли |
|
Фактический порт в Setup, сетевой маршрут и правила доступа. Endpoint обслуживает отдельный сервер, поэтому авторизация ERP к нему не применяется. |
Метрика отсутствует |
Была ли исполнена точка регистрации/записи, совпадает ли экспортируемое имя с правилами |
Счётчик есть, |
Создать нулевые серии для известных статусов до первой операции. |
p95 пустой или неверный |
Использовать |
Трассировки отсутствуют |
Непустой ли |
Известные ограничения
-
Кэши counters, timers и gauges внутри
OpenTelemetryImplоснованы на обычномMutableMap; одновременная первая регистрация одного имени не синхронизирована. -
Все таймеры делят один набор bucket, который нельзя настроить по имени метрики.
-
Gauge не поддерживает теги и записывает только целые значения.
-
HTTP filter собирает
/user/,/admin/и*.vue, но не все маршруты приложения; статические Vue-ресурсы создают серии со статусомunknown. -
event_processor_*использует несовместимые значенияSuccessfulиfailure. -
ServerStateProviderпоказывает telemetry port с учётом одноимённой переменной окружения, ноOpenTelemetryImplчитает порт только из Setup. При переопределении окружением экран состояния может показать не тот порт, на котором слушает exporter. -
OpenTelemetry SDK, Prometheus server и batch processor трассировок явно не закрываются; финальная пачка spans при остановке JVM не гарантирована.
-
Для пакета
ru.bgcrm.telemetryнет отдельных юнит-тестов; изменения API, bucket, экспорта и обработки исключений требуют регрессионной проверки.