Появилась у меня задача по мониторингу и оценке производительности проекта на микросервисной архитектуре. Для решения был выбран Jaeger. Он давно на рынке, активно развивается (не так давно вышла версия 2, в которой упростилось развертывание и появилась интеграция OpenTelemetry). На мой взгляд, Jaeger – отличное решение для трейсинга, но документация ощущается как не до конца собранный пазл: важные вещи разбросаны, а для понимания приходится обращаться к исходному коду или искать примеры в GitHub-репозиториях.
Цель данной статьи показать на практике, как внедрить Jaeger в продукт на микросервисах.
Ссылку на код всего, о чем пойдет речь дальше, можно найти в конце статьи.
Основные понятия
Спан(span) - единица работы, которую выполняет система.
Спан содержит:
одну операцию, запрос или обработку
временные метками начала и конца
контекст (сюда можно погрузить любую информацию)
Спан может входить в состав трейса (trace), который описывает всю цепочку вызовов между сервисами.
Трейс(trace) – один, или несколько спанов, связанных между собой. Трейс представляет всю последовательность действий, происходящих при обработке одного запроса — от начала до конца.
Подопытный проект
Для примера используются три сервиса и очередь, упакованные в docker контейнеры.

task_receiver – получает от пользователя 2 числа и создает задачу для worker в очереди
worker – получает задачу из очереди, делит одно число на другое, создает задачу в очереди для printer
printer – получает задачу из очереди и выводит результат в консоль
Интеграция Jaeger
Добавляем необходимые контейнеры:
Jaeger – содержит сам Jaeger
Elasticksearch – хранилище спанов
Prometheus – сервис сбора и хранения метрик
spark_job – сервис построения графов зависимостей спанов (запускается периодически, например через cron)

