Привет, Хабр!

Меня зовут Валентин, я DevOps-инженер команды Platform V Kintsugi. Мы развиваем облачный сервис и часто сталкиваемся с задачами эксплуатации распределённых систем, созданных на основе Kubernetes и Istio/Platform V Synapse Service Mesh.

Одна из самых неприятных задач — поиск проблем в обработке запроса внутри Service Mesh на базе Envoy. Типичная ситуация: один из сервисов внезапно начинает получать 503, появляются неожиданные повторы, запросы перестают проходить из-за AuthorizationPolicy или уходят не в тот upstream. При этом само приложение работает нормально, в его журналах нет ошибок, а конфигурация Istio, на первый взгляд, выглядит корректной.

Мы начинаем разбираться и смотрим access-журналы. Видим результат обработки запроса, но не всегда понимаем, что именно Envoy получил, какие заголовки и тело запроса он увидел, какие фильтры уже успели его изменить и почему в итоге запрос пошёл именно по этому пути. Сетевой дамп тоже не всегда помогает. При включённом mTLS на уровне сети мы можем увидеть зашифрованный трафик, но не тот HTTP-запрос, который уже расшифровал и обрабатывает Envoy. Получается неприятный диагностический разрыв, когда приложение видит одно, сеть показывает другое, а решение принимает Envoy. Возникает главный вопрос, что на самом деле видит Envoy и приложение в момент обработки HTTP-запроса?

В этой статье разберём, как с помощью Envoy Tap Filter заглянуть внутрь этого процесса — перехватить HTTP-запрос и ответ уже после расшифровки mTLS, отфильтровать только нужный трафик и сохранить результат в файл. На практике посмотрим, как подключить Tap Filter к Istio через EnvoyFilter, настроить точечный отбор запросов, выбрать формат сохранения тела и проанализировать полученные JSON-файлы. В результате получим инструмент для тех случаев, когда стандартных журналов недостаточно, а сетевой дамп показывает только зашифрованный транспорт.

Почему стандартных инструментов бывает недостаточно

Tap Filter полезен в ситуациях, когда стандартных инструментов диагностики недостаточно. Для анализа проблем можно использовать access-журналы Envoy, снять сетевой дамп с помощью tcpdump или исследовать трафик в Wireshark. Однако у каждого из этих инструментов есть свои ограничения:

  • Access-журналы содержат только итоговую информацию о запросе, результате его обработки и не показывают полный HTTP-запрос, тело сообщения или изменения, которые вносят HTTP-фильтры Envoy.

  • Сетевой дамп (tcpdump, Wireshark) показывает происходящее на уровне TCP и TLS. В Service Mesh взаимодействие между сервисами обычно защищено mTLS, и он чаще всего содержит лишь зашифрованный трафик. А использование tcpdump далеко не всегда удобно или вообще возможно.

Tap Filter позволяет перехватывать HTTP и gRPC-трафик прямо во время обработки запроса прокси — уже после расшифровки mTLS и до передачи запроса приложению или следующему сервису. Благодаря этому можно увидеть реальные заголовки, тело запроса и ответа, а также увидеть запрос в той точке цепочки, где стоит Tap.

Упрощённо цепочка обработки HTTP-запроса выглядит так:

Tap — это обычный HTTP-фильтр Envoy, поэтому имеет значение место его установки в цепочке. Если фильтр стоит после JWT-аутентификации или Lua-фильтра, то он увидит уже изменённый запрос. Если разместить его раньше, то можно наблюдать исходный HTTP-запрос до применения остальных фильтров. Поэтому позиция Tap в HTTP filter chain определяет, какую версию запроса мы увидим. В нашем случае установка непосредственно перед router позволяет наблюдать запрос после обработки расположенными перед Tap HTTP-фильтрами.

Механизм Tap: match-конфигурация, Sink и форматы данных

Механизм Tap состоит из трёх независимых частей:

  • Tap Filter встраивается в цепочку HTTP-фильтров и получает доступ к проходящему трафику.

  • Match-конфигурация определяет, какие запросы необходимо перехватывать. Например, можно отбирать только запросы к определённому пути, методу или виртуальному хосту.

  • Sink определяет, куда будут отправлены результаты перехвата.

