Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 7 из 25 ~20 мин +50 XP

Инструментирование метрики

Логи рассказывают истории об отдельных запросах, но вопросы «сколько», «как быстро» и «растёт ли» отвечают метрики. В go-kit метрики — третий сквозной слой: пакет metrics даёт три интерфейса с реализациями для Prometheus и других бэкендов, а instrumenting middleware считает вызовы и латентность endpoint'ов — рядом с loggingMiddleware из урока 06. Ты объявишь метрики stringsvc через prometheus.NewCounterFrom и NewHistogramFrom, соберёшь middleware с Observe и Add, смонтируешь /metrics через promhttp.Handler и научишься читать p90 латентности через histogram_quantile. «go kit метрики» и «prometheus go kit» перестанут быть заклинаниями.

Содержание урока · 11 разделов

Три интерфейса пакета metrics#

Открой исходники github.com/go-kit/kit/metrics — там три интерфейса, и весь дизайн наблюдаемости курса держится на них. Counter описывает монотонно растущую величину: Add(delta float64) и ничего больше — уменьшать счётчик нельзя по определению; типичный пример — число принятых запросов. Gauge принимает конкретные значения в моменте: Set(value) и Add(delta) — глубина очереди, число активных соединений, температура. Histogram повторяет наблюдения одного типа величины — Observe(value float64) — и сводит их в статистику: buckets или quantiles; штатный пример из доков — латентность HTTP-запросов. Каждый интерфейс начинается с метода With(labelValues ...string), возвращающего инструмент с привязанными метками — про него ниже. Почему это не интерфейсы prometheus напрямую: контракт go-kit метрики принадлежит твоему коду, а реализация — подключаемая. В пакете есть реализации для Prometheus, DogStatsD, InfluxDB, Graphite; stringsvc компилируется против любой из них, не меняя ни строки в middleware. Это тот же ход, что и endpoint: одна сигнатура — много транспортов, поэтому go kit метрики выглядят одинаково в любом бэкенде.

Объявляем метрики stringsvc

GO
import (
        stdprometheus "github.com/prometheus/client_golang/prometheus"
        kitprometheus "github.com/go-kit/kit/metrics/prometheus"
)

var fieldKeys = []string{"method", "error"}

requestCount := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
        Namespace: "stringsvc",
        Subsystem: "string_service",
        Name:      "request_count",
        Help:      "Number of requests received.",
}, fieldKeys)

requestLatency := kitprometheus.NewHistogramFrom(stdprometheus.HistogramOpts{
        Namespace: "stringsvc",
        Subsystem: "string_service",
        Name:      "request_duration_seconds",
        Help:      "Request duration in seconds.",
        Buckets:   stdprometheus.DefBuckets,
}, fieldKeys)

Конвенция имён: namespace_subsystem_name — итоговые серии stringsvc_string_service_request_count и ..._request_duration_seconds; суффиксы _count/_seconds/_total Prometheus дорисует сам. Пара импортов обязательна: клиент Prometheus под алиасом stdprometheus и go-kit реализация под kitprometheus. fieldKeys — имена меток, которыми middleware потом разделяет методы и исходы. NewCounterFrom и NewHistogramFrom не только создают серии, но и регистрируют их в глобальном реестре через prometheus.MustRegister.

With и метки: кардинальность — единственная опасность#

Метод With(labelValues...) возвращает новый инструмент с привязанными значениями меток, объявленных при создании: requestCount.With("method", "uppercase", "error", "false").Add(1) увеличит одну конкретную серию, не трогая остальные. Пары идут в том же порядке, что и fieldKeys при объявлении, — перепутал порядок, и error получит значение uppercase, без паники и без предупреждения. Метки — способ разрезать одну метрику на осмысленные срезы: по методу, по исходу, по коду ответа. А теперь правило, которое спасает от продовых инцидентов: кардинальность. Каждая уникальная комбинация значений меток — отдельная серия в памяти Prometheus и в твоём сервисе; label из user_id, IP или uuid превращает счётчик в миллионы серий, которые никто не читает, но все собирают. Метка должна быть категорией с коротким списком значений: method — два значения, error — два, статус-код — десяток. Если хочешь копнуть в конкретного пользователя — это работа логов из урока 06 и request_id, а не метрик. Граница простая: метрики отвечают «сколько и как быстро», логи — «что именно случилось».

instrumenting middleware: Counter и Histogram на endpoint'е

