Tracing
Todo hook de uma chamada recebe o mesmo PredictContext.run_id, então um tracer consegue
correlacionar os eventos de início, fim e erro (e quaisquer spans que ele abrir) sem manter sua
própria contabilidade.
- run_id
- Um tracer mínimo
- Ciclo de vida do span
- Spans de erro
- OpenTelemetry
- Propagação distribuída
- Chamadas aninhadas
run_id
- Uma string
uuid4().hex, criada uma vez por chamada pública (predict_batch,system_one,Router.predict,ONNXAgent.system_one).Router.predict_batchcria uma por solicitação em vez disso, orun_idque uma chamadaRouter.predictpara aquela solicitação teria tido. - Compartilhado por todo hook daquela chamada, incluindo
on_erroreon_predict_end. - Não é global nem persistido: ele identifica uma chamada dentro do processo. Coloque-o nos seus logs e nas cargas de saída para correlacionar entre sistemas.
- Distinto por chamada, então duas chamadas nunca colidem.
call A: run_id=1a2b... start ──┐
├─ end
error ─┘
call B: run_id=9f8e... start ─── end
Um tracer mínimo
Mantenha um mapa de run_id para o span que você abriu no início, e feche-o no fim ou no erro.
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 sempre roda, ele é o lugar natural para fechar um span, e ele consegue ver
ctx.error no caminho de falha.
Ciclo de vida do 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 erro
on_predict_end roda no caminho de falha com ctx.error definido, então um único ponto de
fechamento trata os dois:
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)
Se você só implementar on_error, lembre que ele roda antes de on_predict_end; não feche o span nos
dois ou você contará em dobro.
OpenTelemetry
O exemplo examples/hooks/otel.py registra contadores e um
histograma. Para spans de verdade, conduza a API do OTel a partir do tracer. Os hooks são síncronos,
então use o exportador síncrono (ou enfileire e exporte a partir de um 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)
Defina hooks_raise=False para que uma indisponibilidade do tracer nunca falhe uma solicitação.
Propagação distribuída
run_id é uma string comum, então inclua-o em tudo que sai do processo: linhas de log, a carga enviada
a um serviço de auditoria, ou um cabeçalho HTTP se uma decisão disparar uma chamada a jusante.
def audit(ctx):
requests.post(
"https://audit.example/decisions",
json=record(ctx),
headers={"X-Laya-Run-Id": ctx.run_id},
timeout=2,
)
Lembre que uma chamada bloqueante como essa trava a thread que a chama; enfileire-a em vez disso ao servir de forma concorrente. Veja padrões.
Chamadas aninhadas
Um hook que chama predict de novo inicia uma nova chamada com um novo run_id. O pai e o filho são
independentes a menos que você os ligue por conta própria. Capture o id do pai e o repasse:
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"))
Um run_id pai, uma chamada filha por estado. Um corpo que lê ctx.states[0] liga a primeira decisão
de um lote e descarta silenciosamente o resto.
Proteja contra recursão (veja antipadrões); a proteção mais fácil é um
agente enricher separado, sem hooks.
Veja também
- Referência da API: o
PredictContextcompleto. - Padrões e antipadrões: tracing não bloqueante.