Такое разделение делает Tap удобным инструментом диагностики. Один и тот же фильтр можно использовать для различных сценариев, меняя только правила отбора трафика и способ сохранения результатов.

Tap поддерживает несколько форматов представления данных. Чаще всего используются:

  • JSON_BODY_AS_BYTES: тело кодируется в Base64. Подходит для передачи бинарных данных и является форматом по умолчанию.

  • JSON_BODY_AS_STRING: тело сообщения выводится как строка. Наиболее удобный вариант для анализа JSON и текстовых HTTP-запросов.

  • PROTO_TEXT: текстовое представление protobuf-структур Envoy.

  • PROTO_BINARY: бинарный protobuf-формат для последующей машинной обработки.

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

PROTO_TEXT даёт ту же самую структуру данных, что и JSON-варианты, но в textproto-нотации, а PROTO_BINARY вообще не предназначен для чтения глазами, потому что без декодера это просто набор байтов.

После того как Tap Filter сформировал событие, его необходимо сохранить или передать получателю, за это отвечает sink. Envoy поддерживает несколько вариантов:

  • file per tap — сохранение каждого события в отдельный файл;

  • streaming admin— потоковая передача через Admin API;

  • gRPC sink — отправка событий на внешний сервис по протоколу gRPC.

Каждый из вариантов ориентирован на свой сценарий использования.

Streaming admin удобен для локальной диагностики и чаще всего встречается в примерах из официальной документации Envoy. Но он полностью зависит от доступности административного интерфейса Envoy. Если admin API не включён или закрыт, воспользоваться sink не получится.

gRPC Sink предназначен для централизованного сбора событий с одного или нескольких экземпляров Envoy. В этом случае необходимо реализовать или использовать внешний gRPC-сервис, принимающий сообщения Tap по соответствующему protobuf API. Это удобно при построении собственных систем диагностики и мониторинга, но нужна дополнительная инфраструктура.

В статье мы будем использовать file per tap. Этот способ не зависит от наличия Admin API, не требует внешнего коллектора и хорошо подходит для работы в Kubernetes. После генерирования тестового трафика достаточно открыть каталог с результатами в sidecar-контейнере и проанализировать сформированные файлы. Несмотря на простоту, этот вариант сохраняет всю необходимую информацию о перехваченном HTTP-трафике и отлично подходит для разовых расследований и изучения работы Envoy.

Интеграция Tap Filter в Istio через EnvoyFilter

Сам по себе Tap Filter — это расширение Envoy, которое ничего не знает об Istio. Поэтому первый вопрос при использовании Service Mesh: как добавить этот фильтр в sidecar-прокси?

Управлением конфигурацией Envoy занимается Control Plane (istiod), который распространяет xDS-конфигурацию на все sidecar-прокси. Для большинства типовых задач Istio предоставляет собственные API: VirtualService, DestinationRule, AuthorizationPolicy и другие. Но Tap Filter относится к низкоуровневым возможностям Envoy, и отдельного CRD для его настройки в Istio не существует. Поэтому для подключения используется ресурс EnvoyFilter.

EnvoyFilter позволяет точечно вмешаться в конфигурацию прямо перед её передачей в sidecar. Он помогает добавлять собственные фильтры, изменять существующие или переопределять отдельные параметры Envoy. Именно через него мы добавим HTTP Tap Filter в цепочку обработки запросов. Tap — это HTTP-фильтр, поэтому встраивать его нужно в HttpConnectionManager. В терминах EnvoyFilter это означает применение патча к объекту HTTP_FILTER внутри сетевого фильтра envoy.filters.network.http_connection_manager.

Сам HttpConnectionManager — это тоже Network Filter, внутри которого располагается цепочка HTTP фильтров. На практике приходится работать сразу с двумя уровнями конфигурации: сначала найти нужный HttpConnectionManager, а затем вставить в него HTTP Tap Filter.

Ещё одна особенность ресурса — возможность ограничить его действие конкретными рабочими нагрузками. За это отвечает селектор workloadSelector. Он позволяет:

  • включить Tap только для одного сервиса;

  • исследовать поведение конкретного компонента;

  • не изменять конфигурацию остальных sidecar в mesh.

