---
title: "Итоговый проект: мониторинг веб-сервиса"
description: "Итоговый проект курса: мониторинг веб сервиса go — HTTP-метрики по route и method, бизнес-счётчик заказов, go_-метрики памяти и дашборд с тремя алертами."
course: "Мониторинг Go с Prometheus: метрики, Grafana, алерты"
url: "https://golangmaster.ru/courses/prometheus/lessons/25-itogovyj-proekt-prom/"
published: "2026-09-19"
updated: "2026-09-24"
minutes: 24
---

# Итоговый проект: мониторинг веб-сервиса

*Курс «Мониторинг Go с Prometheus: метрики, Grafana, алерты», урок 25 из 25. ~24 минут. Обновлено 24.09.2026. Источник: https://golangmaster.ru/courses/prometheus/lessons/25-itogovyj-proekt-prom/*

Мониторинг веб-сервиса на Go собирается по одному чертежу: счётчик запросов с route, method и status, гистограмма латентности, датчик обработчиков в работе, бизнес-счётчик заказов и go_-метрики памяти — в итоговом проекте курса этот чертёж складывается в один файл metrics.go, дашборд и три алерта.

Понедельник, и вам передают сервис на двадцать хендлеров: /metrics отдаёт страницу, которую никто не читает с момента релиза, а каждое расследование начинается с писем поддержки. Задача на неделю сформулирована так, как в [предыдущем уроке](/courses/prometheus/lessons/24-ekspluataciya/): дежурный отвечает на вопрос «что сломалось» за тридцать секунд. Мониторинг веб-сервиса на Go к этому моменту собран по частям во всех двадцати четырёх уроках — здесь он собирается в проект: один файл metrics.go, middleware, список панелей дашборда и первые три алерта. План честный: за один вечер файл встаёт в сервис, за второй — конфиг скрейпа из [двадцать третьего урока](/courses/prometheus/lessons/23-scrape-config/) ведёт метрики в Prometheus, за неделю — дежурство начинает верить графикам.

### Мониторинг веб-сервиса Go: один файл metrics.go

Проект живёт в одном пакете metrics, чтобы любой импорт получал готовые объекты, а регистрация происходила ровно один раз — собственный реестр из [двадцать второго урока](/courses/prometheus/lessons/22-reestry-i-namespaces/) держит дисциплину имён и изолирует тесты. Внутри четыре группы: HTTP-метрики трёх сигналов — счётчик, гистограмма и inflight; бизнес-счётчик заказов с суммой чека; инфраструктурный слой — go_- и process_-метрики рантайма; и ничего лишнего. У каждой метрики есть читатель на дашборде и вопрос, на который она отвечает, — правило прошлого урока, проверенное на каждой строке файла. Имя домена shop_ стоит в Namespace, модуль — в Subsystem, и итоговые имена читаются как оглавление кода: по метрике видно, чей это файл.

### metrics.go: весь проект метрик

```go
package 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 — иначе каждый идентификатор заказа создаёт свою серию, и [урок про кардинальность](/courses/prometheus/lessons/17-kardinalnost/) становится не теорией, а инцидентом мониторинга. Проба готовности и метрики монтируются до логгера и аутентификации по порядку слоёв из [урока chi про middleware](/courses/chi/lessons/18-middleware-poryadok/) — иначе график RPS дёргается на каждом опросе оркестратора. Обёртка ответа — из набора chi middleware.WrapResponseWriter: она запоминает статус, который хендлер запишет позже, и лейбл status получает истинное значение, а не дефолт.

### Middleware в chi

```go
func 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 растёт в точке оформления — одна точка, один инкремент, как в [уроке про бизнес-метрики](/courses/prometheus/lessons/10-biznes-metriki/); статус отличает успешные заказы от отказанных оплаты. Сумма чека идёт в гистограмму OrderAmount: у гистограммы есть скрытые серии _sum и _count, и средняя сумма за окно считается делением их скоростей — это работает даже когда сама гистограмма не открыта на дашборде. Хранить среднее в gauge-е — ошибка: между рестартами значение обнуляется и график показывает пилообразную ложь; гистограмма же кумулятивна и честна на любом окне. Денежные вопросы дежурному не нужны, но продукт смотрит на них каждый понедельник, и это второй настоящий читатель вашего файла metrics.go.

### go_-метрики: память процесса на панели

