Содержание урока · 10 разделов
- Мониторинг веб-сервиса Go: один файл metrics.go
- HTTP-метрики: inflight и гистограмма по route и method
- Бизнес-метрики: заказы и суммы
- go_-метрики: память процесса на панели
- Дашборд итогового проекта
- Три алерта, с которых начинается дежурство
- Пробный прогон: проект проверяется за вечер
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Понедельник, и вам передают сервис на двадцать хендлеров: /metrics отдаёт страницу, которую никто не читает с момента релиза, а каждое расследование начинается с писем поддержки. Задача на неделю сформулирована так, как в предыдущем уроке: дежурный отвечает на вопрос «что сломалось» за тридцать секунд. Мониторинг веб-сервиса на Go к этому моменту собран по частям во всех двадцати четырёх уроках — здесь он собирается в проект: один файл metrics.go, middleware, список панелей дашборда и первые три алерта. План честный: за один вечер файл встаёт в сервис, за второй — конфиг скрейпа из двадцать третьего урока ведёт метрики в Prometheus, за неделю — дежурство начинает верить графикам.
Мониторинг веб-сервиса Go: один файл metrics.go#
Проект живёт в одном пакете metrics, чтобы любой импорт получал готовые объекты, а регистрация происходила ровно один раз — собственный реестр из двадцать второго урока держит дисциплину имён и изолирует тесты. Внутри четыре группы: HTTP-метрики трёх сигналов — счётчик, гистограмма и inflight; бизнес-счётчик заказов с суммой чека; инфраструктурный слой — go_- и process_-метрики рантайма; и ничего лишнего. У каждой метрики есть читатель на дашборде и вопрос, на который она отвечает, — правило прошлого урока, проверенное на каждой строке файла. Имя домена shop_ стоит в Namespace, модуль — в Subsystem, и итоговые имена читаются как оглавление кода: по метрике видно, чей это файл.
metrics.go: весь проект метрик
GOpackage metrics
import "github.com/prometheus/client_golang/prometheus"
var (
Requests = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "shop", Name: "http_requests_total",
Help: "Запросы по маршруту, методу и статусу",
},
[]string{"route", "method", "status"},
)
Duration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "shop", Name: "http_request_duration_seconds",
Help: "Латентность запросов",
Buckets: []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5},
},
[]string{"route", "method"},
)
Inflight = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "shop", Name: "http_inflight_requests",
Help: "Обработчики в работе по маршруту",
},
[]string{"route"},
)
Orders = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "shop", Subsystem: "orders",
Name: "total", Help: "Заказы по типу и статусу",
},
[]string{"type", "status"},
)
OrderAmount = prometheus.NewHistogram(
prometheus.HistogramOpts{
Namespace: "shop", Subsystem: "orders",
Name: "amount_usd", Help: "Сумма заказа, доллары",
Buckets: []float64{5, 10, 25, 50, 100, 250, 500},
},
)
)Четыре объекта закрывают весь чертёж: Requests даёт трафик и ошибки разом — лейбл status превращает его в долю падений; Duration собирает хвост латентности по маршруту и методу; Inflight показывает насыщение обработки; Orders и OrderAmount — бизнес-слой, деньги отдельно от трафика. Бакеты гистограммы выбраны под веб-запросы до пяти секунд, бакеты суммы — под чек магазина; оба списка проверяются по реальным данным, а не по вкусу.
HTTP-метрики: inflight и гистограмма по route и method#
Middleware связывает метрики с chi и обязана делать ровно три вещи: поднять Inflight на входе и опустить на выходе, замерить длительность, записать запрос с итоговым статусом. Маршрут берётся из роутера через FullPath, а не из сырого URL — иначе каждый идентификатор заказа создаёт свою серию, и урок про кардинальность становится не теорией, а инцидентом мониторинга. Проба готовности и метрики монтируются до логгера и аутентификации по порядку слоёв из урока chi про middleware — иначе график RPS дёргается на каждом опросе оркестратора. Обёртка ответа — из набора chi middleware.WrapResponseWriter: она запоминает статус, который хендлер запишет позже, и лейбл status получает истинное значение, а не дефолт.
Middleware в chi
GOfunc Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route := chi.RouteContext(r.Context()).RoutePattern()
if route == "" || route == "/*" {
next.ServeHTTP(w, r) // нечестный маршрут — без лейблов
return
}
ww := middleware.WrapResponseWriter(w)
start := time.Now()
Inflight.WithLabelValues(route).Inc()
defer Inflight.WithLabelValues(route).Dec()
next.ServeHTTP(ww, r)
Duration.WithLabelValues(route, r.Method).
Observe(time.Since(start).Seconds())
Requests.WithLabelValues(route, r.Method,
strconv.Itoa(ww.Status())).Inc()
})
}Маршрут без шаблона — health-check-и и мусорные пути — не получает лейблов и не размножает кардинальность. Inflight растёт до хендлера и падает через defer: датчик показывает реальную одновременность обработки. Запись статуса и наблюдения идут после next.ServeHTTP, когда итоговый статус уже известен; strconv.Itoa переводит число в строку лейбла без аллокаций fmt.
Бизнес-метрики: заказы и суммы#
Бизнес-слой отвечает на вопросы продукта, которые не выводятся из HTTP: сколько заказов оформлено, какого типа, какова средняя сумма чека. Счётчик Orders с лейблами type и status растёт в точке оформления — одна точка, один инкремент, как в уроке про бизнес-метрики; статус отличает успешные заказы от отказанных оплаты. Сумма чека идёт в гистограмму OrderAmount: у гистограммы есть скрытые серии _sum и _count, и средняя сумма за окно считается делением их скоростей — это работает даже когда сама гистограмма не открыта на дашборде. Хранить среднее в gauge-е — ошибка: между рестартами значение обнуляется и график показывает пилообразную ложь; гистограмма же кумулятивна и честна на любом окне. Денежные вопросы дежурному не нужны, но продукт смотрит на них каждый понедельник, и это второй настоящий читатель вашего файла metrics.go.
go_-метрики: память процесса на панели#
Инфраструктурный слой бесплатен — Go-коллектор из одиннадцатого урока приносит go_goroutines, go_memstats_* и process_-метрики, — но выбирать из него нужно два-три графика, а не все сорок серий. Память читается парой: go_memstats_alloc_bytes показывает, сколько живых объектов держит куча, process_resident_memory_bytes — сколько процесс занял у операционной системы. Расхождение этих линий — диагностический жест из урока про горутины: RSS растёт при ровном alloc — буферы и невозвращённые страницы; alloc растёт при ровном RSS — нормальная работа сборщика. Третья линия — go_goroutines: плато при ровном трафике здорово, линейный рост — утечка из-за запроса без таймаута. Четвёртая — go_gc_duration_seconds: рост пауз сборщика при ровном трафике показывает, что живые объекты прижались к лимитам, и панель подскажет его раньше, чем сервис начнёт шататься по латентности. Итого панель процесса занимает четыре графика и отвечает на вопрос «не течёт ли сервис» до всякого профилирования.
Дашборд итогового проекта#
| Панель | Запрос | Сигнал |
|---|---|---|
| RPS сервиса | sum by (route) (rate(shop_http_requests_total[1m])) | трафик |
| Доля ошибок | sum 5xx rate / sum всех rate | ошибки |
| p50 и p99 | histogram_quantile по shop_http_request_duration_seconds_bucket | латентность |
| Обработчики в работе | sum(shop_http_inflight_requests) | насыщение |
| Заказы в секунду | sum(rate(shop_orders_total{status="ok"}[5m])) | бизнес |
| Средний чек | rate(amount_usd_sum) / rate(amount_usd_count) | бизнес |
| Горутины и RSS | go_goroutines, process_resident_memory_bytes | процесс |
Семь запросов дашборда
GO# Трафик по маршруту
sum by (route) (rate(shop_http_requests_total[1m]))
# Доля ошибок
sum(rate(shop_http_requests_total{status=~"5.."}[5m]))
/ sum(rate(shop_http_requests_total[5m]))
# p99 латентности по маршруту
histogram_quantile(0.99, sum by (le, route) (
rate(shop_http_request_duration_seconds_bucket[5m])))
# Насыщение, бизнес и процесс
sum(shop_http_inflight_requests)
sum(rate(shop_orders_total{status="ok"}[5m]))
rate(shop_orders_amount_usd_sum[5m])
/ rate(shop_orders_amount_usd_count[5m])
process_resident_memory_bytesСемь запросов — семь панелей первого экрана: порядок сверху вниз совпадает с порядком чтения дежурного. Разбивка p99 по route и le показывает маршрут-виновник без второго запроса, средний чек считается делением скрытых серий гистограммы, а панель процесса замыкает список. Переменная route вверху дашборда делается по шаблону из тринадцатого урока.
Три алерта, с которых начинается дежурство#
Алерты ставятся после недели наблюдения, и их ровно три. Первый — доля ошибок выше одного процента в течение пяти минут: единственное правило, которое почти всегда требует действия, и его выражение уже написано в уроке про алерты. Второй — p99 выше согласованного бюджета в течение десяти минут: пользователи заметили раньше дежурного, и по разбивке route видно, куда смотреть. Третий — Watchdog: правило, которое горит всегда, а внешний канал ловит его тишину, — так обнаруживается пропажа самого мониторинга и целых сервисов, потому что up == 0 у мёртвого таргета не виден, если скрейпить больше некому. Каждый алерт в аннотации содержит ссылку на дашборд и сценарий первого действия; алерт без сценария — это шум с шаблоном письма, и его место в корзине.
Пробный прогон: проект проверяется за вечер#
Собранный проект проверяется вечером без продакшена: сервис под нагрузкой из урока про стресс-тест показывает все семь панелей живыми линиями. Что должно измениться на глазах: RPS повторяет профиль генератора, доля ошибок растёт на специально сломанном хендлере, p99 отзывается на замедленную зависимость, inflight поднимается до потолка пула, заказы и средний чек ползут по сценарию генератора, горутины возвращаются к базовой линии после снятия нагрузки. Панель без движения — это либо битое имя серии, либо лейбл, который никогда не пишется; обе находки стоят десяти минут прогоне, потому что в проде они стоят разбора. Прогон заодно калибрует бакеты гистограммы: если весь рост собрался в верхнем бакете — границы переехали, и список стоит пересмотреть до выката. После такого вечера дашборд встречает первый настоящий инцидент с живыми, а не пустыми приборами.
Проект закончен, и на каждую из семи панелей вешается правило с порогом — через месяц дежурный выключает канал: сорок уведомлений за неделю, из них три требовали действий. Порог на память без сценария действия сработает на каждом релизе с большим кэшем, алерт на inflight — на каждом пике трафика, который сервис честно переварил. Правило прежнее: правило существует, только когда названо действие, которое человек обязан сделать по нему. Три алерта первого дня — потолок, а не стартовый минимум.
Проверка: лейблы HTTP-метрик
Почему в счётчике Requests проекта лейбл route берётся из RoutePattern, а не из сырого пути?
Проверка: две линии памяти
RSS процесса растёт вторые сутки, go_memstats_alloc_bytes стоит на месте. Первая гипотеза?
Сколько серий у Requests
Прочитайте код и выберите, что он выведет:
// маршруты: /api/orders, /api/health
// методы: GET, POST
// статусы: 200, 409, 500
// все комбинации реально встречалисьСоберите середину middleware
Допишите датчик, наблюдение и запись запроса.
Проект за один день
Расставьте шаги внедрения проекта.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: итоговый проект
Песочница сайта знает только fmt, strings, strconv, math, sort, errors и time — client_golang и chi собираются локально. Чек-лист сборки: metrics.go компилируется, middleware ставится до логгера, curl по /metrics показывает shop_-метрики, конфиг из двадцать третьего урока даёт зелёный up, и на каждую панель дашборда есть живая линия — пустая панель означает битое имя метрики.
Проект замыкает курс, и каждая строка metrics.go имеет адрес происхождения: счётчик — из урока про Counter, гистограмма и бакеты — из седьмого, лейблы и их словарь — из шестого, HTTP-обёртки — из девятого, бизнес-счётчики — из десятого, запросы панелей — из урока про PromQL. Дашборд собран по тринадцатому уроку, алерты — по четырнадцатому, порядок внедрения — по двадцать четвёртому, а логи с теми же лейблами — из урока про корреляцию. Весь маршрут курса — на странице Prometheus: от молчащего сервиса до дежурства, которое отвечает за тридцать секунд.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Проект живёт в одном пакете metrics: счётчик, гистограмма, inflight, бизнес-слой и свой реестр.
- Middleware берёт маршрут из RoutePattern: сырой путь с идентификаторами умножает серии без предела.
- Средний чек — rate(_sum) на rate(_count) гистограммы суммы; gauge со средним лжёт после рестарта.
- Память читается парой alloc против RSS: расхождение линий отделяет буферы от утечки объектов.
- Дашборд первого экрана — семь панелей в порядке чтения дежурного: трафик, ошибки, латентность, насыщение, бизнес, процесс.
- Три алерта стартового дня: доля ошибок, p99 выше бюджета и Watchdog на тишину.
- Каждая панель и правило обязаны иметь читателя и сценарий действия — иначе это расход и шум.
Домашнее задание
Соберите проект целиком: metrics.go с четырьмя метриками и собственным реестром, middleware для chi с FullPath и WrapResponseWriter, бизнес-инкременты в точке заказа. Подключите сервис к Prometheus из докер-композа, добейтесь зелёного up и соберите дашборд из семи панелей. Нагрузите сервис из двадцатого урока и убедитесь, что каждая панель живёт. Через неделю наблюдения включите три алерта и прогоните каждый по сценарию: доля ошибок на специально сломанном хендлере, p99 на замедленном, тишина Watchdog на выключенном сервере.
Личные заметкиПусто
Вопросы и ответы по уроку
Можно ли вместо своего middleware взять promhttp.InstrumentHandler?
Можно, и для старта это быстрый путь: готовые обёртки дают duration, счётчик и inflight с разумным набором лейблов. Свой middleware в проекте оправдан, когда нужен единый словарь имён shop_, лейбл route из роутера и точка, где обучение метрикам совпадает с бизнес-логикой; готовые обёртки при этом остаются хорошим эталоном для сверки.
Сколько бизнес-метрик допустимо в проекте?
Столько, сколько вопросов продукта отвечает на панели, которые открывают по понедельникам: заказы по типу, сумма чека, доля отказов оплаты. Правило то же, что для технических метрик: у каждой серии есть читатель и вопрос. Отличие бизнес-слоя в аудитории — смотрите не только SRE, поэтому Help пишется словами продукта.
Что делать, когда дашборд разросся?
Вернуться к правилу читателя: панель, которую месяц не открывали, уезжает во второй экран или удаляется. Первый экран остаётся семью панелями красного метода плюс бизнес-строка, разрезы по route живут на втором экране с переменной, а релизные сравнения — на третьем. Дашборд — интерфейс дежурного, и его сдержанность — часть мониторинга.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
WebSocket в Go: real-time чаты и уведомления
Курс WebSocket на Go: gorilla/websocket, эхо-сервер, hub-паттерн, комнаты, авторизация соединений, ping/pong, reconnect и масштабирование. 25 уроков.
- Поднимать WS-сервер и апгрейдить HTTP-соединения
- Писать readPump/writePump без гонок на каждое соединение
- Строить hub: регистрацию, рассылку и комнаты
- Авторизовывать соединения и ограничивать сообщения