Traçage
Chaque hook d’un appel reçoit le même PredictContext.run_id, donc un traceur peut corréler les
événements de start, de fin et d’erreur (et tout span qu’il ouvre) sans tenir sa propre comptabilité.
- run_id
- Un traceur minimal
- Durée de vie du span
- Spans d’erreur
- OpenTelemetry
- Propagation distribuée
- Appels imbriqués
run_id
- Une chaîne
uuid4().hex, créée une fois par appel public (predict_batch,system_one,Router.predict,ONNXAgent.system_one).Router.predict_batchen crée un par requête à la place, lerun_idqu’aurait eu un appelRouter.predictpour cette requête. - Partagé par chaque hook de cet appel, y compris
on_erroreton_predict_end. - Ni global ni persisté : il identifie un appel au sein du processus. Mets-le dans tes logs et tes charges utiles sortantes pour corréler entre systèmes.
- Distinct par appel, donc deux appels n’entrent jamais en collision.
call A: run_id=1a2b... start ──┐
├─ end
error ─┘
call B: run_id=9f8e... start ─── end
Un traceur minimal
Garde une table de run_id vers le span que tu as ouvert au start, et ferme-le à la fin ou à
l’erreur.
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()])
Comme on_predict_end tourne toujours, c’est l’endroit naturel pour fermer un span, et il peut voir
ctx.error sur le chemin d’échec.
Durée de vie du 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 d’erreur
on_predict_end tourne sur le chemin d’échec avec ctx.error défini, donc un seul site de fermeture
gère les deux :
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 tu n’implémentes que on_error, souviens-toi qu’il se déclenche avant on_predict_end ; ne ferme
pas le span dans les deux ou tu compteras double.
OpenTelemetry
L’exemple examples/hooks/otel.py
enregistre des compteurs et un histogramme. Pour de vrais spans, pilote l’API OTel depuis le traceur.
Les hooks sont synchrones, alors utilise l’exportateur synchrone (ou mets en file et exporte depuis 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)
Définis hooks_raise=False pour qu’une panne du traceur ne fasse jamais échouer une requête.
Propagation distribuée
run_id est une simple chaîne, alors inclus-le dans tout ce qui quitte le processus : lignes de log,
charge utile envoyée à un service d’audit, ou en-tête HTTP si une décision déclenche un appel en aval.
def audit(ctx):
requests.post(
"https://audit.example/decisions",
json=record(ctx),
headers={"X-Laya-Run-Id": ctx.run_id},
timeout=2,
)
Souviens-toi qu’un appel bloquant comme celui-ci bloque le thread appelant ; mets-le en file à la place quand tu sers en parallèle. Voir motifs.
Appels imbriqués
Un hook qui rappelle predict démarre un nouvel appel avec un nouveau run_id. Le parent et l’enfant
sont indépendants sauf si tu les lies toi-même. Capture l’id du parent et passe-le :
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"))
Un run_id parent, un appel enfant par état. Un corps qui lit ctx.states[0] lie la première
décision d’un lot et abandonne silencieusement les autres.
Protège contre la récursion (voir anti-motifs) ; la garde la plus simple
est un agent enricher séparé sans hooks.
Voir aussi
- Référence de l’API : le
PredictContextcomplet. - Motifs et anti-motifs : traçage non bloquant.