Инфраструктурный слой бесплатен — Go-коллектор из [одиннадцатого урока](/courses/prometheus/lessons/11-default-metriki/) приносит go_goroutines, go_memstats_* и process_-метрики, — но выбирать из него нужно два-три графика, а не все сорок серий. Память читается парой: go_memstats_alloc_bytes показывает, сколько живых объектов держит куча, process_resident_memory_bytes — сколько процесс занял у операционной системы. Расхождение этих линий — диагностический жест из [урока про горутины](/courses/prometheus/lessons/16-metriki-gorutin/): 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 вверху дашборда делается по шаблону из [тринадцатого урока](/courses/prometheus/lessons/13-grafana-obzor/).

### Три алерта, с которых начинается дежурство

Алерты ставятся после недели наблюдения, и их ровно три. Первый — доля ошибок выше одного процента в течение пяти минут: единственное правило, которое почти всегда требует действия, и его выражение уже написано в [уроке про алерты](/courses/prometheus/lessons/14-alerts-obzor/). Второй — p99 выше согласованного бюджета в течение десяти минут: пользователи заметили раньше дежурного, и по разбивке route видно, куда смотреть. Третий — Watchdog: правило, которое горит всегда, а внешний канал ловит его тишину, — так обнаруживается пропажа самого мониторинга и целых сервисов, потому что up == 0 у мёртвого таргета не виден, если скрейпить больше некому. Каждый алерт в аннотации содержит ссылку на дашборд и сценарий первого действия; алерт без сценария — это шум с шаблоном письма, и его место в корзине.

### Пробный прогон: проект проверяется за вечер

Собранный проект проверяется вечером без продакшена: сервис под нагрузкой из [урока про стресс-тест](/courses/prometheus/lessons/20-stress-test/) показывает все семь панелей живыми линиями. Что должно измениться на глазах: RPS повторяет профиль генератора, доля ошибок растёт на специально сломанном хендлере, p99 отзывается на замедленную зависимость, inflight поднимается до потолка пула, заказы и средний чек ползут по сценарию генератора, горутины возвращаются к базовой линии после снятия нагрузки. Панель без движения — это либо битое имя серии, либо лейбл, который никогда не пишется; обе находки стоят десяти минут прогоне, потому что в проде они стоят разбора. Прогон заодно калибрует бакеты гистограммы: если весь рост собрался в верхнем бакете — границы переехали, и список стоит пересмотреть до выката. После такого вечера дашборд встречает первый настоящий инцидент с живыми, а не пустыми приборами.

> **Типичная ошибка: алерт на каждый график.** Проект закончен, и на каждую из семи панелей вешается правило с порогом — через месяц дежурный выключает канал: сорок уведомлений за неделю, из них три требовали действий. Порог на память без сценария действия сработает на каждом релизе с большим кэшем, алерт на inflight — на каждом пике трафика, который сервис честно переварил. Правило прежнее: правило существует, только когда названо действие, которое человек обязан сделать по нему. Три алерта первого дня — потолок, а не стартовый минимум.

### Проверка: лейблы HTTP-метрик

**Вопрос:** Почему в счётчике Requests проекта лейбл route берётся из RoutePattern, а не из сырого пути?

- ✅ Сырой путь с идентификаторами размножает серии без предела
- Сырой путь короче и не помещается в лейбл
- RoutePattern кэшируется и работает быстрее
- Лейбл route обязателен по формату Prometheus

Маршрут /api/orders/41701 в лейбле создаёт серию на каждый заказ — та же катастрофа кардинальности, что userID из семнадцатого урока. Нормализованный шаблон роутера держит словарь значений коротким: число серий ограничено числом маршрутов кода, а не числом заказов.

### Проверка: две линии памяти

**Вопрос:** RSS процесса растёт вторые сутки, go_memstats_alloc_bytes стоит на месте. Первая гипотеза?

- ✅ Буферы и невозвращённые страницы: куча в порядке, процесс раздал память ОС
- Утечка живых объектов: куча растёт
- Сборщик мусора остановился
- Метрика alloc не скрейпится

Живые объекты показывал бы alloc — он ровный. RSS растёт при ровном alloc из-за буферов, фрагментации и страниц, которые рантайм не вернул системе: это поведение из урока про горутины и память, и гипотеза проверяется профилем аллокаций, а не новым рестартом.

### Сколько серий у Requests

```go
// маршруты: /api/orders, /api/health
// методы:   GET, POST
// статусы:  200, 409, 500
// все комбинации реально встречались
```

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

