
Не просто счётчик: модуль metric превращает Angie в мощную аналитическую платформу
Мы привыкли, что веб-сервер - это чёрный ящик, который просто гонит трафик. Стандартные метрики Angie представлены широким спектром. Но что, если нам надо еще больше? Что если прямо на уровне сервера, без изменения кода приложения, можно в реальном времени видеть, что именно клиенты добавляют в корзину, строить гистограммы времени ответа бэкендов или подсчитать буквально что угодно и делать это с производительностью атомарных операций в памяти? Сегодня разбираем мощнейший модуль metric в Angie.
Введение: Модуль metric - это просто
В предыдущей статье Многогранный мониторинг Angie мы разобрали, насколько удобно и просто можно настроить мониторинг. Однако, пользователям необходимо получать больше информации. Новый модуль metric открывает перед нами новые возможности.
Представьте, что мы можем нарисовать график значения любой переменной, не важно, как мы ее получили и заполнили. При этом нам даже не нужно парсить логи, ставить дополнительных агентов или встраивать счетчики в странички.
Чтобы не закружилась голова, пойдем от простого к сложному. Вот простой пример: мы хотим знать, какими браузерами к нам ходят чаще. Мы просто посчитаем все запросы по заголовку User-Agent, который сохраняется в переменной $http_user_agent.
Переменная
$http_<имя>, где последняя часть имени переменной соответствует имени заголовка запроса, приведенному к нижнему регистру, с заменой символов тире на символы подчеркивания, заполняется при поступлении запроса.
Для тех, кто уже пользуются Angie, объяснять что такое API нет необходимости. Мы просто включим его в конфиге:
location /api/ { api /status/; access_log off; }
Для нового читателя рекомендую к прочтению мою предыдущую статью про метрики - Многогранный мониторинг Angie, форка веб-сервера nginx
Вся дальнейшая настройка сводится к двум строчкам в конфиге. Определяем зону наверху (где будем считать) и выставляем счетчик в нужном нам месте (что будем считать). Все, метрика готова:
http { metric_zone browsers:1m count; metric browsers $http_user_agent on=request; #....... }
Выполнив запрос к серверу,
curl 127.0.0.1 --user-agent "Firefox"
в настроенном ранее API в новой ветке 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; #....... } #....... }
И в API мы сможем увидеть уже среднее значение по странам, что-то вроде:
{ "discarded": 0, "metrics": { "ZA": 4.266, "US": 2.1193750000000004, "RU": 0.844 } }
Здесь пытливый читатель может резонно спросить: что, если я хочу смотреть и количество запросов и время сразу. Что же мне, описывать кучу зон? Ответ - нет. Всего лишь надо настроить комплексную зону metric_complex_zone:
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 } }
Но не так уж и сложно
Браузеры и запросы - это все интересно. Что, если мы встанем на место пользователя нашим ресурсом и задумаемся, насколько наш ресурс выглядит хорошо со стороны? Можно ли это выразить как-то в цифрах? Оказывается, можно. Есть такой индекс удовлетворённости пользователей - APDEX.
Это международный стандарт измерения производительност�� приложений, ориентированный на удовлетворённость пользователей. Он превращает разброс времени отклика системы в единый числовой индекс качества работы с точки зрения пользователя.
Вычисляется он по простой формуле. Количество хороших ответов системы на клиентский запрос сложить с половиной от средних по качеству ответа запросов и разделить на общее число запросов (включая самые плохие). Конечно, в силу того что мы не можем опросить каждого пользователя, что считать хорошим а что плохим, решаем только мы.
Давайте посчитаем насколько быстро открывается главная страничка нашего сайта. Для этого мы возьмем переменную $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 будут попадать все значения, включая неудовлетворительные, что можно расценивать как общий счетчик количества запросов.
Теперь мы готовы посчитать APDEX по формуле (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; } #.......
Добавим новый target в конфиг Prometheus:
- job_name: "angie_apdex_metric" scheme: http metrics_path: /prometheus/apdex static_configs: - targets: ['localhost:80']
А в самом Prometheus выполним расчет готовой метрики APDEX по формуле (sum(angie_apdex_curapdex_stats{le="2"}) + sum(angie_apdex_curapdex_stats{le="8"})) / 2 / sum(angie_apdex_curapdex_stats{le="inf"})

