Ручная инструментация
Ручная инструментация дает полный контроль над тем, какие операции трассируются, какие атрибуты и события записываются. Она позволяет создавать спаны в тех местах кода, которые не покрываются автоматической инструментацией: бизнес-логика, кастомные обработчики, внутренние вызовы. Для ручной инструментации используется OpenTelemetry SDK
На практике автоматическая и ручная инструментация дополняют друг друга. Начните с автоматической — она покроет типовые протоколы. Затем добавляйте ручные спаны в местах, где не хватает контекста для диагностики.
Примеры в этом разделе приведены на Python. Для других языков API аналогичен — см. документацию OpenTelemetry SDK
Инициализация трейсера
Трейсер — объект, через который создаются спаны. Получите его один раз в модуле и используйте во всех функциях:
from opentelemetry import trace
tracer = trace.get_tracer("my_service.orders")
Аргумент get_tracer — имя модуля или компонента. Для именования можно использовать __name__ (чтобы легко находить файл с кодом) или фиксированную строку (например, my_service.orders), чтобы имя компонента не менялось при рефакторинге и перемещении файлов.
Если вы запускаете приложение через автоинструментацию, TracerProvider настраивается автоматически. Если нет — инициализируйте провайдер до вызова get_tracer.
Создание спанов
Основной способ создать спан — контекстный менеджер start_as_current_span. Название отражает поведение: метод создает спан и делает его текущим в контексте — благодаря этому вложенные спаны автоматически становятся дочерними. При выходе из блока спан завершается:
def handle_order(order_id):
with tracer.start_as_current_span("handle_order") as span:
span.set_attribute("order.id", order_id)
result = process(order_id)
return result
Вложенные спаны
Спаны, созданные внутри другого спана, автоматически становятся дочерними. Это формирует дерево операций в трейсе:
def handle_order(order_id):
with tracer.start_as_current_span("handle_order"):
with tracer.start_as_current_span("validate_payment"):
validate(order_id)
with tracer.start_as_current_span("update_inventory"):
update(order_id)
В интерфейсе Monium Traces этот код создаст три спана: handle_order с двумя дочерними — validate_payment и update_inventory.
Именование спанов
Используйте короткие, стабильные имена с низкой кардинальностью:
checkout.calculate_total— хорошо.process_order_a3f82c1— плохо (содержит уникальный идентификатор).
Для сетевых операций следуйте конвенциям OpenTelemetry: GET /api/orders для HTTP, mypackage.MyService/GetOrder для gRPC.
Виды спанов
При создании спана можно указать его вид. Вид определяет роль операции в распределенном взаимодействии:
from opentelemetry.trace import SpanKind
with tracer.start_as_current_span("GET /api/orders", kind=SpanKind.SERVER):
...
|
Вид |
Описание |
|
|
Обработка входящего запроса от другого сервиса |
|
|
Исходящий запрос к другому сервису |
|
|
Внутренняя операция, не связанная с удаленным вызовом (по умолчанию) |
|
|
Отправка сообщения в очередь |
|
|
Получение сообщения из очереди и его обработка (спан охватывает весь цикл от получения до завершения обработки) |
Атрибуты
Атрибуты — пары «ключ — значение», которые добавляют контекст к спану. Они используются для поиска и фильтрации в интерфейсе Monium Traces.
with tracer.start_as_current_span("handle_order") as span:
span.set_attribute("order.id", order_id)
span.set_attribute("order.type", "online")
span.set_attribute("order.items_count", len(items))
Для типовых операций используйте атрибуты из семантических конвенций OpenTelemetry
События
События фиксируют важные моменты внутри спана. Каждое событие содержит временную метку, название и (опционально) атрибуты:
with tracer.start_as_current_span("process_batch") as span:
span.add_event("Batch processing started", {"batch.size": 100})
process(batch)
span.add_event("Batch processing completed")
Типичные применения: начало или завершение этапа, промах кэша, повторная попытка, результат валидации.
Статусы и ошибки
Каждый спан имеет статус. По умолчанию спан создается со статусом UNSET, что означает успешное завершение операции без ошибок. Используйте OK только тогда, когда хотите явно застраховаться от переопределения статуса автоинструментацией или другим кодом.
Ошибки можно отразить в трейсе двумя способами.
Исключение пробрасывается наружу. Контекстный менеджер start_as_current_span по умолчанию перехватывает исключение, вышедшее из блока with, записывает его как событие, устанавливает статус ERROR и пробрасывает дальше. Достаточно не ловить исключение внутри блока:
# Исключение выйдет из with — OTel запишет его в спан, выставит ERROR и пробросит дальше
with tracer.start_as_current_span("charge_payment"):
if order_id < 0:
raise ValueError(f"Invalid order ID: {order_id}")
Исключение перехватывается и обрабатывается. Если вы обрабатываете ошибку внутри блока, исключение не выходит за пределы with — автоматическая запись контекстным менеджером не срабатывает. Вызовите record_exception и set_status вручную.
Важно: record_exception и set_status — независимые операции. record_exception создает событие с типом exception, сообщением и стектрейсом, но не устанавливает статус ERROR. Без явного вызова set_status спан не будет помечен как ошибочный. Согласно спецификации OTel, исключение следует записывать как событие только вместе с установкой статуса ERROR — именно такой паттерн отражает реальную ошибку в трейсе.
from opentelemetry.trace import Status, StatusCode
with tracer.start_as_current_span("handle_order") as span:
try:
result = process(order_id)
return result
except Exception as e:
# Событие: тип, сообщение, стектрейс
span.record_exception(e)
# Помечает спан как ошибочный
span.set_status(Status(StatusCode.ERROR, str(e)))
Спаны со статусом ERROR выделяются в интерфейсе Monium Traces.
Propagation контекста
Контекст трассировки (trace_id, span_id) передается между сервисами через заголовки запросов в формате W3C Trace Context
Внутри одного процесса контекст передается автоматически: каждый новый спан, созданный через start_as_current_span, наследует текущий контекст и становится дочерним.
При межсервисном взаимодействии контекст нужно передавать явно. Если вы используете автоинструментацию для HTTP или gRPC, это происходит автоматически. Для других протоколов внедрите контекст вручную:
from opentelemetry.propagate import inject, extract
headers = {}
inject(headers)
# headers теперь содержит traceparent и tracestate —
# передайте их вместе с запросом
# На принимающей стороне:
ctx = extract(incoming_headers)
with tracer.start_as_current_span("process", context=ctx):
...
Полный пример
Консольная программа с ручной инструментацией. Демонстрирует все основные приемы: инициализацию провайдера, создание вложенных спанов, атрибуты, событие и обработку ошибки.
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode, SpanKind
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# Инициализация провайдера.
# Параметры подключения берутся из переменных окружения OTEL_*.
resource = Resource.create()
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("my_service.orders")
def validate_order(order_id: int) -> None:
"""
Исключение пробрасывается наружу из блока with.
Контекстный менеджер перехватит его, запишет в спан и выставит ERROR автоматически.
"""
with tracer.start_as_current_span("validate_order") as span:
span.set_attribute("order.id", order_id)
if order_id <= 0:
raise ValueError(f"Invalid order ID: {order_id}")
span.add_event("Validation passed")
def charge_payment(order_id: int, amount: float) -> bool:
"""
Исключение перехватывается внутри блока with.
Автоматическая запись не срабатывает — вызываем record_exception и set_status явно.
"""
with tracer.start_as_current_span("charge_payment") as span:
span.set_attribute("order.id", order_id)
span.set_attribute("payment.amount", amount)
try:
if amount <= 0:
raise ValueError(f"Invalid payment amount: {amount}")
span.add_event("Payment charged")
return True
except Exception as e:
# record_exception записывает событие с типом, сообщением и стектрейсом.
# set_status — отдельный вызов: без него спан не будет помечен как ошибочный.
span.record_exception(e)
span.set_status(Status(StatusCode.ERROR, str(e)))
return False
def update_inventory(order_id: int) -> None:
with tracer.start_as_current_span("update_inventory") as span:
span.set_attribute("order.id", order_id)
span.add_event("Inventory updated")
def process_order(order_id: int, amount: float) -> None:
# SpanKind.SERVER — точка входа, обрабатывающая входящий запрос.
# Дочерние спаны создаются внутри и наследуют контекст автоматически.
with tracer.start_as_current_span("process_order", kind=SpanKind.SERVER) as span:
span.set_attribute("order.id", order_id)
span.set_attribute("order.amount", amount)
span.add_event("Order processing started")
try:
validate_order(order_id)
except ValueError as e:
span.set_status(Status(StatusCode.ERROR, str(e)))
print(f"Order {order_id} failed: {e}")
return
if not charge_payment(order_id, amount):
span.set_status(Status(StatusCode.ERROR, "Payment failed"))
print(f"Order {order_id} failed: payment rejected")
return
update_inventory(order_id)
span.add_event("Order processing completed")
print(f"Order {order_id} processed successfully")
if __name__ == "__main__":
process_order(1, 50.0) # успешный сценарий
process_order(-1, 50.0) # ошибка валидации
process_order(2, -1.0) # ошибка оплаты
# Дожидаемся отправки всех буферизованных спанов перед завершением процесса
provider.shutdown()
Для запуска установите зависимости и задайте переменные окружения из раздела Настройка подключения:
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp
export OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
export OTEL_SERVICE_NAME=my-service
export OTEL_EXPORTER_OTLP_ENDPOINT="ingest.monium.yacloudkz.tech:443"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Api-Key <API-ключ>,x-monium-project=folder__<идентификатор_каталога>"
python app.py