GO
func instrumentingMiddleware(
        requestCount metrics.Counter,
        requestLatency metrics.Histogram,
) endpoint.Middleware {
        return func(next endpoint.Endpoint) endpoint.Endpoint {
                return func(ctx context.Context, request interface{}) (response interface{}, err error) {
                        defer func(begin time.Time) {
                                failed := err != nil
                                if f, ok := response.(endpoint.Failer); ok {
                                        failed = failed || f.Failed() != nil
                                }
                                lvs := []string{
                                        "method", "uppercase",
                                        "error", fmt.Sprint(failed),
                                }
                                requestCount.With(lvs...).Add(1)
                                requestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
                        }(time.Now())

                        response, err = next(ctx, request)
                        return response, err
                }
        }
}

Прямой родственник loggingMiddleware из урока 06: тот же defer, тот же begin, но вместо строки в лог — Add(1) на счётчике и Observe латентности в секундах на гистограмме. Разница в проверке исхода: err читает инфраструктурный канал, а Failer на response добавляет бизнес-ошибки из Err-поля урока 02 — иначе все 400 выглядели бы в метриках как успех. Метки lvs собираются на каждый запрос — это честная цена за срезы.

Что считать: исход, латентность и честный error-лейбл#

Разберём middleware по косточкам, потому что здесь три решения, которые определяют качество всей наблюдаемости. Первое — что такое ошибка. Канал err endpoint'а несёт инфраструктурные сбои: дедлайн, недоступная база, сломанный декодер. Бизнес-ошибки из урока 02 едут в Err-поле response через Failer и по каналу err не проходят — middleware, считающая только err != nil, честно рапортует error=false клиенту, получившему 400. Проверка response.(endpoint.Failer) в defer закрывает дыру: failed — это объединение двух каналов. Второе — единицы латентности: time.Since(begin).Seconds() даёт float64 в секундах, и это конвенция Prometheus; суффикс _seconds в имени метрики не украшение, а обязательная часть контракта. Третье — где считать: на endpoint'е, а не в сервисе и не в декодере. Endpoint-middleware видит и request, и response, и err, транспорт-агностичен и одинаково работает для HTTP сегодня и gRPC из урока 13 завтра; сервисный уровень для метрик пригодится в уроке 15, когда захочется доменных величин вроде числа сохранённых профилей.

Сборка в main: слои и /metrics

GO
var svc StringService
svc = stringService{}
svc = loggingService{logger, svc}   // service middleware из урока 15 (тизер)
upper := makeUppercaseEndpoint(svc)

upper = loggingMiddleware(logger)(upper)     // логи: снаружи
upper = instrumentingMiddleware(requestCount, requestLatency)(upper) // метрики: внутри

mux := http.NewServeMux()
mux.Handle("/uppercase", kithttp.NewServer(upper, decodeUppercaseRequest, encodeResponse))
mux.Handle("/metrics", promhttp.Handler()) // экспорт Prometheus

logger.Log("msg", "HTTP", "addr", ":8080")
log.Fatal(http.ListenAndServe(":8080", mux))

Порядок обёрток выбирают по семантике: лог снаружи пишет время вместе со всем, что внутри, метрики внутри измеряют сам endpoint — так took в логе и латентность в гистограмме измеряют разные вещи осознанно. promhttp.Handler() из клиента Prometheus отдаёт все зарегистрированные серии по GET /metrics: NewCounterFrom и NewHistogramFrom уже сделали MustRegister при создании.

Читаем /metrics: счётчики растут, гистограммы квантуют#

Смонтировали — посмотрим глазами Prometheus. request_count — счётчик, и его сырое значение почти бесполезно: интересен темп, поэтому в запросах к Prometheus счётчики оборачивают в rate(): rate(stringsvc_string_service_request_count[5m]) — среднее число запросов в секунду за пять минут; разделив по метке error=true, получишь долю сбоев без единой строчки своего кода. Гистограмма богаче: клиент Prometheus сам считает sum и count и раскладывает наблюдения по бакетам; запросы идут по серии _bucket с меткой le (less or equal). Латентность p90: histogram_quantile(0.9, sum(rate(stringsvc_string_service_request_duration_seconds_bucket[5m])) by (le)) — стандартное заклинание, которое стоит один раз записать в дашборд. Аптайм-алерты строятся на тех же сериях: резкий скачок p99 или обнуление rate() подскажут о деградации раньше пользователей. Почему гистограмма, а не среднее: среднее прячет хвосты, а хвосты — где страдают пользователи; p90 говорит «90 процентов запросов быстрее этого». Дефолтные DefBuckets покрывают диапазон от миллисекунд до десятков секунд — для старта достаточно, а под свой профиль бакеты настраиваются в HistogramOpts. Summary — альтернатива с готовыми квантилями на клиенте (NewSummaryFrom, его использует канонический stringsvc из репозитория go-kit), но summary нельзя агрегировать между инстансами — поэтому современный дефолт курсов и платформ: histogram.