Благодаря этому вместо глобального применения достаточно выбрать один или несколько сервисов для диагностики. В нашем примере далее мы применим EnvoyFilter только к одному сервису, чтобы увидеть принцип работы Tap, не затрагивая остальные компоненты Service Mesh.

Практическая часть: стенд, манифесты и настройка доступа к файлам

Теория закончилась — теперь посмотрим, как использовать Tap Filter на практике. В качестве тестового стенда будем использовать Kubernetes-кластер с установленным Istio. Тестовое приложение — простой web-сервис на основе nginx с меткой app: webapp, назначение которого — принимать наши запросы и формировать ответы на них. В качестве клиента — под client с утилитой curl на борту. С таким набором можно быстро генерировать HTTP-запросы и анализировать результаты перехвата.

Для демонстрации развернём тестовое приложение webapp с включённым Istio sidecar. Ниже приведён сокращённый манифест Deployment, содержащий только настройки, необходимые для исследования работы Tap Filter.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  labels:
    app: webapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
      annotations:
        sidecar.istio.io/inject: "true"
        sidecar.istio.io/userVolume: |
          [{"name":"tmp","emptyDir":{}}]
        sidecar.istio.io/userVolumeMount: |
          [{"name":"tmp","mountPath":"/tmp"}]
    spec:
      containers:
        - name: webapp
          image: nginx:1.25-webapp # Образ тестового веб-приложения, принимающего подключения на порту 8080
          ports:
            - containerPort: 8080
              name: http

Ключевой момент — аннотации sidecar.istio.io/userVolume и sidecar.istio.io/userVolumeMount. Они создают emptyDir и монтируют его в /tmp контейнера istio-proxy. Том нужен, потому что корневая ФС istio-proxy по умолчанию read-only, и без него Envoy не сможет записать tap-файлы. Приложению каталог не нужен, поэтому монтируем его только в sidecar.

В нашей конфигурации:

file_per_tap:
  path_prefix: /tmp/tap_

Envoy будет генерировать файлы вида:

/tmp/tap_123456789.json

Проверить это можно командой:

kubectl exec -ti deploy/webapp -c istio-proxy -- ls -l /tmp

В примере выбрано:

sidecar.istio.io/userVolume: |
  [{"name":"tmp","emptyDir":{}}]
sidecar.istio.io/userVolumeMount: |
  [{"name":"tmp","mountPath":"/tmp"}]

Это значит, что tap-файлы попадают в эфемерное хранилище, жизненный цикл которого привязан к поду. При удалении пода эти данные также удаляются. Для диагностики это удобно, так как ничего не пишется в постоянные тома, а перехваченные данные не остаются в кластере после завершения расследования.

Конфигурация фильтрации: выборочное включение и условия перехвата (and_match)

Теперь необходимо встроить Tap Filter в HTTP-конвейер sidecar-прокси. Для этого создадим ресурс EnvoyFilter, который:

  • применяется только к поду webapp с помощью workloadSelector;

  • находит HttpConnectionManager;

  • вставляет envoy.filters.http.tap непосредственно перед фильтром envoy.filters.http.router.

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: tap-filter
spec:
  workloadSelector:
    labels:
      app: webapp
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: SIDECAR_INBOUND
        listener:
          filterChain:
            filter:
              name: envoy.filters.network.http_connection_manager
              subFilter:
                name: envoy.filters.http.router
      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.filters.http.tap
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.tap.v3.Tap
            common_config:
              # конфигурацию перехвата и вывода — разберём ниже

В этом примере Tap встраивается во входящий HTTP-конвейер (SIDECAR_INBOUND) непосредственно перед фильтром envoy.filters.http.router. Это означает, что перехват выполняется для входящих запросов прямо перед их передачей приложению. Таким образом фильтр наблюдает итоговый HTTP-запрос — уже после того, как остальные компоненты конвейера внесли в него свои изменения.

После добавления фильтра остаётся указать место для сохранения результатов. Мы будем использовать sink типа file per tap, так каждое перехваченное событие запишется в отдельный файл внутри каталога sidecar-контейнера. Начнём с формата по умолчанию — JSON_BODY_AS_BYTES:

