Prometheus и Grafana: как метрики попадают на график
Глава проходит путь метрики сервиса от строки в /metrics до панели Grafana. По снимкам стенда разобрано, как Prometheus опрашивает приложение, как читать таблицу и график запроса и из чего состоит дашборд.
Приложение отдаёт только текущие значения. Историю из них собирает Prometheus, который регулярно их читает.
- Раздел
- Основы наблюдаемости
- Уровень
- Начальный · Python знаком
- Связанные главы
- 1.4 — метрики, 1.3 — OpenTelemetry
- Зачем это знать
- Понимать, откуда берётся каждое число на панели и где искать ошибку, если его нет
§1Кто за что отвечает
Предыдущие главы разбирали, какие данные нужны для расследования. Эта глава описывает инструменты, которые эти данные собирают, хранят и показывают. Всё вместе часто называют «Grafana» или «мониторинг», но за каждый участок пути отвечает свой компонент.
Кассир пробивает покупки, и в кассе видна текущая сумма. Бухгалтер каждый вечер записывает эту сумму в книгу, и так появляется история. Директор смотрит графики, построенные по книге.
Приложение считает запросы и показывает текущие значения. Prometheus регулярно читает их и сохраняет с временем. Grafana строит панели по сохранённым данным.
На стенде работают эти три компонента: API — обычный Python-процесс, Prometheus и Grafana — контейнеры Docker.
| Компонент | Что делает | Чего не делает |
|---|---|---|
| Приложение с инструментацией | Считает события и длительности, отдаёт текущие значения по адресу /metrics | Не хранит историю: после перезапуска счётчики начинаются с нуля |
| Prometheus | Регулярно опрашивает приложения, хранит временные ряды, выполняет запросы PromQL и проверяет правила оповещения | Не хранит логи и трассы; полноценные дашборды не рисует |
| Grafana | Отправляет запросы к источникам данных и показывает результаты панелями | Сама метрики не собирает и не хранит |
| Alertmanager | Получает сработавшие оповещения от Prometheus, группирует их и отправляет в почту или мессенджер | Условия оповещений не вычисляет |
| OpenTelemetry | Создаёт телеметрию в коде и передаёт её через Collector в хранилища | Данные не хранит и графики не строит |
Дальше глава идёт по пути одного числа: от строки кода, которая увеличивает счётчик, до цифры на панели Grafana.
Первоисточник: Prometheus · Overview ↗
Путь числа начинается в приложении. Следующий раздел показывает код, который это число отдаёт.
§2Приложение отдаёт текущие значения
Ответ /metrics по строкам разобран в главе 1.4. Здесь показано, как этот ответ формируется в коде.
@app.get("/metrics", include_in_schema=False)
async def metrics():
lines = ["# HELP lab_http_requests_total Completed products requests.",
"# TYPE lab_http_requests_total counter"]
for status, value in totals.items():
lines.append(f'lab_http_requests_total{{route="/products",status="{status}"}} {value}')
...
return Response("\n".join(lines) + "\n", media_type="text/plain; version=0.0.4")| Фрагмент | Что делает |
|---|---|
totals | Словарь с двумя счётчиками: {"200": ..., "500": ...}. Обработчик /products увеличивает нужный в конце каждого запроса |
{{route=...}} | Двойные фигурные скобки в f-строке выводят одинарные: так в тексте появляется {route="/products",status="200"} |
include_in_schema=False | Прячет адрес из Swagger: он нужен Prometheus, людям он не нужен |
Response(...) | Возвращает строки обычным текстом в формате, который принимает Prometheus |
Этот адрес возвращает только текущее состояние счётчиков. Два запроса к /metrics подряд покажут два значения, но хранить их по времени приложение не будет. Историю создаёт тот, кто регулярно читает адрес.
На стенде формат собран вручную, чтобы механизм был виден целиком. В рабочих приложениях используют библиотеку prometheus_client: она ведёт счётчики и гистограммы, правильно экранирует значения меток и работает, когда приложение запущено в нескольких процессах.
Приложение отдаёт только текущие значения. Историю создаёт тот, кто их регулярно читает, и это Prometheus.
§3Prometheus опрашивает приложение
Опрос (scrape) — HTTP-запрос, которым Prometheus читает метрики с адреса приложения. Опросы повторяются с заданным интервалом, и каждое прочитанное значение сохраняется с отметкой времени.
Такую схему называют сбором по запросу (pull): запрос отправляет система мониторинга, а приложение только отвечает. Пройдите по шагам схемы и посмотрите, в какую сторону идёт HTTP-запрос и в какую — данные.
/metricsопрос → хранениепанельКнопка «Следующий шаг» проходит четыре шага по кругу. Рамкой выделен компонент, который работает на этом шаге; стрелка между приложением и Prometheus показывает, куда идёт HTTP-запрос и куда — данные.
Стрелки на схемах мониторинга часто рисуют от приложения к Prometheus, по направлению движения данных, а сетевой запрос при этом идёт в обратную сторону. Это нужно помнить при настройке сети: Prometheus должен иметь доступ к адресу приложения, а в приложении адрес Prometheus не записывают.
Какие адреса опрашивать, Prometheus берёт из файла конфигурации. На стенде он короткий.
global:
scrape_interval: 5s
scrape_configs:
- job_name: telemetry-lab
static_configs:
- targets: ['host.docker.internal:8015']| Строка | Что означает |
|---|---|
scrape_interval: 5s | Опрашивать каждые 5 секунд. По умолчанию Prometheus опрашивает раз в минуту; на стенде интервал короткий, чтобы изменения появлялись быстро |
job_name: telemetry-lab | Имя группы целей. Оно попадёт в каждый ряд меткой job |
targets | Список адресов. Путь /metrics не указан: он используется по умолчанию |
host.docker.internal | Имя, по которому контейнер обращается к компьютеру, на котором он запущен. API работает не в контейнере, поэтому Prometheus ищет его на хост-машине |
Бывает и обратная схема: метрики отправляет приложение. Её используют для коротких задач, которые завершаются раньше, чем их успеют опросить, например для ночного скрипта резервного копирования. Для них у Prometheus есть отдельный компонент Pushgateway. Долго работающий веб-сервис удобнее опрашивать напрямую.
Первоисточник: Prometheus · Configuration ↗
Опрос настроен. Прежде чем писать запросы, проверяют, что он проходит.
§4Проверка цели на странице Target health
После запуска Prometheus первым делом открывают страницу состояния целей. Она показывает, получается ли вообще опрашивать приложение. Если здесь ошибка, все остальные запросы и панели будут пустыми.
Одна группа целей telemetry-lab, в ней одна цель. Зелёная полоса слева означает, что все цели группы доступны.
- Группа целей.
telemetry-lab— значениеjob_nameиз prometheus.yml. - 1 / 1 up. сколько целей группы доступны.
- Endpoint. полный адрес опроса.
- Labels. метки, которые Prometheus добавит к каждому ряду этой цели.
- Last scrape. когда был последний опрос и сколько он длился.
- State. UP — последний опрос прошёл успешно.
| Колонка | Значение на снимке | Что означает |
|---|---|---|
| Endpoint | http://host.docker.internal:8015/metrics | Адрес из targets и путь по умолчанию |
| Labels | instance="host.docker.internal:8015", job="telemetry-lab" | Метки, которые Prometheus добавит к каждому ряду этой цели |
| Last scrape | 3.194s ago · 2ms | Последний опрос был 3,2 секунды назад и занял 2 мс |
| State | UP | Последний опрос завершился успешно |
Метки instance и job приложение не отдавало, их добавляет Prometheus. Благодаря им ряды разных экземпляров одного сервиса не смешиваются, даже если метрики называются одинаково.
По колонке Last scrape проверяют интервал: при scrape_interval: 5s значение «сколько секунд назад» не должно превышать пяти. Если цель недоступна, в колонке State появится DOWN и текст ошибки, например connection refused — по указанному адресу никто не слушает.
Цель в состоянии UP, значит, значения попадают в Prometheus. Следующий раздел получает их запросом.
§5Текущие значения в таблице
На вкладке Query в поле выражения вводят запрос на языке PromQL и нажимают Execute. Простейший запрос состоит из имени метрики. Он возвращает последние значения всех её рядов.
Prometheus нашёл два ряда и показал последнее значение каждого.
- Выражение. только имя метрики, без функций.
- Evaluation time. момент, на который считаются значения; пусто — «сейчас».
- Load time и Result series. время выполнения запроса и число найденных рядов.
- Ряд. имя метрики и полный набор меток.
- Значение. последнее значение счётчика: 363 успешных запроса.
Каждая строка таблицы — один временной ряд: слева имя и полный набор меток, справа значение. Из четырёх меток route и status пришли из ответа приложения, а instance и job добавил Prometheus при опросе.
Значения 363 и 38 — счётчики с момента запуска API: 363 успешных запроса и 38 ошибочных. Поле Evaluation time позволяет посмотреть значения на любой момент в прошлом.
Для вопроса «как работает сервис» эти числа мало что дают: 38 ошибок могли накопиться за сутки или за минуту. Нужна скорость, и её даёт следующий запрос.
Счётчик с запуска мало говорит о текущей работе. Следующий раздел переводит его в скорость.
§6Скорость на графике
На вкладке Graph вводят выражение, которое переводит счётчик в скорость и делит её по кодам ответа.
sum by (status) (rate(lab_http_requests_total[1m]))Выражение читают изнутри наружу.
- lab_http_requests_total[1m]
Все значения каждого ряда за последнюю минуту. При опросе раз в 5 секунд это около двенадцати точек.
- rate(...)
Средний прирост в секунду за эту минуту. Функция учитывает сбросы счётчика при перезапуске, о которых шла речь в главе 1.4.
- sum by (status) (...)
Сложить ряды с одинаковой меткой
statusи убрать остальные метки. Будь экземпляров три, их скорости сложились бы в одну линию на каждый код.
Две линии: верхняя — ответы 200, нижняя, которая появляется в середине, — ответы 500. По вертикали — запросы в секунду.
- Выражение. скорость запросов по кодам ответа.
- Диапазон. график за последние 5 минут.
- Ступеньки. окно [1m] заполняется постепенно, каждая ступенька — новый опрос.
- Ответы 200. ровный участок около 2 запросов в секунду.
- Спад линии 200. часть запросов стала ошибками, медленные замедлили темп.
- Ответы 500. нижняя линия поднимается до 0,4 запроса в секунду.
Снимок сделан сразу после специального прогона. Две минуты скрипт tools/traffic.py отправлял только быстрые запросы, затем полторы минуты — смесь, где каждый четвёртый запрос завершается ошибкой, а каждый двадцать пятый выполняется медленно.
| Участок графика | Что видно | Почему так |
|---|---|---|
| Слева, до начала нагрузки | Нулевая линия | Запросов к API не было |
| Подъём ступеньками | Скорость 200 растёт около минуты | Окно [1m] заполняется постепенно; ступенька — каждый новый опрос раз в 5 секунд |
| Ровный участок около 2,0 | Два успешных запроса в секунду | Скрипт ждёт 0,4 с между запросами, а запрос длится около 85 мс |
| Появление нижней линии | Скорость 500 растёт до 0,4 | Каждый четвёртый запрос стал ошибкой |
| Верхняя линия снижается до 1,3 | Успешных ответов стало меньше | Четверть запросов ушла в ошибки, а медленные запросы по 3 с уменьшили общий темп |
| Спад справа | Обе линии падают | Нагрузка закончилась, и окно [1m] постепенно освобождается |
Нагрузка поменялась мгновенно, а линии меняются около минуты, потому что скорость усредняется по последней минуте окна [1m]. Короткое окно быстрее реагирует, но сильнее скачет. Длинное сглаживает шум, но позже показывает изменения.
Почему окно rate не берут короче четырёх интервалов опроса
Функция rate считает прирост по точкам, попавшим в окно, а точек в окне столько, сколько опросов туда поместилось. Пример рассчитан для обычной рабочей настройки scrape_interval: 15s.
| Окно | Точек в окне | Что получится на графике |
|---|---|---|
[20s] | 1 или 2 | Прирост посчитать не из чего: при одной точке значения нет, линия рвётся |
[30s] | 2 или 3 | Линия есть, но один пропущенный опрос снова разрывает её |
[1m] | 4 или 5 | Линия непрерывна и переживает один пропущенный опрос |
[5m] | 20 или 21 | Линия гладкая, но изменение нагрузки видно с задержкой в несколько минут |
Поэтому окно берут не меньше четырёх интервалов опроса. При scrape_interval: 15s это [1m], при 5 секундах на стенде хватило бы и [20s], но в главе взято [1m], чтобы линия была ровнее.
Интервал опроса
Определяет, как часто появляются точки.
Окно
Определяет, по скольким точкам считается скорость. Ради ровной линии достаточно подобрать окно, интервал менять не нужно.
- 0,43
- скорость ответов 500 на втором участке
- 1,3
- скорость ответов 200 на том же участке
Каждый четвёртый запрос: так и была задана нагрузка. Совпадение подтверждает, что метрика и запрос устроены правильно.
Первоисточник: Prometheus · rate() ↗
В интерфейсе Prometheus выполняют отдельные запросы. Для постоянного наблюдения те же запросы собирают в панели.
§7Дашборд Grafana
В интерфейсе Prometheus проверяют отдельные запросы, а для постоянного наблюдения используют Grafana. Она хранит набор панелей, каждая выполняет свой запрос к источнику данных и показывает результат.
Дашборд — страница Grafana с набором панелей. У каждой панели есть источник данных, запрос, диапазон времени и способ отображения.
Источник данных на стенде настраивается файлом, поэтому после запуска контейнера ничего не нужно указывать вручную.
apiVersion: 1
datasources:
- name: Prometheus · стенд
type: prometheus
uid: prometheus-lab
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false| Строка | Что означает |
|---|---|
url: http://prometheus:9090 | Адрес работает внутри Docker Compose: контейнеры одного проекта находят друг друга по имени сервиса |
access: proxy | Запросы к Prometheus отправляет сервер Grafana. Браузер пользователя к Prometheus не обращается |
editable: false | Источник нельзя изменить через интерфейс, поэтому стенд у всех одинаковый |
Четыре панели: два счётчика с момента запуска сверху, скорость и средняя длительность снизу.
- Диапазон и обновление. последние 5 минут, автообновление каждые 5 секунд.
- Завершённые запросы. сумма обоих рядов счётчика с запуска API: 401.
- Ошибки HTTP 500. только ряд с кодом 500: 38.
- Сброс счётчика. падение к нулю — перезапуск API перед прогоном.
- Поток запросов. скорость всех запросов в секунду.
- Средняя длительность. прирост суммы длительностей, делённый на прирост числа запросов.
| Панель | Запрос PromQL | Что показывает |
|---|---|---|
| Завершённые запросы · с запуска | sum(lab_http_requests_total) | Сумма обоих рядов счётчика |
| Ошибки HTTP 500 · с запуска | sum(lab_http_requests_total{status="500"}) | Только ряд с кодом 500 |
| Поток запросов · запросов/с | sum(rate(lab_http_requests_total[1m])) | Скорость всех запросов |
| Средняя длительность · обработчик | rate(..._sum[1m]) / rate(..._count[1m]) | Прирост суммы длительностей, делённый на прирост числа запросов |
Панели согласуются с Prometheus. Верхняя левая показывает 401, это 363 + 38 из таблицы выше. Верхняя правая показывает 38. Числа совпадают, потому что Grafana выполнила запросы к тому же Prometheus.
На мини-графиках под крупными цифрами видно, как сбрасывается счётчик. Слева линия держится на ровном уровне, затем резко падает к нулю и снова растёт: ровный уровень — счётчик предыдущего запуска API, падение — перезапуск процесса перед прогоном, рост — новый счёт с нуля.
Нижняя правая панель показывает среднюю длительность. На первом участке она держится около 83 мс: все запросы быстрые. На втором поднимается до 185 мс из-за медленных запросов: каждый двадцать пятый длится 3 секунды и добавляет к среднему около 3000 / 25 = 120 мс. Быстрые ошибки немного тянут среднее вниз. Та же картина была в главе 1.4: пара долгих запросов сильно сдвигает среднее.
Первоисточник: Grafana · Fundamentals ↗
Все три компонента показаны в работе. Последний раздел описывает, как они запущены на стенде.
§8Как запущен стенд
Prometheus и Grafana запускаются одной командой через Docker Compose. Файл описывает оба контейнера и их настройки.
name: makarov-telemetry-intro
services:
prometheus:
image: prom/prometheus:v3.5.0
ports: ['127.0.0.1:9095:9090']
volumes: ['./prometheus.yml:/etc/prometheus/prometheus.yml:ro']
extra_hosts: ['host.docker.internal:host-gateway']
grafana:
image: grafana/grafana:12.1.1
ports: ['127.0.0.1:3015:3000']
environment:
GF_AUTH_ANONYMOUS_ENABLED: 'true'
GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer
GF_AUTH_DISABLE_LOGIN_FORM: 'true'
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro| Строка | Что означает |
|---|---|
image | Образ с точной версией. Без версии Docker возьмёт последнюю, и через полгода стенд может выглядеть иначе, чем на снимках |
ports: 127.0.0.1:9095:9090 | Порт 9090 контейнера доступен на порту 9095 компьютера, и только с локального адреса. С других машин стенд недоступен |
volumes | Подключает файлы конфигурации внутрь контейнера. Суффикс :ro — только для чтения |
extra_hosts: host-gateway | Добавляет имя host.docker.internal на Linux. В Docker Desktop для macOS и Windows это имя есть и без настройки |
GF_AUTH_* | Включают анонимный просмотр и скрывают форму входа |
- docker compose up -d
Скачивает образы при первом запуске и запускает контейнеры в фоне. API к этому моменту уже запущен с
--host 0.0.0.0. - docker compose ps
Показывает состояние контейнеров.
- docker compose down
Останавливает и удаляет контейнеры этого проекта, не затрагивая другие.
§9Частые ошибки
| Что делают | Что получается | Как правильно |
|---|---|---|
| Ждут, что приложение само сохранит историю метрик | После перезапуска значения пропадают, графиков нет | Настроить опрос Prometheus и проверить цель на странице Target health |
| Строят панели, не проверив состояние цели | Пустые графики принимают за отсутствие запросов | Сначала убедиться, что цель в состоянии UP |
| Выводят сырой счётчик с подписью «запросов в минуту» | Число только растёт и не отражает нагрузку | Использовать rate или increase за окно |
| Считают, что Grafana хранит данные | Ищут пропавшие данные не в той системе | Проверять источник данных и сам запрос панели |
| Выбирают окно rate короче двух интервалов опроса | В окне меньше двух точек, линия прерывается | Брать окно не меньше четырёх интервалов опроса |
| Открывают стенд с анонимным доступом наружу | Любой в сети видит данные | Публиковать порты только на 127.0.0.1 |
§10Проверьте себя
Кто отправляет HTTP-запрос при сборе метрик по схеме pull?
Prometheus. Он отправляет GET к адресу /metrics приложения, а приложение возвращает текущие значения в ответе.
На странице Target health цель в состоянии DOWN с ошибкой connection refused. Что проверить?
Запущен ли API, совпадает ли порт, слушает ли API адрес, доступный из контейнера (0.0.0.0, а не только 127.0.0.1), и разрешается ли имя host.docker.internal.
Откуда в рядах метрики появились метки instance и job, если приложение их не отдаёт?
Их добавляет Prometheus при опросе: job — из job_name конфигурации, instance — из адреса цели.
Что вычисляет выражение sum by (status) (rate(lab_http_requests_total[1m]))?
Среднюю скорость запросов в секунду за последнюю минуту, сложенную по всем рядам с одинаковым кодом ответа. Получается по одной линии на каждый код.
Нагрузка изменилась мгновенно, а линия rate(...[1m]) меняется около минуты. Почему?
Скорость усредняется по окну в одну минуту. Новые значения вытесняют старые из окна постепенно, поэтому линия меняется плавно.
§11Шпаргалка
- Приложение отдаёт текущие значения, Prometheus хранит историю, Grafana показывает.
- Сбор по запросу (pull): Prometheus опрашивает /metrics с интервалом scrape_interval.
- Target health: UP — опрос успешен; метрика up = 1 или 0.
- Метки instance и job добавляет Prometheus.
- Имя метрики в запросе — последние значения всех её рядов.
- rate(x[1m]) — средний прирост в секунду за минуту с учётом сбросов.
- sum by (метка) — сложить ряды, оставив только указанную метку.
- Панель Grafana = источник + запрос + диапазон + отображение.
- Средняя длительность из гистограммы: rate(_sum) / rate(_count).