Полезные инструкции, которые помогут вам в вашем путешествии по Observability

OpenTelemetry зарекомендовала себя как стандарт наблюдаемости после выхода из стандартов OpenTracing и OpenCensus. Он пытается стандартизировать различные виды сигналов (журналы, трассировки, метрики). Сигнал наблюдаемости, относящийся к этой статье, — это следы.

Чтобы лучше понять, стоит прочитать основные концепции OpenTelemetry.

Мы используем Grafana Tempo в качестве бэкенда для наших трейсов. Это позволяет нам хранить информацию о трассировке в недорогом объектном хранилище вместо размещения базы данных ElasticSearch при использовании такого инструмента, как Jaeger.

Поскольку мы предоставляем общие кластеры Kubernetes командам разработчиков приложений, еще одной очень важной функцией Grafana Tempo является мультиарендность.

Основной поток

У каждой команды разработчиков есть набор пространств имен Kubernetes с меткой tenant_id. Приложения поддерживаются библиотекой OpenTelemetry (см. список поддерживаемых языков по этой ссылке).

Трассировки создаются в приложениях, а затем отправляются сборщику OpenTelemetry, установленному в кластере с потрясающим OpenTelemetry Kubernetes Operator. Коллекторный трубопровод состоит из трех основных частей:

  • Приемники — это компоненты, которые принимают трассировки, отправленные от рабочих нагрузок. Мы используем встроенный otlpприемник, который принимает трейсы, отправленные через gRPC. Коллектор может принимать трассы из разных источников, используя разные конфигурации приемников.
  • Процессоры могут преобразовывать, фильтровать или маршрутизировать трассы. Мы используем процессор для обогащения трассировок информацией об именах Kubernetes Pod и Namespace отправляющих рабочих нагрузок и извлекаем идентификатор арендатора из метки Namespace.
  • Экспортеры отправляют трейсы на бэкенды вроде Grafana Tempo. Здесь мы используем otlp exporter через gRPC.

Конвейер сборщика для рабочих нагрузок извлекает идентификатор арендатора из метки пространства имен с помощью процессора k8sattributes. (Процессору требуются некоторые разрешения для запросов к серверу Kubernetes API. См. файл README процессора.)

receivers:
  otlp:
    protocols:
      grpc:
processors:
  k8sattributes/default:
    extract:
      labels:
      - tag_name: tenantId
        key: "tenant-id"
        from: namespace
  routing:
    from_attribute: tenantId
    attribute_source: resource
    table:
    - value: tenant1
      exporters: [logging, otlp/tenant1]
    - value: tenant2
      exporters: [logging, otlp/tenant2]
exporters:
  otlp/tenant1:
    endpoint: tempo.grafana.svc.cluster.local:4317
    tls:
      insecure: true
    headers:
      X-Scope-OrgID: tenant1
  otlp/tenant2:
    endpoint: tempo.grafana.svc.cluster.local:4317
    headers:
      X-Scope-OrgID: tenant2
    tls:
      insecure: true
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [k8sattributes/default, routing]
      exporters: [ otlp/tenant1, otlp/tenant2]

Далее мы определяем экспортера для каждого арендатора, который у нас есть. Все они настраивают один и тот же URL-адрес конечной точки для темпа Grafana, но имеют другое значение заголовка X-Scope-OrgID. Процессор маршрутизации направляет трассировки различным экспортерам в зависимости от идентификатора арендатора.

Это немного громоздко, но нет возможности использовать переменные внутри блока заголовков для экспортера otlp. Это позволило бы нам иметь только одну запись экспортера с переменным содержимым в заголовке X-Scope-OrgID.

Ingress-Nginx и OpenTelemetry

