diff --git a/README.md b/README.md index 957bf25..8b1e319 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,25 @@ # Преобразование логов http accesslog в метрики с vector.dev -В данном каталоге лежат файлы описывающий подход, позволяющий генерировать метрики из логов без переписывания кода трансформов под каждый сервис. +В данном каталоге лежат файлы описывающий подход, позволяющий генерировать [метрики из логов](https://vector.dev/docs/reference/configuration/transforms/log_to_metric/) без переписывания кода трансформов [vector.dev](https://vector.dev/) под каждый сервис. -**Внимание!** [Версия v1](https://github.com/vseinstrumentiru/vector.dev-metrics-to-logs-helper/tree/1.0.0) содержала [баг #1](/../../issues/1) который проявлялся, только если вы добавляли разные определения метрик (разные лейблы, разные фильтры). В текущей версии это исправлено (см. [новый граф обработки](docs/vector_topology-separate-metric-fileters.svg)). +**Описание:** 1. Полагаемся на то, что все сервисы пишут логи в фиксированном формате JSON и имеют одинаковый набор обязательных полей. См. [example_logs] 2. Для кодогенерации использован Ansible и jinja2 шаблоны. Генерируем toml файлы конфигурации vector.dev и тесты к ним, где нужно. 3. Метрики определяются в файле [ansible-playbook/vars/metrics-catalog..yml], после этого запускаем генерацию через ansible. См пример в Makefile. По умолчанию генерируется для `env=testing`, чтобы генерировать для production запускать `VECTOR_ENV=production make <команда>` -4. Отдельной задачей конфигурации выгружаются на серверы с агрегаторами vector.dev, и для применения новой конфигурации выполняется перезапуск процесса vector +4. Отдельной задачей конфигурации выгружаются на серверы с агрегаторами vector.dev, и для применения новой конфигурации выполняется перезапуск процесса vector (здесь не приведены). + + +## Как было раньше + +Каждый раз под новый источник мы писали новый код VRL, часто делая copy/paste с незначительными правками. Это могло занимать с отладкой и тестами весь рабочий день. +Потому в 2023 нас это перестало устраивать и мы придумали это подход. Подробно читайте https://habr.com/ru/articles/809801/ и https://habr.com/ru/articles/864614/. ## Что нам дал рефакторинг * Мы вместо 5 часов теперь тратим 10-30 минут на добавление/изменение метрик с учетом выкатки на прод * Появилась автоматическая валидация по схеме, теперь ошибку при описании метрки допустить сложнее -* Теперь для добавления новой метрки не нужно знать как это закодировать на языку VRL - достаточно YAML девелопера ) +* Теперь для добавления новой метрки не нужно знать как это закодировать на языке VRL - достаточно YAML девелопера ) ## Ограничения @@ -31,6 +37,43 @@ 4. Выполните сборку и тесты `VECTOR_ENV= make test-vector-transforms`, если не указать VECTOR_ENV, то используется `VECTOR_ENV=testing` 5. Созданные файлы смотрите в каталоге [.generated/vector_config] +## Граф компонентов vector.dev + +В приведенной конфигурации создается следующий граф компонентов: + +![Граф компонентов vector.dev](docs/vector_topology-v1.2.svg) + + +## Как писать фильтры в metric-catalog.yml правлиьно + +* При описании указывайте фильтры по полям с наименьшим количеством значений и сравниваемых через простое сравнение без регулярных выражений - это улучшит производительнсоть обработки. +* В последнию очередь добавляйте условия с регулярными выржениями re/nre, т.к. чем позже они в выражении, тем больше шанс, что простые условия отсеят событие раньше. +* Старайтесь всегда сначала обходиться простыми условиями, и лишь при невозможности их применения переходить на условия с регулярными выржениями re/nre. + +Пример более тяжелого для вычисления фильтра (плохой пример): + + - selector: Исключаем из метрик логи канарееченого релиза, которые получен для запросов из внутренней сети. Чтобы это не попадало SLO + filter: + service_name: + re: "^.*-canary(-.*)?$" + namespace: + re: ".*-production" + is_internal_traffic: + eq: "1" + + +Тот же фильтр с измененным порядокм полей будет вычисляться быстрее (хороший пример): + + - selector: Исключаем из метрик логи канарееченого релиза, которые получен для запросов из внутренней сети. Чтобы это не попадало SLO + filter: + is_internal_traffic: # << поставили на первое место простой фильтр + eq: "1" + namespace: + re: ".*-production" + service_name: + re: "^.*-canary(-.*)?$" + + ## Контакты Если вам интересны подробности вы можете писать нам, см. сайт https://vitech.team/ ("По вопросам сотрудничества") или приходите работать к нам. diff --git a/ansible-playbook/vars/metrics-catalog.testing.yml b/ansible-playbook/vars/metrics-catalog.testing.yml index 4b55688..e35fc6b 100644 --- a/ansible-playbook/vars/metrics-catalog.testing.yml +++ b/ansible-playbook/vars/metrics-catalog.testing.yml @@ -12,12 +12,16 @@ metrics_catalog: exclude_events: - selector: Исключаем из метрик логи канарееченого релиза, которые получен для запросов из внутренней сети. Чтобы это не попадало SLO filter: - is_internal_traffic: - eq: "1" namespace: eq: "webshop-production" + is_internal_traffic: + eq: "1" service_name: re: "^.*-canary(-.*)?$" + - selector: Исключаем /health(z) + filter: + http_path: + re: "/healthz?" metrics: vi_http: @@ -33,33 +37,33 @@ metrics_catalog: ## - selector: "Запросы к главной странице интернет магазина" filter: - http_path: # имя поля в событии лога (входящие данные трансформа vector.dev) - # возможные условия отбора: eq -> ==, neq -> !=, re -> match_any(), nre -> !match_any. - # Порядок вычисления условий в группе: neq, nre, eq, re, (oneOfRe - не реализован) - eq: "/" # оператор и значение для сравнениея namespace: eq: "webshop-production" service_name: re: "^webshop(?:-canary)?$" + http_path: # имя поля в событии лога (входящие данные трансформа vector.dev) + # возможные условия отбора: eq -> ==, neq -> !=, re -> match_any(), nre -> !match_any. + # Порядок вычисления условий в группе: neq, nre, eq, re, (oneOfRe - не реализован) + eq: "/" # оператор и значение для сравнениея http_method: eq: "GET" - selector: "Запросы на создание заказа в интернет магазине" filter: - http_path: - re: "^/order$|^/order-fast$" namespace: eq: "webshop-production" service_name: re: "^webshop(?:-canary)?$" + http_path: + re: "^/order$|^/order-fast$" - selector: "Открытие страницы производителя в интернет магазине" filter: - http_path: - re: "^/vendor/(?P([^/]+/)+)$" - label_override: "/vendor/:name" namespace: eq: "webshop-production" service_name: re: "^webshop(?:-canary)?$" + http_path: + re: "^/vendor/(?P([^/]+/)+)$" + label_override: "/vendor/:name" testdata: # указывать в ключах имена полей на выходе из transforms.http_accesslog-k8s - test: input_values: @@ -83,12 +87,12 @@ metrics_catalog: - test: canary input_values: service_name: "webshop-canary" + namespace: "webshop-production" container_name: "frontend" cluster_name: "test::dc2-old" http_path: "/vendor/gigant-2/" http_method: "GET" http_status_code: "200" - namespace: "webshop-production" duration_sec: "0.3" kubernetes.pod_node_name: "ox2-kub-prod18" expect: @@ -99,6 +103,32 @@ metrics_catalog: cluster_name: "test::dc2-old" container_name: "frontend" service_name: "webshop-canary" + - selector: "Все запросы на Webshop Circuit Breaker" + filter: + service_name: + eq: "webshop-cb" + http_path: + re: ".*" + label_override: ":url_path" + testdata: # указывать в ключах имена полей на выходе из transforms.http_accesslog-k8s + - test: + input_values: + http_path: "/brand/abc-123/sub-1234/" + http_method: "GET" + http_status_code: "200" + duration_sec: "2.1" + cluster_name: "any_dc" + container_name: "any_cn" + kubernetes.pod_node_name: "ox2-kub-prod18" + service_name: "webshop-cb" + expect: + label_values: + path: ":url_path" + method: "GET" + status: "200" + cluster_name: "any_dc" + container_name: "any_cn" + service_name: "webshop-cb" # Метрика vi_http_request_duration_seconds request_duration_seconds: # требуется для transforms.metrics-http-accesslog-k8s - бакеты определены там metric_type: histogram diff --git a/docs/vector_topology-v1.2.svg b/docs/vector_topology-v1.2.svg new file mode 100644 index 0000000..8e25d84 --- /dev/null +++ b/docs/vector_topology-v1.2.svg @@ -0,0 +1,230 @@ + + + + + + +%3 + + + +kafka_raw + +kafka_raw + + + +kafka + +kafka + + + +kafka_raw->kafka + + + + + +app_k8s_log_streams + +app_k8s_log_streams + + + +http_accesslog-k8s_prepared + +http_accesslog-k8s_prepared + + + +app_k8s_log_streams->http_accesslog-k8s_prepared + + +http_accesslog-k8s_logs + + + +kafka->app_k8s_log_streams + + + + + +http_accesslog-k8s + +http_accesslog-k8s + + + +metrics-http-accesslog-k8s_exclude_filter + +metrics-http-accesslog-k8s_exclude_filter + + + +http_accesslog-k8s->metrics-http-accesslog-k8s_exclude_filter + + + + + +http_accesslog-k8s_prepared->http_accesslog-k8s + + + + + +log-metric-collector_metric_vi_http_request_duration_seconds + +log-metric-collector_metric_vi_http_request_duration_seconds + + + +metrics-http-accesslog-k8s + +metrics-http-accesslog-k8s + + + +log-metric-collector_metric_vi_http_request_duration_seconds->metrics-http-accesslog-k8s + + + + + +log-metric-prepare_metric_vi_http_request_duration_seconds + +log-metric-prepare_metric_vi_http_request_duration_seconds + + + +log-metric-prepare_metric_vi_http_request_duration_seconds->log-metric-collector_metric_vi_http_request_duration_seconds + + + + + +log-metric-collector_metric_vi_http_requests_total + +log-metric-collector_metric_vi_http_requests_total + + + +log-metric-collector_metric_vi_http_requests_total->metrics-http-accesslog-k8s + + + + + +log-metric-prepare_metric_vi_http_requests_total + +log-metric-prepare_metric_vi_http_requests_total + + + +log-metric-prepare_metric_vi_http_requests_total->log-metric-collector_metric_vi_http_requests_total + + + + + +log-metric-collector_metric_vi_kubedns_http_request_duration_seconds + +log-metric-collector_metric_vi_kubedns_http_request_duration_seconds + + + +log-metric-collector_metric_vi_kubedns_http_request_duration_seconds->metrics-http-accesslog-k8s + + + + + +log-metric-prepare_metric_vi_kubedns_http_request_duration_seconds + +log-metric-prepare_metric_vi_kubedns_http_request_duration_seconds + + + +log-metric-prepare_metric_vi_kubedns_http_request_duration_seconds->log-metric-collector_metric_vi_kubedns_http_request_duration_seconds + + + + + +log-metric-collector_metric_vi_kubedns_http_requests_total + +log-metric-collector_metric_vi_kubedns_http_requests_total + + + +log-metric-collector_metric_vi_kubedns_http_requests_total->metrics-http-accesslog-k8s + + + + + +log-metric-prepare_metric_vi_kubedns_http_requests_total + +log-metric-prepare_metric_vi_kubedns_http_requests_total + + + +log-metric-prepare_metric_vi_kubedns_http_requests_total->log-metric-collector_metric_vi_kubedns_http_requests_total + + + + + +metrics-http-accesslog-k8s_exclude_filter->log-metric-prepare_metric_vi_http_request_duration_seconds + + + + + +metrics-http-accesslog-k8s_exclude_filter->log-metric-prepare_metric_vi_http_requests_total + + + + + +metrics-http-accesslog-k8s_exclude_filter->log-metric-prepare_metric_vi_kubedns_http_request_duration_seconds + + + + + +metrics-http-accesslog-k8s_exclude_filter->log-metric-prepare_metric_vi_kubedns_http_requests_total + + + + + +metrics-cardinality-limiter + +metrics-cardinality-limiter + + + +access_log_metrics + +access_log_metrics + + + +metrics-cardinality-limiter->access_log_metrics + + + + + +metrics-http-accesslog-k8s->metrics-cardinality-limiter + + + + + diff --git a/files/aggregator/tests/logs-to-metrics_gen_tests.toml.j2 b/files/aggregator/tests/logs-to-metrics_gen_tests.toml.j2 index 6991b6a..a96cb6b 100644 --- a/files/aggregator/tests/logs-to-metrics_gen_tests.toml.j2 +++ b/files/aggregator/tests/logs-to-metrics_gen_tests.toml.j2 @@ -1,9 +1,11 @@ -############################################################################### -# GENERATED METRICS TESTS -# ВНИМАНИЕ! Тесты привязаны к metrics_catalog -> metrics-catalog.{{vector_environment}}.yml -############################################################################### +### +### GENERATED TESTS form metrics_catalog.{{vector_environment}}.yml/*testdata +### +{% set _metrics_catalog = metrics_catalog %} -{% for namespace, metrics in metrics_catalog.metrics.items() -%} +# Metrics selectors tests + +{% for namespace, metrics in _metrics_catalog.metrics.items() -%} {%- for metric_name, metric_conf in metrics.items() -%} {%- for selector_conf in metric_conf.event_selectors -%} @@ -14,7 +16,7 @@ {%- for test_conf in selector_conf.testdata -%} {%- set test_index = (loop.index0 | string) -%} - {%- set test_name = "transforms.metrics-http-accesslog-k8s: check metric " + metric_full_name + "/selector[" + selector_index + "]/test[" + test_index + "]" -%} + {%- set test_name = "metrics_catalog: check metric " + metric_full_name + "/selector[" + selector_index + "]/test[" + test_index + "]" -%} [[tests]] name = "{{ test_name }}" @@ -35,23 +37,26 @@ [[tests.outputs]] extract_from = "metrics-http-accesslog-k8s" - {# Выводим текст чтобы в случае ошибки конфигурации упасть при валидации #} + + {# Выводим текст чтобы в случае ошибки конфигурации упасть при валидации #} {%- set metric_def_labels = metric_conf.label_logfield_mapping.keys() | sort | join(', ') -%} {%- set metric_test_expect_labels = test_conf.expect.label_values.keys() | sort | join(', ') -%} {%- set metric_test_labels_ok = (metric_def_labels == metric_test_expect_labels) -%} {% if not metric_test_labels_ok %}{# сообщение без переносов должно быть, чтобы при сбое валидации в vector было видно целиком #} ->>> AnsibleTemplateError: Ошибка при генерации кода теста из шаблона. Тест: {{ test_name }}. Должно быть полное соответствие меток в label_logfield_mapping метрики и label_values теста. Сравните: label_logfield_mapping = [{{ metric_def_labels }}] и selector[{{ selector_index}}]/test[{{test_index}}].expect.label_values = [{{ metric_test_expect_labels }}] + >>> AnsibleTemplateError: Ошибка при генерации кода теста из шаблона. Должно быть полное соответствие меток в {{ metric_namespace}}/{{metric_name}} label_logfield_mapping и label_values тестов. Сравните: label_logfield_mapping = [{{ metric_def_labels }}] и selector[{{ selector_index}}].testdata.test[{{test_index}}].expect.label_values = [{{ metric_test_expect_labels }}] {% endif %} [[tests.outputs.conditions]] type = "vrl" source = ''' + {% for label_name, _ in metric_conf.label_logfield_mapping.items() -%} + assert!(!exists(.tags.pod_name)) + {# проверить имя метрики и перфикса -#} assert_eq!(.namespace, "{{ metric_namespace }}") assert!(includes([{{ metrics.keys() | map('to_json') | join(', ') }}], "{{ metric_name }}")) - {% for label_name, _ in metric_conf.label_logfield_mapping.items() -%} {# проверяем что метрика имеет нужные label -#} assert!(exists(.tags.{{ label_name }})) {# проверить совпадения значения label метрик по label_logfield_mapping из метрики -#} diff --git a/files/aggregator/tests/logs-to-metrics_tests.production.toml.j2 b/files/aggregator/tests/logs-to-metrics_tests.production.toml.j2 deleted file mode 100644 index 8184169..0000000 --- a/files/aggregator/tests/logs-to-metrics_tests.production.toml.j2 +++ /dev/null @@ -1,12 +0,0 @@ -{% if vector_environment == 'production'%} -############################################################################### -# Tests: transforms.metrics-http-accesslog-k8s_ -# ВНИМАНИЕ! Тест привязан к metrics-catalog.production.yml, если он изменить там имена, то тесты тут надо поправить тоже! -############################################################################### - -# Исправность функционирования самих трансформов проверяется по metrics-catalog.testing.yml -# Добавить сюда тесты для герерации метрик по metrics-catalog.production.yml, если нужно что-то проверить для них - - -{# end --> if vector_environment == 'production' #} -{% endif %} diff --git a/files/aggregator/tests/logs-to-metrics_tests.testing.toml.j2 b/files/aggregator/tests/logs-to-metrics_tests.testing.toml.j2 deleted file mode 100644 index 044e702..0000000 --- a/files/aggregator/tests/logs-to-metrics_tests.testing.toml.j2 +++ /dev/null @@ -1,218 +0,0 @@ -{% if vector_environment == 'testing'%} -############################################################################### -# Tests: transforms.metrics-http-accesslog-k8s_reducer_vi_http_requests_total -# ВНИМАНИЕ! Тест привязан к metrics-catalog.testing.yml, если он изменить имя в vi_http/requests_total, то тест сломается! -############################################################################### - -[[tests]] - name = "transforms.metrics-http-accesslog-k8s_reducer_vi_http_requests_total: 050.1 it filters events correctly" - - [[tests.inputs]] - insert_at = "http_accesslog-k8s" - type = "log" - [tests.inputs.log_fields] - # Данные о сервисе - service_name = "webshop" - namespace = "webshop-production" - # Данные Kubernetes (которые еще не изъяты) - "kubernetes.pod_labels.\"pod-template-hash\"" = "85f59bbfd6" - "kubernetes.pod_node_name" = "ox2-kub-prod18" - "kubernetes.pod_uid" = "93bde4d0-9731-4785-a80e-cd27ba8ad7c2" - "kubernetes.pod_owner" = "test::pod_owner" - 'kubernetes.pod_annotations."containers.vitech.team/frontend.log_source"' = "http_accesslog" - # Данные контейнера извлеченные ранее - cluster_name = "test::dc2-old" - container_name = "frontend" - container_image = "registry-sd.vi.net/site-vi/vi-ru/frontend:202109140836-master" - pod_name = "webshop-bff-85f59bbfd6-pcshd" - pod_ip = "176.59.54.236" - deployment = "webshop" - # Обработанное сообщение лога - "parsed_accesslog_message.request_id" = "6a432b1e521b042789c1e984aee21127" - "parsed_accesslog_message.user" = "" - "parsed_accesslog_message.address" = "176.59.54.236" - "parsed_accesslog_message.bytes_received"= 4799 - "parsed_accesslog_message.bytes_sent" = 14720 - "parsed_accesslog_message.protocol" = "HTTP/1.1" - "parsed_accesslog_message.scheme" = "https" - "parsed_accesslog_message.method" = "GET" - "parsed_accesslog_message.host" = "nn.vi.ru" - "parsed_accesslog_message.path" = "/vendor/gigant/" - "parsed_accesslog_message.request_uri" = "/vendor/gigant/?brand_product_block_ab=0" - "parsed_accesslog_message.request_query" = "brand_product_block_ab=0" - "parsed_accesslog_message.referrer" = "https://www.vi.ru/" - "parsed_accesslog_message.user_agent" = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0" - "parsed_accesslog_message.request_time" = 0.3 - "parsed_accesslog_message.status" = 200 - "parsed_accesslog_message.upstream_addr" = "127.0.0.1:4000" - "parsed_accesslog_message.upstream_bytes_received" = 14693 - "parsed_accesslog_message.upstream_response_time" = 0.217 - "parsed_accesslog_message.upstream_status" = "200" - - [[tests.outputs]] - extract_from = "metrics-http-accesslog-k8s_reducer_vi_http_requests_total" - - [[tests.outputs.conditions]] - type = "vrl" - source = ''' - assert_eq!(.http_request_id, "6a432b1e521b042789c1e984aee21127") - ''' - -[[tests]] - name = "transforms.metrics-http-accesslog-k8s: 050.2 it filters out not matching events (by http_path)" - no_outputs_from = ["metrics-http-accesslog-k8s"] - - [[tests.inputs]] - insert_at = "http_accesslog-k8s" - type = "log" - [tests.inputs.log_fields] - # Данные о сервисе - service_name = "personal-cabinet" - namespace = "webshop-production" - # Данные Kubernetes (которые еще не изъяты) - "kubernetes.pod_labels.\"pod-template-hash\"" = "85f59bbfd6" - "kubernetes.pod_node_name" = "ox2-kub-prod18" - "kubernetes.pod_uid" = "93bde4d0-9731-4785-a80e-cd27ba8ad7c2" - "kubernetes.pod_owner" = "test::pod_owner" - 'kubernetes.pod_annotations."containers.vitech.team/frontend.log_source"' = "http_accesslog" - # Данные контейнера извлеченные ранее - cluster_name = "test::dc2-old" - container_name = "frontend" - container_image = "registry-sd.vi.net/site-vi/vi-ru/frontend:202109140836-master" - pod_name = "personal-cabinet-bff-85f59bbfd6-pcshd" - pod_ip = "176.59.54.236" - deployment = "personal-cabinet-bff" - # Обработанное сообщение лога из transform:http_accesslog-k8s - "parsed_accesslog_message.request_id" = "6a432b1e521b042789c1e984aee21127" - "parsed_accesslog_message.user" = "" - "parsed_accesslog_message.address" = "176.59.54.236" - "parsed_accesslog_message.bytes_received"= 4799 - "parsed_accesslog_message.bytes_sent" = 14720 - "parsed_accesslog_message.protocol" = "HTTP/1.1" - "parsed_accesslog_message.scheme" = "https" - "parsed_accesslog_message.method" = "GET" - "parsed_accesslog_message.host" = "nn.vi.ru" - "parsed_accesslog_message.path" = "/TEST:PATH_NOT_EXISTS/" - "parsed_accesslog_message.request_uri" = "/TEST:PATH_NOT_EXISTS/?q=query" - "parsed_accesslog_message.request_query" = "q=query" - "parsed_accesslog_message.referrer" = "https://www.vi.ru/" - "parsed_accesslog_message.user_agent" = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0" - "parsed_accesslog_message.request_time" = 0.3 - "parsed_accesslog_message.status" = 200 - "parsed_accesslog_message.upstream_addr" = "127.0.0.1:4000" - "parsed_accesslog_message.upstream_bytes_received" = 14693 - "parsed_accesslog_message.upstream_response_time" = 0.217 - "parsed_accesslog_message.upstream_status" = "200" - - -############################################################################### -# Tests: transforms.metrics-http-accesslog-k8s_reducer_vi_http_requests_total -############################################################################### - -[[tests]] - name = "transforms.metrics-http-accesslog-k8s_reducer_vi_http_requests_total: 050.4 it prepares http_accesslog event (replaces http_path value with label_override in re)" - - [[tests.inputs]] - insert_at = "metrics-http-accesslog-k8s_exclude_filter" - type = "log" - - [tests.inputs.log_fields] - service_name = "webshop" - namespace = "webshop-production" - # Данные Kubernetes (которые еще не изъяты) - "kubernetes.pod_labels.\"pod-template-hash\"" = "85f59bbfd6" - "kubernetes.pod_node_name" = "ox2-kub-prod18" - "kubernetes.pod_uid" = "93bde4d0-9731-4785-a80e-cd27ba8ad7c2" - "kubernetes.pod_owner" = "test::pod_owner" - 'kubernetes.pod_annotations."containers.vitech.team/frontend.log_source"' = "http_accesslog" - # Данные контейнера извлеченные ранее - cluster_name = "test::dc2-old" - container_name = "frontend" - container_image = "registry-sd.vi.net/site-vi/vi-ru/frontend:202109140836-master" - pod_name = "webshop-85f59bbfd6-pcshd" - pod_ip = "176.59.54.236" - deployment = "webshop" - # Обработанное сообщение лога - "http_request_id" = "6a432b1e521b042789c1e984aee21128" - "http_url" = "https://nn.vi.ru/vendor/gigant/?brand_product_block_ab=0" - "http_scheme" = "https" - "http_host" = "nn.vi.ru" - "http_port" = 443 - "http_method" = "GET" - "http_path" = "/vendor/gigant/" - "http_query_string" = "brand_product_block_ab=0" - "http_version" = "HTTP/1.1" - "http_referer" = "https://www.vi.ru/" - "http_user_agent" = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0" - "http_status_code" = 200 - "duration_sec" = 0.300 - "traceparent" = "" - "trace_id" = "" - "network_bytes_written" = 14720 - "network_client_ip" = "176.59.54.236" - "upstream_addr" = "127.0.0.1:4000" - "upstream_network_bytes_written" = 14693 - "upstream_duration_sec" = 0.217 - - [[tests.outputs]] - extract_from = "metrics-http-accesslog-k8s_reducer_vi_http_requests_total" - - [[tests.outputs.conditions]] - type = "vrl" - source = ''' - # Должна была произайти замена .http_path для снижения кардинальности метрики по этой метке - assert_eq!(.http_path, "/vendor/:name") - ''' - -############################################################################### -# Tests: transforms.metrics-http-accesslog-k8s-filter-metric_vi_kubedns_http_requests_total -############################################################################### - -[[tests]] - name = "transforms.metrics-http-accesslog-k8s_reducer_vi_kubedns_http_requests_total: 050.5 it skips events not from host hvaing local suffix" - no_outputs_from = ["metrics-http-accesslog-k8s-filter-metric_vi_kubedns_http_requests_total"] - - [[tests.inputs]] - insert_at = "metrics-http-accesslog-k8s_exclude_filter" - type = "log" - - [tests.inputs.log_fields] - service_name = "webshop" - namespace = "webshop-production" - # Данные Kubernetes (которые еще не изъяты) - "kubernetes.pod_labels.\"pod-template-hash\"" = "85f59bbfd6" - "kubernetes.pod_node_name" = "ox2-kub-prod18" - "kubernetes.pod_uid" = "93bde4d0-9731-4785-a80e-cd27ba8ad7c2" - "kubernetes.pod_owner" = "test::pod_owner" - 'kubernetes.pod_annotations."containers.vitech.team/frontend.log_source"' = "http_accesslog" - # Данные контейнера извлеченные ранее - cluster_name = "test::dc2-old" - container_name = "frontend" - container_image = "registry-sd.vi.net/site-vi/vi-ru/frontend:202109140836-master" - pod_name = "webshop-85f59bbfd6-pcshd" - pod_ip = "176.59.54.236" - deployment = "webshop" - # Обработанное сообщение лога - "http_request_id" = "6a432b1e521b042789c1e984aee21128" - "http_url" = "https://nn.vi.ru/vendor/gigant/?brand_product_block_ab=0" - "http_scheme" = "https" - "http_host" = "nn.i-m-a-host-without-l-o-c-a-l-suffix.ru" - "http_port" = 443 - "http_method" = "GET" - "http_path" = "/vendor/gigant/" - "http_query_string" = "brand_product_block_ab=0" - "http_version" = "HTTP/1.1" - "http_referer" = "https://www.vi.ru/" - "http_user_agent" = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0" - "http_status_code" = 200 - "duration_sec" = 0.300 - "traceparent" = "" - "trace_id" = "" - "network_bytes_written" = 14720 - "network_client_ip" = "176.59.54.236" - "upstream_addr" = "127.0.0.1:4000" - "upstream_network_bytes_written" = 14693 - "upstream_duration_sec" = 0.217 - -{# end --> if vector_environment == 'testing' #} -{% endif %} diff --git a/files/aggregator/transforms/logs-to-metrics.toml.j2 b/files/aggregator/transforms/logs-to-metrics.toml.j2 index bf908a4..6a399ed 100644 --- a/files/aggregator/transforms/logs-to-metrics.toml.j2 +++ b/files/aggregator/transforms/logs-to-metrics.toml.j2 @@ -3,189 +3,115 @@ # ############################################################################### +# GENERATED CODE. DO NOT CHANGE IT! +############################################################################### + +{% from 'macro-build_selector_matching_expr.j2' import build_selector_matching_expr, field_value_replacer with context %} [transforms.metrics-http-accesslog-k8s_exclude_filter] type = "filter" inputs = ["http_accesslog-k8s"] -condition = ''' +condition = { type = "vrl", source = ''' + exclude_event = false -exclude_event = false {%- for selector_conf in metrics_catalog.exclude_events -%} -{{ ' ' }} || # selector_id: selectors/{{loop.index0 }} -(( true {# оборачиваем выражения одного selector #} - {%- for logfield_name, fconf in selector_conf.filter.items() %} -{# newline #} -{# Тут выполняем выбор оператора сравнения и констуируем выражение -#} -{# DEBUG: {{ logfield_name }}: {{ fconf }} -#} - {%- if fconf.eq is defined and fconf.eq != "" %} -&& includes([to_string!(.{{ logfield_name }})], "{{ fconf.eq }}") - {%- endif -%} - {%- if fconf.neq is defined and fconf.neq != "" %} -&& !includes([to_string!(.{{ logfield_name }})], "{{ fconf.neq }}") - {%- endif -%} - {%- if fconf.re is defined %} - {%- if fconf.re != "" %} -&& match(to_string!(.{{ logfield_name }}), r'{{ fconf.re }}') - {%- else %} -[transforms.metrics-http-accesslog-k8s_exclude_filter] Build text: ERROR: invalid regexp '{{ fconf.re }}' for {{ namespace }}/{{ metric_name }}, selector: {{ selector_conf.selector }}, filter: re - {%- endif -%} - {%- endif -%} - {%- if fconf.nre is defined %} - {%- if fconf.nre != "" %} -&& !match(.{{ logfield_name }}, r'{{ fconf.nre }}') - {%- else %} -Build text: ERROR: invalid regexp '{{ fconf.nre }}' for {{ namespace }}/{{ metric_name }}, selector: {{ selector_conf.selector }}, filter: nre - {%- endif -%} - {%- endif -%} - {%- if fconf.oneOfRe is defined %} -&& match_any!(.{{ logfield_name }}, [ - {%- for one_re in fconf.oneOfRe %} -r'{{ one_re }}', - {%- endfor %} -]) - {%- endif -%} - {% endfor %} -{{ ' ' }} )) -{%- endfor %} -{{ ' ' }} -{# newline #} + {%- for event_selector in metrics_catalog.exclude_events -%} + {%- set selector_id = "selectors/" + loop.index0 | string -%} + || # selector_id: {{ selector_id }} + {{ build_selector_matching_expr(event_selector, "exclude_event, selector: {{ event_selector.selector }}") }} + {%- endfor %} -is_allowed = !exclude_event -is_allowed -''' + is_allowed = !exclude_event + is_allowed +''' } -# 1) Создаем трансформ с фильтром под каждую метрику (уникальное имя фильтра с общим префиксом) +# 1) Трасформ должен отбирать события подходящие для фильтра и сразу выполнять замены в данном событии через label_override # 2) Создаем определения метрик под каждую метрику # 3) Собираем все источники метрик в общий metrics-http-accesslog-k8s + # Проходим по всем namespace и метрикам, создаем условия отбора для событий из input -{%- set _filter_name_prefix = "metrics-http-accesslog-k8s-filter-metric_" %} -{%- for namespace, metric in metrics_catalog.metrics.items() -%} - {%- for metric_name, metric_conf in metric.items() -%} - {%- set _metric_full_name = namespace + "_" + metric_name -%} - {%- set _filter_name = _filter_name_prefix + _metric_full_name %} +{%- set _remap_name_prefix = "log-metric-prepare_metric_" %} +{%- set _collector_name_prefix = "log-metric-collector_metric_" %} +{% set _all_collector_inputs = [] %} + +{%- for _metric_namespace, _metric_def in metrics_catalog.metrics.items() -%} + {%- for _metric_name, _metric_conf in _metric_def.items() -%} + {%- set _metric_full_name = _metric_namespace + "_" + _metric_name -%} + {%- set _remap_name = _remap_name_prefix + _metric_full_name %} + {%- set _collector_name = _collector_name_prefix + _metric_full_name %} + {{- _all_collector_inputs.append( _collector_name ) }} {{ ' ' }} ############################################################################### -# GENERATED CODE: for {{ _filter_name }} -[transforms.{{ _filter_name }}] -type = "filter" -inputs = ["metrics-http-accesslog-k8s_exclude_filter"] -condition = ''' - false {%- for selector_conf in metric_conf.event_selectors -%} -{{ ' ' }} || # selector_id: {{ namespace }}/{{ metric_name }}/selectors/{{loop.index0 }} -(( true {# оборачиваем выражения одного selector #} - {%- for logfield_name, fconf in selector_conf.filter.items() %} -{# newline #} -{# Тут выполняем выбор оператора сравнения и констуируем выражение -#} -{# DEBUG: {{ logfield_name }}: {{ fconf }} -#} - {%- if fconf.eq is defined and fconf.eq != "" %} - && includes([to_string!(.{{ logfield_name }})], "{{ fconf.eq }}") - {%- endif -%} - {%- if fconf.neq is defined and fconf.neq != "" %} - && !includes([to_string!(.{{ logfield_name }})], "{{ fconf.neq }}") - {%- endif -%} - {%- if fconf.re is defined %} - {%- if fconf.re != "" %} - && match(to_string!(.{{ logfield_name }}), r'{{ fconf.re }}') - {%- else %} -[transforms.metrics-http-accesslog-k8s_routes] Build text: ERROR: invalid regexp '{{ fconf.re }}' for {{ namespace }}/{{ metric_name }}, selector: {{ selector_conf.selector }}, filter: re - {%- endif -%} - {%- endif -%} - {%- if fconf.nre is defined %} - {%- if fconf.nre != "" %} - && !match(.{{ logfield_name }}, r'{{ fconf.nre }}') - {%- else %} -Build text: ERROR: invalid regexp '{{ fconf.nre }}' for {{ namespace }}/{{ metric_name }}, selector: {{ selector_conf.selector }}, filter: nre - {%- endif -%} - {%- endif -%} - {%- if fconf.oneOfRe is defined %} - && match_any!(.{{ logfield_name }}, [ - {%- for one_re in fconf.oneOfRe %} -r'{{ one_re }}', - {%- endfor %} -]) - {%- endif -%} - {% endfor %} -{{ ' ' }} )) - {%- endfor -%} - {{ ' ' }} - ''' -{# newline #} - {%- endfor %} -{%- endfor %} - - +# GENERATED CODE. DO NOT CHANGE IT! ############################################################################### -{%- for namespace, metric in metrics_catalog.metrics.items() -%} - {%- for metric_name, metric_conf in metric.items() -%} - {%- set _metric_full_name = namespace + "_" + metric_name -%} - {%- set _input_name = _filter_name_prefix + _metric_full_name -%} - {%- set _reducer_name = "metrics-http-accesslog-k8s_reducer_" + _metric_full_name %} -# GENERATED CODE: for {{ _metric_full_name }} -[transforms.{{ _reducer_name }}] + +[transforms.{{ _remap_name }}] type = "remap" -inputs = [ "{{ _input_name }}" ] +inputs = ["metrics-http-accesslog-k8s_exclude_filter"] +# +drop_on_abort = true source = ''' - ### - ## Делаем замены значений в полях на константы для снижения кардинальности метрик. Например, заменяем .http_path = /product/perchatki на .http_path = /product/:name - ## - {% set _string_fields = dict() %} - {# newline #} - {%- for selector_conf in metric_conf.event_selectors -%} - {% for logfield_name, fconf in selector_conf.filter.items() -%} - {% if logfield_name not in _string_fields -%} - {%- set _= _string_fields.update( {logfield_name: 1 }) -%} -.{{ logfield_name }} = to_string(.{{ logfield_name }}) ?? "error:not_a_string:{{ logfield_name }}" # требуется, чтобы не ругался replace -{# newline #} - {%- endif -%} -{# newline #} -{#- Тут выполняем выбор оператора сравнения и констуируем выражение -#} - {%- if fconf.re is defined %} - {%- if fconf.re != "" %} - {%- if fconf.label_override is defined and fconf.label_override != "" %} -.{{ logfield_name }} = replace(.{{ logfield_name }}, r'{{ fconf.re }}', s'{{ fconf.label_override }}'); -{# newline #} - {%- endif %} - {%- else %} -[{{ _reducer_name }}] Build text: ERROR: invalid regexp '{{ fconf.re }}' for {{ namespace }}/{{ metric_name }}, selector: {{ selector_conf.selector }}, filter: re - {%- endif %} - {%- endif %} - {%- endfor %} - {%- endfor %} +{% for event_selector in _metric_conf.event_selectors -%} + {%- set selector_id = loop.index0 | string %} + {%- set selector_name = event_selector.selector %} + {%- set source_ref = _metric_full_name + "/selector[" + selector_id + "]: " + selector_name %} + + # source_ref: {{ source_ref }} + if ( {{ build_selector_matching_expr(event_selector, source_ref) }} ) { +{% for logfield_name, operator in event_selector.filter.items() %} + {%- if operator.re is defined %} + {%- if operator.re != "" %} + {%- if operator.label_override is defined and operator.label_override != "" %} + ## ! нужно снижение кардинальности значений - установлен label_override +{# logfield_name всегда строка и валидируется через json-schaema в исходных данных #} + .{{ logfield_name }} = to_string!(.{{ logfield_name }}); + .{{ logfield_name }} = replace(.{{ logfield_name }}, r'{{ operator.re }}', s'{{ operator.label_override }}'); + # + {%- else %} + {# no label_override #} + {%- endif %} + {%- else %} + Build text: ERROR: invalid regexp '{{ operator.re }}' in filter: re expr in {{ source_ref }}, + {%- endif %} +{{ '' }} + {%- endif %} + {%- endfor %} +true; + } + {%- if not loop.last %} + else {{' '}} + {%- else %} + else { + abort # совпадений не было найдено, удалить это событие + } + {%- endif %} +{% endfor %} + ''' -{{ ' ' }} {# newline #} - {%- endfor %} -{%- endfor %} ### ### GENERATED METRCS DEFINITIONS ### ############################################################################### -{% set metric_sources = [] %} -{%- for namespace, metric in metrics_catalog.metrics.items() -%} - {%- for metric_name, metric_conf in metric.items() -%} - {%- set _metric_full_name = namespace + "_" + metric_name %} - {%- set _input_name = "metrics-http-accesslog-k8s_reducer_" + _metric_full_name -%} - {%- set _transform_name = "metrics-http-accesslog-k8s-metric-" + _metric_full_name %} - {{- metric_sources.append( _transform_name ) }} - -[transforms.{{ _transform_name }}] + +[transforms.{{ _collector_name }}] type = "log_to_metric" -inputs = [ "{{ _input_name }}" ] - - [[transforms.{{ _transform_name }}.metrics]] - type = "{{ metric_conf.metric_type }}" - field = "{{ metric_conf.group_by_logfield }}" - name = "{{ metric_name }}" -{% if namespace != "" %} - namespace = "{{ namespace }}" +inputs = [ "{{ _remap_name }}" ] + + [[transforms.{{ _collector_name }}.metrics]] + type = "{{ _metric_conf.metric_type }}" + field = "{{ _metric_conf.group_by_logfield }}" +{% if _metric_namespace != "" %} + namespace = "{{ _metric_namespace }}" {% endif %} + name = "{{ _metric_name }}" - [transforms.{{ _transform_name }}.metrics.tags] + [transforms.{{ _collector_name }}.metrics.tags] environment = "{{ vector_environment }}" - {% for label, logfield in metric_conf.label_logfield_mapping.items() -%} + {% for label, logfield in _metric_conf.label_logfield_mapping.items() -%} {{ label }} = "{{ '{{' }}{{ logfield }}{{ '}}' }}" {% endfor %} @@ -193,7 +119,9 @@ inputs = [ "{{ _input_name }}" ] {% endfor %} +############################################################################### + [transforms.metrics-http-accesslog-k8s] type = "filter" -inputs = [{{ metric_sources | map("to_json") | join(',')}}] +inputs = [{{ _all_collector_inputs | map("to_json") | join(',')}}] condition = '''true''' diff --git a/files/aggregator/transforms/macro-build_selector_matching_expr.j2 b/files/aggregator/transforms/macro-build_selector_matching_expr.j2 new file mode 100644 index 0000000..b350cd8 --- /dev/null +++ b/files/aggregator/transforms/macro-build_selector_matching_expr.j2 @@ -0,0 +1,21 @@ +{%- macro build_selector_matching_expr(event_selector, source_ref) -%} +{%- set _conditions = [] -%} +{%- set _field_conditions = event_selector.filter.items() -%} + {%- for logfield_name, operator in _field_conditions -%} + {%- if operator.eq is defined and operator.eq != "" -%} + {{ _conditions.append("( to_string!( ." ~ logfield_name ~ " ) == \"" ~ operator.eq | replace('\"', '\\\"') ~ "\" )") }} + + {%- elif operator.neq is defined and operator.neq != "" -%} + {{ _conditions.append("( to_string!( ." ~ logfield_name ~ " ) != \"" ~ operator.neq | replace('\"', '\\\"') ~ "\" )") }} + + {%- elif operator.re is defined and operator.re != "" -%} + {{ _conditions.append("match( to_string!( ." ~ logfield_name ~ " ), r'" ~ operator.re | replace('\'', '\\\'') ~ "' )") }} + + {%- elif operator.nre is defined and operator.nre != "" -%} + {{ _conditions.append("!match( to_string!( ." ~ logfield_name ~ " ), r'" ~ operator.nre | replace('\'', '\\\'') ~ "' )") }} + + {%- endif -%} + {%- endfor -%} + +{{ _conditions|join(" && ") }} +{%- endmacro -%}