Добавление телеметрии и диагностика

Счётчик

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 не запускается, порт занят

Свободен ли server.telemetry.port; он не должен совпадать с HTTP-портом приложения или exporter другого процесса.

/metrics недоступен

Фактический порт в Setup, сетевой маршрут и правила доступа. Endpoint обслуживает отдельный сервер, поэтому авторизация ERP к нему не применяется.

Метрика отсутствует

Была ли исполнена точка регистрации/записи, совпадает ли экспортируемое имя с правилами _total и _milliseconds, не ищется ли серия с ещё не созданным набором labels.

Счётчик есть, increase() не показывает первую ошибку

Создать нулевые серии для известных статусов до первой операции.

p95 пустой или неверный

Использовать _bucket, сохранить label le при агрегации и убедиться, что диапазон запроса содержит измерения.

Трассировки отсутствуют

Непустой ли server.telemetry.trace.endpoint, указан ли полный OTLP HTTP path /v1/traces, доступен ли collector и вызывается ли span() вообще.

Известные ограничения

  • Кэши 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, экспорта и обработки исключений требуют регрессионной проверки.