Ingress-Nginx поддерживает OpenTelemetry, начиная с версии 1.7.0 (выпущенной 24 марта 2023 г. См. запрос на включение #9062). Мы с нетерпением ждали эту новую функцию, потому что она позволяет собирать трассировочную информацию не только на уровне рабочей нагрузки, но уже на один шаг раньше, когда трафик поступает в кластер Kubernetes.

Чтобы включить интеграцию OpenTelemetry с помощью диаграммы Ingress-Nginx helm, просто добавьте следующие параметры:

controller:
  config:
    enable-opentelemetry: "true"
    opentelemetry-config: "/etc/nginx/opentelemetry.toml"
    opentelemetry-operation-name: "HTTP $request_method $service_name $uri"
    opentelemetry-trust-incoming-span: "true"
    otlp-collector-host: "otel-collector.grafana.svc.cluster.local"
    otlp-collector-port: "4317"
    otel-max-queuesize: "2048"
    otel-schedule-delay-millis: "5000"
    otel-max-export-batch-size: "512"
    otel-service-name: "nginx-proxy" # Opentelemetry resource name
    otel-sampler: "AlwaysOn" # Also: AlwaysOff, TraceIdRatioBased
    otel-sampler-ratio: "1.0"
    otel-sampler-parent-based: "false"

Хотя рабочие нагрузки всегда выполняются внутри пространства имен Kubernetes, к которому в качестве метки прикреплен идентификатор клиента, контроллер Ingress-Nginx является общим развертыванием, управляемым командой платформы, и информацию о арендаторе нельзя извлечь из пространства имен.

Нам пришлось извлечь информацию об арендаторе из другого места, чтобы направить трассировки, исходящие от входного контроллера, к правильному арендатору Grafana Tempo.

Директивы Ingress-Nginx OpenTelemetry

Хотя документации контроллера Ingress-Nginx в настоящее время недостаточно, мы нашли необходимую информацию в модуле, который используется для части OpenTelemetry. Он расположен в следующем проекте GitHub.

Нам удалось найти директивы, предоставляемые модулем OpenTelemetry. Особенно нам была интересна директива opentelemetry_attribute. Это позволяет нам добавлять пользовательские атрибуты к промежуткам.

Мы настроили входной контроллер, чтобы добавить эти директивы в каждый блок сервера в конфигурации nginx. Пример конфигурации ниже относится к рулевой диаграмме:

controller:
  config:
    server-snippet: |
      opentelemetry_attribute "ingress.namespace" "$namespace";
      opentelemetry_attribute "ingress.service_name" "$service_name";
      opentelemetry_attribute "ingress.name" "$ingress_name";
      opentelemetry_attribute "ingress.upstream" "$proxy_upstream_name";

После внесения этого изменения конфигурации трассировки, отправляемые контроллером входящего трафика, содержат информацию о пространстве имен Ingresses, имени и том, куда он направляет трафик.

Но где взять идентификатор арендатора? К счастью для нас, все наши пространства имен следуют схеме именования, такой как tenantId-<namespaceName>. Мы можем извлечь идентификатор арендатора из атрибута пространства имен, прикрепленного к диапазонам.

Вышеупомянутый трубопровод коллектора изменен, чтобы сделать это возможным. Вот как выглядит код:

receivers:
  otlp:
    protocols:
      grpc:
processors:
  k8sattributes/default:
    extract:
      labels:
      - tag_name: tenantId
        key: "tenant-id"
        from: namespace
  # in case of ingress-nginx traces the ingress namespace is in the attributes.
  # we need to extract the account id from there and eventually transfer it to the resources
  # see Resource: https://opentelemetry.io/docs/concepts/glossary/#resource
  # vs Attribute: https://opentelemetry.io/docs/concepts/glossary/#attribute
  attributes/ingress:
    actions:
    - key: "ingress.namespace"
      pattern: "(?P<temp_tenant_id>.{6})"<<< 
      action: extract # this overwrites existing keys
  groupbyattrs: # copy the attribute containing the tenant id to the resource attribute which is used for routing
    keys:
      - temp_tenant_id
  resource/ingress:
    attributes:
    - key: "tenantId"
      from_attribute: temp_tenant_id
      action: insert
    - key: "temp_tenant_id"
      action: delete
routing:
  from_attribute: tenantId
  attribute_source: resource
  table:
  - value: tenant1
    exporters: [logging, otlp/tenant1]
  - value: tenant2
    exporters: [logging, otlp/tenant2]
exporters:
  otlp/tenant1:
    endpoint: tempo.grafana.svc.cluster.local:4317
    tls:
      insecure: true
    headers:
      X-Scope-OrgID: tenant1
  otlp/tenant2:
    endpoint: tempo.grafana.svc.cluster.local:4317
    headers:
      X-Scope-OrgID: tenant2
    tls:
      insecure: true
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [k8sattributes/default, attributes/ingress, groupbyattrs, resource/ingress, routing]
      exporters: [ otlp/tenant1, otlp/tenant2]

При использовании этого конвейера используется информация о пространстве имен Ingress-Nginx. Идентификатор арендатора извлекается с помощью регулярного выражения и сохраняется во временном файле attribute. Затем attribute копируется в resourcesс помощью процессора groupbyattrs.

Это необходимо, потому что OpenTelemetry имеет концепцию attributes и resources. Процессор k8sattributes хранит информацию в части ресурсов. Мы уже используем эти атрибуты для маршрутизации (поэтому мы настроили attribute_source: resource).

Это немного некрасиво, но нам нужна помощь, чтобы найти лучший способ сделать это. Если у вас есть идеи, не стесняйтесь оставлять предложения в комментариях!

Следующие шаги

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

  • пакетный процессор для пакетной отправки трасс
  • процессор хвостовой выборки для фильтрации трассировок для конечных точек, таких как /actuator или /health, которые очень часто вызываются сервером Kubernetes API, но не приносят реальной ценности для записи в Grafana Tempo

Использование Grafana Tempo и сборщика OpenTelemetry для нас сейчас в новинку. Если какие-то важные пункты выше отсутствуют, и вы видите дальнейшие оптимизации, оставьте подсказку в комментариях.

Надеюсь, инструкции помогут и вам в вашем путешествии по Observability!

Дальнейшее чтение