Базовый набор метрик сервиса#

МетрикаТипМеткиВопрос, на который отвечает
request_countCountermethod, errorсколько запросов и какая доля сбоев
request_duration_secondsHistogrammethod, errorкак быстро: p50/p90/p99 латентности
in_flight_requestsGauge—сколько запросов обрабатывается прямо сейчас
queue_depthGaugequeueсколько задач ждёт в очереди воркеров
result_sizeHistogrammethodсколько данных отдаём: строки, байты, элементы
retries_totalCountermethodкак часто endpoint ходит повторно

Пакет metrics против клиента Prometheus: зачем посредник#

Справедливый вопрос: клиент github.com/prometheus/client_golang и так умеет CounterVec и HistogramVec — зачем ещё один слой? Причин три, и все они видны в коде middleware. Первая — интерфейсы: instrumentingMiddleware принимает metrics.Counter, а не *prometheus.CounterVec; подменить бэкенд — заменить конструктор в main, middleware компилируется как есть. Вторая — единый With: в клиенте Prometheus метки передаются при каждом вызове cv.With(lvs).Inc(), и сигнатуры различаются между типами; в go-kit With возвращает новый инструмент — выражение requestCount.With(lvs...).Add(1) читается одинаково для counter, gauge и histogram. Третья — чистота слоёв: домен и endpoint не знают ни о prometheus, ни о любых других клиентах — зависимость от инфраструктуры живёт только в main, как и должно быть по слоям из урока 01. Плата честная: тонкая обёртка и один порядок лейблов, который надо соблюдать. Для микросервисной команды, где сервисов десятки, посредник экономит больше, чем стоит — а тестам он достаётся бонусом: подмени metrics.Counter фейковым счётчиком в памяти, и middleware проверяется без Prometheus и без сети.

Шесть правил инструментирования#

  1. Считай на endpoint'е: instrumenting middleware видит request, response и err и не зависит от транспорта.
  2. Одна метрика — один вопрос: счётчик на темп, гистограмма на распределение, gauge на мгновенное состояние.
  3. Латентность — в секундах float64 с суффиксом _seconds; Observe(time.Since(begin).Seconds()).
  4. Метки — категории с коротким списком значений; идентификаторы и адреса убивают кардинальностью.
  5. error-лейбл объединяет err endpoint'а и Failer на response — иначе бизнес-отказы выглядят успехом.
  6. Метрики объявляются и регистрируются в main; домен и endpoint про Prometheus не знают.

Каким типом метрики измеряют латентность запросов и почему?

Почему instrumenting middleware — endpoint-middleware, а не строки в сервисе?

Что произойдёт при повторном вызове NewCounterFrom с теми же CounterOpts на старте второго экземпляра сервера в одном процессе?

Add без With: куда упадёт единица?

Прочитайте код и выберите, что он выведет:

requestCount := kitprometheus.NewCounterFrom(opts, []string{"method", "error"})

// Один программист делает так:
requestCount.With("method", "uppercase", "error", "false").Add(1)
// Второй — забыл With:
requestCount.Add(1)

Забыли про Failer: что покажет error-лейбл?

Прочитайте код и выберите, что он выведет:

// Простой вариант middleware без проверки Failer:
defer func(begin time.Time) {
        lvs := []string{"method", "uppercase", "error", fmt.Sprint(err != nil)}
        requestCount.With(lvs...).Add(1)
        requestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
}(time.Now())

// Сервис вернул ErrEmpty: response = UppercaseResponse{Err: "empty string"}, err = nil

Собери счётчик и его использование

Восстанови объявление метрики и middleware. Пропуски — по порядку: конструктор go-kit, opts-структура клиента, метод привязки меток, метод инкремента.

requestCount := kitprometheus.(stdprometheus.{ Namespace: "stringsvc", Subsystem: "string_service", Name: "request_count", Help: "Number of requests received.", }, fieldKeys) // внутри instrumenting middleware: requestCount.("method", "uppercase", "error", "false").(1)

Путь одного измерения

Расставь этапы измерения POST /uppercase в правильном порядке: от прихода запроса до квантиля на дашборде.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Термины урока

Термин

metrics.Counter

нажмите, чтобы перевернуть
Определение

