Учимся писать свой Prometheus exporter на Go с нуля. Часть 1
- четверг, 9 июля 2026 г. в 00:00:14
Чтобы измерять нагрузку, ошибки, задержки и потребление ресурсов, осознанно поддерживать SLO и настраивать алерты, не обойтись без Prometheus. Он принимает текстовый формат на HTTP-эндпоинте. Только вот Kafka, PostgreSQL, сетевое железо и Redis метрики по HTTP не отдают, либо понимают только JMX, SNMP и собственный REST API. Так что без переводчика не обойтись.
Всем привет! Я DevOps-разработчик из MTC Web Services. Этим материалом я открываю цикл из пяти постов, благодаря которому вы шаг за шагом напишете собственный Prometheus exporter с нуля на языке Go. Но просто написать код может и ИИ. Моя же цель — дать вам не только практическую, но и теоретическую основу, чтобы в будущем вы могли легко разработать свой exporter под любую задачу.
Если вдруг вам уже знакома теория и важнее практика — заглядывайте в оглавление за практикой. В качестве примера я выбрал kafka_exporter: он покрывает несколько типов источников, позволяет реализовать паттерн Custom Collector и его легко тестировать локально. А еще есть много популярных kafka_exporter, с которыми мы сможем сравнить наш результат. Код буду коммитить в репозиторий.

Я разделил курс на пять основных частей, вот основные блоки:
Часть 1. Основы Prometheus и каркас будущего exporter.
Часть 2. Настраиваем сбор метрик Kafka.
Часть 3. Делаем exporter production ready.
Часть 4. Тестирование, сборка и эксплуатация.
Часть 5. Exporter для любой задачи.
3. Краткие итоги
Прежде чем писать код, разберемся, как все работает и почему именно так. Рассмотрим общую модель.

Prometheus — это система мониторинга и база данных временных рядов (time-series database, TSDB). А если совсем по-простому, то:
Prometheus периодически ходит по HTTP к определенному списку сервисов, забирает у них текущие значения метрик и складывает их в свою базу с привязкой ко времени.
Ключевое здесь — это то, что он ходит сам, то есть работает по модели pull.
Есть два подхода к сбору метрик:
Push — приложение само отправляет метрики в центральный сервер, а сервер просто принимает данные (например, StatsD, Graphite).
Pull — центральный сервер сам периодически опрашивает приложения, а оно просто держит открытым HTTP-эндпоинт. В нашем случае exporter — это и есть тот HTTP-эндпоинт, который ждет, пока к нему придет Prometheus.
Почему pull-модель удобна для SRE/DevOps-инженеров, отвечающих за состояние инфраструктуры:
Prometheus сам решает, когда и как часто собирать те или иные метрики приложения (определяется интервал scrape).
Легко понять «живо» ли приложение, если scrape не удался — то цель down. Получаем дополнительную метрику up.
Относительная простота: приложение просто отдает текстовый документ по HTTP через метод GET.
Процесс одного опроса называется scrape. По умолчанию Prometheus делает scrape каждые 15–60 секунд, но это гибко настраивается.
Exporter — это «переводчик-посредник» между приложением и Prometheus. Вот схема работы exporter: Exporter делает три вещи:
1. Подключается к целевой системе ее родным способом. Например, для Kafka подключается по протоколу Kafka через библиотеку Sarama.
2. Получает у нее сырые данные. Например, список топиков, offset’s, число брокеров и другие.
3. Переводит их в формат Prometheus и отдает по HTTP на эндпоинт /metrics. Команда Prometheus зафиксировала его в своей конфигурации как default, а затем он стал де-факто стандартом всей экосистемы.
Наш будущий exporter — ровно такой посредник между Kafka и Prometheus.
В каких случаях необходим свой exporter? Когда для системы нет готового либо готовый не отдает нужные тебе метрики. Часто проще написать 200–300 строк своего кода, чем мучиться и разбираться с чужим.
Exporter отдает метрики в формате текста. Вот как, к примеру, выглядит ответ на GET /metrics для одного из существующих kafka_exporter:

