Многогранный мониторинг Angie — продолжение истории#
Продолжаем историю о мониторинге Angie: разбираем модуль metric — от простого счётчика запросов по браузерам до индекса APDEX, гистограмм времени ответа бэкендов и бизнес-метрик корзины, и всё это без сторонних агентов и парсинга логов.

Не просто счётчик: модуль metric превращает Angie в мощную аналитическую платформу#
Мы привыкли, что веб-сервер — это чёрный ящик, который просто гонит трафик.
Стандартные метрики Angie представлены широким спектром. Но что, если нам надо ещё больше? Что если прямо на
уровне сервера, без изменения кода приложения, можно в реальном времени видеть,
что именно клиенты добавляют в корзину, строить гистограммы времени ответа
бэкендов или подсчитать буквально что угодно и делать это с производительностью
атомарных операций в памяти? Сегодня разбираем мощнейший модуль metric в Angie. В предыдущей статье мы разобрали, насколько
удобно и просто можно настроить мониторинг. Однако пользователям необходимо
получать больше информации. Новый модуль metric открывает перед нами новые
возможности. Представьте, что мы можем нарисовать график значения любой переменной, не важно,
как мы её получили и заполнили. При этом нам даже не нужно парсить логи, ставить
дополнительных агентов или встраивать счётчики в странички. Чтобы не закружилась голова, пойдём от простого к сложному. Вот простой пример:
мы хотим знать, какими браузерами к нам ходят чаще. Мы просто посчитаем все
запросы по заголовку User-Agent,
который сохраняется в переменной Переменная Для тех, кто уже пользуются Angie, объяснять что такое API нет
необходимости. Мы просто включим его в конфиге: Для нового читателя рекомендую к прочтению мою предыдущую статью про метрики —
Многогранный мониторинг Angie, форка веб-сервера nginx. Вся дальнейшая настройка сводится к двум строчкам в конфиге. Определяем зону
наверху (где будем считать) и выставляем счётчик в нужном нам месте (что будем
считать). Всё, метрика готова: Выполнив запрос к серверу, в настроенном ранее API в новой ветке Теперь метрика Усложним задачу. Кажется, что сайт летает, но пользователи из Далёкой страны
жалуются на тормоза. Метрика с гео всё расставит по местам. Если мы добавим к
метрике И в API мы сможем увидеть уже среднее значение по странам, что-то вроде: Здесь пытливый читатель может резонно спросить: что, если я хочу смотреть и
количество запросов и время сразу. Что же мне, описывать кучу зон? Ответ — нет.
Всего лишь надо настроить комплексную зону metric_complex_zone: И — вуаля! — получили что хотели: Браузеры и запросы — это всё интересно. Что, если мы встанем на место
пользователя нашим ресурсом и задумаемся, насколько наш ресурс выглядит хорошо
со стороны? Можно ли это выразить как-то в цифрах? Оказывается, можно. Есть такой
индекс удовлетворённости пользователей — APDEX. Это международный стандарт измерения производительности приложений,
ориентированный на удовлетворённость пользователей. Он превращает разброс времени
отклика системы в единый числовой индекс качества работы с точки зрения
пользователя. Вычисляется он по простой формуле. Количество хороших ответов системы на
клиентский запрос сложить с половиной от средних по качеству ответа запросов и
разделить на общее число запросов (включая самые плохие). Конечно, в силу того
что мы не можем опросить каждого пользователя, что считать хорошим а что плохим,
решаем только мы. Давайте посчитаем насколько быстро открывается главная страничка нашего сайта.
Для этого мы возьмём переменную Добавим зону: И При вычислении метрики каждый запрос будет увеличивать соответствующий счётчик,
если новое значение не превосходит порога бакета. В бакет Теперь мы готовы посчитать APDEX по формуле Добавим новый target в конфиг Prometheus: А в самом Prometheus выполним расчёт готовой метрики APDEX по формуле: Теперь мы можем глядя на график решить, стоит ли уделять внимание вопросу
повышения производительности. APDEX должен неудержимо стремиться к 1. Вообразим типичный интернет-магазин. Вам как разработчику или DevOps-инженеру
нужно знать, как ведут себя бэкенды. Не просто «среднее время ответа», а
полное распределение задержек по каждому upstream-серверу, чтобы выявить
«болото». Как собрать эти метрики? Классические пути — лезть в логи
приложения (высокая латентность, нагрузка на диск), встраивать SDK в код
(сложность, дополнительные зависимости) или ставить sidecar-агенты (оверхэд). С другой стороны, для бизнес-процесса необходимо ещё знать «какие товары самые
популярные», например, сколько штук определённого товара было добавлено за
последний час. У нас есть какой-то крутой игрушечный онлайн-магазин с каталогом крутых
товаров на порту 80 и тремя бэкенд-серверами (8080, 8081, 8082). Клиент через
POST-запрос отправляет в корзину массив товаров с Создаём зону metric_complex_zone с именем
Определяем переменные, которые будут заполняться далее: Замечу, что здесь мы будем считать метрику на момент запроса от клиента
( Для того чтобы получить доступ к телу запроса на такой ранней стадии, будем
использовать Lua. Примечание На мгновение отвлечёмся. Резонный вопрос: скажется ли на общей
производительности использование модуля metric? Ответ: нет, но. Важно
понимать, каким образом вычисляются переменные, используемые модулем. Сам по
себе модуль не даёт дополнительной нагрузки на CPU, однако сложные вычисления
переменных могут внести вклад в утилизацию процессора. В интересующем нас location включаем Я не стану детально описывать dummy-бэкэнды, скажу просто, что все они одинаково
отрисовывают страничку, но с рандомной задержкой (для получения более похожих на
прод метрик) и отдельно обрабатывают запрос на Чтобы не тыцать по кнопачкам долго и муторно, запустим небольшой скрипт, который
будет складывать в корзину товары рандомно: И посмотрим на наполнение зоны: JSON-формат — это хорошо для отладки, но для production нужна интеграция с
Prometheus. Как уже делали ранее в данной статье, добавим ещё один target в конфиг Prometheus,
и метрики из Angie автоматически появятся в Prometheus. Для удобства запустим ещё один скрипт, запрашивающий вывод метрик в формате p8s
каждую секунду: И наконец, мы увидим данные в привычном формате p8s: Любители красивых графиков могут впоследствии добавить в Grafana. Пока мы мониторим бизнес-логику, почему бы не приглядеться и к инфраструктуре? Для вычисления нужных нам переменных добавим следующий каскад из Дело в том, что при возникновении события proxy_next_upstream стандартные переменные При возникновении 502, мы не заполняем данную метрику. Добавим зону: И метрику: Теперь для каждого upstream-сервера мы собираем гистограмму времени ответа
с детальными бакетами, а также максимальное, минимальное время
и общее количество запросов. Запрос к API покажет, какой бэкенд начал «провисать»: если у бэкэнда
наблюдается отклонение от среднего значения времени ответа — пора бить тревогу. В основном коде я использовал случайные задержки Представленные примеры — лишь верхушка айсберга. Модуль metric в Angie стирает
грань между инфраструктурным и бизнес-мониторингом, позволяя внедрить практически
любую пользовательскую метрику, до которой только сможем добраться. Хотите
узнавать, из какого региона приходят самые дорогие лиды, не подключая сторонние
аналитические системы? Или в реальном времени видеть просадки скорости для
конкретной версии мобильного приложения? Достаточно добавить пару строк в конфиг.
Дерзайте, попробуйте — всё проще, чем кажется.Введение: Модуль metric — это просто#
$http_user_agent.$http_<имя>, где последняя часть имени переменной соответствует
имени заголовка запроса, приведённому к нижнему регистру, с заменой символов
тире на символы подчёркивания, заполняется при поступлении запроса.location /api/ {
api /status/;
access_log off;
}
http {
metric_zone browsers:1m count;
metric browsers $http_user_agent on=request;
#.......
}
curl 127.0.0.1 --user-agent "Firefox"
metric_zones мы увидим простой счётчик:{
"http": {
"metric_zones": {
"browsers": {
"discarded": 0,
"metrics": {
"Firefox": 1
}
}
}
}
}
Firefox будет тикать каждый раз когда к нам придёт запрос с
заголовком User-Agent: Firefox.$request_time, то сможем посчитать уже среднее значение по ключу.
Например, ключ будет значением переменной $geoip2_country_iso_code из
geoip2:http {
geoip2 /var/lib/GeoIP/GeoLite2-Country.mmdb {
auto_reload 1h;
$geoip2_country_iso_code country iso_code;
}
metric_zone geo:1m average mean;
#.......
location / {
#.......
metric geo $geoip2_country_iso_code=$request_time on=end;
#.......
}
#.......
}
{
"discarded": 0,
"metrics": {
"ZA": 4.266,
"US": 2.1193750000000004,
"RU": 0.844
}
}
http {
geoip2 /var/lib/GeoIP/GeoLite2-Country.mmdb {
auto_reload 1h;
$geoip2_country_iso_code country iso_code;
}
metric_complex_zone geo:1m expire=on discard_key="old" {
avg_time average mean;
total count;
}
#.......
location / {
#.......
metric geo $geoip2_country_iso_code=$request_time on=end;
#.......
}
#.......
}
"metrics": {
"US": {
"avg_time": 2.483,
"total": 3
},
"RU": {
"avg_time": 0.707,
"total": 2
},
"ZA": {
"avg_time": 3.2255,
"total": 2
}
}
Но не так уж и сложно#
$request_time, так как мы смотрим на скорость
работы с точки зрения пользователя. Будем считать, что клиентский запрос
обработан хорошо, если время ответа не более 2-х секунд. Согласно спецификации
открытого стандарта APDEX
(п. 4.1), удовлетворительное время должно быть 4 хорошего времени, то есть, в
нашем случае не более 8 секунд. Всё остальное — уже плохо. Для наших подсчётов
нам подойдёт гистограмма.http {
#.......
metric_zone curapdex:1m histogram 2 8 inf;
metric с уже определённым ключом:location / {
#.......
metric curapdex curapdex=$request_time;
#.......
}
inf будут попадать
все значения, включая неудовлетворительные, что можно расценивать как общий
счётчик количества запросов.(0.5 * удовлетворительно + хорошо)
/ (общее количество запросов), для этого преобразуем нашу внутреннюю структуру
данных в родной для Prometheus формат на лету с помощью шаблона:http {
#.......
# Описание шаблона Prometheus для метрики "apdex"
prometheus_template apdex {
'angie_apdex_curapdex_stats{le="$1"}' $p8s_value
path=~^/http/metric_zones/curapdex/metrics/curapdex/(.+)$
type=histogram;
}
#.......
# Prometheus metrics_path: /prometheus/apdex
location /prometheus/apdex {
prometheus apdex;
access_log off;
}
#.......
- job_name: "angie_apdex_metric"
scheme: http
metrics_path: /prometheus/apdex
static_configs:
- targets: ['localhost:80']
(sum(angie_apdex_curapdex_stats{le="2"}) + sum(angie_apdex_curapdex_stats{le="8"})) / 2 / sum(angie_apdex_curapdex_stats{le="inf"})

Даже если сложно, то возможно#
id и quantity. Наша
цель — считать не просто факты добавления, а общее количество штук по
интересующим нас товарам.Разбор конфига#
awesome_customs. В ней мы будем хранить гистограмму размера заказов, сумму,
счётчик и среднее значение для каждого товара.metric_complex_zone awesome_customs:1m discard_key="other" {
stats histogram 64 256 1024 4096 16384 +Inf;
sum gauge;
count count;
avg_size average exp;
}
server {
listen 80;
set $product_name '';
set $product_quantity '';
#.......
}
on=request). Т. е. ещё до того как запрос будет обработан бэкэндом. Иногда
такое поведение необходимо, чтобы узнавать о возможных проблемах ещё до их
появления.lua_need_request_body on;, парсим тело,
заполняем переменные и директивой metric мы говорим Angie, что нужно
подсчитать метрику:location /addToCart {
lua_need_request_body on;
access_by_lua '
ngx.req.read_body()
local body = ngx.req.get_body_data()
if body then
local id = tonumber(string.match(body, \'"id":(%d+)\'))
local qty = tonumber(string.match(body, \'"quantity":(%d+)\'))
if id == 516970094 then
ngx.var.product_name = "awesome_product"
elseif id == 516970095 then
ngx.var.product_name = "other_product"
elseif id == 516970096 then
ngx.var.product_name = "premium_item"
end
if qty then
ngx.var.product_quantity = tostring(qty)
end
end
';
#.......
metric awesome_customs $product_name=$product_quantity on=request;
#.......
}
location /addToCart:
while true; do
ids=(516970094 516970095 516970096)
quantity=$(( RANDOM % 300 + 1 ))
random_index=$(( RANDOM % 3 ))
selected_id=${ids[$random_index]}
curl -s -X POST http://127.0.0.1/addToCart \
-H "Content-Type: application/json" \
-d '[{"id":'"$selected_id"',"quantity":'"$quantity"'}]'
echo ""
sleep 1
clear
done
curl -s http://127.0.0.1/api/http/metric_zones/awesome_customs/
{
"discarded": 0,
"metrics": {
"premium_item": {
"stats": {
"64": 42,
"256": 139,
"1024": 162,
"4096": 162,
"16384": 162,
"+Inf": 162
},
"sum": 22625,
"count": 162,
"avg_size": 51.3921395219927
},
"other_product": {
"stats": {
"64": 36,
"256": 162,
"1024": 181,
"4096": 181,
"16384": 181,
"+Inf": 181
},
"sum": 27182,
"count": 181,
"avg_size": 137.831010765459
},
"awesome_product": {
"stats": {
"64": 29,
"256": 141,
"1024": 173,
"4096": 173,
"16384": 173,
"+Inf": 173
},
"sum": 27757,
"count": 173,
"avg_size": 125.604625279747
}
}
}
# Описание шаблона Prometheus для метрики "awesome_customs"
prometheus_template awesome_customs {
'angie_awesome_product_metric_stats{le="$1"}' $p8s_value
path=~^/http/metric_zones/awesome_customs/metrics/awesome_product/stats/(.+)$
type=histogram;
'angie_awesome_product_metric_sum' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/awesome_product/sum
type=summary;
'angie_awesome_product_metric_stats_count' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/awesome_product/count
type=counter;
'angie_awesome_product_metric_avg_size' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/awesome_product/avg_size
type=gauge;
'angie_other_product_metric_stats{le="$1"}' $p8s_value
path=~^/http/metric_zones/awesome_customs/metrics/other_product/stats/(.+)$
type=histogram;
'angie_other_product_metric_sum' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/other_product/sum
type=summary;
'angie_other_product_metric_stats_count' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/other_product/count
type=counter;
'angie_other_product_metric_avg_size' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/other_product/avg_size
type=gauge;
'angie_premium_item_metric_stats{le="$1"}' $p8s_value
path=~^/http/metric_zones/awesome_customs/metrics/premium_item/stats/(.+)$
type=histogram;
'angie_premium_item_metric_sum' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/premium_item/sum
type=summary;
'angie_premium_item_metric_stats_count' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/premium_item/count
type=counter;
'angie_premium_item_metric_avg_size' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/premium_item/avg_size
type=gauge;
}
- job_name: "awesome_customs_metric"
scheme: http
metrics_path: /prometheus/awesome_customs
static_configs:
- targets: ['localhost:80']
while true; do
curl -s http://127.0.0.1/prometheus/awesome_customs
echo ""
sleep 1
clear
done
# Angie Prometheus template "awesome_customs"
# TYPE angie_awesome_product_metric_stats histogram
angie_awesome_product_metric_stats{le="64"} 66
angie_awesome_product_metric_stats{le="256"} 276
angie_awesome_product_metric_stats{le="1024"} 317
angie_awesome_product_metric_stats{le="4096"} 317
angie_awesome_product_metric_stats{le="16384"} 317
angie_awesome_product_metric_stats{le="+Inf"} 317
# TYPE angie_awesome_product_metric_sum summary
angie_awesome_product_metric_sum 47885
# TYPE angie_awesome_product_metric_stats_count counter
angie_awesome_product_metric_stats_count 317
# TYPE angie_awesome_product_metric_avg_size gauge
angie_awesome_product_metric_avg_size 237.88480352817184
# TYPE angie_other_product_metric_stats histogram
angie_other_product_metric_stats{le="64"} 67
angie_other_product_metric_stats{le="256"} 251
angie_other_product_metric_stats{le="1024"} 294
angie_other_product_metric_stats{le="4096"} 294
angie_other_product_metric_stats{le="16384"} 294
angie_other_product_metric_stats{le="+Inf"} 294
# TYPE angie_other_product_metric_sum summary
angie_other_product_metric_sum 44436
# TYPE angie_other_product_metric_stats_count counter
angie_other_product_metric_stats_count 294
# TYPE angie_other_product_metric_avg_size gauge
angie_other_product_metric_avg_size 72.03072242530414
# TYPE angie_premium_item_metric_stats histogram
angie_premium_item_metric_stats{le="64"} 63
angie_premium_item_metric_stats{le="256"} 261
angie_premium_item_metric_stats{le="1024"} 307
angie_premium_item_metric_stats{le="4096"} 307
angie_premium_item_metric_stats{le="16384"} 307
angie_premium_item_metric_stats{le="+Inf"} 307
# TYPE angie_premium_item_metric_sum summary
angie_premium_item_metric_sum 47677
# TYPE angie_premium_item_metric_stats_count counter
angie_premium_item_metric_stats_count 307
# TYPE angie_premium_item_metric_avg_size gauge
angie_premium_item_metric_avg_size 224.87369442985199
map:map $r_upstream $last_upstream {
default $upstream_addr;
"~.*,\s*(.*)$" $1;
"backend" "";
'' '';
}
map $upstream_status $r_upstream {
default $upstream_addr;
"~.*502$" '';
}
map $upstream_response_time $last_response_time {
default $upstream_response_time;
"~.*,\s*(.*)$" $1;
- "";
}
$upstream_response_time и
$upstream_addr будут содержать в себе времена и адреса от нескольких ответов,
разделённых запятыми. Нас интересуют последние значения в переменных, так как это
и есть время и адрес пира из блока upstream обработавшего запрос.metric_complex_zone upstream_time:10m discard_key="other" {
stats histogram 0.5 1 3 5 10 20 50 inf;
max_time max;
min_time min;
total_req count;
}
metric upstream_time $last_upstream=$last_response_time on=end;
curl -s http://127.0.0.1/api/http/metric_zones/upstream_time
{
"discarded": 0,
"metrics": {
"127.0.0.1:8080": {
"stats": {
"0.5": 224,
"1": 267,
"3": 432,
"5": 568,
"10": 771,
"20": 771,
"50": 771,
"inf": 771
},
"max_time": 7.475,
"min_time": 0,
"total_req": 771
},
"127.0.0.1:8081": {
"stats": {
"0.5": 610,
"1": 770,
"3": 770,
"5": 770,
"10": 770,
"20": 770,
"50": 770,
"inf": 770
},
"max_time": 0.701,
"min_time": 0,
"total_req": 770
},
"127.0.0.1:8082": {
"stats": {
"0.5": 174,
"1": 770,
"3": 770,
"5": 770,
"10": 770,
"20": 770,
"50": 770,
"inf": 770
},
"max_time": 0.901,
"min_time": 0,
"total_req": 770
}
}
}
Заключение#
ngx.sleep(delay_ms / 1000)
перед ответом, и читатель сможет самостоятельно «покрутить» их, если сочтёт такой
пример интересным для тестирования. Например:content_by_lua_block {
-- Случайная задержка от 2с до 7с в мс
local delay_ms = math.random(2000, 7000)
ngx.log(ngx.INFO, "Adding delay: ", delay_ms, "ms")
ngx.sleep(delay_ms / 1000) -- ngx.sleep принимает секунды
Полный конфиг angie.conf
user angie;
worker_processes auto;
worker_rlimit_nofile 65536;
error_log /var/log/angie/error.log notice;
pid /run/angie.pid;
load_module modules/ngx_http_geoip2_module.so;
load_module modules/ndk_http_module.so;
load_module modules/ngx_http_lua_module.so;
events {
worker_connections 65536;
}
http {
geoip2 /var/lib/GeoIP/GeoLite2-Country.mmdb {
auto_reload 1h;
$geoip2_country_iso_code country iso_code;
}
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent us="$upstream_status" '
'"$http_referer" uag="$http_user_agent" addr="$upstream_addr" country="$geoip2_country_iso_code" '
'time="$upstream_response_time" curapdex="$metric_curapdex_key" l_addr="$last_upstream" l_time="$last_response_time"';
access_log /var/log/angie/access.log main;
metric_zone browsers:1m count;
metric_complex_zone geo:1m expire=on discard_key="old" {
avg_time average mean;
total count;
}
metric_zone curapdex:1m histogram 2 8 inf;
metric_complex_zone awesome_customs:1m discard_key="other" {
stats histogram 64 256 1024 4096 16384 +Inf;
sum gauge;
count count;
avg_size average exp;
}
metric_complex_zone upstream_time:10m discard_key="other" {
stats histogram 0.5 1 3 5 10 20 50 inf;
max_time max;
min_time min;
total_req count;
}
# Описание шаблона Prometheus для метрики "awesome_customs"
prometheus_template awesome_customs {
'angie_awesome_product_metric_stats{le="$1"}' $p8s_value
path=~^/http/metric_zones/awesome_customs/metrics/awesome_product/stats/(.+)$
type=histogram;
'angie_awesome_product_metric_sum' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/awesome_product/sum
type=summary;
'angie_awesome_product_metric_stats_count' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/awesome_product/count
type=counter;
'angie_awesome_product_metric_avg_size' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/awesome_product/avg_size
type=gauge;
'angie_other_product_metric_stats{le="$1"}' $p8s_value
path=~^/http/metric_zones/awesome_customs/metrics/other_product/stats/(.+)$
type=histogram;
'angie_other_product_metric_sum' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/other_product/sum
type=summary;
'angie_other_product_metric_stats_count' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/other_product/count
type=counter;
'angie_other_product_metric_avg_size' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/other_product/avg_size
type=gauge;
'angie_premium_item_metric_stats{le="$1"}' $p8s_value
path=~^/http/metric_zones/awesome_customs/metrics/premium_item/stats/(.+)$
type=histogram;
'angie_premium_item_metric_sum' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/premium_item/sum
type=summary;
'angie_premium_item_metric_stats_count' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/premium_item/count
type=counter;
'angie_premium_item_metric_avg_size' $p8s_value
path=/http/metric_zones/awesome_customs/metrics/premium_item/avg_size
type=gauge;
}
# Описание шаблона Prometheus для метрики "apdex"
prometheus_template apdex {
'angie_apdex_curapdex_stats{le="$1"}' $p8s_value
path=~^/http/metric_zones/curapdex/metrics/curapdex/(.+)$
type=histogram;
}
upstream backend {
zone backend 256k;
server 127.0.0.1:8080;
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}
map $r_upstream $last_upstream {
default $upstream_addr;
"~.*,\s*(.*)$" $1;
"backend" "";
'' '';
}
map $upstream_status $r_upstream {
default $upstream_addr;
"~.*502$" '';
}
map $upstream_response_time $last_response_time {
default $upstream_response_time;
"~.*,\s*(.*)$" $1;
- "";
}
server {
listen 80;
set $product_name '';
set $product_quantity '';
location / {
proxy_pass http://backend;
proxy_set_header x-product-name $product_name;
proxy_read_timeout 10s;
metric upstream_time $last_upstream=$last_response_time on=end;
metric browsers $http_user_agent=$request_time on=end;
metric geo $geoip2_country_iso_code=$request_time on=end;
metric curapdex curapdex=$request_time;
}
location /addToCart {
lua_need_request_body on;
access_by_lua '
ngx.req.read_body()
local body = ngx.req.get_body_data()
if body then
local id = tonumber(string.match(body, \'"id":(%d+)\'))
local qty = tonumber(string.match(body, \'"quantity":(%d+)\'))
if id == 516970094 then
ngx.var.product_name = "awesome_product"
elseif id == 516970095 then
ngx.var.product_name = "other_product"
elseif id == 516970096 then
ngx.var.product_name = "premium_item"
end
if qty then
ngx.var.product_quantity = tostring(qty)
end
end
';
proxy_pass http://backend;
proxy_set_header x-product-name $product_name;
metric awesome_customs $product_name=$product_quantity on=request;
metric upstream_time $last_upstream=$last_response_time on=end;
}
location /api/ {
api /status/;
access_log off;
}
# Prometheus metrics_path: /prometheus/awesome_customs
location /prometheus/awesome_customs {
prometheus awesome_customs;
access_log off;
}
# Prometheus metrics_path: /prometheus/apdex
location /prometheus/apdex {
prometheus apdex;
access_log off;
}
}
# Раздел с dummy бэкэндами
server {
listen 8080;
location / {
default_type 'text/html';
content_by_lua_block {
-- Случайная задержка от 2с до 7с в мс
local delay_ms = math.random(2000, 7000)
ngx.log(ngx.INFO, "Adding delay: ", delay_ms, "ms")
ngx.sleep(delay_ms / 1000) -- ngx.sleep принимает секунды
ngx.say([[
<!DOCTYPE html>
<html>
<body>
<h2>Shop Products</h2>
<ul>
]])
local ids = {516970094, 516970095, 516970096}
local names = {"Awesome Product", "Other Product", "Premium Item"}
for i = 1, 3 do
ngx.say(string.format([[
<li>
<strong>%s</strong> (ID: %d)<br>
Quantity: <input type="number" id="qty-%d" value="1" min="1" style="width:60px;">
<button onclick="postToCart(%d)">
Add to Cart
</button>
<pre style="background:#eee;padding:5px;margin-top:5px;">
POST /addToCart
Content-Type: application/json
[{"id":%d,"quantity":<span id="display-qty-%d">1</span>}]</pre>
</li>
]], names[i], ids[i], ids[i], ids[i], ids[i], ids[i]))
end
ngx.say([[
</ul>
<script>
function postToCart(id) {
const quantity = document.getElementById('qty-' + id).value;
fetch('/addToCart', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify([{id: id, quantity: parseInt(quantity)}])
}).then(r => alert('Product ' + id + ' added! Quantity: ' + quantity));
document.getElementById('display-qty-' + id).textContent = quantity;
}
document.addEventListener('DOMContentLoaded', function() {
const inputs = document.querySelectorAll('input[type="number"]');
inputs.forEach(input => {
const id = input.id.replace('qty-', '');
input.addEventListener('input', function() {
document.getElementById('display-qty-' + id).textContent = this.value;
});
});
});
</script>
</body>
</html>
]])
}
location /addToCart {
default_type text/plain;
return 200 "Thank you for requesting $http_x_product_name.\n";
}
}
}
server {
listen 8081;
location / {
default_type 'text/html';
content_by_lua_block {
-- Случайная задержка от 0.5с до 700мс в мс
local delay_ms = math.random(50, 700)
ngx.log(ngx.INFO, "Adding delay: ", delay_ms, "ms")
ngx.sleep(delay_ms / 1000) -- ngx.sleep принимает секунды
ngx.say([[
<!DOCTYPE html>
<html>
<body>
<h2>Shop Products</h2>
<ul>
]])
local ids = {516970094, 516970095, 516970096}
local names = {"Awesome Product", "Other Product", "Premium Item"}
for i = 1, 3 do
ngx.say(string.format([[
<li>
<strong>%s</strong> (ID: %d)<br>
Quantity: <input type="number" id="qty-%d" value="1" min="1" style="width:60px;">
<button onclick="postToCart(%d)">
Add to Cart
</button>
<pre style="background:#eee;padding:5px;margin-top:5px;">
POST /addToCart
Content-Type: application/json
[{"id":%d,"quantity":<span id="display-qty-%d">1</span>}]</pre>
</li>
]], names[i], ids[i], ids[i], ids[i], ids[i], ids[i]))
end
ngx.say([[
</ul>
<script>
function postToCart(id) {
const quantity = document.getElementById('qty-' + id).value;
fetch('/addToCart', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify([{id: id, quantity: parseInt(quantity)}])
}).then(r => alert('Product ' + id + ' added! Quantity: ' + quantity));
document.getElementById('display-qty-' + id).textContent = quantity;
}
document.addEventListener('DOMContentLoaded', function() {
const inputs = document.querySelectorAll('input[type="number"]');
inputs.forEach(input => {
const id = input.id.replace('qty-', '');
input.addEventListener('input', function() {
document.getElementById('display-qty-' + id).textContent = this.value;
});
});
});
</script>
</body>
</html>
]])
}
location /addToCart {
default_type text/plain;
return 200 "Thank you for requesting $http_x_product_name.\n";
}
}
}
server {
listen 8082;
location / {
default_type 'text/html';
content_by_lua_block {
-- Случайная задержка от 100мс до 9с в мс
local delay_ms = math.random(100, 9000)
ngx.log(ngx.INFO, "Adding delay: ", delay_ms, "ms")
ngx.sleep(delay_ms / 1000) -- ngx.sleep принимает секунды
ngx.say([[
<!DOCTYPE html>
<html>
<body>
<h2>Shop Products</h2>
<ul>
]])
local ids = {516970094, 516970095, 516970096}
local names = {"Awesome Product", "Other Product", "Premium Item"}
for i = 1, 3 do
ngx.say(string.format([[
<li>
<strong>%s</strong> (ID: %d)<br>
Quantity: <input type="number" id="qty-%d" value="1" min="1" style="width:60px;">
<button onclick="postToCart(%d)">
Add to Cart
</button>
<pre style="background:#eee;padding:5px;margin-top:5px;">
POST /addToCart
Content-Type: application/json
[{"id":%d,"quantity":<span id="display-qty-%d">1</span>}]</pre>
</li>
]], names[i], ids[i], ids[i], ids[i], ids[i], ids[i]))
end
ngx.say([[
</ul>
<script>
function postToCart(id) {
const quantity = document.getElementById('qty-' + id).value;
fetch('/addToCart', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify([{id: id, quantity: parseInt(quantity)}])
}).then(r => alert('Product ' + id + ' added! Quantity: ' + quantity));
document.getElementById('display-qty-' + id).textContent = quantity;
}
document.addEventListener('DOMContentLoaded', function() {
const inputs = document.querySelectorAll('input[type="number"]');
inputs.forEach(input => {
const id = input.id.replace('qty-', '');
input.addEventListener('input', function() {
document.getElementById('display-qty-' + id).textContent = this.value;
});
});
});
</script>
</body>
</html>
]])
}
location /addToCart {
default_type text/plain;
return 200 "Thank you for requesting $http_x_product_name.\n";
}
}
}
}
Вывод: Почему это меняет правила игры#