---
title: "Бизнес-метрики: заказы и оплаты"
description: "Бизнес метрики go на Prometheus: заказы и оплаты — CounterVec по payment_type, Gauge очереди платежей, конверсия в PromQL и двойной подсчёт на ретраях."
course: "Мониторинг Go с Prometheus: метрики, Grafana, алерты"
url: "https://golangmaster.ru/courses/prometheus/lessons/10-biznes-metriki/"
published: "2026-09-19"
updated: "2026-09-24"
minutes: 18
---

# Бизнес-метрики: заказы и оплаты

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

Бизнес метрики go-сервиса — заказы, выручка, очередь платежей — измеряются теми же Counter, Gauge и Histogram, что и технические, но зритель другой: продукт, а не дежурный. В уроке — shop_orders_total по payment_type, счётчик выручки, конверсия в PromQL и самая дорогая ошибка темы: двойной подсчёт на ретраях.

### Бизнес метрики 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» — половина всех драм этой темы, и ниже у него отдельный разбор.

### Заказы и выручка

```go
package 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 не существует. Когда такой вопрос возникает регулярно, это сигнал завести отчётную систему рядом, а не изобретать изощрённые запросы: у [счётчика](/courses/prometheus/lessons/04-counter/) память заканчивается на рестарте, и это не исправить ничем.

### Очередь платежей и их длительность

```go
var 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. Если ретраи неизбежны, заведите отдельный счётчик попыток — он покажет их долю, не трогая число событий.

> **Деньги и float64.** 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](/courses/prometheus/lessons/12-promql-basics/); и правило: каждое ложное срабатывание либо чинится, либо алерт гасится вовсе — молча игнорируемый алерт хуже его отсутствия.

### Вариация: продукт просит срез, а не новую метрику

Вариация примера, которая экономит месяцы: прежде чем заводить новую метрику, проверьте, что срез не получается из существующей. Продукт спрашивает «сколько заказов с доставкой в регионы» — если лейбл delivery_region существовал с первого дня, ответом будет суммирование rate по нему; если нет — метрика даётся дорого: релиз, накопление истории заново, тихие графики. Отсюда практика первого дня: сядьте с аналитиком и выпишите срезы, которые точно спросят — способ оплаты, регион, платформа клиента, — и заведите их лейблами сразу. Спорная граница: не больше пяти-шести лейблов на метрику и только с малым числом значений, иначе кардинальность съест выигрыш. Список срезов живёт рядом с кодом метрик и пересматривается раз в квартал: срез, который никто не открывал за квартал, — кандидат на удаление.

### Проверка: тип для выручки

**Вопрос:** Накопленная выручка за всё время — какой тип метрики?

- Gauge: значение меняется
- ✅ Counter: только растёт
- Histogram
- Summary

Деньги суммируются и не убывают — это контракт Counter. Возвраты заводят отдельной метрикой, а не отрицательным Add: счётчик после рестарта и так начинается с нуля, отрицательные значения сломали бы rate.

### Проверка: рестарт процесса

**Вопрос:** Что случится с shop_orders_total после рестарта процесса?

- Серии сохранятся, значения продолжатся с прежних
- ✅ Счётчики начнутся с нуля, а rate в Prometheus компенсирует сброс
- Метрика исчезнет до первого заказа
- Ничего: значения хранятся в базе

Счётчики живут в памяти процесса. rate видит уменьшение значения и считает его сбросом, добавляя последнее значение к дельте; но абсолютные суммы после рестарта врут — поэтому отчёты строят в базе.

### Чему равен shop_payments_pending?

```go
paymentStarted()   // Inc
paymentStarted()   // Inc
paymentDone(0.4)   // Dec
paymentStarted()   // Inc
```

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

- 1
- ✅ 2
- 3
- 0

Два Inc, один Dec, ещё один Inc: 1, 2, 1, 2. Дисбаланс Inc и Dec — классическая утечка Gauge: проваленный платёж, который покинул очередь без Dec, навсегда завышает значение и делает метрику бесполезной.

### Соберите вектор заказов

Допишите создание вектора, имя лейбла способа оплаты и инкремент ребёнка.

```go
var ordersTotal = prometheus.___(
        prometheus.CounterOpts{
                Name: "shop_orders_total",
                Help: "Оформленные заказы",
        },
        []string{"___"},
)

func orderCompleted(o Order) {
        ordersTotal.___(o.PaymentType).Inc()
}
```

Подсказка: Вектор создаёт NewCounterVec, лейбл способа оплаты называется payment_type, ребёнок достаётся через WithLabelValues.

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

### Соберите счётчик выручки

Расставьте строки объявления счётчика выручки и добавления суммы после коммита.

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

1. `var revenue = prometheus.NewCounter(prometheus.CounterOpts{`
2. `        Name: "shop_revenue_rubles_total", Help: "Оплаченная выручка",`
3. `})`
4. `func orderCompleted(amount float64) {`
5. `        revenue.Add(amount)`
6. `}`

### Карточки: бизнес-метрики

- **Бизнес-метрика** — Метрика из языка предметной области: заказы, выручка, очередь платежей. Читается продуктом, служит основой алертов на бизнес-события.
- **Выручка как Counter** — Деньги только суммируются: shop_revenue_rubles_total с Add(amount) после коммита. Единица в имени; точный рубль живёт в базе, метрика — для тренда.
- **Очередь платежей как Gauge** — shop_payments_pending: Inc на входе, Dec на выходе. Дисбаланс пары — утечка значения; алерт на рост без убывания ловит застрявшие платежи.
- **Двойной подсчёт** — Одно бизнес-событие посчитано метрикой дважды из-за ретрая, повтора очереди или повторной доставки. Противоядие: Inc после коммита и идемпотентность.
- **Источник истины** — База данных для отчётности и сверок; метрики — для трендов, дашбордов и алертов. После рестарта счётчики начинаются с нуля.
- **Конверсия в PromQL** — Деление rate двух счётчиков: rate(shop_orders_total[5m]) / rate(shop_product_views_total[5m]). Считается запросом, не кодом.

Бизнес-слои готовы: заказы, деньги, очередь и длительность платежей измеряются, конверсия считается запросом. Дальше курс разворачивается внутрь процесса: в следующем уроке разбираем встроенные метрики Go-приложения — горутины, память и сборку мусора, которые уже лежат в вашем /metrics бесплатно. Предыдущий шаг темы — метрики HTTP из девятого урока: бизнес-счётчики обычно ставят рядом с ними, в том же сервисном слое. Если в лейблы бизнес-метрик тянет положить id заказа или пользователя — перечитайте урок про кардинальность до деплоя, а не после. Полный план — на странице курса Prometheus. И финальная оговорка о среде: client_golang исполняется локальным Go, песочница сайта его не запустит; соберите сервис, сделайте несколько тестовых заказов и убедитесь curl-ом, что инкременты встали после коммита, а не до него.

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

- Бизнес-метрики — те же 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 разбираются позже в курсе.
