Программа курса

Модуль 02 / Урок 5 из 27

OpenTelemetry SDK: создаём спаны руками

Теория и практика

Автоинструментирование закрывает HTTP и БД, но бизнес-операции («сопоставить заказ с курьером», «пересчитать тариф») — только вы. Учимся создавать спаны вручную и правильно.

Анатомия трассировщика

  • TracerProvider — корневой объект, глобален на процесс; настраивается один раз при старте (экспортёр, сэмплер, ресурсы);
  • Tracer — по одному на инструментируемый модуль (tracer = trace.get_tracer("shop.orders"));
  • Span — создан tracer'ом, завершён один раз (span.end());
  • Текущий контекст — активный спан хранится в context (в Python — ContextVar, в Java — ThreadLocal): дочерние спаны автоматически цепляются к нему.

Минимальный ручной спан

from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode

tracer = trace.get_tracer("shop.orders")

def assign_courier(order_id):
    with tracer.start_as_current_span("assign_courier") as span:
        span.set_attribute("order.id", order_id)
        span.set_attribute("courier.strategy", "nearest")
        try:
            courier = find_nearest(order_id)        # внутренний вызов
            span.set_attribute("courier.id", courier.id)
            return courier
        except NoCouriersError:
            span.set_status(Status(StatusCode.ERROR, "no couriers available"))
            raise

Обратите внимание: start_as_current_span делает спан «активным» — всё, что вызовется внутри, станет его детьми. Контекстный менеджер закроет спан даже при исключении.

Правила хорошего спана

  • Имя = низкой карточности и стабильное: assign_courier, а не assign_courier_1001. Переменные части — в атрибуты, не в имя;
  • Спан = значимая операция. Спан на каждую итерацию цикла из 10 000 шагов — зло; один спан на весь батч + события на проблемы;
  • Атрибуты — то, что пригодится в поиске: id сущностей, стратегия, размер батча;
  • Ошибки: статус ERROR + record_exception:
    try:
        risky()
    except Exception as e:
        span.record_exception(e)                 # событие с стектрейсом в спане
        span.set_status(Status(StatusCode.ERROR, str(e)))
        raise
  • События (span events) — точечные пометки: span.add_event("cache.miss", {"key": k}).

Вложенность и独立ные (неактивные) спаны

# Вложенные спаны — просто вложенный with:
with tracer.start_as_current_span("reprice_cart"):
    with tracer.start_as_current_span("load_prices"):
        ...
    with tracer.start_as_current_span("apply_promo"):
        ...

# Спан без активации (не станет parent-ом для окружения):
with tracer.start_span("background_flush") as span:
    span.end()
⚠️ Распространённая ошибка: спан, переживший запрос (стартовали — передали в background worker — забыли end()). Незавершённый спан не экспортируется вообще или «висит» в памяти. Правило: у каждого start есть гарантированный end, даже в error-путях.

Ресурсы: кто шлёт данные

Resource — атрибуты самого источника (не запроса): service.name, service.version, deployment.environment, k8s.pod.name. Задаются один раз:

export OTEL_SERVICE_NAME=orders
export OTEL_RESOURCE_ATTRIBUTES=service.version=1.4.2,deployment.environment=prod

service.name — важнейший атрибут: по нему Jaeger группирует всё. Без него получите anonymous-service, по которому ничего не найти.

Практика

  1. В сервисе из практикума 1.4 добавьте 3 ручных спана: бизнес-операция + два вложенных шага. Убедитесь в UI, что дерево трёхуровневое.
  2. Сымитируйте ошибку (исключение в бизнес-спане) — найдите в UI красный спан, запись exception-события и статус.
  3. Создайте спан с именем, содержащим order_id, посмотрите на список операций сервиса в UI (dropdown Operations) и поймите, почему так делать нельзя.
  4. Добавьте OTEL_RESOURCE_ATTRIBUTES с версией и средой; найдите их в процесс-тегах трейса.
Мини-квиз

1. Что делает start_as_current_span отличным от start_span? — Активирует спан в контексте: вложенные вызовы станут его детьми.

2. Почему имя спана «GET /orders/1001» плохо? — Взрывает список операций и хранилище; низкая карточность в имени, id — в атрибуты.

3. Куда записать «retry attempt 3» внутри операции? — В span event, не в новый спан и не в имя.