∿Метрики и
наблюдаемость
Главы темы 1
1.8

Prometheus и Grafana: как метрики попадают на график

Глава проходит путь метрики сервиса от строки в /metrics до панели Grafana. По снимкам стенда разобрано, как Prometheus опрашивает приложение, как читать таблицу и график запроса и из чего состоит дашборд.

Главное в главе

Приложение отдаёт только текущие значения. Историю из них собирает Prometheus, который регулярно их читает.

Раздел
Основы наблюдаемости
Уровень
Начальный · Python знаком
Связанные главы
1.4 — метрики, 1.3 — OpenTelemetry
Зачем это знать
Понимать, откуда берётся каждое число на панели и где искать ошибку, если его нет

§1Кто за что отвечает

Предыдущие главы разбирали, какие данные нужны для расследования. Эта глава описывает инструменты, которые эти данные собирают, хранят и показывают. Всё вместе часто называют «Grafana» или «мониторинг», но за каждый участок пути отвечает свой компонент.

Сравнение из жизни
Кассир, бухгалтер и директор

Кассир пробивает покупки, и в кассе видна текущая сумма. Бухгалтер каждый вечер записывает эту сумму в книгу, и так появляется история. Директор смотрит графики, построенные по книге.

Приложение, Prometheus и Grafana

Приложение считает запросы и показывает текущие значения. Prometheus регулярно читает их и сохраняет с временем. Grafana строит панели по сохранённым данным.

Приложениесчитает и отдаёт /metrics
Prometheusопрашивает, хранит, считает PromQL
Grafanaзапрашивает и рисует панели

На стенде работают эти три компонента: API — обычный Python-процесс, Prometheus и Grafana — контейнеры Docker.

КомпонентЧто делаетЧего не делает
Приложение с инструментациейСчитает события и длительности, отдаёт текущие значения по адресу /metricsНе хранит историю: после перезапуска счётчики начинаются с нуля
PrometheusРегулярно опрашивает приложения, хранит временные ряды, выполняет запросы PromQL и проверяет правила оповещенияНе хранит логи и трассы; полноценные дашборды не рисует
GrafanaОтправляет запросы к источникам данных и показывает результаты панелямиСама метрики не собирает и не хранит
AlertmanagerПолучает сработавшие оповещения от Prometheus, группирует их и отправляет в почту или мессенджерУсловия оповещений не вычисляет
OpenTelemetryСоздаёт телеметрию в коде и передаёт её через Collector в хранилищаДанные не хранит и графики не строит

Дальше глава идёт по пути одного числа: от строки кода, которая увеличивает счётчик, до цифры на панели Grafana.

Первоисточник: Prometheus · Overview ↗

Путь числа начинается в приложении. Следующий раздел показывает код, который это число отдаёт.

§2Приложение отдаёт текущие значения

Ответ /metrics по строкам разобран в главе 1.4. Здесь показано, как этот ответ формируется в коде.

Python · фрагмент app.pyФайл целиком ↓
@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
← HTTP GET
Prometheusопрос → хранение
← запрос
Grafanaпанель

1. Приложение обновляет свои измерения при обработке запросов.

Рис. 1.8.1 кто кого вызывает при сборе метрик

Кнопка «Следующий шаг» проходит четыре шага по кругу. Рамкой выделен компонент, который работает на этом шаге; стрелка между приложением и Prometheus показывает, куда идёт HTTP-запрос и куда — данные.

Стрелки на схемах мониторинга часто рисуют от приложения к Prometheus, по направлению движения данных, а сетевой запрос при этом идёт в обратную сторону. Это нужно помнить при настройке сети: Prometheus должен иметь доступ к адресу приложения, а в приложении адрес Prometheus не записывают.

Какие адреса опрашивать, Prometheus берёт из файла конфигурации. На стенде он короткий.

YAML · prometheus.ymlФайл целиком ↓
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 первым делом открывают страницу состояния целей. Она показывает, получается ли вообще опрашивать приложение. Если здесь ошибка, все остальные запросы и панели будут пустыми.

Рис. 1.8.2 страница Status → Target health в Prometheus 3.5

Одна группа целей telemetry-lab, в ней одна цель. Зелёная полоса слева означает, что все цели группы доступны.

  1. Группа целей. telemetry-lab — значение job_name из prometheus.yml.
  2. 1 / 1 up. сколько целей группы доступны.
  3. Endpoint. полный адрес опроса.
  4. Labels. метки, которые Prometheus добавит к каждому ряду этой цели.
  5. Last scrape. когда был последний опрос и сколько он длился.
  6. State. UP — последний опрос прошёл успешно.
Открыть в полном размере ↗
КолонкаЗначение на снимкеЧто означает
Endpointhttp://host.docker.internal:8015/metricsАдрес из targets и путь по умолчанию
Labelsinstance="host.docker.internal:8015", job="telemetry-lab"Метки, которые Prometheus добавит к каждому ряду этой цели
Last scrape3.194s ago · 2msПоследний опрос был 3,2 секунды назад и занял 2 мс
StateUPПоследний опрос завершился успешно

Метки instance и job приложение не отдавало, их добавляет Prometheus. Благодаря им ряды разных экземпляров одного сервиса не смешиваются, даже если метрики называются одинаково.

По колонке Last scrape проверяют интервал: при scrape_interval: 5s значение «сколько секунд назад» не должно превышать пяти. Если цель недоступна, в колонке State появится DOWN и текст ошибки, например connection refused — по указанному адресу никто не слушает.

