Перейти к основному содержимому

Prometheus-метрики

Go-сервисы Ensi отдают метрики производительности в формате Prometheus, в той же идеологии, что и PHP-сервисы на пакетах laravel-metrics / laravel-prometheus. Имена основных HTTP-метрик совместимы с платформенным дашбордом, поэтому один и тот же scrape и те же запросы в Grafana в целом применимы и к Go.

Как Prometheus в кластере подхватывает таргеты сервисов (секрет ensi-metrics-scrape-config, проверка Targets) — описано в devops-гайде сбор метрик с приложений. Ниже — что происходит внутри Goravel-сервиса: какие метрики пишутся, как включить scrape-порт и что нужно не забыть при подключении HTTP-клиентов.

Код разделён так:

  • коллекторы и запись значений живут в app/common/metrics
  • HTTP middleware входящих запросов — в app/common/http/middleware
  • учёт исходящих вызовов — в app/common/http/clientmiddleware
  • отдельный scrape-сервер — в app/adapters/prometheus
  • Для долгих artisan-команд (queue:work, kafka:consume, schedule:run) scrape поднимается обвязкой в app/adapters/observability.

Включение и scrape-порт

Метрики включаются конфигом config/metrics.go и переменными окружения:

ПеременнаяСмысл
PROMETHEUS_ENABLEDвключить сбор и scrape-сервер (metrics.enabled)
PROMETHEUS_HOSTадрес прослушивания (по умолчанию 0.0.0.0)
PROMETHEUS_PORTпорт scrape (по умолчанию 9091)

Когда metrics.enabled=true и порт задан, вместе с web-приложением стартует runner app:prometheus. Он поднимает отдельный HTTP-сервер (не путать с основным API) и отдаёт реестр коллекторов на пути /metrics. Именно этот адрес обычно регистрируют в scrape-конфиге Prometheus как таргет сервиса.

В конфиге также можно задать:

  • ignore_routes — список имён роутов (с поддержкой glob вроде path.Match), для которых входящие метрики не пишутся; полезно отфильтровать healthcheck или служебные ручки, если они зарегистрированы как именованные роуты;
  • http_stats_default_buckets — границы histogram для app_http_stats_default (по умолчанию от 5 ms до 10 s).

Имя приложения в лейбле app берётся из app.name в конфиге сервиса — то же значение, по которому удобно фильтровать в Grafana.

Какие метрики пишет сервис

Набор сознательно близок к PHP, чтобы дашборды и алерты оставались узнаваемыми.

Входящий HTTP

Middleware HttpMetrics вешается на глобальный стек HTTP (в bootstrap приложения) и на каждый запрос:

  1. создаёт request-scoped LatencyProfiler и кладёт его в context;
  2. замеряет полное время обработки;
  3. после ответа пишет метрики с endpoint вида METHOD /route/template (шаблон пути, а не сырой URL с id) и кодом статуса.
МетрикаТипЛейблыСмысл
app_http_requests_totalcounterapp, endpoint, codeчисло входящих HTTP-запросов
app_http_request_duration_secondscounterapp, endpoint, code, typeсумма секунд, потраченных на сегменты обработки
app_http_stats_defaulthistogramappраспределение полного времени ответа

Лейбл type в duration-метрике раскладывает время запроса по сегментам. То, что в PHP часто называли php (время «своего» кода), в Go называется code: это остаток полного времени минус явно учтённые куски. Исходящие HTTP-вызовы к другим сервисам Ensi попадают в тип http_client, если на клиенте подключён middleware метрик (см. ниже). Другие типы можно добавлять в profiler вручную через AddTimeQuant / AddAsyncTimeQuant, если появится необходимость (например, отдельный учёт тяжёлой работы вне HTTP-клиента).

Исходящий HTTP (клиенты)

МетрикаТипЛейблыСмысл
app_http_client_requests_totalcounterapp, hostчисло исходящих запросов
app_http_client_seconds_totalcounterapp, hostсумма секунд исходящих запросов

Лейбл host — хост (или host:port) базового URL клиента. Эти метрики заполняются только если в цепочку middleware сгенерированного клиента добавлен clientmiddleware.Metrics(baseURL).

LatencyProfiler и разбиение времени

На время обработки входящего запроса middleware создаёт profiler и кладёт его в context. Middleware исходящего клиента, увидев profiler в context, добавляет интервалы типа http_client. В конце запроса profiler считает длительности по типам: сумма перекрывающихся async-интервалов не двойится, а «хвост» полного wall-time записывается как code.