typed_config:
  "@type": type.googleapis.com/envoy.extensions.filters.http.tap.v3.Tap
  common_config:
    static_config:
      match_config:
        any_match: true
      output_config:
        sinks:
          - format: JSON_BODY_AS_BYTES
            file_per_tap:
              path_prefix: /tmp/tap_

Разберём параметры:

  • match_config.any_match перехватывает весь HTTP-трафик, проходящий через фильтр. В реальных сценариях вместо any_match обычно используют более точные условия (URI, метод, заголовки и так далее), чтобы не записывать лишние данные (к этому мы вернёмся чуть позже);

  • output_config определяет, как будут представлены результаты перехвата и куда они будут сохранены;

  • format: JSON_BODY_AS_BYTES— тело HTTP-сообщений кодируется в Base64;

  • file_per_tap.path_prefix — префикс пути, по которому Envoy будет создавать файлы с результатами перехвата. Для каждого события создаётся отдельный файл с автоматически сгенерированным именем.

Примечание. Для создания tap-файлов у Envoy должны быть права на запись в указанный каталог. Sidecar-контейнер istio-proxy работает не от root, поэтому убедитесь, что выбранный path_prefix указывает на директорию, доступную ему на запись.

Ограничиваем область сбора:

workloadSelector:
  labels:
    app: webapp

EnvoyFilter применится только к поду с меткой app: webapp. Так можно включать Tap выборочно, не изменяя конфигурацию остальных sidecar-прокси в Service Mesh.

Не стоит оставлять any_match: true в реальной работе — под запись попадёт весь трафик сервиса. Зададим более точные условия:

match_config:
  and_match:
    rules:
      - http_request_headers_match:
          headers:
            - name: ":method"
              string_match:
                exact: "POST"
      - http_request_headers_match:
          headers:
            - name: ":path"
              string_match:
                prefix: "/test-tap"
      - http_request_headers_match:
          headers:
            - name: "x-debug"
              string_match:
                exact: "true"

Здесь задаются условия, при выполнении которых Tap будет сохранять трафик. В нашем случае используется оператор and_match, поэтому все три условия должны выполняться одновременно:

  • HTTP-метод — POST;

  • путь начинается с /test-tap, где /test-tap — тестовый endpoint приложения webapp, к которому будут адресованы наши запросы;

  • присутствует заголовок x-debug: true.

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

Настраиваем параметры вывода:

output_config:
  max_buffered_rx_bytes: 4096
  max_buffered_tx_bytes: 4096
  sinks:
    - format: JSON_BODY_AS_BYTES
      file_per_tap:
        path_prefix: /tmp/tap_

Параметры max_buffered_rx_bytes и max_buffered_tx_bytes определяют максимальный объём буфера для тела запроса и ответа. В этом примере Envoy сохранит не более 4096 байтов для каждого направления. Если тело сообщения окажется больше, то в файл попадёт только его начало.

Собираем всё вместе и получаем итоговый манифест:

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: tap-filter
spec:
  workloadSelector:
    labels:
      app: webapp
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: SIDECAR_INBOUND
        listener:
          filterChain:
            filter:
              name: envoy.filters.network.http_connection_manager
              subFilter:
                name: envoy.filters.http.router
      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.filters.http.tap
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.tap.v3.Tap
            common_config:
              static_config:
                match_config:
                  and_match:
                    rules:
                      - http_request_headers_match:
                          headers:
                            - name: ":method"
                              string_match:
                                exact: "POST"
                      - http_request_headers_match:
                          headers:
                            - name: ":path"
                              string_match:
                                prefix: "/test-tap"
                      - http_request_headers_match:
                          headers:
                            - name: "x-debug"
                              string_match:
                                exact: "true"
                output_config:
                  max_buffered_rx_bytes: 4096
                  max_buffered_tx_bytes: 4096
                  sinks:
                    - format: JSON_BODY_AS_BYTES
                      file_per_tap:
                        path_prefix: /tmp/tap_

Демонстрация работы: запуск теста, чтение файлов и проверка Base64

После применения EnvoyFilter отправим тестовый запрос к сервису webapp:

kubectl exec -ti deploy/client -c client -- \
  curl -i -X POST \
  -H "x-debug: true" \
  -H "Content-Type: application/json" \
  webapp:8080/test-tap \
  -d '{"message": "This message is for testing tap filter"}'

HTTP/1.1 200 OK
server: envoy
content-type: application/octet-stream,application/json
content-length: 45
x-envoy-upstream-service-time: 7

{"status":"ok","message":"Hello from webapp"}

Видим, что в каталоге /tmp sidecar-контейнера был создан файл:

kubectl exec -ti deploy/webapp -c istio-proxy -- ls -lh /tmp/
total 4.0K
-rw-r--r--. 1 10002 10001 3.7K Aug 23 12:27 tap_393295138313463914.json

Ознакомимся с его содержимым:

kubectl exec -ti deploy/webapp -c istio-proxy -- cat /tmp/tap_393295138313463914.json
{
  "http_buffered_trace": {
    "request": {
      "headers": [
        { "key": ":authority", "value": "webapp:8080" },
        { "key": ":path", "value": "/test-tap" },
        { "key": ":method", "value": "POST" },
        { "key": ":scheme", "value": "http" },
        { "key": "user-agent", "value": "curl/7.74.0" },
        { "key": "accept", "value": "*/*" },
        { "key": "x-debug", "value": "true" },
        { "key": "content-type", "value": "application/json" },
        { "key": "content-length", "value": "53" },
        { "key": "x-forwarded-proto", "value": "http" },
        { "key": "x-request-id", "value": "3ec2615a-78d6-96e2-9c8f-5f89fc445bdd" },
        { "key": "x-envoy-attempt-count", "value": "1" },
        { "key": "x-forwarded-client-cert", "value": "By=spiffe://cluster.local/ns/demo/sa/default;Hash=...;URI=spiffe://cluster.local/ns/demo/sa/default" },
        { "key": "x-b3-traceid", "value": "0d67cbb9750fce8b4e574835eaabf0a3" },
        { "key": "x-b3-spanid", "value": "e98c9909af027a87" },
        { "key": "x-b3-parentspanid", "value": "4e574835eaabf0a3" },
        { "key": "x-b3-sampled", "value": "1" }
      ],
      "body": {
        "as_bytes": "eyJtZXNzYWdlIjogIlRoaXMgbWVzc2FnZSBpcyBmb3IgdGVzdGluZyB0YXAgZmlsdGVyIn0=",
        "truncated": false
      },
      "trailers": []
    },
    "response": {
      "headers": [
        { "key": ":status", "value": "200" },
        { "key": "server", "value": "istio-envoy" },
        { "key": "content-type", "value": "application/octet-stream,application/json" },
        { "key": "content-length", "value": "45" },
        { "key": "x-envoy-upstream-service-time", "value": "0" }
      ],
      "body": {
        "as_bytes": "eyJzdGF0dXMiOiJvayIsIm1lc3NhZ2UiOiJIZWxsbyBmcm9tIHdlYmFwcCJ9",
        "truncated": false
      },
      "trailers": []
    }
  }
}

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

В сгенерированном файле мы видим объекты запроса и объекты ответа. Из содержимого видно, что в наш сервис пришёл запрос с заданными заголовками и телом сообщения. Обратите внимание, что кроме отправленных заголовков в перехваченном запросе есть служебные заголовки, добавленные самим mesh: идентификатор запроса x-request-id, трейсинг-заголовки x-b3-* и x-forwarded-client-cert с идентификатором вызывающего сервиса. Именно эта информация чаще всего и оказывается нужна при разборе инцидентов, она показывает, что реально дошло до приложения после обработки всеми фильтрами Envoy.

Посмотрим подробнее на тело сообщения, которое пришло в виде последовательности байтов, закодированных в Base64:

"eyJtZXNzYWdlIjogIlRoaXMgbWVzc2FnZSBpcyBmb3IgdGVzdGluZyB0YXAgZmlsdGVyIn0="

Проверим:

echo "eyJtZXNzYWdlIjogIlRoaXMgbWVzc2FnZSBpcyBmb3IgdGVzdGluZyB0YXAgZmlsdGVyIn0=" | base64 --decode
{"message": "This message is for testing tap filter"}

