Содержание урока · 10 разделов
- Бизнес метрики go: заказы и оплаты
- Метрики заказа: где ставить Inc и что помнить о рестартах
- Деньги после рестарта: что видит график, а что нет
- Четыре типа на службе у продукта
- Алерты на бизнес-события
- Инцидент: алерт «заказов нет» в четыре утра
- Вариация: продукт просит срез, а не новую метрику
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Бизнес метрики go: заказы и оплаты#
Зелёный дашборд с латентностью — и ноль заказов за час: так выглядит сломанный платёжный провайдер при совершенно здоровом приложении. Технические метрики отвечают на вопрос «жив ли сервис», бизнес-метрики — «работает ли бизнес»; второе при первом не гарантировано. Разница в устройстве небольшая, в последствиях большая: на заказы смотрят продукт и аналитики, от них считают конверсию и выручку, по ним ставят самые дорогие алерты — «заказы упали до нуля» будильник громче любого CPU. Приятный бонус: те же четыре типа метрик из второго урока, тот же client_golang, только имена и лейблы теперь из языка предметной области. Осторожность нужна в двух местах. Первое — семантика: бизнес-числа любят сверять с базой, поэтому у каждой метрики должно быть ясное определение, что именно посчитано. Второе — кардинальность: искушение положить в лейбл id заказа или пользователя здесь даже сильнее, чем в HTTP-метриках, и разбирать её будем ещё раз в конце.
Счётчик заказов почти всегда вектор: платят картой и наличкой, подписками и разовыми покупками — лейбл payment_type с двумя-четырьмя значениями ничего не стоит и открывает срезы для продукта. Имя строится от домена: shop_orders_total, shop_revenue_rubles_total — префикс сервиса, сущность, единица измерения в имени. Выручка — тоже Counter: деньги суммируются и только растут, Add(amount) ложится идеально; единицу кладут прямо в имя, чтобы рубли не спутать с копейками и центами. Средний чек отдельной метрикой не заводят: это частное rate(revenue) на rate(orders) в PromQL, и производная в коде создала бы вторую правду об одном числе. Инкременты живут в сервисном слое, сразу после успешного завершения бизнес-операции: не в HTTP-хендлере, который может не знать исхода транзакции, и не до записи в базу. Порядок «сначала коммит, потом Inc» — половина всех драм этой темы, и ниже у него отдельный разбор.
Заказы и выручка
GOpackage main
import (
"github.com/prometheus/client_golang/prometheus"
)
var ordersTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "shop_orders_total",
Help: "Оформленные заказы, попытки без ретраев",
},
[]string{"payment_type"},
)
var revenueRubles = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "shop_revenue_rubles_total",
Help: "Оплаченная выручка в рублях",
},
)
func orderCompleted(o Order) {
ordersTotal.WithLabelValues(o.PaymentType).Inc()
revenueRubles.Add(o.Amount)
}Два счётчика: вектор по способу оплаты и плоский счётчик денег. В Help записана семантика — оформленные заказы без ретраев: через полгода никто не помнит, считает ли метрика попытки или события, и подпись спасает разборы. Add принимает сумму одним вызовом; инкременты живут в orderCompleted — там, где бизнес-операция фактически завершена.
Метрики заказа: где ставить Inc и что помнить о рестартах#
Классическая ловушка: инкремент стоит до записи в базу, сетевая ошибка уводит обработчик в ретрай — счётчик прибавил два, заказ в базе один. Правильный порядок только один: успешный коммит, затем Inc; упавшая попытка счётчик не трогает. Реверс тоже правда: выручку из метрик никогда не сверяют с бухгалтерией — это инструмент тренда и алертов, источник истины живёт в базе. Второе обязательное знание: счётчики живут в памяти процесса и после рестарта обнуляются. Prometheus компенсирует сброс в rate, но «выручка с начала суток» на графике будет прыгать после каждого деплоя — абсолютные суммы считают в базе, а метрика отвечает на «как сейчас относительно обычно»: rate за окно, сравнение с прошлой неделей, дельта после релиза. Третье: очередь платежей и их длительность — не счётчики. Число платежей в обработке меняется в обе стороны, это Gauge; время проведения платежа — распределение, это Histogram с сеткой под таймауты провайдера.
Деньги после рестарта: что видит график, а что нет#
Ошибка интерпретации, из-за которой спорят с бухгалтерией. Руководитель смотрит график выручки за сутки: после обеденного деплоя линия резко упала и пошла ниже — «мы потеряли деньги». Реальность: рестарт обнулил счётчик в памяти; скорость rate компенсирует, но наивный график накопленной суммы рисует обрыв, и всё, что было до обеда, из суммы за сутки исчезло. Отсюда два чтения одного графика: прирост за окно честен всегда, абсолютный накопленный уровень после рестарта — нет. Практическое следствие: суммы за период считает база, метрика отвечает за динамику относительно нормы; дельта «сегодня против прошлой среды» корректна, а «выручка с начала месяца» в Prometheus не существует. Когда такой вопрос возникает регулярно, это сигнал завести отчётную систему рядом, а не изобретать изощрённые запросы: у счётчика память заканчивается на рестарте, и это не исправить ничем.
Очередь платежей и их длительность
GOvar pendingPayments = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "shop_payments_pending",
Help: "Платежи в обработке прямо сейчас",
},
)
var paymentDuration = prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "shop_payment_duration_seconds",
Help: "Время проведения платежа",
Buckets: []float64{0.1, 0.5, 1, 5, 15, 60},
},
)
func paymentStarted() { pendingPayments.Inc() }
func paymentDone(seconds float64) {
pendingPayments.Dec()
paymentDuration.Observe(seconds)
}Gauge растёт и убывает: Inc при старте платежа, Dec при завершении — дисбаланс этих вызовов и есть утечка значения, о которой ниже. Сетка гистограммы построена под таймауты платёжного провайдера: границы до минуты, потому что попытка дольше минуты обычно и есть провал, и p99 здесь важнее средней.
Конверсия и выручка — это запросы, не метрики
GO# доля заказов от просмотров товара: конверсия за окно 5 минут
rate(shop_orders_total[5m]) / rate(shop_product_views_total[5m])
# выручка в час
rate(shop_revenue_rubles_total[5m]) * 3600
# платежи, которые висят прямо сейчас
shop_payments_pendingКонверсия и выручка считаются запросами, а не метриками: в коде живут только первичные события. Деление rate на rate даёт долю — по отдельным payment_type сработает без изменений, потому что лейблы совпадают. Умножение на 3600 переводит секунды в час; это единственная арифметика над деньгами в уроке, и она в PromQL, а не в Go.
Двойной подсчёт коварен слоями: HTTP-клиент повторил запрос сам, балансировщик отправил на второй инстанс, очередь доставила сообщение дважды — а метрика честно посчитала каждую попытку. Противоядие в коде: Inc строго после коммита, идемпотентные операции с ключом заказа и подписанная семантика в Help. Если ретраи неизбежны, заведите отдельный счётчик попыток — он покажет их долю, не трогая число событий.
Counter принимает float64, и копейки в рублях после миллионов сложений накопят плавающую погрешность. Рабочая схема: считать в минимальных единицах — копейках, центах — целым числом и передавать Add(float64(kopecks)), либо держать рубли и помнить, что точность метрики — до тренда, не до баланса. Точный рубль всё равно живёт в базе: метрика для наблюдения, не для кассы.
Четыре типа на службе у продукта#
| Что меряем | Тип | Метрика и срезы |
|---|---|---|
| Оформленные заказы | CounterVec | shop_orders_total{payment_type} |
| Выручка | Counter | shop_revenue_rubles_total, Add(amount) после коммита |
| Платежи в обработке | Gauge | shop_payments_pending, Inc и Dec парами |
| Время проведения платежа | Histogram | shop_payment_duration_seconds, сетка до 60 секунд |
Возвраты — обязательная пара к счётчику выручки. Отрицательным Add мерить их нельзя — контракт Counter, — поэтому заводят shop_refunds_rubles_total и считают нетто-выручку запросом: rate выручки минус rate возвратов. Бонус схемы — доля возвратов, которую продукт любит больше выручки: деление двух rate, ноль строчек нового кода и отдельная линия на дашборде.
Алерты на бизнес-события#
Бизнес-метрики дают алерты, которых не бывает у технических. Первый — тишина: заказов нет. Если днём rate(shop_orders_total[5m]) стабильно не ноль, а упал до нуля, приложение может быть здоровым — сломался провайдер, кончились лимиты, отвязался платёжный вебхук; такой алерт ставят с учётом времени суток, ночью пороги другие. Второй — аномалия величины: выручка в час заметно ниже скользящей недели при живом трафике — растёт доля отмен или обесценился средний чек. Третий — застревание: shop_payments_pending растёт и не убывает десятки минут, значит, платежи висят, и скоро это увидит поддержка через жалобы. Правило баланса: алерты на бизнес-события дороже технических и ложно срабатывают чаще — ставьте их на события с реальными деньгами и не заводите на каждое колебание, иначе дежурный научится их игнорировать, а это хуже, чем их отсутствие.
Инцидент: алерт «заказов нет» в четыре утра#
Инцидент из разряда «сам себя наказал». На витрине заказов поставили алерт: rate(shop_orders_total[5m]) ниже порога — сообщение в чат дежурному. В четыре утра воскресенья витрина законно пустела, алерт разбудил человека, человек отключил канал, а через две недели настоящая поломка платёжного провайдера прошла незамеченной — ровно потому, что канал уже никто не читал. Ошибка не в метрике и не в пороге — в отсутствии сезонности: у бизнеса есть суточный и недельный ритм, и порог, честный для вторника днём, лжёт ночью. Рабочая схема: сравнение с тем же часом прошлой недели, а не с абсолютным нулём; отдельный тихий алерт на полное отсутствие событий через absent; и правило: каждое ложное срабатывание либо чинится, либо алерт гасится вовсе — молча игнорируемый алерт хуже его отсутствия.
Вариация: продукт просит срез, а не новую метрику#
Вариация примера, которая экономит месяцы: прежде чем заводить новую метрику, проверьте, что срез не получается из существующей. Продукт спрашивает «сколько заказов с доставкой в регионы» — если лейбл delivery_region существовал с первого дня, ответом будет суммирование rate по нему; если нет — метрика даётся дорого: релиз, накопление истории заново, тихие графики. Отсюда практика первого дня: сядьте с аналитиком и выпишите срезы, которые точно спросят — способ оплаты, регион, платформа клиента, — и заведите их лейблами сразу. Спорная граница: не больше пяти-шести лейблов на метрику и только с малым числом значений, иначе кардинальность съест выигрыш. Список срезов живёт рядом с кодом метрик и пересматривается раз в квартал: срез, который никто не открывал за квартал, — кандидат на удаление.
Проверка: тип для выручки
Накопленная выручка за всё время — какой тип метрики?
Проверка: рестарт процесса
Что случится с shop_orders_total после рестарта процесса?
Чему равен shop_payments_pending?
Прочитайте код и выберите, что он выведет:
paymentStarted() // Inc
paymentStarted() // Inc
paymentDone(0.4) // Dec
paymentStarted() // IncСоберите вектор заказов
Допишите создание вектора, имя лейбла способа оплаты и инкремент ребёнка.
Соберите счётчик выручки
Расставьте строки объявления счётчика выручки и добавления суммы после коммита.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: бизнес-метрики
Бизнес-слои готовы: заказы, деньги, очередь и длительность платежей измеряются, конверсия считается запросом. Дальше курс разворачивается внутрь процесса: в следующем уроке разбираем встроенные метрики Go-приложения — горутины, память и сборку мусора, которые уже лежат в вашем /metrics бесплатно. Предыдущий шаг темы — метрики HTTP из девятого урока: бизнес-счётчики обычно ставят рядом с ними, в том же сервисном слое. Если в лейблы бизнес-метрик тянет положить id заказа или пользователя — перечитайте урок про кардинальность до деплоя, а не после. Полный план — на странице курса Prometheus. И финальная оговорка о среде: client_golang исполняется локальным Go, песочница сайта его не запустит; соберите сервис, сделайте несколько тестовых заказов и убедитесь curl-ом, что инкременты встали после коммита, а не до него.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Бизнес-метрики — те же Counter, Gauge и Histogram, но имена и лейблы из языка домена: shop_orders_total, payment_type.
- Выручка — Counter с Add(amount); средний чек и конверсия считаются в PromQL, не в коде.
- Inc строго после успешного коммита: иначе ретрай удваивает заказ.
- Счётчики живут до рестарта: тренды и алерты — в метриках, отчёты и сверки — в базе.
- Очередь платежей — Gauge с парными Inc и Dec; время платежа — Histogram с сеткой под провайдера.
- order_id и user_id в лейблы не идут: кардинальность убьёт Prometheus быстрее, чем вы заметите.
Домашнее задание
Добавьте в учебный сервис вектор заказов по payment_type и счётчик выручки; инкремент поставьте строго после «коммита» — заглушки достаточно. Сделайте несколько тестовых заказов, включая один с проваленной первой попыткой, и проверьте curl-ом, что счётчик прибавил ровно один заказ. Затем перезапустите процесс и посмотрите, как обнулились счётчики и что это значит для графика. Нужен локальный Go.
Личные заметкиПусто
Вопросы и ответы по уроку
Стоит ли писать в метрики сумму заказа и его id?
Сумма — да, в Counter через Add: выручка суммируется по определению. Id — никогда: кардинальность растёт с каждым заказом. Разовые крупные суммы и разборы отдельных заказов — дело базы, а не Prometheus.
Кто должен инкрементить бизнес-счётчик: хендлер или сервис?
Слой, где бизнес-операция фактически завершена: после коммита транзакции или подтверждения провайдера. Хендлер может не знать исхода, а воркер очереди — знает; инкремент живёт рядом с коммитом, а не рядом с входом запроса.
Как алертить на падение заказов?
Порогом по rate с учётом времени суток: днём rate(shop_orders_total[5m]) ниже обычного — сигнал. Для полной тишины полезен absent по метрике; точные правила и Alertmanager разбираются позже в курсе.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
WebSocket в Go: real-time чаты и уведомления
Курс WebSocket на Go: gorilla/websocket, эхо-сервер, hub-паттерн, комнаты, авторизация соединений, ping/pong, reconnect и масштабирование. 25 уроков.
- Поднимать WS-сервер и апгрейдить HTTP-соединения
- Писать readPump/writePump без гонок на каждое соединение
- Строить hub: регистрацию, рассылку и комнаты
- Авторизовывать соединения и ограничивать сообщения