Содержание урока · 10 разделов
- prometheus namespace: префикс, который экономит grep
- Свой Registry в Go: prometheus.NewRegistry
- Изоляция тестов: свой реестр в go test
- Дисциплина имён: словарь префиксов сервиса
- Из чего собирается имя метрики
- Спорное мнение: дефолтный реестр — не зло
- Переезд с дефолтного реестра на свой
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Сервис стартует на стенде после внедрения общей библиотеки оплаты — и падает на первой же секунде с сообщением duplicate metrics collector registration attempted. Паника честная: и библиотека, и приложение зарегистрировали счётчик с именем payments_total через MustRegister, а два коллектора с одним именем в одном реестре запрещены. До этой встречи многие месяцы живут, не задумываясь о реестре: promhttp.Handler() из третьего урока обслуживает дефолтный глобальный регистр, и всё кажется бесплатным. Но глобальное состояние — то, о чём библиотека не имеет права забывать: она не знает, кто ещё живёт в том же процессе. В этом уроке разберём, из чего собирается имя метрики, как завести собственный реестр и когда это оправдано, и как тесты перестают видеть чужие метрики.
prometheus namespace: префикс, который экономит grep#
Итоговое имя метрики собирается из трёх частей: Namespace, Subsystem и Name, склеенных подчёркиваниями — пустые части пропускаются. В GaugeOpts и CounterOpts поля называются именно так, и это единственный легальный способ получить префикс: имя не склеивают конкатенацией строк, потому что Help и настройки принадлежат метрике целиком. Зачем префикс: в /metrics смешиваются go_-метрики рантайма, process_-метрики процесса, метрики библиотек и ваши собственные, и без доменного префикса grep по нужной группе превращается в чтение сотен строк. Префикс службы — скажем, shop_ — сразу отделяет метрики домена от чужих, а второй уровень subsystem_ группирует их внутри: shop_orders_queue_depth читается как маршрут к владельцу кода. Тот же словарь используется в PromQL из двенадцатого урока и в переменных дашборда: грамотный префикс экономит минуты на каждом разборе. Словарь живёт рядом с кодом метрик: две строчки в комментарии пакета — какой префикс у домена и какие суффиксы единиц — экономят час обсуждения на каждом код-ревью.
Имя из трёх частей
GOqueueDepth := prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "shop",
Subsystem: "orders",
Name: "queue_depth",
Help: "Глубина очереди заказов на обработку",
})
// Итоговое имя: shop_orders_queue_depth
paidTotal := prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "shop",
Subsystem: "payments",
Name: "paid_total",
Help: "Успешные оплаты",
})
// Итоговое имя: shop_payments_paid_totalЧасти склеиваются подчёркиваниями, и по имени сразу видно владельца: shop_orders — модуль заказов, shop_payments — модуль платежей. Единицы держат в Name по конвенции: _total для счётчиков, _seconds и _bytes для наблюдаемых величин. Help обязан быть у каждой метрики — он виден в /metrics и в подсказках дашборда.
Свой Registry в Go: prometheus.NewRegistry#
Реестр — это словарь коллекторов, и в client_golang он реализован интерфейсами Registerer и Gatherer. Дефолтный реестр глобален: DefaultRegisterer принимает всё, что зарегистрировали через prometheus.MustRegister, а promhttp.Handler() собирает метрики из DefaultGatherer. Свой реестр создаётся одной строкой — prometheus.NewRegistry() — и живёт по тем же правилам: MustRegister добавляет коллекторы и паникует на дубликате, Register возвращает ошибку вместо паники. Два следствия, о которых забывают. Первое: в новом реестре нет ничего, включая go_- и process_-метрики, — их вносит вручную NewGoCollector() и NewProcessCollector(), если они нужны. Второе: promhttp.Handler() дефолтный реестр не увидит — на HTTP выставляют HandlerFor(reg, promhttp.HandlerOpts{}). Для сокращения записи есть promauto.With(reg): конструкторы сразу регистрируют метрику в вашем реестре и возвращают готовый объект.
Свой реестр и экспозиция через HandlerFor
GOreg := prometheus.NewRegistry()
reg.MustRegister(
collectionDuration,
queueDepth,
paidTotal,
prometheus.NewGoCollector(), // go_* — по желанию
prometheus.NewProcessCollector( // process_* — по желанию
prometheus.ProcessCollectorOpts{}),
)
http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))MustRegister принимает список коллекторов: реестр один, паника на дубликате сработает только при настоящем конфликте имён внутри него. HandlerFor привязывает HTTP-хендлер к конкретному реестру, поэтому в /metrics попадает ровно то, что зарегистрировано здесь, — поведение, которое дефолтный глобальный регистр не гарантирует.
Изоляция тестов: свой реестр в go test#
Тесты метрик на дефолтном реестре ломаются двумя способами: значения накапливаются между тестами, потому что глобальный регистр один на процесс, и параллельные пакеты паникуют на дубликатах имён. Лечение — новый реестр на каждый тест: конструктор приложения получает его параметром, тест создаёт свежий, регистрирует свои метрики и проверяет значения через reg.Gather(). Gather возвращает срез семейств метрик в protobuf-структурах — из него берут значение нужного счётчика и сравнивают с ожиданием; это тот же интерфейс, которым Prometheus собирает данные по сети, только без HTTP. Для строгой проверки есть NewPedanticRegistry(): он дополнительно проверяет согласованность коллектора — тот же набор серий в Describe и Collect, что ловит расхождение декларации и реальности, о котором восемнадцатый урок писал применительно к своим коллекторам; pedantic-режим включают в CI на пакете метрик, где цена расхождения выше скорости. Бюджет прост: реестр в тесте — одна строка, а расхождение значений между тестами ищется час; Unregister существует, но в тестах им пользуются редко — новый реестр дешевле и чище, чем разрегистрация половины коллекторов.
Библиотека в init-функции регистрирует свои метрики через prometheus.MustRegister — и теперь два инстанса её клиента в одном процессе невозможны: второй MustRegister паникует при импорте. Чинить это на стороне приложения дорого, потому что init выполняется до main. Правило для библиотек: глобальное состояние не трогать — либо заводить собственный реестр и отдавать хендлер, либо принимать Registerer параметром конструктора. Приложение возвращает долг: передаёт реестр явно, а не надеется, что библиотека догадается.
Дисциплина имён: словарь префиксов сервиса#
Namespace работает, когда словарь префиксов один на команду. Верхний уровень — домен сервиса: shop_, billing_, notifications_; он же совпадает с job_name в конфиге скрейпа из следующего урока, и по графику сразу ясно, чей это сервис. Второй уровень — модуль: orders_, payments_, auth_. Дальше — имя с единицей по конвенции Prometheus: счётчики заканчиваются на _total, латентности на _seconds, размеры на _bytes, а базовые единицы не множатся: миллисекунды не пишут в имя, значение переводят в секунды. Лейблы не дублируют имя: route у счётчика запросов — да, но node или env в лейбле сервиса обычно избыточны, это та же кардинальная дисциплина, что в уроке семнадцатом. Практическая проверка словаря — тест на пять секунд: по одному имени метрики в /metrics должно быть видно, какой модуль отвечает, и в каких единицах значение.
Из чего собирается имя метрики#
| Часть | Что несёт | Пример |
|---|---|---|
| Namespace | домен или имя сервиса | shop |
| Subsystem | модуль внутри домена | orders |
| Name | суть и единица измерения | queue_depth |
| Итоговое имя | склейка подчёркиваниями | shop_orders_queue_depth |
| _total | суффикс счётчика по конвенции | shop_payments_paid_total |
| go_, process_ | метрики рантайма и процесса | go_goroutines |
Спорное мнение: дефолтный реестр — не зло#
Спорная часть урока: для монолита с одним приложением свой реестр часто лишний. Дефолтный регистр даёт ноль строк конфигурации, promhttp.Handler() работает из коробки, и пока в процессе живёт только ваш код, риск дубликата остаётся штатной паникой при старте — шумной, но честной: она падает в момент запуска, а не в разгар дня. Граница проходит там, где появляется второй владелец метрик: общая библиотека, встраиваемый модуль, несколько инстансов одного клиента, тесты. Тогда глобальное состояние перестаёт принадлежать вам, и свой реестр становится способом явной передачи владения. Отсюда практика, которая не спорит ни с одной стороной: приложению можно жить на дефолте, но каждый публичный пакет обязан принимать Registerer параметром — тогда переезд на свой реестр занимает одну строку в конструкторе, а не переписывание половины зависимостей.
Переезд с дефолтного реестра на свой#
Переезд занимает один коммит, если приложение собирало метрики через публичные конструкторы. Шаги: создать реестр, заменить prometheus.MustRegister на reg.MustRegister для всех объявлений, поменять promhttp.Handler() на HandlerFor(reg, ...) и прогнать тесты — они ловят глобальное состояние первым. Затем инвентаризация: curl по старому хендлеру показывает пустоту, и это правильно — метрики переехали, а не пропали; убедитесь зелёным up на новом пути из конфига скрейпа. Две ловушки переезда стоят отдельного абзаца. Первая — библиотеки, написанные в глобальный регистр: их вызовы продолжают складывать метрики в DefaultRegisterer, и на дашборде возникает расщепление половин; лечится параметром Registerer в конструкторе, как в warn-врезке выше. Вторая — забыть о go_-коллекторе: новый реестр пуст, и привычные go_goroutines исчезают, пока NewGoCollector не внесён вручную. Обе ловушки видны на первой странице /metrics, поэтому переезд проверяется глазами, а не только зелёными тестами.
Проверка: итоговое имя
Namespace: shop, Subsystem: payments, Name: failed_total. Какое имя окажется в /metrics?
Проверка: дубликат регистрации
Что произойдёт при повторном MustRegister того же коллектора?
Что увидит curl в свой реестр
Прочитайте код и выберите, что он выведет:
reg := prometheus.NewRegistry()
reg.MustRegister(ordersTotal)
http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
// curl localhost:8080/metricsСоберите свой реестр
Создайте реестр, зарегистрируйте метрику и выставьте хендлер.
Путь метрики через свой реестр
Расставьте шаги от объявления до экспозиции.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: реестры и имена
Песочница сайта исполняет только fmt, strings, strconv, math, sort, errors и time — client_golang требует локального Go. Соберите сервис из третьего урока с собственным реестром, добавьте тест с reg.Gather() и попробуйте зарегистрировать два коллектора с одним именем: паника при старте покажет, зачем нужен словарь префиксов.
Реестр и namespace замыкают инженерную часть курса: метрики объявлены, названы и изолированы. Счётчики, на которых проверялись имена, выросли из урока про Counter, лейблы — из шестого, а связка с логами осталась в предыдущем уроке. Дальше — prometheus.yml и скрейпинг: Prometheus ищет ваш /metrics по job_name, который логично сделать равным namespace из этого урока. Когда метрик станет много, урок про кардинальность напомнит про бюджет серий, а весь маршрут — на странице Prometheus. Домашняя проверка словаря проста: откройте /metrics своего проекта, разбейте имена по префиксам и проверьте, что по каждому виден владелец и единица измерения — если нет, словарь пора дополнять до следующих релизов.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Имя метрики собирается из Namespace, Subsystem и Name: префикс домена и модуля вместо конкатенации строк.
- Свой реестр — prometheus.NewRegistry: пуст на старте, go_- и process_-коллекторы вносятся вручную.
- Экспозиция своего реестра — promhttp.HandlerFor, дефолтный глобальный регистр при этом не участвует.
- Дубликат имени в реестре — паника при старте: fail fast, который честнее молчаливой замены.
- Библиотека не пишет в глобальное состояние: свой реестр или Registerer в параметре конструктора.
- В тестах свой реестр на каждый тест и проверка значений через reg.Gather().
- Словарь префиксов: домен, модуль, имя с единицей; job_name в конфиге скрейпа совпадает с namespace.
Домашнее задание
Переведите учебный сервис на собственный реестр: NewRegistry, MustRegister для трёх своих метрик с Namespace и Subsystem, HandlerFor на /metrics и ручная регистрация Go-коллектора. Проверьте curl-ом, что go_-метрики появились только после явной регистрации. Напишите тест с отдельным реестром на каждый случай и проверьте значение счётчика через reg.Gather(). Затем добавьте в проект библиотечный пакет, принимающий Registerer параметром, и убедитесь, что два инстанса пакета не паникуют.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем Namespace отличается от обычного префикса в Name?
Технически — ничем: итоговое имя одинаково. Разница в структуре кода: Namespace и Subsystem задают уровень владельца один раз в опциях, а конкатенация строк расползается по вызовам и забывается при рефакторинге. Плюс Help и настройки принадлежат метрике целиком, и по опциям видно, кто отвечает за имя.
Нужен ли свой реестр в тестах, если тесты не проверяют значения метрик?
Если метрики регистрируются в конструкторе приложения, тесты всё равно создают инстансы — и на глобальном реестре второй тест с тем же набором имён упадёт. Свой реестр на тест снимает и эту проблему, и накопление значений между тестами. Даже без проверок значений это одна строка в setup-функции.
Можно ли зарегистрировать одну метрику в два реестра?
Можно: коллектор не привязан к реестру жёстко, Registerer — это интерфейс. Но метрика с общим состоянием в двух реестрах соберёт значения вместе, и каждый реестр покажет одну и ту же серию. Если нужна изоляция — два инстанса метрики, по одному на реестр, и лейбл для разделения, а не общая метрика в двух местах.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
WebSocket в Go: real-time чаты и уведомления
Курс WebSocket на Go: gorilla/websocket, эхо-сервер, hub-паттерн, комнаты, авторизация соединений, ping/pong, reconnect и масштабирование. 25 уроков.
- Поднимать WS-сервер и апгрейдить HTTP-соединения
- Писать readPump/writePump без гонок на каждое соединение
- Строить hub: регистрацию, рассылку и комнаты
- Авторизовывать соединения и ограничивать сообщения