Рассмотрим ближе:
# HELP kafka_brokers Number of Brokers in the Kafka Cluster. # TYPE kafka_brokers gauge kafka_brokers 3 # HELP kafka_topic_partitions Number of partitions for this Topic # TYPE kafka_topic_partitions gauge kafka_topic_partitions{topic="orders"} 6 kafka_topic_partitions{topic="payments"} 3
На этот формат мы будем опираться при написании кода, так что разберем его построчно:
# HELP <имя> <текст> — человекочитаемое описание метрик, нужно для понимания пользователям.
# TYPE <имя> <тип> — тип метрики (gauge, counter, …). Основные типы метрик, их рассмотрим позже.
<имя>{лейблы} <значение> — имя самой метрики, набор лейблов в {} (их мы тоже рассмотрим ниже) и непосредственно само числовое значение метрики.
Каждая строка с данными — это одна серия (time series). Например, kafka_topic_partitions{topic="orders"} и kafka_topic_partitions{topic="payments"} — это две разные серии одной метрики, которые различаются лейблом topic.
Конечно, этот текст не придется писать руками, его сгенерирует библиотека client_golang. Но понимать, что получается на выходе, важно, ведь мы еще не раз будем проверять работу нашего exporter командой curl по ходу его написания.
Prometheus знает четыре основных типа метрик:

Монотонно растущее значение, которое только увеличивается (или сбрасывается в 0 при рестарте). Примеры: число обработанных запросов, число ошибок, число отправленных байт.
http_requests_total{method="GET"} 1027
Само абсолютное значение counter не всегда бывает полезным. Смысл появляется, когда мы берем от него скорость роста функцией rate():
— rate(http_requests_total[5m]) — количество запросов в секунду за последние пять минут.
Naming convention: обратите внимание, что имя counter по-хорошему должно заканчиваться на _total.
Метрика, значение которого может расти и падать в любую сторону. Это «мгновенный снимок» системы. Примеры таких метрик: температура, использование памяти, число активных соединений, число брокеров и другие.
kafka_topic_partitions{topic="orders"} 6
Так как мы пишем свой exporter для Kafka, то для нас важно: почти все метрики Kafka — это gauge. Число брокеров, партиций, offset, lag — все это текущее состояние системы, которое мы измеряем в момент scrape. Поэтому в нашем exporter будут в основном метрики типа gauge.
Подойдет, когда важно распределение значений — например, латентность запросов. Histogram раскладывает наблюдения по «корзинам» (buckets): сколько запросов уложилось в 0.1с, 0.5с, 1с и так далее.
http_request_duration_seconds_bucket{le="0.1"} 243 http_request_duration_seconds_bucket{le="0.5"} 891 http_request_duration_seconds_bucket{le="1.0"} 1020 http_request_duration_seconds_bucket{le="+Inf"} 1024 http_request_duration_seconds_sum 476.3 http_request_duration_seconds_count 1024
le означает “less or equal”. Bucket’s кумулятивные — каждый включает все предыдущие. +Inf всегда равен общему числу наблюдений.
Из histogram можно считать перцентили через PromQL:
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
— p99 латентности за последние пять минут.
Naming convention: суффикс с единицей измерения (_seconds, bytes и другие). Автоматически добавляются bucket, sum, count.
Тоже считает перцентили, но вычисляет их на стороне клиента (в самом приложении), а не на стороне Prometheus.
rpc_duration_seconds{quantile="0.5"} 0.012 rpc_duration_seconds{quantile="0.9"} 0.034 rpc_duration_seconds{quantile="0.99"} 0.127 rpc_duration_seconds_sum 8953.1 rpc_duration_seconds_count 27143
Лейблы — это пары ключ="значение", которые «нарезают» одну метрику на множество серий:
kafka_consumergroup_lag{consumergroup="billing", topic="orders", partition="0"} 42 kafka_consumergroup_lag{consumergroup="billing", topic="orders", partition="1"} 17
Это одна метрика kafka_consumergroup_lag, но две серии — для партиций 0 и 1. В Prometheus потом можно делать запросы вроде:
— sum(kafka_consumergroup_lag) by (consumergroup) — суммарный lag по каждой группе;
— kafka_consumergroup_lag{topic="orders"} — lag только для топика orders.
Каждая уникальная комбинация лейблов является отдельной серией, которая порождает новую запись в памяти Prometheus. Поэтому, если лейбл может принимать много значений, например user_id, email или другие, аналогичные этим, будет генерироваться очень много серий, что потенциально может положить Prometheus. Это называется cardinality explosion.
Важно запомнить: лейблы подходят для значений с ограниченным, предсказуемым набором, например топик, партиция, имя группы, — их максимум тысячи. Не нужно использовать в качестве лейбла то, что никак не ограничено — ID-сообщения или offset как значение лейбла. Так что стоит держать в голове, что partition как лейбл — ок, а вот значение offset — это уже значение метрики.
У Prometheus строгие, но простые правила именования. Соблюдать их важно, чтобы ваши метрики не выбивались из общей экосистемы. Вот основные:
Формат: <namespace>_<subsystem>_<name>_<unit>. — namespace — префикс под систему, например, kafka. subsystem — подсистема, например broker. unit — единица измерения в базовых единицах: только секунды и байты. Получившееся название: kafka_topic_partition_current_offset.
snake_case только [a-zA-Z0-9_]. Никаких дефисов и точек.
Counter заканчивается на _total.
Единицы измерений в качестве суффикса: seconds, bytes, _ratio.
Метрика должна иметь один смысл — это значит, что одна метрика измеряет ровно одну конкретную вещь и по ее названию сразу понятно, что именно.
Рассмотрим общую схему доставки метрик на примере Kafka:

В нашем курсе мы настроим exporter для сбора метрик.
Основные принципы exporter: exporter не хранит метрики во времени, историю хранит сам Prometheus. Exporter отдает текущие значения метрик, в момент scrape Prometheus. То есть exporter собирает свежие данные на каждый новый запрос к
/metrics.
Prometheus работает по pull-модели: сам ходит к exporter на /metrics.
Exporter — это переводчик из родного протокола системы в формат Prometheus.
Формат метрик — простой текст с # HELP, # TYPE и строками данных.
Типы: counter (только растет, rate()), gauge (мгновенное значение). В Kafka почти всё — gauge.
Лейблы нарезают метрику на серии, нужно учитывать кардинальность.
Имя метрики: namespace_subsystem_name_unit, snake_case, базовые единицы.
Exporter отдает снимок состояния на каждый scrape, историю хранит Prometheus.
Итак, exporter под капотом — это обычный HTTP-сервер, который отдает текстовые данные по /metrics. Так что первым шагом напишем его.
Каждый проект в языке Go называется модулем. В корне проекта находится файл go.mod, который описывает имя модуля и его зависимости. Имя модуля — это путь импорта. Обычно это адрес репозитория, например github.com/имя/kafka-exporter.
Пакет net/http — это стандартная библиотека языка Go, с помощью которого мы поднимем свой HTTP-сервер, без установки каких-либо сторонних пакетов. В контексте данного пакета нам важны следующие параметры:
Handler — функция, обрабатывающая запрос. Сигнатура: func(w http.ResponseWriter, r *http.Request), w для записи ответа, r для чтения запроса.
ServeMux — маршрутизатор. Он сопоставляет URL-путь с конкретным handler’ом: mux.HandleFunc("/path", handler).
Server — сам сервер. Отвечает за то, на каком адресе слушать и какой mux использовать. Функция server.ListenAndServe() запускает его.
Будем использовать свой ServeMux, а не глобальный
http.HandleFunc, потому что глобальный регистрирует handler’ы в общем DefaultServeMux, в который могут «втихаря» прописаться сторонние пакеты (это известная проблема, кому интересно, рекомендую прочитать отдельно об этом — проблема с pprof и DefaultServeMux). Свой mux будет изолированным.
Я полагаю, что вы уже знакомы с основами языка Go, поэтому не буду показывать этапы установки и настройки окружения для работы. Свой код я буду хранить в репозитории kafka_exporter_prometheus.
Начнем работу:
mkdir my-kafka-exporter && cd my-kafka-exporter go mod init github.com/<yourname>/my-kafka-exporter
Если не хотите публиковать свое решение, то рекомендую использовать локальный module path в названии модуля:
mkdir my-kafka-exporter && cd my-kafka-exporter go mod init study/my-kafka-exporter
Создадим в корне проекта файл main.go:
package main import ( "log" "net/http" ) func main() { // Создаем свой ServeMux — явный и изолированный, а не используем глобальный. Пустой маршрутизатор mux := http.NewServeMux() // Регистрируем handler на корневой путь "/" mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("kafka_exporter is active\n")) }) // Описываем сервер, слушаем на порту 9308, используем наш mux. server := &http.Server{ Addr: ":9308", Handler: mux, } log.Println("Listening on :9308") // ListenAndServe блокирует выполнение, пока сервер работает. При возврате ошибки (например, порт занят) — логируем. log.Fatal(server.ListenAndServe()) }
Запускаем код:
go run . # для остановки сервера Ctrl+C
В соседнем терминале проверяем работу:
curl localhost:9308 # можно также curl http://localhost:9308/
Разберем, что мы сделали:
main — точка входа в программу. Мы выбрали порт :9308, потому что у Prometheus есть реестр портов exporter’s и для kafka_exporter закреплен этот порт.
log.Fatal(server.ListenAndServe()) — чтобы сервис вернул ошибку при падении.
Реальный exporter должен иметь минимум три эндпоинта. Добавим их в наш код и поставим заглушку на время. Также вынесем настройку сервера в отдельную функцию setup, чтобы в дальнейшем было удобно тестировать и добавлять флаги:
package main import ( "log" "net/http" ) const ( listenAddr = ":9308" metricsPath = "/metrics" ) func main() { setup(listenAddr, metricsPath) } func setup(listenAddr, metricsPath string) { // Создаем свой ServeMux — явный и изолированный, а не используем глобальный. Пустой маршрутизатор mux := http.NewServeMux() // Регистрируем handler на корневой путь "/" mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("kafka_exporter is active\n")) }) // эндпоинт /healthz — это проба, сервис живой. Например, k8s проверяет — жив ли процесс. Пока просто отвечаем "ok" mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) }) // эндпоинт /metrics — здесь будут метрики. Пока заглушка в формате Prometheus. mux.HandleFunc(metricsPath, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; version=0.0.4") w.Write([]byte("# HELP my_exporter_up Is the exporter running.\n")) w.Write([]byte("# TYPE my_exporter_up gauge\n")) w.Write([]byte("my_exporter_up 1\n")) }) // Описываем сервер, слушаем на порту 9308, используем наш mux. server := &http.Server{ Addr: listenAddr, Handler: mux, } log.Println("Listening on :9308") // ListenAndServe блокирует выполнение, пока сервер работает. При возврате ошибки (например, порт занят) — логируем. log.Fatal(server.ListenAndServe()) }
Запустим и проверим работу:
go run . curl http://localhost:9308/healthz # ok curl http://localhost:9308/metrics # # HELP my_exporter_up Is the exporter running. # # TYPE my_exporter_up gauge # my_exporter_up 1
Итак, что мы сделали:
/healthz — стандартный эндпоинт для Kubernetes health-проб. На него можно повесить livenessProbe и readinessProbe. Пока оставим так, дальше мы поправим его.
Content-Type: text/plain; version=0.0.4 — официальный media-type формата экспозиции Prometheus версии 0.0.4. Prometheus по нему понимает, как парсить ответ. Когда мы подключим client_golang, заголовок будет выставляться автоматически.
/metrics — пока отдает текст, написанный вручную. Дальше мы будем использовать библиотеку для этого, чтобы не мучаться руками.
разделили: main — как запустить, setup — что делать.
В прошлом примере мы отдавали метрики на /metrics, написанные вручную. Теперь автоматизируем это и подключим официальную библиотеку Prometheus для Go — client_golang. Он будет делать форматирование, экранирование лейблов, заголовки, а также из коробки отдавать системные метрики самого процесса.
Библиотека github.com/prometheus/client_golang состоит из двух частей, которые нам понадобятся:
prometheus — ядро: содержит типы метрик (Gauge, Counter, …), registry и интерфейсы.
promhttp — готовый HTTP-handler, который превращает содержимое реестра в текст необходимого нам формата и отдаёт его на /metrics.
Для нас важно понять, как работает registry. Registry — это коллекция всех метрик приложения. Логика следующая:
Регистрируешь метрику в registry (prometheus.MustRegister(...)).
В момент scrape promhttp обходит registry, у каждой метрики спрашивает текущее значение и собирает итоговый текстовый документ.
Есть глобальный registry по умолчанию (prometheus.DefaultRegisterer). При вызове prometheus.MustRegister, метрика попадает именно туда, а promhttp.Handler() по умолчанию читает именно его. Для начала этого достаточно, во второй части курса мы разберем, как создать свой registry.
А сейчас рассмотрим способы отдачи метрик. Есть два способа:
Прямые метрики — создается объект метрики и в коде у него вызывается .Set(), .Inc(). Значение хранится в библиотеке. Подходит, когда приложение само порождает метрику (например, веб-сервис считает свои запросы).
Custom Collector — необходимо настроить интерфейс, метрики собираются «на лету» в момент scrape. Подходит для exporter’s.
Рассмотрим оба варианта, но в нашем решении будем использовать Custom Collector.
Перейдем к установке библиотеки, следующая команда скачает зависимость и пропишет ее в go.mod/go.sum.:
go get github.com/prometheus/client_golang/prometheus # или можно прописать импорт в коде и вызвать go mod tidy
Теперь перейдем к коду, нас ждет много обновлений и дополнений:
Импортируем библиотеку client_golang/prometheus.
Вместо ручного текста по метрикам на /metrics, мы повесим promhttp.Handler(). Обратите внимание: в этот раз мы используем mux.Handle, а не HandleFunc, потому что promhttp.Handler() возвращает готовый объект http.Handler, а не функцию.
Добавим собственную метрику, в качестве примера возьмем метрику типа Gauge, которая может расти и падать.
package main import ( .... "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) const (... ) // объявляем метрику, NewGauge возвращает объект метрики, у которого есть .Set/.Inc/.Dec. var exporterUp = prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: "kafka", // префикс → kafka_exporter_up Subsystem: "exporter", Name: "up", Help: "1 if the exporter is running.", }) func main() { // регистрируем метрику в registry по умолчанию. MustRegister паникует, если метрику зарегистрировали дважды. prometheus.MustRegister(exporterUp) // выставляем значение метрики. Это прямая метрика — значение хранится в библиотеке. exporterUp.Set(1) setup(listenAddr, metricsPath) } func setup(listenAddr, metricsPath string) { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("kafka_exporter is active\n")) }) mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) }) // эндпоинт /metrics теперь обслуживает promhttp. Он сам читает реестр по умолчанию и форматирует ответ. mux.Handle(metricsPath, promhttp.Handler()) server := &http.Server{Addr: listenAddr, Handler: mux,} log.Println("Listening on :9308") log.Fatal(server.ListenAndServe()) }
Запустим и посмотрим, какие метрики отдаются:
go run . curl http://localhost:9308/metrics # все метрики ... # # HELP go_goroutines Number of goroutines that currently exist. # # TYPE go_goroutines gauge # go_goroutines 7 # # HELP go_memstats_alloc_bytes Number of bytes allocated and still in use. # #TYPE go_memstats_alloc_bytes gauge # go_memstats_alloc_bytes 1.234e+06 ... curl -s http://localhost:9308/metrics | grep kafka_exporter_up # только метрику, которую мы задали # # HELP kafka_exporter_up 1 if the exporter is running. # # TYPE kafka_exporter_up gauge # kafka_exporter_up 1
При обращении к /metrics видим много метрик, которые не создавали. Это дефолтные коллекторы Go-клиента, которые регистрируются в registry по умолчанию автоматически. Мы бесплатно получаем мониторинг здоровья самого exporter-а (не течет ли память, сколько goroutine):
go_* — метрики Go-рантайма: число goroutine, состояние GC, память.
process_* — метрики ОС-процесса: CPU, резидентная память, открытые файловые дескрипторы.
Теперь разберем конструкции, которые использовали в коде:
GaugeOpts{Namespace, Subsystem, Name} — библиотека сама склеивает имя метрики через : kafka + exporter + up = kafkaexporter_up. Еще помните про naming convention?
Help — текст для # HELP, заполняете осмысленно.
MustRegister — добавляет метрику в registry. Префикс Must означает «паника при ошибке» — двойная регистрация, конфликт имен. Для метрик, объявленных на старте, это ок — лучше упасть сразу, чем молча потерять метрику. exporterUp.Set(1) — задаем значение, оно живет внутри объекта Gauge.
В действительности часто нужна не одна серия, а семейство серий с разными лейблами (вспомним kafka_topic_partitions{topic=“…”}). Для этого есть *Vec типы (например GaugeVec).
... var topicPartitions = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "kafka", Subsystem: "topic", Name: "partitions", Help: "Number of partitions for this topic.", }, []string{"topic"}, // имена лейблов ) func main() { prometheus.MustRegister(topicPartitions) // .WithLabelValues(...) возвращает конкретную серию по значениям лейблов. topicPartitions.WithLabelValues("orders").Set(6) topicPartitions.WithLabelValues("payments").Set(3) setup(":9308", "/metricРезультат:s") } ...
Результат:
kafka_topic_partitions{topic="orders"} 6 kafka_topic_partitions{topic="payments"} 3
Кажется, что GaugeVec — это и есть то, что нам нужно для Kafka? Не совсем, у него есть подвох для exporter’s, про который я расскажу дальше. Если коротко, то есть проблема «устаревших» серий, когда топик удалили, а серия осталась. Поэтому выбираем другой инструмент — Collector с ConstMetric. А GaugeVec подойдет для метрик, которые порождает само приложение.
Откуда брать имена пакетов и функций? Чтобы не запоминать все наизусть, используйте документацию.
На нем держится почти любой exporter, в том числе и наш kafka_exporter. После этого код будет читаться легко.
Зачем нужен Collector, если есть Gauge? Ранее мы выставляли значение метрики через exporterUp.Set(1), само значение метрики при этом хранится внутри объекта. Для нашего exporter это создает две проблемы:
Значения на /metrics могут быть устаревшими. Значение метрики может не успеть обновиться до момента scrape. Для корректной работы нужно отдельно настраивать фоновый таймер, синхронизацию - это дополнительная сложность.
Устаревшие серии. При использовании метрики GaugeVec, например, topicPartitions.WithLabelValues("orders"), в момент удаления топика orders из Kafka, сама серия топика сохраняется в GaugeVec до явного DeleteLabelValues. Поэтому, если мы не удалим серию вручную, Prometheus будет считать, что топик все еще есть, так как получает соответствующие метрики.
Решение: использовать Custom Collector + ConstMetric
На каждый новый scrape должно происходить следующее:
Необходимо сходить в источник, в нашем случае в Kafka.
Далее нужно создать метрики на лету из свежих данных, отдать их и забыть про них (это и есть ConstMetric — одноразовая метрика, созданная под конкретный scrape).
Теперь, при удалении топика, следующий scrape просто не получит соответствующие метрики из Kafka, серия автоматически исчезает.
Сделаем свой Collector с помощью интерфейса prometheus.Collector, который состоит всего из двух методов:
type Collector interface { // Describe отправляет в канал описания всех метрик, которые коллектор когда-либо может вернуть. Describe(chan<- *prometheus.Desc) // Collect вызывается на каждый scrape. Здесь мы собираем актуальные данные и отправляем метрики в канал. Collect(chan<- prometheus.Metric) }
*prometheus.Desc — описание метрики: ее полное имя, help-текст и список имен лейблов без значений. Создается один раз, неизменяем и переиспользуется между scrape. Его необходимо подготовить заранее.
desc := prometheus.NewDesc( "kafka_brokers", // полное имя "Number of brokers in the cluster.", // help nil, // имена переменных лейблов nil, // постоянные лейблы (constLabels) )
prometheus.Metric — сама метрика со значением создается в Collect через MustNewConstMetric:
metric := prometheus.MustNewConstMetric( desc, // тот самый Desc prometheus.GaugeValue, // тип значения float64(3), // значение // дальше идут значения лейблов, по порядку имён из Desc )
В Desc мы задаем имена лейблов ([]string{“topic”,“partition”}). В MustNewConstMetric передаем их значения в том же порядке (“orders”, “0”). Их количество должно совпадать, иначе будет паника, это важно учитывать.
Теперь рассмотрим сами методы Describe и Collect и реализацию Collector:
Describe вызывается при регистрации коллектора, его задача — сообщить реестру заранее, какие метрики существуют для валидации и обнаружения конфликтов имен. Отправляем все наши Desc в канал:
func (c *MyCollector) Describe(ch chan<- *prometheus.Desc) { ch <- c.brokersDesc }
Collect вызывается на каждый scrape. Здесь закладывается логика: сходить за данными, создать ConstMetric и отправить их в канал:
func (c *MyCollector) Collect(ch chan<- prometheus.Metric) { value := goAndFetchFromKafka() // сходили за свежими данными ch <- prometheus.MustNewConstMetric( // создали метрику на лету c.brokersDesc, prometheus.GaugeValue, float64(value), ) }
promhttpсам вычитывает все из канала и сформирует текст. В нашем случае каналы — это способ стримить метрики: коллектор может слать их по мере готовности, не собирая в большой слайс.
Давайте добавим в код свой Collector, но пока он будет возвращать только тестовые данные. Подключим настоящую Kafka и будем собирать реальные метрики уже во второй части.
Чтобы сделать код читаемым, вынесем реализацию Collector в отдельный файл exporter.go и положим его рядом с main.go:
package main import ( "github.com/prometheus/client_golang/prometheus" ) const namespace = "kafka" // для имени метрик // Это наши описания метрик — Descriptors. Мы их готовим заранее и переиспользуем var ( clusterBrokers = prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "brokers"), "Number of brokers in the Kafka cluster.", nil, nil, // нет переменных лейблов, нет постоянных ) topicPartitions = prometheus.NewDesc( prometheus.BuildFQName(namespace, "topic", "partitions"), "Number of partitions for this topic.", []string{"topic"}, // один переменный лейбл: topic nil, ) ) // Exporter реализует интерфейс prometheus.Collector type Exporter struct { // Сюда позже добавим клиент Kafka. Пока оставим пустым } func NewExporter() *Exporter { return &Exporter{} } // Describe: перечисляем все метрики, которые умеем отдавать func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- clusterBrokers ch <- topicPartitions } // Collect: вызывается на каждый scrape. Происходит сбор данных func (e *Exporter) Collect(ch chan<- prometheus.Metric) { // Пока тестовые данные. В Части 2 заменим на реальные из Kafka // Метрика без лейблов: число брокеров = 3 ch <- prometheus.MustNewConstMetric( clusterBrokers, prometheus.GaugeValue, float64(3), ) // Метрики с лейблом topic. Эмулируем два топика fakeTopics := map[string]int{"orders": 6, "payments": 3} for topic, partitions := range fakeTopics { ch <- prometheus.MustNewConstMetric( topicPartitions, prometheus.GaugeValue, float64(partitions), topic, ) } } Теперь подключим созданный Collector в main.go: ... func main() { // Регистрируем наш Collector. Теперь на каждый scrape Prometheus будет вызызать e.Collect() exporter := NewExporter() prometheus.MustRegister(exporter) setup(":9308", "/metrics") } func setup(listenAddr, metricsPath string) { mux := http.NewServeMux() mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) }) mux.Handle(metricsPath, promhttp.Handler()) server := &http.Server{ Addr: listenAddr, Handler: mux, } log.Println("Listening on :9308") log.Fatal(server.ListenAndServe()) }
Запускаем и проверяем, что нужные нам метрики отдаются:
go run . curl -s http://localhost:9308/metrics | grep kafka_ # HELP kafka_brokers Number of brokers in the Kafka cluster. # TYPE kafka_brokers gauge kafka_brokers 3 # HELP kafka_topic_partitions Number of partitions for this topic. # TYPE kafka_topic_partitions gauge kafka_topic_partitions{topic="orders"} 6 kafka_topic_partitions{topic="payments"} 3
Можно себя поздравить! Вы подготовили архитектуру exporter, а дальше останется только заменить тестовые данные на реальные из Kafka.
Итак, что мы вынесли из материала:
Prometheus работает по pull-модели: сам ходит к exporter на /metrics.
Exporter — это переводчик из родного протокола системы в формат Prometheus.
Формат метрик — простой текст с # HELP, # TYPE и строками данных.
основные типы метрик: counter (только растет, rate()), gauge (мгновенное значение).
Лейблы нарезают метрику на серии.
Имена метрик: namespace_subsystem_name_unit, snake_case, базовые единицы.
Exporter отдает снимок состояния на каждый scrape, историю хранит Prometheus.
Подняли HTTP-сервер на net/http с собственным ServeMux. Порт 9308 закрепили за kafka_exporter в реестре Prometheus.
Подключили client_golang: prometheus (ядро) + promhttp (handler). Получили дефолтные метрики go_* и process_*.
Паттерн Custom Collector + ConstMetric — это стандарт для exporter: свежие данные на каждый scrape, нет проблемы устаревших серий.
Собрали полноценный каркас exporter на тестовых данных.
Рабочий каркас для будущего exporter готов. В следующей части подключимся к Kafka и настроим сбор метрик кластера.