- ✅ 8 серий: произведение лейблов 2 на 2 на 3
- 12 серий: маршруты умножаются на статусы дважды
- 3 серии: по одной на статус
- Зависит от scrape interval

Серия рождается на каждую встреченную комбинацию значений лейблов: два маршрута, два метода, три статуса — восемь. Максимальный размер словаря считается произведением и проверяется против бюджета кардинальности до релиза; scrape interval влияет на сэмплы в сутки, но не на число серий.

### Соберите середину middleware

Допишите датчик, наблюдение и запись запроса.

```go
Inflight.WithLabelValues(route).___()
defer Inflight.WithLabelValues(route).Dec()

Duration.WithLabelValues(route, r.Method).___(time.Since(start).Seconds())
Requests.___(route, r.Method, "200").Inc()
```

Подсказка: Gauge увеличивается через Inc, гистограмма принимает наблюдение через Observe, а у Vec сначала выбирается набор лейблов.

Правильные ответы в пропусках: `Inc`, `Observe`, `WithLabelValues`

### Проект за один день

Расставьте шаги внедрения проекта.

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

1. `Завести пакет metrics с четырьмя метриками и своим реестром`
2. `Подключить middleware в chi до логгера и аутентификации`
3. `Добавить бизнес-инкременты в точку оформления заказа`
4. `Записать job в prometheus.yml и добиться зелёного up`
5. `Собрать дашборд из семи панелей и проверить на нагрузке`
6. `Через неделю наблюдения включить три алерта с сценариями действий`

### Карточки: итоговый проект

- **metrics.go** — Один пакет метрик: Requests, Duration, Inflight, Orders, OrderAmount. Свой реестр из урока про Registry держит имена и изоляцию тестов.
- **Inflight** — Gauge обработчиков в работе: Inc на входе, Dec через defer. Насыщение обработки при ровном трафике — сигнал насыщения.
- **Средний чек** — Деление rate(_sum) на rate(_count) гистограммы суммы. Gauge со средним лжёт после рестарта — обнуление и пила.
- **RSS против alloc** — process_resident_memory_bytes против go_memstats_alloc_bytes: расхождение линий отделяет буферы от утечки живых объектов.
- **Watchdog** — Правило, которое горит всегда: тишину ловит внешний канал. Обнаруживает пропажу мониторинга и целых сервисов.
- **Семь панелей** — RPS, доля ошибок, p50 и p99, inflight, заказы, средний чек, память. Первый экран дежурного отвечает за тридцать секунд.

> **Проверка на локальном Go.** Песочница сайта знает только fmt, strings, strconv, math, sort, errors и time — client_golang и chi собираются локально. Чек-лист сборки: metrics.go компилируется, middleware ставится до логгера, curl по /metrics показывает shop_-метрики, конфиг из двадцать третьего урока даёт зелёный up, и на каждую панель дашборда есть живая линия — пустая панель означает битое имя метрики.

Проект замыкает курс, и каждая строка metrics.go имеет адрес происхождения: счётчик — из [урока про Counter](/courses/prometheus/lessons/04-counter/), гистограмма и бакеты — из [седьмого](/courses/prometheus/lessons/07-histogram/), лейблы и их словарь — из [шестого](/courses/prometheus/lessons/06-labels/), HTTP-обёртки — из [девятого](/courses/prometheus/lessons/09-http-metriki/), бизнес-счётчики — из [десятого](/courses/prometheus/lessons/10-biznes-metriki/), запросы панелей — из [урока про PromQL](/courses/prometheus/lessons/12-promql-basics/). Дашборд собран по [тринадцатому уроку](/courses/prometheus/lessons/13-grafana-obzor/), алерты — по [четырнадцатому](/courses/prometheus/lessons/14-alerts-obzor/), порядок внедрения — по [двадцать четвёртому](/courses/prometheus/lessons/24-ekspluataciya/), а логи с теми же лейблами — из [урока про корреляцию](/courses/prometheus/lessons/21-logi-i-metriki/). Весь маршрут курса — на [странице Prometheus](/courses/prometheus/): от молчащего сервиса до дежурства, которое отвечает за тридцать секунд.

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

- Проект живёт в одном пакете 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 живут на втором экране с переменной, а релизные сравнения — на третьем. Дашборд — интерфейс дежурного, и его сдержанность — часть мониторинга.