Тело действительно совпадает с тем, что мы отправили в запросе. Формат JSON_BODY_AS_BYTES работает корректно, но для повседневной диагностики неудобен. Декодирование Base64 вручную каждый раз — это лишний шаг. Особенно если тело большое или нужно быстро просмотреть несколько файлов подряд. Немного позже мы посмотрим, как этого избежать. А пока убедимся, что and_match действительно фильтрует трафик, а не просто присутствует в конфигурации. Например, отправим запрос, который не удовлетворяет одному из условий — без заголовка x-debug:

kubectl exec -ti deploy/client -c client -- \
  curl -i -X POST \
  -H "Content-Type: application/json" \
  webapp:8080/test-tap \
  -d '{"message": "no debug header"}'

HTTP/1.1 200 OK
server: envoy
content-type: application/octet-stream,application/json
content-length: 45
x-envoy-upstream-service-time: 1

Запрос обработан успешно, но новых файлов в каталоге не появилось:

kubectl exec -ti deploy/webapp -c istio-proxy -- ls -lh /tmp/
total 4.0K
-rw-r--r--. 1 10002 10001 3.7K Aug 23 12:27 tap_393295138313463914.json

Tap не создал дамп для этого запроса, поскольку одно из условий and_match не выполнилось.

Оптимизация для читаемости: режим JSON_BODY_AS_STRING

Итак, мы рассмотрели работу Tap в режиме JSON_BODY_AS_BYTES, который больше подходит для работы с бинарными данными. Для анализа JSON-сообщений наиболее удобен режим JSON_BODY_AS_STRING. Сконфигурируем наш фильтр для его использования:

output_config:
  max_buffered_rx_bytes: 4096
  max_buffered_tx_bytes: 4096
  sinks:
    - format: JSON_BODY_AS_STRING
      file_per_tap:
        path_prefix: /tmp/tap_

Повторно отправим тестовый запрос:

kubectl exec -ti deploy/client -c client -- \
  curl -i -X POST \
  -H "x-debug: true" \
  -H "Content-Type: application/json" \
  webapp:8080/test-tap \
  -d '{"message": "This message is for testing tap filter"}'

HTTP/1.1 200 OK
server: envoy
content-type: application/octet-stream,application/json
content-length: 45
x-envoy-upstream-service-time: 9

Проверим содержимое каталога /tmp в файловой системе контейнера istio-proxy:

kubectl exec -ti deploy/webapp -c istio-proxy -- ls -lh /tmp/
total 8.0K
-rw-r--r--. 1 10002 10001 3.7K Aug 23 13:06 tap_11310861743920084868.json
-rw-r--r--. 1 10002 10001 3.7K Aug 23 12:27 tap_393295138313463914.json

Когда появился новый файл, взглянем на его содержимое. Нас интересуют объекты, содержащие тело сообщения — теперь оно записано в виде строки:

kubectl exec -ti deploy/webapp -c istio-proxy -- cat /tmp/tap_11310861743920084868.json
{
  "http_buffered_trace": {
    "request": {
      ...
      "body": {
        "as_string": "{\"message\": \"This message is for testing tap filter\"}",
        "truncated": false
      },
      "trailers": []
    },
    "response": {
      ...
      "body": {
        "as_string": "{\"status\":\"ok\",\"message\":\"Hello from webapp\"}",
        "truncated": false
      },
      "trailers": []
    }
  }
}

Примечание. В листинге для читаемости опущены объекты заголовков запроса и ответа. Остальная структура сохранена без изменений.

Как видите, фильтр сформировал данные запроса, готовые к анализу без дополнительного декодирования.

Ограничения размера буфера и флаг truncated

Обратите внимание на поле truncated рядом с телом сообщения — до сих пор оно оставалось без внимания, но именно оно отвечает на вопрос, попало ли сообщение в файл целиком. Разберёмся, от чего это зависит.

Ранее мы задавали размер буфера 4096 байтов для каждого направления. Отправленные нами сообщения этот размер не превышали — именно поэтому в tap-файл попало сообщение целиком, а флаг принял значение false. Теперь сконфигурируем размер входного и исходящего буфера в более наглядное для проверки значение — 32 байта:

output_config:
  max_buffered_rx_bytes: 32
  max_buffered_tx_bytes: 32

