Автоинструментирование закрывает 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.4 добавьте 3 ручных спана: бизнес-операция + два вложенных шага. Убедитесь в UI, что дерево трёхуровневое.
- Сымитируйте ошибку (исключение в бизнес-спане) — найдите в UI красный спан, запись exception-события и статус.
- Создайте спан с именем, содержащим order_id, посмотрите на список операций сервиса в UI (dropdown Operations) и поймите, почему так делать нельзя.
- Добавьте
OTEL_RESOURCE_ATTRIBUTESс версией и средой; найдите их в процесс-тегах трейса.
Мини-квиз
1. Что делает start_as_current_span отличным от start_span? — Активирует спан в контексте: вложенные вызовы станут его детьми.
2. Почему имя спана «GET /orders/1001» плохо? — Взрывает список операций и хранилище; низкая карточность в имени, id — в атрибуты.
3. Куда записать «retry attempt 3» внутри операции? — В span event, не в новый спан и не в имя.