Теперь мы можем глядя на график решить, стоит ли уделять внимание вопросу повышения производительности. APDEX должен неудержимо стремиться к 1.
Даже если сложно, то возможно
Вообразим типичный интернет-магазин. Вам как разработчику или DevOps-инженеру нужно знать, как ведут себя бэкенды. Не просто «среднее время ответа», а полное распределение задержек по каждому upstream-серверу, чтобы выявить «болото». Как собрать эти метрики? Классические пути - лезть в логи приложения (высокая латентность, нагрузка на диск), встраивать SDK в код (сложность, дополнительные зависимости) или ставить sidecar-агенты (оверхэд).
С другой стороны, для бизнес процесса необходимо еще знать "какие товары самые популярные", например, сколько штук определенного товара было добавлено за последний час.
У нас есть какой-то крутой игрушечный онлайн-магазин с каталогом крутых товаров на порту 80 и тремя бэкенд-серверами (8080, 8081, 8082). Клиент через POST-запрос отправляет в корзину массив товаров с id и quantity. Наша цель - считать не просто факты добавления, а общее количество штук по интересующим нас товарам.
Разбор конфига:
Создаём зону metric_complex_zone с именем 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.
На мгновение отвлечемся. Резонный вопрос: скажется ли на общей производительности использование модуля metric? Ответ: нет, но. Важно понимать, каким образом вычисляются переменные, используемые модулем. Сам по себе модуль не дает дополнительной нагрузки на CPU, однако сложные вычисления переменных могут внести вклад в утилизацию процессора.
В интересующем нас location включаем 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; #....... }
Я не стану детально описывать dummy бэкэнды, скажу просто, что все они одинаково отрисовывают страничку, но с рандомной задержкой (для получения более похожих на прод метрик) и отдельно обрабатывают запрос на 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 } } }
JSON-формат - это хорошо для отладки, но для production нужна интеграция с Prometheus.
# Описание шаблона 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; }
Как уже делали ранее в данной статье, добавим еще один target в конфиг Prometheus, и метрики из Angie автоматически появятся в Prometheus.
- job_name: "awesome_customs_metric" scheme: http metrics_path: /prometheus/awesome_customs static_configs: - targets: ['localhost:80']
Для удобства запустим еще один скрипт, запрашивающий вывод метрик в формате p8s каждую секунду:
while true; do curl -s http://127.0.0.1/prometheus/awesome\_customs echo "" sleep 1 clear done
И наконец, мы увидим данные в привычном формате p8s
# 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
Любители красивых графиков могут впоследствии добавить в Grafana
Пока мы мониторим бизнес-логику, почему бы не приглядеться и к инфраструктуре?
Для вычисления нужных нам переменных добавим следующий каскад из 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; - ""; }
Дело в том, что при возникновении события proxy_next_upstream стандартные переменные $upstream_response_time и $upstream_addr будут содержать в себе времена и адреса от нескольких ответов, разделённых запятыми. Нас интересуют последние значения в переменных, так как это и есть время и адрес пира из блока upstream обработавшего запрос.
При возникновении 502, мы не заполняем данную метрику.
Добавим зону:
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;
Теперь для каждого upstream-сервера мы собираем гистограмму времени ответа с детальными бакетами, а также максимальное, минимальное время и общее количество запросов.
Запрос к API покажет, какой бэкенд начал «провисать»: если у бэкэнда наблюдается отклонение от среднего значения времени ответа - пора бить тревогу.
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 принимает секунды
Скрытый текст
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"; } } } }
Вывод: Почему это меняет правила игры
Представленные примеры - лишь верхушка айсберга. Модуль metric в Angie стирает грань между инфраструктурным и бизнес-мониторингом, позволяя внедрить практически любую пользовательскую метрику, до которой только сможем добраться.
Хотите узнавать, из какого региона приходят самые дорогие лиды, не подключая сторонние аналитические системы? Или в реальном времени видеть просадки скорости для конкретной версии мобильного приложения? Достаточно добавить пару строк в конфиг. Дерзайте, попробуйте - всё проще, чем кажется.
