praisonai-bot package. praisonai serve gateway still works exactly as documented here; for a standalone install see praisonai-bot Migration.traceparent / tracestate) across process boundaries so the trace tree doesn’t break at the gateway.
The user sends a turn; the gateway opens a span around every stage — inbound, admission, agent run, each LLM and tool call, outbox, delivery — so latency and failures show up per stage in Jaeger, Tempo, Datadog, or Honeycomb.
When an exporter is attached, an inbound traceparent from a caller becomes the parent of agent.run, and the active context is propagated to downstream LLM / MCP / tool calls.
Quick Start
Simple Usage — the safe, zero-cost default
stage(...) returns a null context manager that ignores its arguments, so tracing adds negligible overhead until you attach a real exporter.With a real tracer — plug a custom hook
stage(name, *, correlation_id, **attrs) context-manager factory satisfies the protocol — no base class needed. This recorder captures each stage as it opens and closes:tracer.start_as_current_span(...) from the OpenTelemetry SDK inside stage. That exporter lives in the separate praisonai-plugins package — core ships only the seam and the no-op default.Continue an inbound trace — parent agent.run under the caller
traceparent; pull it out with extract_carrier and pass it as parent_carrier so agent.run continues the caller’s trace instead of starting a fresh detached one.extract_carrier returns None and stage ignores parent_carrier — the code is safe to write before you attach an exporter.How It Works
The hook is a synchronous context-manager factory, so it wraps both sync and async stages with the samewith ...: block.
With W3C trace-context propagation
When an exporter is attached, the gateway continues the caller’s trace on ingress and re-injects the active context on egress, so a single distributed trace flowsinbound → agent.run → tool / mcp / llm across processes.
With the no-op default, extract_carrier returns None and inject_context leaves the carrier untouched, so the same code runs unchanged with no exporter attached — agent.run simply starts a fresh root span.
Configuration Options
Core exposes the seam through five top-level symbols onpraisonaiagents.gateway:
GatewayTraceHook
@runtime_checkable Protocol — the structural contract a tracer implements.NullGatewayTraceHook
NULL_GATEWAY_TRACE_HOOK
resolve_trace_hook
None.inject_context
traceparent onto an outbound carrier at an egress boundary. No-op in the default.extract_carrier
traceparent / tracestate from an inbound header mapping and return a carrier for stage(..., parent_carrier=...). Returns None in the default.stage contract is deliberately dependency-free — no OpenTelemetry import lives in core:
Propagation methods (new in #4103)
Two additive methods move W3C trace context across process boundaries — one at egress, one at ingress:NullGatewayTraceHook, inject_context is a no-op that leaves carrier untouched, and extract_carrier always returns None. There is no OpenTelemetry import in core.Canonical Stage Names
GATEWAY_TRACE_STAGES is a tuple of the seven canonical span names, so a tracer plugin and the wrapper agree on names without importing each other.
inbound — a turn arrives
inbound — a turn arrives
admit — admission control
admit — admission control
agent.run — the agent turn
agent.run — the agent turn
llm.call — a model call
llm.call — a model call
tool.call — a tool invocation
tool.call — a tool invocation
outbox.enqueue — queued for delivery
outbox.enqueue — queued for delivery
delivery — sent to the user
delivery — sent to the user
Common Patterns
Pattern 1: Correlation id as a span attribute
Pass the inbound turn’s existing correlation id so spans and logs share a single key — the same id you already join logs on.Pattern 2: Wrapping a custom stage
Fire the seam around an async stage with the same synchronouswith block:
Pattern 3: resolve_trace_hook in a constructor
Accept an optional tracer= and resolve it once, so no stage ever branches on None:
Pattern 4: Continue a caller’s trace on ingress
Extract the inbound carrier once and pass it asparent_carrier, so agent.run nests under the caller’s span:
inbound → agent.run → tool / mcp / llm — where the agent turn hangs off the caller’s span instead of starting over.
Pattern 5: Inject the active context on egress
Seed the outbound headers with the provider’s own headers, then inject so the active context wins over any staletraceparent:
agent.run, so the same distributed trace continues across the process boundary instead of breaking at the egress call.
Best Practices
Keep OpenTelemetry out of core
Keep OpenTelemetry out of core
opentelemetry-sdk dependency belongs in the praisonai-plugins exporter, not in your agent code.Reuse the correlation id as a span attribute
Reuse the correlation id as a span attribute
correlation_id to every stage(...) call. Spans and logs then share one key, so a trace in Jaeger and a log line in your aggregator line up on the same id.Do not swallow exceptions in the context manager
Do not swallow exceptions in the context manager
with block is what marks the span as failed. Catching it inside stage hides errors from your tracer. Let it propagate — the no-op default already does.Seed provider headers before injecting the active context
Seed provider headers before injecting the active context
dict(provider_headers) first, then call inject_context(headers). The active traceparent then overwrites any stale one already present in the provider headers, so downstream spans nest under the current turn rather than an old trace.Extract once per turn at the ingress boundary
Extract once per turn at the ingress boundary
extract_carrier(inbound.headers) once when the turn arrives and reuse the returned carrier for the agent.run stage. Re-extracting inside nested stages is wasteful and risks parenting a span under the wrong context.`parent_carrier=None` is always safe
`parent_carrier=None` is always safe
stage ignores parent_carrier and behaves exactly as before. Code that always passes parent_carrier=self._trace.extract_carrier(inbound.headers) works whether or not an exporter is attached.Choosing an Integration Path
stage(name, correlation_id=..., **attrs) call sites continue to work unchanged. The default NullGatewayTraceHook remains a zero-cost no-op with no OpenTelemetry import in core.