Так на графике «куда ушло время запроса» можно отдельно видеть работу своего кода и ожидание апстримов — в том же духе, что stack по type в стандартном дашборде Ensi.

Если исходящий вызов идёт без request context (например, из фоновой джобы без прокинутого profiler), клиентские счётчики app_http_client_* всё равно обновятся, но в app_http_request_duration_seconds этот кусок не попадёт — входящего HTTP-запроса просто нет.

Подключение метрик к HTTP-клиентам

Глобальный HttpMetrics на входящих запросах включается в bootstrap один раз. Для клиентов к другим сервисам middleware нужно добавить явно при регистрации в app/providers/clients, рядом с логированием:

return pim.New(req, clientmiddleware.AppendLogging(pim.Binding, []http.Middleware{
clientmiddleware.Metrics(baseURL),
})), nil

baseURL берут из config/ensi_clients/... — тот же URL, с которым создаётся клиент. Без Metrics исходящие вызовы не отразятся ни в client-счётчиках, ни в сегменте http_client у входящего запроса.

Свои метрики

Стандартный набор покрывает HTTP. Если нужна доменная метрика — размер кэша, число обработанных сообщений Kafka, длительность тяжёлого шага в джобе — её добавляют в тот же Prometheus-реестр, который уже отдаёт scrape-сервер. Отдельный HTTP-эндпоинт или второй registry поднимать не нужно.

Точка расширения — metrics.GetInstruments().Registry(). Это тот же *prometheus.Registry, куда при старте зарегистрированы app_http_*. Когда PROMETHEUS_ENABLED выключен, Registry() вернёт nil: коллекторы создавать и регистрировать не стоит, а запись значений в коде должна спокойно пропускаться.

Практичный порядок такой:

  1. завести коллектор (CounterVec, HistogramVec, GaugeVec и т.п.) рядом с остальными инструментами — в app/common/metrics;
  2. один раз зарегистрировать его в registry при инициализации (после того как GetInstruments() уже создан);
  3. в бизнес-коде обновлять метрику (Inc, Add, Observe, Set).

Пример счётчика обработанных событий:

var syncEventsTotal *prometheus.CounterVec

func RegisterDomainMetrics() {
reg := metrics.GetInstruments().Registry()
if reg == nil {
return
}

syncEventsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "app_sync_events_total",
Help: "Number of processed sync events",
}, []string{"app", "entity", "result"})
reg.MustRegister(syncEventsTotal)
}

func RecordSyncEvent(entity, result string) {
if syncEventsTotal == nil {
return
}
app := facades.Config().GetString("app.name", "Goravel")
syncEventsTotal.WithLabelValues(app, entity, result).Inc()
}

RegisterDomainMetrics вызывают при старте приложения (provider / bootstrap), до того как пойдут запросы или воркеры начнут работу. Иначе первые события могут пройти до регистрации коллектора.

Несколько правил, чтобы метрика оставалась полезной в Grafana и не раздувала кардинальность:

  • имя лучше начинать с app_, в духе платформенных метрик;
  • в лейблы кладут низкокардинальные измерения (entity, result, queue), а не id сущностей, UUID и сырые URL;
  • лейбл app берут из app.name, как у стандартных HTTP-метрик — так удобнее фильтровать в общих дашбордах;
  • для «куда ушло время внутри HTTP-запроса» чаще достаточно LatencyProfiler.AddTimeQuant / AddAsyncTimeQuant (новый type в app_http_request_duration_seconds), а отдельный histogram/counter заводят, когда нужна метрика вне разбиения одного входящего запроса или с другой семантикой.

После регистрации значение появляется на том же /metrics, что и HTTP-счётчики. Локально достаточно включить scrape и сделать curl на порт PROMETHEUS_PORT.

Воркеры и artisan

Сервер метрик запускается вместе с основым веб-сервером, а так же при запуске queue:work, kafka:consume, schedule:run.
Т.е. запустив под консюмера, например, можно обратиться к нему на порт метрик.

Имеет смысл помнить об этом при настройке scrape в Kubernetes: у деплоев воркеров и консьюмеров должен быть доступен тот же PROMETHEUS_PORT, и таргет в ensi-metrics-scrape-config должен указывать на сервис/под, который реально крутит эти процессы.