Появилась у меня задача по мониторингу и оценке производительности проекта на микросервисной архитектуре. Для решения был выбран 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 является надёжным и качественным решением задачи трейсинга распределённых систем.
Ссылки
Немного скринов




