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

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

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

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

Метрики HTTP: InstrumentHandler

HTTP метрики go-сервиса сводятся к одному и тому же набору: длительность, счётчик запросов по статусам, запросы в полёте. promhttp кладёт этот набор в один middleware-слой — InstrumentHandlerDuration и соседей, — а если нужен свой лейбл вроде шаблона маршрута, пишется собственная обёртка на net/http. Разберём оба пути.

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

Метрика HTTP-запроса — самая ходовая в любом дашборде, и заводят её всегда одной пачкой. Трафик: сколько запросов в секунду и по каким маршрутам. Ошибки: доля пятисотых и четвёртых ответов. Длительность: распределение времени ответа и p99 на графике. Плюс пара приправ: число запросов в обработке прямо сейчас и размеры тел запросов-ответов. Триплет «трафик, ошибки, длительность» в мониторинге зовут RED-методом, и его хватает, чтобы заметить и деградацию, и падение. Писать замер времени руками в каждом хендлере — путь к рассинхрону: в одном хендлере defer, в другом забыли, третий меряет время до записи ответа, а четвёртый называет метрику иначе. Middleware решает проблему структурно: одна обёртка вокруг роутера гарантирует замер всем маршрутам, а типы и имена метрик живут в одном месте. Именно так устроены готовые слои promhttp — и именно поэтому собственная обёртка повторяет их форму.

HTTP метрики go: семейство InstrumentHandler#

Пакет promhttp умеет оборачивать http.Handler готовыми слоями, и у всех один знаменатель сигнатуры: InstrumentHandlerX(метрика, next http.Handler) http.Handler. Метрика — не любая, а подходящего типа: счётчику запросов нужен CounterVec с лейблами code и method, гистограмме длительности — HistogramVec с теми же лейблами, in-flight — одиночный Gauge без лейблов. Обёртки проставляют значения сами: код ответа они достают из перехваченного WriteHeader, метод — из запроса, поэтому в хендлерах не появляется ни одной строки про метрики. Слои гнездятся: внешний замеряет длительность, внутренний считает запросы. Объявите в векторе ровно те лейблы, которые слой умеет заполнять: лишний или недостающий — и первый же запрос упадёт паникой inconsistent label cardinality. Поэтому новые связки обкатывают на стенде, а не в проде, и первым тестом после добавления слоя делают обычный прогон хендлера с чтением /metrics.

Два слоя над роутером

GO
package main

import (
        "net/http"

        "github.com/prometheus/client_golang/prometheus"
        "github.com/prometheus/client_golang/prometheus/promhttp"
)

var httpRequests = prometheus.NewCounterVec(
        prometheus.CounterOpts{
                Name: "http_requests_total",
                Help: "Запросы по методу и статусу",
        },
        []string{"method", "code"},
)

var httpDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
                Name:    "http_request_duration_seconds",
                Help:    "Длительность обработки",
                Buckets: prometheus.DefBuckets,
        },
        []string{"method", "code"},
)

func main() {
        prometheus.MustRegister(httpRequests, httpDuration)

        mux := http.NewServeMux()
        mux.HandleFunc("/api", apiHandler)
        mux.Handle("/metrics", promhttp.Handler())

        wrapped := promhttp.InstrumentHandlerDuration(httpDuration,
                promhttp.InstrumentHandlerCounter(httpRequests, mux),
        )
        http.ListenAndServe(":8080", wrapped)
}

Два вектора с одинаковыми лейблами method и code: счётчик запросов и гистограмма длительности. Обёртки гнездятся: внешний InstrumentHandlerDuration меряет всё время обработки, внутренний InstrumentHandlerCounter инкрементит по итоговому статусу — код оба берут из перехваченного WriteHeader. /metrics добавлен в mux до оборачивания, чтобы скрейп не попадал в собственные метрики.

Порядок слоёв и что видит каждый#

Слои можно менять местами, и от порядка зависит семантика. Внешний замер длительности включает всё, что внутри: работу счётчика, логи, сам хендлер — так и надо, потому что латентность для пользователя одна. Если поставить замер внутрь, между счётчиком и хендлером, цифра почти не изменится: накладные расходы обёрток ничтожны по сравнению с сетью, но принципиальная схема остаётся — чем ближе слой к пользователю, тем полнее он видит время. С готовыми слоями порядок роли почти не играет; со своими — играет: middleware, который снимает лейбл маршрута, должен стоять после роутера, иначе маршрут ещё не сопоставлен и в лейбл уйдёт пустая строка. Композиция — сильная сторона net/http: recovery сверху, метрики под ним, свой лейбл маршрута ещё ниже — и каждый слой делает ровно одно дело, не зная о соседях.

Свой middleware: statusRecorder и Observe

GO
type statusRecorder struct {
        http.ResponseWriter
        status int
}

func (r *statusRecorder) WriteHeader(code int) {
        r.status = code
        r.ResponseWriter.WriteHeader(code)
}

func metricsMiddleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                start := time.Now()
                rec := &statusRecorder{ResponseWriter: w, status: 200}
                next.ServeHTTP(rec, r)

                elapsed := time.Since(start).Seconds()
                code := strconv.Itoa(rec.status)
                httpDuration.WithLabelValues(r.Method, code).Observe(elapsed)
        })
}

statusRecorder встраивает http.ResponseWriter и перехватывает WriteHeader: статус запоминается и передаётся оригинальному писателю, остальная поверхность ответа не меняется. Дефолт 200 повторяет соглашение net/http, где ответ без явного WriteHeader считается успешным. Для паникующих хендлеров замер дублируют в defer или ставят слой под recovery — иначе 500 от recovery не попадёт в распределение.

Инцидент: пятисотые, которых не было#

Инцидент, из которого выросло правило этого урока. Сервис под нагрузкой начал паниковать в одном хендлере: recovery перехватывал панику, писал 500, и всё выглядело штатно — доля ошибок в метриках почти не менялась, а поддержка была завалена жалобами. Причина: замер в middleware стоял после next.ServeHTTP, до которого паника не доживала, — Observe не выполнялся, и пятисотые ответы в гистограмме отсутствовали. Ошибка двойная: замер без defer и отсутствие отдельного счётчика ошибок. Чинится двумя строками: Observe в замыкании defer и инкремент при коде пятой сотни. Мораль шире кода: слой, который обещает метрики всем маршрутам, обязан метить и аварийные ответы — иначе самый интересный трафик, ради которого и ставят мониторинг, проходит мимо распределения молча. Проверьте это на стенде: хендлер с паникой — и посмотрите, какая серия родилась. Счётчик http_errors_total рядом с гистограммой — минимальный страховочный полис: когда два числа расходятся, виноват слой, который врёт.

Когда готовых слоёв мало#

Готовые слои знают про code и method, и всё. Как только появляется собственное измерение — шаблон маршрута, идентификатор тенанта, флаг эксперимента, версия API в заголовке, — наступает время собственной обёртки по образцу выше. Скелет всегда один: подменить ResponseWriter обёрткой, запоминающей статус; замерить время вокруг next.ServeHTTP; вычислить лейблы; отдать наблюдение нужному Observer. Три тонкости, на которых спотыкаются. Статус по умолчанию — 200: если хендлер не вызвал WriteHeader явно, в метрике должен оказаться 200, как и в реальном ответе. Паника в хендлере: если замер стоит после next.ServeHTTP без defer, 500-е от recovery-слоя никогда не попадут в гистограмму — а это самые интересные значения. Лейбл маршрута берите из роутера после сопоставления: в chi это chi.RoutePattern(r), в gin — c.FullPath(), и оба вернут пустую строку, если слой стоит выше маршрутизации. Шаблон, а не сырой путь: о кардинальности путей мы подробно говорили в уроке про лейблы.

Семейство InstrumentHandler целиком#

  • InstrumentHandlerCounter — счётчик запросов по лейблам code и method.
  • InstrumentHandlerDuration — длительность обработки в ObserverVec: гистограмме или summary.
  • InstrumentHandlerInFlight — одиночный Gauge с числом запросов, находящихся в обработке прямо сейчас.
  • InstrumentHandlerRequestSize и InstrumentHandlerResponseSize — размеры тела запроса и ответа в байтах, тоже через ObserverVec.
  • InstrumentHandlerTimeToWriteHeader — время до записи первых заголовков: TTFB и зависшие хендлеры.
  • Общий контракт: метрика с лейблами code и method (или их подмножеством), next-хендлер, на выходе http.HandlerFunc.

In-flight растёт: трафик или зависшие хендлеры?#

График in-flight обманывает всех, кто видит его впервые: рост линии читают как рост трафика. Не всегда. In-flight — число запросов в обработке прямо сейчас, и его рост означает произведение двух величин: пришедшего трафика и времени обработки. Трафик ровный, а линия ползёт вверх — значит, запросы обрабатываются дольше: заблокировалась база, завис внешний вызов, кончился пул соединений. Это самый ранний сигнал деградации — раньше пятисотых и роста p99: пул ещё держится, но свободных слотов всё меньше, и метрики горутин вскоре покажут то же самое. Практика: на панели in-flight рисуйте рядом rate запросов — если линии расходятся, виновата латентность, а не наплыв. Вторая крайность — паника на всякий рост: пик-час законно поднимает линию втрое; сравнивайте с прошлой неделей, а не с прошлым часом. Сама метрика почти бесплатна: пара атомарных операций на запрос.

Спорное мнение: готовые слои переоценены#

Спорное мнение, с которым согласны не все: в сервисе со своим роутером и логикой я чаще пишу один собственный middleware, чем собираю три готовых слоя. Аргумент: своя обёртка сразу даёт то, ради чего всё затевалось, — шаблон маршрута в лейбле, единые имена метрик, замер в defer, — а готовые слои дают code и method, после чего их всё равно приходится дополнять. Две обёртки поверх одной — двойной перехват WriteHeader и два места, где можно ошибиться. Граница, где мнение меняется: новый сервис без собственных измерений и прототип — берите InstrumentHandlerCounter и InstrumentHandlerDuration, это двадцать секунд работы и меньше собственного кода в ревью. Правило разветвления простое: как только появился первый собственный лейбл, весь HTTP-слой собирается в одном месте, а не в трёх разных. И один свой middleware проще объяснить новому человеку в команде, чем композицию из трёх обёрток.

