Содержание урока · 11 разделов
- Три интерфейса пакета metrics
- With и метки: кардинальность — единственная опасность
- Что считать: исход, латентность и честный error-лейбл
- Читаем /metrics: счётчики растут, гистограммы квантуют
- Базовый набор метрик сервиса
- Пакет metrics против клиента Prometheus: зачем посредник
- Шесть правил инструментирования
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Три интерфейса пакета 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
GOimport (
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'е
GOfunc 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
GOvar 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 и без сети.
Шесть правил инструментирования#
- Считай на endpoint'е: instrumenting middleware видит request, response и err и не зависит от транспорта.
- Одна метрика — один вопрос: счётчик на темп, гистограмма на распределение, gauge на мгновенное состояние.
- Латентность — в секундах float64 с суффиксом _seconds; Observe(time.Since(begin).Seconds()).
- Метки — категории с коротким списком значений; идентификаторы и адреса убивают кардинальностью.
- error-лейбл объединяет err endpoint'а и Failer на response — иначе бизнес-отказы выглядят успехом.
- Метрики объявляются и регистрируются в 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 в выводе.
Каким типом метрики измеряют латентность запросов и почему?
Почему 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-структура клиента, метод привязки меток, метод инкремента.
Путь одного измерения
Расставь этапы измерения POST /uppercase в правильном порядке: от прихода запроса до квантиля на дашборде.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Термины урока
Что дальше#
Третий наблюдательный слой встал на место: три интерфейса, объявление серий в 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.
Личные заметкиПусто
Вопросы и ответы по уроку
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 на текущую нагрузку и опционально гистограмма размера результата. Каждая новая метрика и метка — обязательство на годы: дашборды, алерты, хранение. Добавляй по вопросу, который реально нужно отвечать, а не по принципу «пригодится».
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
gRPC-Go: высокопроизводительный RPC
Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.
- Писать .proto-схемы и генерировать код
- Реализовывать unary и streaming сервисы
- Управлять метаданными, дедлайнами и отменами
- Строить interceptors для логов и аутентификации