Cap 3: Traces y Spans
Anatomía de un Trace
Un Trace es un árbol de Spans que representa el trabajo completo para una operación lógica.
flowchart TD
ROOT["[ROOT SPAN]\nPOST /checkout\ntrace_id: abc123\nspan_id: 001\nduration: 58ms"] --> AUTH
ROOT --> ORDER
ORDER --> DB["[SPAN]\ndb.query SELECT orders\nspan_id: 003\nduration: 7ms"]
ORDER --> PAY["[SPAN]\ncall PaymentService\nspan_id: 004\nduration: 30ms"]
PAY --> PAY2["[SPAN]\nverify-card\nspan_id: 005\nduration: 25ms"]
PAY --> PAY3["[SPAN]\ncreate-transaction\nspan_id: 006\nduration: 5ms"]
ROOT --> PUB["[SPAN]\npublish order.created\nspan_id: 007\nduration: 3ms"]
AUTH["[SPAN]\nauthenticate-token\nspan_id: 002\nduration: 5ms"]
ORDER["[SPAN]\nprocess-order\nspan_id: 003\nduration: 45ms"]
- Trace ID — Identificador único del trace completo (16 bytes hex)
- Span ID — Identificador único del span (8 bytes hex)
- Parent Span ID — Referencia al span padre (ausente en el root span)
Anatomía de un Span
from opentelemetry import trace
from opentelemetry.trace import SpanKind, StatusCode
from opentelemetry.semconv.trace import SpanAttributes
tracer = trace.get_tracer("com.mycompany.order-service")
with tracer.start_as_current_span(
"process-order",
kind=SpanKind.SERVER,
) as span:
# Atributos: metadatos del span
span.set_attribute("order.id", order_id)
span.set_attribute("order.total_usd", total)
span.set_attribute(SpanAttributes.HTTP_METHOD, "POST")
# Evento: punto en el tiempo dentro del span
span.add_event("validación completada", {"items.count": 3})
span.add_event("pago procesado")
try:
resultado = _procesar(order_id)
# Status OK (implícito si no se setea error)
span.set_status(StatusCode.OK)
return resultado
except Exception as e:
# Registrar el error
span.record_exception(e)
span.set_status(StatusCode.ERROR, str(e))
raise
Campos de un Span
Nombre
El nombre del span es lo más visible en el backend. Convenciones:
# HTTP server span
POST /users/:id ✅ (incluye método y ruta parametrizada)
POST /users/123 ❌ (ruta con valores — alta cardinalidad)
# DB span
SELECT orders ✅
SELECT * FROM orders... ❌ (query completa — muy larga)
# Función interna
validate-payment ✅
ValidatePaymentService.validateCardNumber ❌ (demasiado largo)
SpanKind
Indica el rol del span en la comunicación:
| Kind | Cuándo usarlo |
|---|---|
SERVER | Servidor recibiendo una request remota |
CLIENT | Cliente haciendo una llamada remota |
PRODUCER | Publicando un mensaje a una cola |
CONSUMER | Procesando un mensaje de una cola |
INTERNAL | Operación interna (default) |
# Span de servidor HTTP
with tracer.start_as_current_span("GET /users", kind=SpanKind.SERVER):
...
# Span de llamada a base de datos
with tracer.start_as_current_span("SELECT users", kind=SpanKind.CLIENT):
...
Atributos
Pares clave-valor que describen el span. Tipos soportados: string, int, float, bool y arrays de estos.
span.set_attribute("http.response.status_code", 200)
span.set_attribute("db.system", "postgresql")
span.set_attribute("enduser.id", "user-123")
span.set_attribute("items", ["item-1", "item-2"]) # array
Límites: Por defecto el SDK limita a 128 atributos por span y 12KB por valor. Configurable.
Eventos
Momentos discretos dentro del ciclo de vida del span:
# Evento simple
span.add_event("cache miss")
# Evento con atributos
span.add_event("retry attempt", {
"retry.count": 2,
"retry.reason": "connection timeout",
})
# Evento de excepción (estándar OTel)
try:
...
except Exception as e:
span.record_exception(e) # agrega evento con stack trace
Status
El status comunica si el span terminó con éxito o error:
from opentelemetry.trace import StatusCode
span.set_status(StatusCode.OK)
span.set_status(StatusCode.ERROR, "Payment declined: insufficient funds")
# StatusCode.UNSET es el default — no sobreescribas con OK innecesariamente
Regla práctica: solo setea ERROR cuando hay un error real del negocio o del sistema. Un HTTP 404 en un endpoint de búsqueda no es necesariamente un error.
Links entre Spans
Los Links conectan spans de distintos traces — útil para trabajo asíncrono donde el trace original ya finalizó:
# El trace de la request HTTP terminó, pero el job asíncrono
# debe referenciar ese trace original
from opentelemetry.trace import Link
link = Link(context=contexto_del_trace_original)
with tracer.start_as_current_span(
"process-async-job",
links=[link],
) as span:
...
Casos de uso: queues, batch jobs, fan-out/fan-in.
Span Processors
Los Span Processors interceptan los spans antes de exportarlos:
flowchart LR
S[Span] --> SP1[SimpleSpanProcessor\nSíncrono, para debug]
S --> SP2[BatchSpanProcessor\nAsíncrono, para producción]
SP1 --> EXP1[ConsoleExporter]
SP2 --> EXP2[OTLPExporter]
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# Producción: batch asíncrono
processor = BatchSpanProcessor(
OTLPSpanExporter(endpoint="http://localhost:4317"),
max_export_batch_size=512,
schedule_delay_millis=5000,
)
Ejemplo completo: servicio HTTP + DB
from opentelemetry import trace
from opentelemetry.trace import SpanKind, StatusCode
from opentelemetry.semconv.trace import SpanAttributes
tracer = trace.get_tracer("order-service", "1.0.0")
def get_user_orders(user_id: str, db) -> list:
with tracer.start_as_current_span(
"get-user-orders",
kind=SpanKind.SERVER,
) as span:
span.set_attribute("enduser.id", user_id)
# Sub-span para la query DB
with tracer.start_as_current_span(
"db.query",
kind=SpanKind.CLIENT,
) as db_span:
db_span.set_attribute(SpanAttributes.DB_SYSTEM, "postgresql")
db_span.set_attribute(SpanAttributes.DB_NAME, "orders")
db_span.set_attribute(
SpanAttributes.DB_STATEMENT,
"SELECT * FROM orders WHERE user_id = ?"
)
orders = db.query(
"SELECT * FROM orders WHERE user_id = ?",
user_id
)
db_span.set_attribute("db.rows_returned", len(orders))
span.set_attribute("orders.count", len(orders))
span.add_event("orders retrieved", {"count": len(orders)})
return orders
El resultado en Jaeger/Tempo:
get-user-orders [45ms]
└── db.query [12ms]