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 приложения) и на каждый запрос:
- создаёт request-scoped LatencyProfiler и кладёт его в context;
- замеряет полное время обработки;
- после ответа пишет метрики с endpoint вида
METHOD /route/template(шаблон пути, а не сырой URL с id) и кодом статуса.
| Метрика | Тип | Лейблы | Смысл |
|---|---|---|---|
app_http_requests_total | counter | app, endpoint, code | число входящих HTTP-запросов |
app_http_request_duration_seconds | counter | app, endpoint, code, type | сумма секунд, потраченных на сегменты обработки |
app_http_stats_default | histogram | app | распределение полного времени ответа |
Лейбл type в duration-метрике раскладывает время запроса по сегментам. То, что в PHP часто называли php (время «своего» кода), в Go называется code: это остаток полного времени минус явно учтённые куски. Исходящие HTTP-вызовы к другим сервисам Ensi попадают в тип http_client, если на клиенте подключён middleware метрик (см. ниже). Другие типы можно добавлять в profiler вручную через AddTimeQuant / AddAsyncTimeQuant, если появится необходимость (например, отдельный учёт тяжёлой работы вне HTTP-клиента).
Исходящий HTTP (клиенты)
| Метрика | Тип | Лейблы | Смысл |
|---|---|---|---|
app_http_client_requests_total | counter | app, host | число исходящих запросов |
app_http_client_seconds_total | counter | app, 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: коллекторы создавать и регистрировать не стоит, а запись значений в коде должна спокойно пропускаться.
Практичный порядок такой:
- завести коллектор (
CounterVec,HistogramVec,GaugeVecи т.п.) рядом с остальными инструментами — вapp/common/metrics; - один раз зарегистрировать его в registry при инициализации (после того как
GetInstruments()уже создан); - в бизнес-коде обновлять метрику (
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 должен указывать на сервис/под, который реально крутит эти процессы.