Цель в состоянии UP, значит, значения попадают в Prometheus. Следующий раздел получает их запросом.

§5Текущие значения в таблице

На вкладке Query в поле выражения вводят запрос на языке PromQL и нажимают Execute. Простейший запрос состоит из имени метрики. Он возвращает последние значения всех её рядов.

Рис. 1.8.3 запрос lab_http_requests_total на вкладке Table

Prometheus нашёл два ряда и показал последнее значение каждого.

  1. Выражение. только имя метрики, без функций.
  2. Evaluation time. момент, на который считаются значения; пусто — «сейчас».
  3. Load time и Result series. время выполнения запроса и число найденных рядов.
  4. Ряд. имя метрики и полный набор меток.
  5. Значение. последнее значение счётчика: 363 успешных запроса.
Открыть в полном размере ↗

Каждая строка таблицы — один временной ряд: слева имя и полный набор меток, справа значение. Из четырёх меток route и status пришли из ответа приложения, а instance и job добавил Prometheus при опросе.

Значения 363 и 38 — счётчики с момента запуска API: 363 успешных запроса и 38 ошибочных. Поле Evaluation time позволяет посмотреть значения на любой момент в прошлом.

Для вопроса «как работает сервис» эти числа мало что дают: 38 ошибок могли накопиться за сутки или за минуту. Нужна скорость, и её даёт следующий запрос.

Счётчик с запуска мало говорит о текущей работе. Следующий раздел переводит его в скорость.

§6Скорость на графике

На вкладке Graph вводят выражение, которое переводит счётчик в скорость и делит её по кодам ответа.

PromQL · запросов в секунду по коду ответа
sum by (status) (rate(lab_http_requests_total[1m]))

Выражение читают изнутри наружу.

  1. lab_http_requests_total[1m]

    Все значения каждого ряда за последнюю минуту. При опросе раз в 5 секунд это около двенадцати точек.

  2. rate(...)

    Средний прирост в секунду за эту минуту. Функция учитывает сбросы счётчика при перезапуске, о которых шла речь в главе 1.4.

  3. sum by (status) (...)

    Сложить ряды с одинаковой меткой status и убрать остальные метки. Будь экземпляров три, их скорости сложились бы в одну линию на каждый код.

Рис. 1.8.4 график скорости запросов по кодам ответа за 5 минут

Две линии: верхняя — ответы 200, нижняя, которая появляется в середине, — ответы 500. По вертикали — запросы в секунду.

  1. Выражение. скорость запросов по кодам ответа.
  2. Диапазон. график за последние 5 минут.
  3. Ступеньки. окно [1m] заполняется постепенно, каждая ступенька — новый опрос.
  4. Ответы 200. ровный участок около 2 запросов в секунду.
  5. Спад линии 200. часть запросов стала ошибками, медленные замедлили темп.
  6. Ответы 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], чтобы линия была ровнее.

prometheus.yml

Интервал опроса

Определяет, как часто появляются точки.

Запрос PromQL

Окно

Определяет, по скольким точкам считается скорость. Ради ровной линии достаточно подобрать окно, интервал менять не нужно.

доля ошибок ≈ 0,43 / (1,3 + 0,43) ≈ 25%
0,43
скорость ответов 500 на втором участке
1,3
скорость ответов 200 на том же участке

Каждый четвёртый запрос: так и была задана нагрузка. Совпадение подтверждает, что метрика и запрос устроены правильно.

Первоисточник: Prometheus · rate() ↗

В интерфейсе Prometheus выполняют отдельные запросы. Для постоянного наблюдения те же запросы собирают в панели.

§7Дашборд Grafana

В интерфейсе Prometheus проверяют отдельные запросы, а для постоянного наблюдения используют Grafana. Она хранит набор панелей, каждая выполняет свой запрос к источнику данных и показывает результат.

Дашборд — страница Grafana с набором панелей. У каждой панели есть источник данных, запрос, диапазон времени и способ отображения.

Источник данных на стенде настраивается файлом, поэтому после запуска контейнера ничего не нужно указывать вручную.

YAML · grafana/provisioning/datasources/prometheus.yamlФайл целиком ↓
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Источник нельзя изменить через интерфейс, поэтому стенд у всех одинаковый
Рис. 1.8.5 дашборд «Telemetry Lab · от запроса к метрике» за последние 5 минут

Четыре панели: два счётчика с момента запуска сверху, скорость и средняя длительность снизу.

  1. Диапазон и обновление. последние 5 минут, автообновление каждые 5 секунд.
  2. Завершённые запросы. сумма обоих рядов счётчика с запуска API: 401.
  3. Ошибки HTTP 500. только ряд с кодом 500: 38.
  4. Сброс счётчика. падение к нулю — перезапуск API перед прогоном.
  5. Поток запросов. скорость всех запросов в секунду.
  6. Средняя длительность. прирост суммы длительностей, делённый на прирост числа запросов.
Открыть в полном размере ↗
ПанельЗапрос 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. Файл описывает оба контейнера и их настройки.

YAML · compose.yaml (фрагмент)Файл целиком ↓
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_*Включают анонимный просмотр и скрывают форму входа
  1. docker compose up -d

    Скачивает образы при первом запуске и запускает контейнеры в фоне. API к этому моменту уже запущен с --host 0.0.0.0.

  2. docker compose ps

    Показывает состояние контейнеров.

  3. 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).

Первоисточники