Проверка: лейблы обёрток

Какие лейблы обёртки promhttp проставляют сами?

Проверка: in-flight

Какая метрика нужна InstrumentHandlerInFlight?

Какой код ответа попадёт в метрику?

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

func handler(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("ok"))
}

// внутри middleware:
rec := &statusRecorder{ResponseWriter: w, status: 200}
next.ServeHTTP(rec, r)
code := strconv.Itoa(rec.status)

Соберите слой из готовых блоков

Допишите вектор длительности и обёртку InstrumentHandlerDuration над роутером.

var httpDuration = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "Длительность обработки", Buckets: prometheus., }, []string{"", "code"}, ) wrapped := promhttp.(httpDuration, mux)

Ядро собственного middleware

Расставьте строки замера внутри metricsMiddleware: от старта таймера до наблюдения.

Строки

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

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

Карточки: HTTP-метрики

Термин

InstrumentHandlerCounter

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

Обёртка promhttp: инкрементит CounterVec с лейблами code и method на каждый ответ. Значения проставляются сами из перехваченного WriteHeader.

1 / 5

Слой HTTP-метрик — костяк мониторинга сервиса, и собирать его теперь можно двумя способами: готовыми обёртками или собственной обёрткой с своими лейблами. Дальше по курсу метрики перестают быть техническими: в следующем уроке считаем бизнес-события — заказы, выручку и очереди платежей теми же Counter и Gauge. Для предыдущей части темы полезен урок про Summary — там объяснено, когда ObserverVec стоит заменить summary-версией. Роутер в примерах этого курса — чистый net/http; те же слои ложатся на chi без изменений, потому что chi.Router — обычный http.Handler, знакомство с ним в отдельном курсе. Полная программа — на странице Prometheus. И как всегда: весь код урока исполняется локальным Go — песочница сайта библиотечные пакеты не запускает; соберите сервис, постучите в хендлер curl-ом и посмотрите, какие серии родили обёртки.

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

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

Вывод

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

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

  • Базовый набор HTTP-метрик: счётчик по code и method, гистограмма длительности, Gauge in-flight — RED-метод.
  • Обёртки promhttp имеют сигнатуру InstrumentHandlerX(метрика, next) и сами проставляют code и method.
  • Набор лейблов в векторе обязан совпасть с тем, что проставляет слой: ошибка — паника на первом запросе.
  • Свой middleware — это statusRecorder, замер вокруг next.ServeHTTP и Observe в нужный Observer.
  • Статус по умолчанию 200; замер и Observe ставят так, чтобы 500 от recovery тоже попадали в распределение.
  • /metrics держат вне слоя инструментов, чтобы скрейп не метил сам себя.

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

Возьмите учебный сервис и соберите два варианта: первый — InstrumentHandlerCounter и InstrumentHandlerDuration над mux, второй — собственный middleware со statusRecorder и лейблом шаблона маршрута. Сравните /metrics после пары запросов: какие серии родились, какие лейблы заполнились. Затем уберите лейбл method из вектора и посмотрите на панику на стенде — этот сбой лучше увидеть до прода. Нужен локальный Go.

Личные заметкиПусто
Хранятся только в вашем браузере (Prometheus · Метрики HTTP: InstrumentHandler)

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

Чем InstrumentHandlerDuration отличается от своего middleware?

Только гибкостью: обёртка promhttp знает про code и method и ничего больше. Свой middleware нужен для собственных лейблов — шаблона маршрута, тенанта, версии API — и для нестандартных правил замера.

Почему метрика in-flight — Gauge, а не счётчик?

Это моментальная величина: количество запросов в обработке сейчас. Оно растёт и убывает, поэтому Inc и Dec на входе и выходе. Счётчик умеет только расти и показал бы суммарное число запросов, а не текущую нагрузку.

Нужно ли метрить /metrics?

Обычно нет: скрейп идёт каждые несколько секунд и генерирует стабильный шум в сериях. Регистрируйте /metrics вне оборачиваемого mux; если всё же нужен контроль доступа к экспозиции, заведите отдельные метрики скрейпа, а не общий слой.

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

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

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

WebSocket25 уроков~13 чпрактика25 уроков уже доступны

WebSocket в Go: real-time чаты и уведомления

Курс WebSocket на Go: gorilla/websocket, эхо-сервер, hub-паттерн, комнаты, авторизация соединений, ping/pong, reconnect и масштабирование. 25 уроков.

  • Поднимать WS-сервер и апгрейдить HTTP-соединения
  • Писать readPump/writePump без гонок на каждое соединение
  • Строить hub: регистрацию, рассылку и комнаты
  • Авторизовывать соединения и ограничивать сообщения