Начиная с версии 2 все компоненты Jaeger упакованы в один бинарный файл.
Также понадобится хранилище спанов. Из коробки Jaeger дружит с Elasticsearch и Cassandra, но обещают расширить список. В примере используется Elasticsearch.
Prometheus будет хранить метрики. Без этого в JaegerUI не будет работать вкладка Monitor.
Чтобы строить диаграммы зависимости сервисов необходимо периодически сканировать хранилище спанов и строить граф зависимостей. Для этого используется spark_job. Без построения графа зависимостей в JaegerUI не будет работать вкладка Dependencies.
Настройка
Настраивается Jaeger через конфиг в формате yaml. Далее описаны основные параметры конфига
Сервис (service)
Extensions: используемые расширения, такие как
jaeger_storage
,jaeger_query
иhealthcheckv2
.Pipelines:
Traces: Настраивает обработку трассировок с использованием приёмников, процессоров и экспортеров.
Metrics/Spanmetrics: Настраивает обработку метрик спанов с использованием приёмника и экспортера.
Telemetry:
Resource: Указывает имя сервиса как
jaeger
.Metrics: Включает детализированные метрики для.
Logs: Устанавливает уровень логирования.
Расширения (extensions)
healthcheckv2: Включает проверку состояния сервиса.
jaeger_query: Настраивает хранилища для трассировок и метрик. Определяет параметры UI и gRPC/HTTP эндпоинты.
jaeger_storage: Настраивает хранилище трассировок с использованием Elasticsearch. Определяет параметры индексов (префикс, частота ротации, количество шардов и реплик). Настраивает Prometheus как хранилище метрик.
Коннекторы (connectors)
spanmetrics: Используется для обработки метрик спанов.
Приёмники (receivers)
otlp: Принимает трассировки через gRPC и HTTP.
jaeger: Принимает трассировки через протокол Thrift Compact.
Процессоры (processors)
batch: Обрабатывает трассировки пакетами для повышения производительности.
Экспортеры (exporters)
jaeger_storage_exporter: Экспортирует трассировки в Elasticsearch.
prometheus: Экспортирует метрики в Prometeus.
Также необходимо создать конфиг JaegerUI (jaeger_config_ui.json) и включить в нем Monitor (по умолчанию выключен). Prometeus тоже требует конфиг с указанными портами для сбора метрик. Все конфиги доступны в тестовом проекте по ссылке в конце статьи.
Код
Теперь необходимо добавить создание и обработку спанов в код сервисов проекта.
Создаём jaeger.py следующего содержания:
from jaeger_client import Config
from opentracing.scope_managers.contextvars import ContextVarsScopeManager
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.propagate import inject,extract
from opentelemetry.trace import Status, StatusCode
def init_otel_tracer(service_name='unnamed_service'):
resource = Resource(attributes={
"service.name": service_name
})
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://jaeger:4317", insecure=True))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
return trace.get_tracer(service_name)
def otel_tracer(tracer, span_name='unnamed_span'):
def decorator(func):
def wrapper(*args, **kwargs):
data = args[0]
context = extract(data.get('jaeger_context', {}))
with tracer.start_as_current_span(span_name, context=context) as span:
service_name = trace.get_tracer_provider().resource.attributes.get("service.name")
span.set_attribute('peer.service', service_name)
span.set_attribute('component', span_name)
span.set_attribute('jaeger.trace_id', str(span.context.trace_id))
span.set_attribute('payload', str(args[0]))
for key, value in kwargs.items():
span.set_attribute(f'kwargs.{key}', str(value))
jaeger_context = {}
inject(jaeger_context)
args[0]['jaeger_context'] = jaeger_context
return func(*args, **kwargs)
return wrapper
return decorator
def mark_span_as_error(exc):
current_span = trace.get_current_span()
current_span.set_status(Status(StatusCode.ERROR, str(exc)))
current_span.set_attribute("error", True)
current_span.set_attribute("error.message", str(exc))
Далее в коде каждого сервиса нужно инициализировать tracer, указав имя сервиса и декорировать функцию с помощью декоратора otel_tracer, указав имя функции.
Чтобы можно было построить зависимости между спанами нужно передавать контекст спана. В Jaeger есть методы extract и inject, которые работают с запросами, но в примере используется очередь Redis и контекст надо погружать в задания Redis, чтобы следующий сервис мог извлечь этот контекст и обновить его своими данными. Тогда спаны всех сервисов приобретут зависимости и будут отображаться в UI корректно (цепочкой). В примере это происходит в декораторе otel_tracer. При обработке возникающих ошибок спан тоже нужно помечать “ошибочным”. Для этого используем метод mark_span_as_error, передавая в него само исключение.
Например код сервиса worker выглядит следующим образом:
import redis
import json
from jaeger import init_otel_tracer, otel_tracer, mark_span_as_error
from opentelemetry.propagate import inject
from opentelemetry.trace import Status, StatusCode
r = redis.Redis(host='redis', port=6379, decode_responses=True)
tracer = init_otel_tracer(service_name='worker')
@otel_tracer(tracer, span_name='process_task')
def process_task(task):
a = task["a"]
b = task["b"]
try:
result = a / b
except ZeroDivisionError as exc:
result = "Error: Division by zero"
mark_span_as_error(exc)
task["result"] = result
updated_task = json.dumps(task)
print(f"Processed task: {a} / {b} = {result}")
r.rpush("results", updated_task)
if name == '__main__':
print('worker started')
while True:
task_json = r.blpop("tasks")[1]
task = json.loads(task_json)
process_task(task)
JaegerUI
По умолчанию вебинтерфейс Jaeger сидит на порту 16686 и доступ к нему никак и ничем не ограничен. Существует множество вариантов ограничения доступа. В данном примере ограничение доступа к вебинтерфейсу реализовано с помощью Flask и jwt. Приложение на Flask принимает входящие подключения и при отсутствии токена отправляет на страницу авторизации. Настройки вынесены в .env файл.
Заключение
Тема мониторинга в общем и трейсинга в частности очень широка и здесь можно было бы много еще написать о стандартах и тонкостях настройки. Цель этой статьи помочь сделать первые шаги в этой области, что позволит находить узкие места в работе микросервисной архитектуры, упростит разбор инцидентов и анализ взаимодействия сервисов между собой. Jaeger является надёжным и качественным решением задачи трейсинга распределённых систем.
Ссылки
Немного скринов



