Человеческим языком про метрики 4: PromQL
Это четвертая, финальная часть из цикла статей про метрики. В первой — вводной — я рассказал, почему метрики для сервисов устроены именно так, чем они отличаются от логов, и какую задачу решают. Во второй разобрались с форматом и типами метрик. В третьей — с перцентилями. Теперь, наконец, можно пойти и вывести что-нибудь на графики! На этот раз будет более хардкорно.
Оглавление цикла:
Постоянно держим в уме: метрики не про точность, а про усредненные значения. Есть много операций и тонкостей, при которых данные теряют точность, и это нормально — производные, перцентили, разные corner cases. За всем не уследишь, поэтому заранее считаем, что данные не 100% точные. Метрики вообще не подходят для какого-нибудь биллинга, а вот для оценки состояния систем — вполне.
После прошлых частей можно было поднять приложение, выставить в нем страничку с метриками, посмотреть как выглядят разные типы и так далее. Теперь пора поднять свой Prometheus, хотя бы даже «на коленке», чтобы складывать туда свои метрики и пробовать писать запросы.
Мы будем экспериментировать с его родным веб-интерфейсом. Этого достаточно, чтобы наблюдать за запросами и результатами из БД, и рисовать простые графики. Если у вас есть Grafana, можно все делать в ней: там есть Query Inspector, который тоже показывает запросы/ответы. Но Grafana добавит тормозов и своих странностей в процесс, поэтому на время экспериментов лучше обойтись без нее. Чтобы не превращать статью в devops-гайд по настройке, инструкции по запуску сервера останутся за скобками, проще всего найти любой готовый docker-compose.
Запросы
Вспоминаем из прошлых частей, что Prometheus — это Time Series Database, и в нем хранятся временны́е ряды. Каждая метрика — это временной ряд, можно сказать, отдельная таблица. Имя таблицы — это набор лейблов, а значения — одно число, записанное в разные моменты времени: [(day1, t1), (day2, t2), ...]
. Запросы могут:
достать значения из конкретного ряда за нужное время
то же самое сразу из нескольких рядов
сгруппировать или провести вычисления над результатом
Чтобы было, что выводить на график, нам нужен хотя бы один временной ряд: по горизонтали на графике всегда будет время, по вертикали — значение метрики в этот момент. Чтобы нарисовать несколько линий, нужно получить несколько временны́х рядов. Точный формат ответов будем разбирать дальше — чтобы не запутаться.
Примеры простых запросов
Самый простой запрос
http_requests_total
Попробуйте его или что-нибудь подобное в своих условиях. В результате достанутся все метрики, у которых название – http_requests_total
, и при этом могут быть любые другие лейблы, например с разными verb
или url
. В запрос неявно подставится текущее время. То есть мы запросим показания за один timestamp. Ответ выглядит как-то так:
один timestamp;
лейблы какого-то ряда, который попал под запрос;
значение из этого ряда в этот момент времени;
лейблы второго ряда, который тоже попал под запрос;
значение из второго ряда в этот же момент времени;
…и так далее для всех рядов.
Это называется instant vector, про него подробнее будет чуть ниже.
Кстати, название метрики — не какая-то особенная штука, а синтаксический сахар. Это просто лейбл с названием __name__
. Вот запрос, который делает то же самое:
{__name__="http_requests_total"}
Достаем метрику с подходящими лейблами
Получаем такой же набор значений разных рядов в один момент времени, тот же тип ответа — instant vector.
http_requests_total{job="prometheus",group="canary"}
Есть еще странный формат запросов с квадратными скобками
Пока что просто пример: достаем метрику с подходящими лейблами, и для каждой точки собираем массив предыдущих точек за 1 минуту. Ответ на такой запрос приходит в другом формате — range vector. Что это за зверь и зачем это вообще нужно – см. ниже.
http_requests_total{job="prometheus",group="canary"}[1m]
Простые типы
Их всего два:
Строки — чтобы запрашивать лейблы и их значения.
Числа — только double, они могут участвовать в функциях.
Помним, что значение метрики — всегда одно число в один момент времени, больше никаких данных нет.
Типы выражений и API
Дальше разберем два сложных типа, которыми тоже оперируют функции: instant vector и range vector. Описание намеренно упрощенное, чтобы было легче разобраться. Еще будет уточнение про типы, которые используются в API между Prometheus и Grafana: vector и matrix, и как все это друг с другом соотносится.
Тип instant vector
Массив из key-value, где key – метрика с лейблами, а value — значение в запрошенный момент времени. Это единственное, что рисуется на графиках. Все остальное надо привести к этому типу какой-нибудь функцией (агрегировать).
Почему такая матрешка? Дело в том, что один запрос может вернуть сразу несколько временны́х рядов. Вспоминаем, что одна метрика с разными значениями лейблов — это разные ряды. Поэтому instant vector-ы всегда возвращаются набором key-value, где ключ — это полное описание ряда (имя метрики и все лейблы), чтобы отличать один ряд от другого.
Один instant vector привязан к одному timestamp. Чтобы из этого получилось что-то полезное (для отображения на графике) — к запросам обычно добавляются диапазон времени и шаг, а в ответе возвращаются наборы instant vector-ов, попавшие под диапазон. И в интерфейсе Prometheus, и в Grafana, это делается автоматически, поэтому в самих запросах можно сконцентрироваться именно на том, какие данные мы хотим достать. Единственное, что может показаться неожиданным — если в запрошенный timestamp нет значения в БД, то Prometheus будет искать значения с timestamp меньше, то есть отматывать назад, по умолчанию в пределах 5 минут.
Пример
Как-то так выглядит значение, которе мы сохраняли в базу каждые 30 секунд, например метрика http_requests_total
с двух реплик приложения.
Метрика (временной ряд) | Время (unix timestamp) | Значение (double) |
---|---|---|
| 1615973700 | 0 |
| 1615973700 | 0 |
| 1615973730 | 0 |
| 1615973730 | 1 |
| 1615973760 | 1 |
| 1615973760 | 4 |
| 1615973790 | 3 |
| 1615973790 | 7 |
| 1615973820 | 4 |
| 1615973820 | 10 |
Если запросить
http_requests_total{instance="app1"}
в момент времени 1615973790 то получим instant vector с одной метрикой, у которого value =(1615973790, 3)
Если запросить
http_requests_total
в момент времени 1615973760 то получим instant vector с двумя метриками, у которых разные value:(1615973760, 1)
и(1615973760, 4)
и разные теги,app1
иapp2
Если запросить
http_requests_total{instance="app1"}
в момент времени 1615973795 — такого timestamp в базе нет, поэтому подойдет ближайшее значение из прошлого, то есть с timestamp 1615973790, результат будет как в первом случае
Про рисование instant vector на графике будет ниже.
Селекторы instant vetcor-ов
Метрики и лейблы можно матчить по равно/неравно и регулярками, подошло/не подошло. Операторы: = != =~ !~
http_requests_total{environment=~"staging|testing|(dev.*)", method!="GET"}
Тип range vector
Уже сложнее: массив из key-value, где key – метрика, а value – это значение в запрошенный момент времени + массив из [предыдущих значений за какой-то интервал]. Без паники, ниже разберемся зачем это нужно, и все станет понятно. Отличие от instant vector только в более сложном value. Причина, почему есть key, все та же: один запрос может вернуть много временных рядов с разными лейблами.
Так вот, про то, почему value здесь такой хитрый...
Помните, что такое производная?
Если вы еще не убежали, попробуем разобраться по-инженерному, без идеально точного матана. Производная какой-то функции — это вот такая формула:
( f(x+0.1) - f(x) ) / 0.1
Число 0.1
здесь — какой-то малый шаг. Формула нам показывает, на сколько изменится значение нашей функции f()
, если мы на чуть-чуть сдвинем x
. Делить на 0.1
нужно, потому что так удобнее работать с этой штукой: не важно, какой шаг мы возьмем — большой или маленький, если значение f()
меняется одинаково, то и значение производной окажется одинаковым.
Чтобы посчитать производную, нужно иметь хотя бы два значения функции
Можно немного считерить и считать производную не для значений (x, x+0.1)
, а для (x-0.1, x)
, то есть для текущего и предыдущего. Если у нас есть несколько предыдущих точек — еще лучше. Можно посчитать хитрее и еще делать интерполяцию/экстраполяцию. То есть мы хотим иметь набор значений [f(x), f(x-0.1), f(x-0.2), f(x-0.3), ...]
.
Вот для этого и нужен range vector: для каждого момента времени x
мы имеем точку со значением метрики в этот момент: f(x)
, и еще заглядываем в прошлое и берем несколько предыдущих точек: [f(x-30s), f(x-60s), f(x-90s), ...]
. 30 секунд здесь берутся из-за того, что Prometheus собирает значения метрик с таким интервалом, между ними просто ничего нет.
Range vector используется не только для производной: все функции, которые считают какое-то «изменение за небольшой интервал», например,
delta
, принимают на вход этот тип — просто для унификации.
Селекторы range vector-ов
Как написать запрос, возвращающий ranve vector? Так же, как и обычный запрос, только добавляем интервал в квадратных скобках в конце.
http_requests_total{job="prometheus"}[5m]
На что влияет число в квадратных скобках: в каком-то смысле, благодаря ему можно тюнить степень «сглаживания» или «разрешение» (точность).
больше интервал — более гладкий результат, видно тренд (общую картину);
меньше интервал — более шумный результат, видно пики и резкие скачки;
слишком малый интервал и не захватили минимум две точки — нет результата.
Range vector нельзя вывести на график (см. пример ниже — слишком много данных, непонятно, что выводить). Его нужно сначала обработать какой-то функцией и получить instant vector, который уже нормально рисуется. Собственно, для этого он и нужен — чтобы можно было применять всякие навороченные функции, которым нужны не только точки для графика, но и «взгляд в прошлое» по каждой точке, чтобы брать производную, считать дельту и прочее. Конкретные функции и их юзкейсы будут ниже, а пока что посмотрим, как достать range vector из базы.
Пример
Отталкиваемся от примера выше (про instant vector), только теперь запрос такой: http_requests_total{instance="app1"}[1m]
. Чтобы не потеряться, он вернет только одну метрику, поэтому она в таблице не упоминается.
Время (unix timestamp) | Значение (double) | Предыдущие значения за 1 минуту |
---|---|---|
1615973700 | 0 |
|
1615973730 | 0 |
|
1615973760 | 1 |
|
1615973790 | 3 |
|
1615973820 | 4 |
|
Увеличим интервал:
http_requests_total{instance="app1"}[2m]
.
Время (unix timestamp) | Значение (double) | Предыдущие значения за 2 минуты |
---|---|---|
1615973700 | 0 |
|
1615973730 | 0 |
|
1615973760 | 1 |
|
1615973790 | 3 |
|
1615973820 | 4 |
|
Разбирать пример, в котором один запрос возвращает две метрики, нет смысла: логика та же, что и раньше, только для каждой метрики — свои данные.
Выбор интервала
Зависит от сценариев использования.
Для запросов, которые пишутся руками во время экспериментов: хотите сглаженный график, чтобы было видно, что с ним происходит в целом — берите интервал побольше. Если нужно видеть, как значение «скачет» — берите поменьше, но не менее 1 минуты (
2*scrape_interval
, который у нас 30 секунд).Для запросов, которые вы пишете в Grafana, важно учитывать текущий масштаб графика, поэтому используйте переменную
$__rate_interval
(и ни в коем случае не$__interval
).Если вдруг у вас вместо Prometheus используется Clickhouse, из-за бага в парсере запросов кликхауса, вы не сможете использовать графановские умные интервалы, используйте свой кастомный.
Чтобы подробнее объяснить эти моменты, понадобится отдельная статья, уже с прицелом на Grafana и с примерами из нее, поэтому здесь оставим без деталей.
Типы в API
Если открыть веб-интерфейс Prometheus, попробовать разные запросы и посмотреть на трафик (в Devtools по F12, например), то можно офигеть увидеть странную вещь: на самом деле типы данных, которые мы можем получить, называются vector
и matrix
. А еще запросы на вкладках Table
и Graph
с одним и тем же текстом ходят на разные URLы и возвращают разные типы!
Сейчас попробуем разобраться, что к чему, на примерах. Откройте в браузере интерфейс своего Prometheus-а, и сделайте 4 запроса на странице. Естественно, это должны быть запросы ваших существующих метрик, чтобы ответы были непустыми.
Table, запрос одной метрики, например
http_requests_total{instance="app1"}
.Table, запрос со временем в квадратных скобках, по той же метрике, например
http_requests_total{instance="app1"}[1m]
.Graph с тем же запросом метрики.
Graph с тем же запросом со временем в квадратных скобках (да, в интерфейсе будет ошибка, так и нужно).
Пробуйте нажимать Execute
у этих запросов и смотреть в Devtools, что возвращается. Лучше так, чем километровые json-ы, скопированные в статью все равно их никто не будет разглядывать.
А теперь объяснения:
Метрика
Запрос http_requests_total{instance="app1"}
из вкладки Table — это запрос instant vector-а. В json нам возвращается vector
. В запросе видно, что добавляется time
— мы просим instant vector на один момент времени.
Матрица
Запрос http_requests_total{instance="app1"}[1m]
из вкладки Table — это запрос range vector-а. В json нам возвращается matrix
. В запрос тем же образом добавилось time
. Единственное, на что нужно обратить внимание — выше мы разбирали, что value состоят из точки и [массива прошлых точек]. Но на самом деле можно упростить и хранить все в одном массиве, просто добавив «текущую» точку к началу массива. Например, вместо (1,[2,3,4])
Prometheus делает [1,2,3,4]
.
График
Запрос http_requests_total{instance="app1"}
, такой же как и первый, но уже переключенный на вкладку Graph. Теперь мы получаем ответ типа matrix
! Почему?
Дело в том, что одиночный instant vector не нарисуешь на графике, это просто точка в один момент времени (или несколько точек, если вернулись ряды с разными лейблами). А нам нужно много точек за диапазон. Поэтому в запросе теперь не time
, а start, end, step
. В результате структура key-value остается такой же, мы можем одним запросом вернуть несколько метрик, но в value уже лежит массив вроде [(timestamp1, 1), (timestamp2, 10), ...]
— значения за разные моменты времени. По структуре оно получилось таким же, как и результат запроса range vector-а, который с квадратными скобками. Поэтому тип ответа используется один и тот же, matrix
.
Ошибка
А что будет, если попросить нарисовать график из запроса range vector-а? То есть взять http_requests_total{instance="app1"}[1m]
и переключить на вкладку Graph
? Матрица матриц? Трехмерный график? Было бы прикольно, но получим просто ошибку: range vector нельзя запросить за диапазон и нарисовать, потому что получилось бы больше данных, чем влезает в матрицу, которая по сути и есть 2D-график.
Теперь про разные функции и запросы. В реальности, когда для запросов используется Grafana, добавляются еще сложности с ее переменными — но это тема отдельной статьи.
Offset
Если хотим запросить данные относительно текущего времени назад, например, чтобы сравнить с прошлым днем.
http_requests_total offset 1d
Вложенные запросы
Так делать можно, но пропустим. Это уже сложности, которые поначалу не нужны.
OR
Логическое «ИЛИ» над одним тегом костылится с помощью регулярных выражений.
http_requests_total{app=~"apache|nginx|iis.*"}
AND
Логическое «И» над одним тегом костылится, если в запросе написать тег несколько раз с разными условиями.
# найдет все tag, начинающиеся на aaa И заканчивающиеся на bbb
metric{tag=~"aaa.*", tag=~".*bbb"}
С помощью этой фичи можно, например, попробовать обойти ограничения регулярных выражений (грабли с жадностью/нежадностью).
JOIN
В PromQL есть примерное подобие join-ов: 1-to-1, 1-to-N, N-to-1, но с ходу непонятно, как ими пользоваться: называется vector matching и имеет довольно страшный синтаксис, хотя статья неплохо объясняет, как это работает. Полностью тащить сюда документацию не хочется, и даже один реалистичный пример будет громоздким, поэтому придется пропустить.
Операторы и группировки
Только для instant vectors
Обычная арифметика и логика. Оперировать можно числами и векторами, т.е. пересечь два вектора, сравнить с числом и т.д.
Например, вот запрос, который умножит значения на 10 и вернет только те, которые больше или равны 50:
metric{tag="value"} * 10 >= 50
Запрос, который вернет пересечение: значения, у которых полностью совпадающий набор тегов в обоих запросах:
metric1 and metric2{tag="something"}
Запрос, который вернет разницу. Например, посчитаем сколько RAM занято:
total_ram{instance="host"} - free_ram{instance="host"}
Аналогично or
(объединение) и unless
(дополнение).
Агрегация
Только для instant vectors
Есть функции, которые делают что-то вроде GROUP BY
. Предположим, есть такие метрики:
http_requests_total{app="nginx"}
http_requests_total{app="apache"}
Это разные временны́е ряды, и если рисовать их на графике «в лоб» запросом http_requests_total
, получим две разные линии. Если мы хотим объединить их в один график, нужна агрегация. Например, сумма:
sum(http_requests_total)
Результат — один вектор, значит и график будет один. Значения, очевидно, сумма значений всех временных рядов, попавших под запрос.
Если у нас метрика с кучей тегов, но отобразить нужно только определенные, то можно сгруппировать по тегам. В результате получим несколько векторов, в зависимости от того, какие получились группы. Например, мы пишем метрику запросов с тегами app, instance, datacenter, region
. Выведем по графику на каждый app+instance
, просуммировав по датацентрам и регионам. В результате получим ряды с разными app и instance, но вообще не будет datacenter и region, так как их мы схлопнули в сумму:
sum by (app, instance) (http_requests_total)
Можно переставить местами части запроса, то есть вот это — тот же самый запрос. Пишите, как вам удобно:
sum (http_requests_total) by (app, instance)
Можно группировать по «всему кроме тега»:
sum without (instance) (http_requests_total)
# или поменять местами:
sum (http_requests_total) without (instance)
Список всех агрегаторов — в документации Prometheus.
Функции
Бывают для instant и range векторов
rate(range vector)
Наш главный друг и товарищ! Это что-то вроде навороченной производной, которая пытается рисовать гладкий график и учитывает всякие corner cases (см. ниже). Применяется к возрастающим счетчикам, чтобы показать прирост за какое-то время. Например, количество обработанных запросов, переданных байт и т.д.
rate(http_requests_total{app="nginx"}[5m])
считает скорость прироста в секунду (запросов/сек);
подходит только для
counter
т.к. полагается на возрастание;учитывает сбросы метрики на 0, например при рестартах приложений;
экстраполируется;
есть
irate
для резко прыгающих счетчиков.
increase(range vector)
Не надо использовать increase, лучше берите rate
, так в документации советуют!
increase(http_requests_total{app="nginx"}[5m])
считает прирост за интервал (сколько запросов пришло);
подходит только для
counter
т.к. полагается на возрастание;учитывает сбросы счетчика на 0, например при рестарте;
экстраполируется (поэтому бывают дроби даже когда прирост целый).
По сути это сахар для rate() * интервал
. Попробуйте нарисовать какую-нибудь метрику с ним и с rate. Увидите закономерность. Например, для интервала в 1 час, эти штуки дадут одинаковый результат:
rate(metric[1h])
increase(metric[1h])/3600
delta(range vector)
Считает разницу между первым и последним значением в интервале. То есть промежуточные значения отбрасываются. Поэтому можно пропустить резкое изменение, которое быстро откатилось. Вроде бы похож на более грубую версию increase()
, но используется для метрик, которые могут убывать или скакать туда-сюда.
delta(ram_free{host="postgresql"}[5m])
считает разницу за интервал (сколько запросов пришло);
подходит только для
gauge
;экстраполируется (поэтому бывают дроби даже когда изменение целое).
deriv(range vector)
Производная. Аналог rate()
, но для метрик, которые могут убывать или скакать туда-сюда. Не учитывает ситуации, когда метрика обнулилась, и может сойти с ума в этот момент.
deriv(ram_free{host="postgresql"}[5m])
считает производную, т.е. прирост в секунду (запросов/сек);
подходит только для
gauge
;не учитывает сбросы в 0.
histogram_quantile(instant vector)
Специальная функция для работы с гистограммами (которые пишут бакеты типа le="1s"
). Позволяет посчитать нужный перцентиль из гистограммы. При этом сами гистограммы можно агрегировать, например sum by (tag)
.
Возможность агрегировать гитсограммы и только после этого считать перцентили — их киллер-фича, и основное отличие от
Summary
.
Например, у вас две реплики приложения. Они обе пишут гистограммы с распределением запросов, каждая реплика — свою. Чтобы вывести один график, их надо объединить в одну. Это делается уже в Prometheus, после этого можно обсчитать гистограммы и получить перцентили, которые мы уже хотим видеть на графике. Если приложения вместо Histogram пишут Summary — их уже нельзя объединить или сложить! Подробнее об этом было в предыдущей статье.
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
histogram_quantile(
0.95,
sum by (url, le) (
rate(http_request_duration_seconds_bucket[5m])
)
)
считает квантили;
только для
histogram
с бакетами (тегиle
).
aggr_over_time(range vector)
Агрегаторы типа sum и max работают только для instant vectors, но если очень нужно — есть похожие функции-агрегаторы для range vectors, которые делают агрегацию на каждом интервале. То есть это как delta()
, только max
/min
/avg
и т.д. В простых случаях все это не нужно, но возможность есть.
avg_over_time
min_over_time
max_over_time
sum_over_time
count_over_time
quantile_over_time
stddev_over_time (отклонение)
stdvar_over_time (дисперсия)
Бонус
Для тех, кто героически остался с нами до конца — пара заметок о граблях, на которые легко налететь при первых попытках реализовать сбор метрик у себя.
(не)Персистентность
Ваши сервисы, которые выставляют метрики, не живут вечно — они рано или поздно обновятся или перезапустятся. Их метрики при этом сбросятся и начнут отсчитываться с нуля. Это нормально и учтено в модели данных и запросов Prometheus. Важно об этом помнить, чтобы использовать подходящие типы и функции, например:
rate()
учитывает такую ситуацию, но ожидает, что значение метрики всегда возрастает — без этого математика не сойдется. Поэтому совершенно нормально сделать счетчик «сколько приложение обработало запросов», и начинать его с нуля после рестарта.С другой стороны,
deriv()
работает с gauge, но не обрабатывает ситуации, когда метрика сбросилась в ноль. Обычно это и не нужно: gauge используется для показателей, которые не отсчитываются с нуля, например, свободная RAM. Когда приложение перезапустилось, метрика сразу будет показывать актуальное значение и периодически его обновлять.
Таким образом, не нужно писать метрики в БД на своей стороне или как-то обеспечивать их персистентность. Кроме того, вам поначалу может быть вообще все равно, потому что это такой частный случай: ну и что, подумаешь — иногда метрику штормит при деплоях или когда приложение упало, какая-то секунднаямелочь. Но это станет важно в дальнейшем, когда вы захотите сделать алерты. С алертами эта особенность станет огромной головной болью, причиной ложных срабатываний, костылей и переписываний. Лучше сразу разобраться, как ведут себя метрики при рестартах приложений, и пользоваться подходящими функциями в запросах. Даже если на первый взгляд кажется, что некоторые функции похожи и делают одно и то же.
Реплики приложения
Если у вас есть несколько реплик одного приложения, скорее всего каждая реплика будет писать метрики с одинаковыми лейблами. Это станет проблемой, потому что Prometheus соберет с разных реплик одинаковые показания:
# реплика 1:
api_http_requests_total{method="POST", url="/login"} 2
# реплика 2:
api_http_requests_total{method="POST", url="/login"} 3
Раз лейблы одинаковые, то эти два значения, собранные в примерно один и тот же timestamp, попадут в один и тот же временной ряд! То есть, у нас будет мешанина из противоречивых данных. При построении графика это будут две очень близкие друг к другу точки. При следующем scrape ситуация повторится.
Время (unix timestamp) | Значение (double) |
---|---|
1615973700 | 2 |
1615973701 | 3 |
1615973730 | 2 |
1615973731 | 5 |
Сколько запросов было обработано на самом деле всеми репликами? Сначала 5, потом 7, но эту информацию мы уже никак не достанем — она вся сложена в один последовательный ряд. Поэтому каждая реплика должна добавлять какой-то уникальный для себя лейбл — имя реплики, hostname, id pod-а в kubernetes, что угодно, позволяющее отличить метрику этой реплики от другой.
# реплика 1:
api_http_requests_total{method="POST", url="/login", replica="app_1"} 2
# реплика 2:
api_http_requests_total{method="POST", url="/login", replica="app_2"} 3
Тогда значения сложатся в разные ряды, и можно будет посчитать их сумму, или вывести на графики по отдельности, если понадобится.
Это конец цикла. Теперь вы знаете кунг-фу основы и принципы Prometheus, немного математической магии, и какие инструменты для обработки метрик есть под рукой.
Что дальше? Можно отправляться в Grafana — писать запросы и рисовать графики. Потом возвращаться в приложение и добавлять метрики, если чего-то не хватает. Следующая статья, если захотите продолжения, уже будет сборником рецептов для Grafana. Там не будет гайда, какие метрики собирать и как их правильно отображать — это все зависит только от ваших потребностей и специфики. И, наконец, на основе графиков можно делать алерты, например средствами той же Grafana. На самом деле, алерты – это самая мощная штука, ради которой вообще нужны были метрики. Но это уже тема для других статей.