Trazado
Cada hook de una llamada recibe el mismo PredictContext.run_id, así que un tracer puede correlacionar los
eventos de inicio, fin y error (y cualquier span que abra) sin llevar su propia contabilidad.
- run_id
- Un tracer mínimo
- Ciclo de vida de un span
- Spans de error
- OpenTelemetry
- Propagación distribuida
- Llamadas anidadas
run_id
- Una cadena
uuid4().hex, creada una vez por llamada pública (predict_batch,system_one,Router.predict,ONNXAgent.system_one).Router.predict_batchcrea una por solicitud en su lugar, larun_idque habría tenido una llamada aRouter.predictpara esa solicitud. - La comparten todos los hooks de esa llamada, incluidos
on_erroryon_predict_end. - No es global ni se persiste: identifica una llamada dentro del proceso. Ponla en tus registros y en los payloads salientes para correlacionar entre sistemas.
- Distinta en cada llamada, así que dos llamadas nunca chocan.
call A: run_id=1a2b... start ──┐
├─ end
error ─┘
call B: run_id=9f8e... start ─── end
Un tracer mínimo
Mantén un mapa de run_id al span que abriste al inicio, y ciérralo al final o en caso de error.
import time
class Tracer:
def __init__(self):
self.spans = {}
def on_predict_start(self, ctx):
self.spans[ctx.run_id] = {
"model": ctx.model,
"started_at": ctx.started_at,
}
def on_predict_end(self, ctx):
span = self.spans.pop(ctx.run_id, None)
if span is None:
return
emit_span(
name="laya.predict",
run_id=ctx.run_id,
model=ctx.model,
duration_ms=ctx.elapsed_ms,
usage=ctx.usage,
ok=ctx.error is None,
)
def on_error(self, ctx):
# on_error runs before on_predict_end; leaving the span for on_predict_end is fine,
# or close it here if you prefer.
pass
agent = laya.load("convaiinnovations/laya", hooks=[Tracer()])
Como on_predict_end se ejecuta siempre, es el lugar natural para cerrar un span, y puede ver
ctx.error en la ruta de fallo.
Ciclo de vida de un span
on_predict_start ──► open span (run_id, model, started_at)
│
├─ inference
│
on_error ──► record ctx.error on the span
│
on_predict_end ──► close span (elapsed_ms, usage, ok)
Spans de error
on_predict_end se ejecuta en la ruta de fallo con ctx.error establecido, así que un único punto de
cierre cubre ambos casos:
def on_predict_end(self, ctx):
span = self.spans.pop(ctx.run_id, None)
if span is None:
return
if ctx.error is not None:
span["status"] = "error"
span["error_type"] = type(ctx.error).__name__
span["error_message"] = str(ctx.error)
span["duration_ms"] = ctx.elapsed_ms
emit(span)
Si solo implementas on_error, recuerda que se dispara antes de on_predict_end; no cierres el
span en ambos o lo contarás dos veces.
OpenTelemetry
El ejemplo examples/hooks/otel.py registra contadores y un
histograma. Para spans reales, maneja la API de OTel desde el tracer. Los hooks son síncronos, así que usa el
exportador síncrono (o encola y exporta desde un worker).
from opentelemetry import trace
tracer = trace.get_tracer("laya")
class OTelHooks:
def __init__(self):
self.spans = {}
def on_predict_start(self, ctx):
span = tracer.start_span("laya.predict", attributes={"laya.run_id": ctx.run_id, "laya.model": ctx.model})
self.spans[ctx.run_id] = span
def on_predict_end(self, ctx):
span = self.spans.pop(ctx.run_id, None)
if span is None:
return
if ctx.usage:
span.set_attribute("laya.input_tokens", ctx.usage["input_tokens"])
if ctx.error is not None:
span.record_exception(ctx.error)
span.set_status(trace.Status(trace.StatusCode.ERROR))
span.end()
laya.load("convaiinnovations/laya", hooks=[OTelHooks()], hooks_raise=False)
Establece hooks_raise=False para que una caída del tracer nunca haga fallar una solicitud.
Propagación distribuida
run_id es una cadena simple, así que inclúyela en todo lo que salga del proceso: líneas de registro, el payload
que se envía a un servicio de auditoría, o una cabecera HTTP si una decisión desencadena una llamada posterior.
def audit(ctx):
requests.post(
"https://audit.example/decisions",
json=record(ctx),
headers={"X-Laya-Run-Id": ctx.run_id},
timeout=2,
)
Recuerda que una llamada bloqueante como esta detiene el hilo que la invoca; mejor encólala cuando sirvas de forma concurrente. Consulta los patrones.
Llamadas anidadas
Un hook que vuelve a llamar a predict inicia una nueva llamada con una nueva run_id. La llamada padre y la
hija son independientes a menos que las enlaces tú mismo. Captura el id del padre y pásalo:
def enrich(ctx):
for state in ctx.states: # a hook sees every state of the call
child = enricher.predict(state, EXTRA_QUESTIONS)
record_child_span(parent_run_id=ctx.run_id, child_run_id=child.get("run_id"))
Una run_id padre, una llamada hija por estado. Un cuerpo que lee ctx.states[0] enlaza la primera
decisión de un lote y descarta las demás en silencio.
Protégete contra la recursión (consulta los antipatrones); la protección
más fácil es un agente enricher aparte, sin hooks.
Ver también
- Referencia de la API: el
PredictContextcompleto. - Patrones y antipatrones: trazado sin bloqueos.