С точки зрения запроса ничего не меняется:

kubectl exec -ti deploy/client -c client -- \
  curl -i -X POST \
  -H "x-debug: true" \
  -H "Content-Type: application/json" \
  webapp:8080/test-tap \
  -d '{"message": "This message is for testing tap filter"}'

HTTP/1.1 200 OK
server: envoy
content-type: application/octet-stream,application/json
content-length: 45
x-envoy-upstream-service-time: 6

{"status":"ok","message":"Hello from webapp"}

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

Фрагмент tap-файла:

"request": {
  ...
  "body": {
    "as_string": "{\"message\": \"This message is for",
    "truncated": true
  }
},
"response": {
  ...
  "body": {
    "as_string": "{\"status\":\"ok\",\"message\":\"Hello ",
    "truncated": true
  },
  "trailers": []
}

Примечание. В листинге для читаемости опущены объекты заголовков запроса и ответа. Остальная структура сохранена без изменений.

Envoy записал только первые байты каждого сообщения:

Request (RX):

{"message": "This message is for

Response (TX):

{"status":"ok","message":"Hello

Проверим размер сохранённого фрагмента запроса:

echo -n '{"message": "This message is for' | wc -c
32

Аналогично для ответа:

echo -n '{"status":"ok","message":"Hello ' | wc -c
32

В обоих случаях получаем ровно 32 байта — то есть значение, заданное параметрами max_buffered_rx_bytes и max_buffered_tx_bytes.

Примечание. Не стоит измерять длину фрагмента непосредственно по содержимому поля as_string в tap-файле. Оно хранит JSON-экранированное представление данных, в котором кавычки записаны как \", поэтому визуально строка выглядит длиннее фактически сохранённого объёма. Фактический размер определяется значениями max_buffered_rx_bytes и max_buffered_tx_bytes и измеряется в байтах, а не в символах. В нашем примере тело сообщения состоит только из ASCII-символов, каждый из которых в кодировке UTF-8 занимает один байт, поэтому количество символов совпадает с количеством байтов, которое показано в результатах выполнения команд.

В обоих случаях флаг "truncated": true показывает, что сообщение было усечено при записи в tap-файл. Обратите внимание, что сохранённые фрагменты уже не являются корректным JSON, строки и объект обрываются посередине — это ключевая особенность Tap:

  • Envoy сохраняет только первые N байтов тела сообщения;

  • оставшаяся часть тела не попадает в tap-событие;

  • Tap не пытается аккуратно завершить JSON или восстановить структуру документа.

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

Заключение и рекомендации по использованию

На этом базовая настройка Tap заканчивается — всё остальное сводится к тому, как именно вы формулируете условия перехвата и что делаете с полученными файлами.

В статье мы разобрали, как устроен Tap Filter в Envoy, как подключить его в Istio через EnvoyFilter и как настроить сохранение перехваченного трафика в файлы. Посмотрели, чем отличаются форматы JSON_BODY_AS_BYTES и JSON_BODY_AS_STRING, научились ограничивать область перехвата с помощью workloadSelector и правил and_match; выяснили, что происходит с телом сообщения при превышении размера буфера. Этих возможностей достаточно для решения большинства задач точечной диагностики HTTP-трафика: мы можем увидеть запрос и ответ в конкретной точке обработки Envoy, включая заголовки, добавленные Service Mesh, и расшифрованное тело сообщения.

На момент написания статьи HTTP Tap Filter в Envoy — экспериментальная функциональность в стадии активной разработки, поэтому его стоит рассматривать прежде всего как инструмент диагностики и точечного анализа трафика, а не как постоянный механизм наблюдения. Он хорош, когда есть конкретная гипотеза и понятно, какой именно трафик нужно поймать. Чем шире условия перехвата, тем дороже это обходится sidecar-прокси, и тем важнее становится то, как именно сформулированы правила отбора. Об этом и поговорим в следующей части.

Примеры в статье проверялись на Envoy и Istio версии 1.28.7. Синтаксис Tap Filter относится к конкретной версии Envoy, поэтому перед применением конфигурации в другом окружении рекомендуется сверить её с API соответствующей версии.

Спасибо за внимание!