---
title: "Инструментирование метрики"
description: "Метрики Prometheus в go-kit: Counter, Gauge и Histogram, prometheus.NewCounterFrom и NewHistogramFrom, instrumenting middleware, латентность и /metrics."
course: "go-kit: микросервисный тулкит"
url: "https://golangmaster.ru/courses/go-kit/lessons/07-metriki/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

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

*Курс «go-kit: микросервисный тулкит», урок 7 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-kit/lessons/07-metriki/*

Логи рассказывают истории об отдельных запросах, но вопросы «сколько», «как быстро» и «растёт ли» отвечают метрики. В 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» перестанут быть заклинаниями.

### Три интерфейса пакета 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_count | Counter | method, error | сколько запросов и какая доля сбоев |
| request_duration_seconds | Histogram | method, error | как быстро: p50/p90/p99 латентности |
| in_flight_requests | Gauge | — | сколько запросов обрабатывается прямо сейчас |
| queue_depth | Gauge | queue | сколько задач ждёт в очереди воркеров |
| result_size | Histogram | method | сколько данных отдаём: строки, байты, элементы |
| retries_total | Counter | method | как часто 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 не знают.

> **Взрыв кардинальности: метка, которая съест память.** Одна неосторожная метка вроде user_id или remote_addr — и сервис генерирует новые серии на каждый запрос: Prometheus задыхается, память процесса растёт, дашборды врут. Симптом узнаешь по графику количества серий, растущему линейно со временем. Лечение: метка — категория (метод, исход, код ответа); идентификаторы — в логи и трейсинг, но не в метрики. Пересоздание серий без рестарта невозможно — лейблы объявляют раз и навсегда.

> **Честно о запуске кода.** Урок требует установленного Go и модулей github.com/go-kit/kit и github.com/prometheus/client_golang — в песочнице курса сторонние модули не исполняются, поэтому интерактивы построены на чтении кода. Локально: go get обоих, собери stringsvc с middleware и /metrics, дёрни пару раз POST /uppercase и curl localhost:8080/metrics — найди свои серии request_count и request_duration_seconds_bucket в выводе.

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

- Counter — время суммируется, потом делим на count
- Gauge — фиксируем последнее значение латентности
- ✅ Histogram — Observe собирает распределение, из бакетов считаются p50/p90/p99
- Summary — единственный способ, histogram так не умеет

Латентность — величина с распределением: нужен не максимум или среднее, а хвосты. Histogram.Observe раскладывает наблюдения по бакетам, и histogram_quantile считает квантили на стороне Prometheus, агрегируя между инстансами. Counter хранит только сумму, Gauge — последнее значение, а Summary считает квантили на клиенте и не агрегируется между инстансами.

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

- В сервисе недоступен time.Now
- ✅ Так метрики работают на любом транспорте, а сервис остаётся без зависимости от metrics
- Endpoint-middleware быстрее — обёртки выполняются в отдельных горутинах
- Сервис не может вернуть err, а метрика требует его

Тот же аргумент, что и с логами в уроке 06: endpoint транспорт-агностичен, обёртка видит request, response и err — и одна и та же метрика обслужит HTTP сегодня и gRPC завтра. Сервис не получает ни одного нового импорта, доменные тесты не трогаются. Горутин нет — это обычная обёртка функции.

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

- Ничего: реестр игнорирует дубликаты
- ✅ Паника от prometheus.MustRegister — дублирующая регистрация запрещена
- Счётчик обнулится и начнёт считать заново
- Ошибка компиляции: две переменные с одним именем метрики

NewCounterFrom вызывает prometheus.MustRegister — вариант регистрации, паникующий при конфликте. Это осознанный контракт: дубликат серии почти всегда ошибка сборки, и лучше упасть на старте, чем отдавать /metrics с мусором. Поэтому конструкторы метрик живут в main или в одной фабрике, а не по всему коду.

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

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

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

**Что выведет программа?**

- Второй Add вызовет панику: метки обязательны
- Обе единицы попадут в одну серию — With лишь удобство
- ✅ Появится отдельная серия с пустыми значениями method и error
- Второй Add молча проигнорируется

With не обязателен: без меток счётчик инкрементирует серию с пустыми значениями лейблов — в /metrics появится request_count{method="",error=""}. Никакой паники и никакого объединения: это третья, «мусорная» серия рядом с честными. Отсюда правило: не экспортируй сырой инструмент — отдавай middleware уже с With или следи за лейблами ревью.

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

```go
// Простой вариант 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
```

**Что выведет программа?**

- error=true — middleware угадает бизнес-ошибку
- ✅ error=false — Failer не проверен, 400 посчитан успехом
- Метрика не запишется — err равен nil
- Обе серии: error=true и error=false одновременно

Адаптер из урока 02 кладёт бизнес-ошибку в Err-поле и возвращает err = nil — канал endpoint'а чист. Middleware без проверки endpoint.Failer увидит только err != nil и запишет error=false: график сбоев зелёный, а клиенты ловят 400. Именно поэтому в канонном middleware стоит проверка response.(endpoint.Failer) — два канала ошибок объединяются в один честный лейбл.

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

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

```go
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)
```

Подсказка: Первый пропуск — фабрика из go-kit реализаций; второй — структура опций клиента Prometheus; затем привязка меток и монотонный инкремент.

Правильные ответы в пропусках: `NewCounterFrom`, `CounterOpts`, `With`, `Add`

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

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

Правильный порядок строк:

1. `ListenAndServe принял запрос, mux нашёл Server`
2. `loggingMiddleware снаружи зафиксировал begin`
3. `instrumentingMiddleware внутри зафиксировал свой begin`
4. `endpoint вызвал сервис и вернул response`
5. `defer middleware собрал lvs, сделал Add(1) и Observe(seconds)`
6. `энкодер записал ответ клиенту`
7. `Prometheus соскрёп /metrics и сложил наблюдения в бакеты`
8. `histogram_quantile посчитал p90 на дашборде`

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

- **metrics.Counter** — Интерфейс go-kit: With(labelValues...) Counter и Add(delta float64) — монотонный рост; типичный счётчик запросов.
- **metrics.Histogram** — With и Observe(value float64): распределение величины — латентность, размер ответа; квантили считаются по бакетам в Prometheus.
- **metrics.Gauge** — With, Set(value) и Add(delta): мгновенное состояние — глубина очереди, число активных соединений.
- **kitprometheus.NewCounterFrom** — Конструктор go-kit реализации: CounterOpts клиента Prometheus + имена меток; создаёт CounterVec и регистрирует через MustRegister.
- **instrumentingMiddleware** — Endpoint-middleware с defer: requestCount.With(lvs...).Add(1) и requestLatency.Observe(time.Since(begin).Seconds()); error-лейбл объединяет err и Failer.
- **promhttp.Handler()** — Хендлер GET /metrics из клиента Prometheus: отдаёт все зарегистрированные серии в текстовом формате экспозиции.

### Что дальше

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

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

- Пакет 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.

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

### 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 на текущую нагрузку и опционально гистограмма размера результата. Каждая новая метрика и метка — обязательство на годы: дашборды, алерты, хранение. Добавляй по вопросу, который реально нужно отвечать, а не по принципу «пригодится».