Интерфейс go-kit: With(labelValues...) Counter и Add(delta float64) — монотонный рост; типичный счётчик запросов.

1 / 6

Что дальше#

Третий наблюдательный слой встал на место: три интерфейса, объявление серий в main, instrumenting middleware рядом с loggingMiddleware и /metrics для Prometheus. Обещание урока 04 про ServerAfter выполнено с запасом: считай хоть на endpoint-обёртке, хоть через хук транспорта — измеряется одно и то же. Освежить устройство обёрток можно в уроке 02-endpoint, опции транспорта — в уроке 04-http-transport, кодеки — в уроке 05-codeki, а пару к метрикам — в уроке 06-logirovanie; карта курса — на странице курса. Дальше — урок 08-tracing: трейсинг через endpoint middleware, Jaeger, спаны, которые свяжут все три слоя наблюдаемости одним trace id. А после трейсинга — расширенная механика поверх обёрток: rate limiting и circuit breaker, про которые endpoint из урока 02 уже намекнул.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Пакет metrics — три интерфейса: Counter (Add), Gauge (Set/Add), Histogram (Observe); у всех есть With(labelValues...).
  • Реализации подключаются в main: kitprometheus.NewCounterFrom/NewHistogramFrom над stdprometheus-опциями, регистрация через MustRegister.
  • instrumenting middleware — endpoint-обёртка: Add(1) на запрос, Observe(time.Since(begin).Seconds()) на латентность.
  • error-лейбл должен объединять err endpoint'а и Failer на response — иначе бизнес-ошибки считаются успехом.
  • Метки — категории с коротким списком значений: user_id или IP в лейбле взрывают кардинальность.
  • /metrics через promhttp.Handler(); счётчики читаются через rate(), латентность — histogram_quantile по _bucket.
  • Посредник metrics держит домен и endpoint чистыми: смена бэкенда — одна строка в main.

Домашнее задание

Возьми stringsvc с логами из урока 06 и добавь полный метрический контур. Первое: объяви request_count как Counter и request_duration_seconds как Histogram с DefBuckets — имена по конвенции namespace_subsystem_name, метки method и error. Второе: напиши instrumentingMiddleware(requestCount, requestLatency) endpoint.Middleware с defer, Observe в секундах и объединением err и Failer в один лейбл; повесь его внутрь loggingMiddleware и проверь порядок обёрток в main. Третье: смонтируй promhttp.Handler() на /metrics и убедись curl'ом, что после пары запросов на /uppercase и одного с ErrEmpty появились серии с error=true и error=false. Четвёртое: собери дашборд из трёх запросов Prometheus — rate(request_count[5m]) по method, доля error=true, p90 через histogram_quantile; запиши, сколько бакетов попало в p90-диапазон. Пятое: прогони нагрузку ab или hey на минуту и объясни, почему gauge in_flight_requests ведёт себя иначе, чем счётчик, — и добавь его, обнуляя Set(0) в начале и Set(current) в defer.

Личные заметкиПусто
Хранятся только в вашем браузере (go-kit · Инструментирование метрики)

Вопросы и ответы по уроку

Histogram или Summary для латентности?

По умолчанию Histogram: квантили считаются на стороне Prometheus через histogram_quantile и агрегируются между инстансами, а бакеты настраиваются под твой профиль. Summary считает квантили на клиенте — точнее на одном инстансе, но его нельзя суммировать по кластеру. Канонический stringsvc из репозитория go-kit использует Summary, современная практика — Histogram.

Почему не использовать клиент Prometheus напрямую в middleware?

Потому что middleware тогда навсегда зависит от github.com/prometheus/client_golang: сменить бэкенд или собрать сервис под DogStatsD не получится без правки каждого места. Интерфейсы пакета metrics изолируют зависимость в main: одна строка меняет реализацию, middleware компилируется против metrics.Counter и metrics.Histogram.

Сколько метрик нужно сервису на старте?

Трёх-четырёх достаточно: request_count с method и error, request_duration_seconds той же разметки, gauge на текущую нагрузку и опционально гистограмма размера результата. Каждая новая метрика и метка — обязательство на годы: дашборды, алерты, хранение. Добавляй по вопросу, который реально нужно отвечать, а не по принципу «пригодится».

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

gRPC25 уроков~14 чпродвинутый25 уроков уже доступны

gRPC-Go: высокопроизводительный RPC

Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.

  • Писать .proto-схемы и генерировать код
  • Реализовывать unary и streaming сервисы
  • Управлять метаданными, дедлайнами и отменами
  • Строить interceptors для логов и аутентификации