The gateway now ships in the
praisonai-bot package. praisonai serve gateway still works exactly as documented here; for a standalone install see praisonai-bot Migration.Quick Start
Simple Usage — the safe, zero-cost default
Do nothing and the gateway holds a no-op hook. Every
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
Any object with a matching A production exporter follows the same shape but calls
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.How It Works
The hook is a synchronous context-manager factory, so it wraps both sync and async stages with the samewith ...: block.
| Step | What happens |
|---|---|
Enter with | The hook starts the span for that stage name |
| Body runs | The stage (sync or async) executes inside the span’s scope |
Exit with | The hook ends the span |
| Exception raised | Propagates out and the span is marked failed — implementations must not swallow it |
Configuration Options
Core exposes the seam through five top-level symbols onpraisonaiagents.gateway:
GatewayTraceHook
@runtime_checkable Protocol — the structural contract a tracer implements.NullGatewayTraceHook
Zero-cost no-op default class used when tracing is disabled.
NULL_GATEWAY_TRACE_HOOK
Shared stateless singleton of the no-op default.
resolve_trace_hook
Returns the supplied hook, or the no-op default when passed
None.stage contract is deliberately dependency-free — no OpenTelemetry import lives 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.
| Stage name | Fires around |
|---|---|
inbound | Reading a new message off a channel adapter |
admit | Admission / rate-limit / concurrency gating |
agent.run | One full agent turn |
llm.call | An individual LLM call inside a turn |
tool.call | An individual tool call inside a turn |
outbox.enqueue | Handing a reply to the outbound queue |
delivery | Actually sending the reply on the channel |
inbound — a turn arrives
inbound — a turn arrives
The message reaches the gateway. Span attributes typically carry the channel and correlation id.
admit — admission control
admit — admission control
Concurrency and rate-limit policies decide whether the turn proceeds or is rejected.
agent.run — the agent turn
agent.run — the agent turn
The agent processes the turn end to end. This is usually the parent span for the model and tool calls below.
llm.call — a model call
llm.call — a model call
A single LLM request. Attributes commonly include the model name.
tool.call — a tool invocation
tool.call — a tool invocation
A single tool execution. Attributes commonly include the tool name.
outbox.enqueue — queued for delivery
outbox.enqueue — queued for delivery
The reply is placed on the outbox for reliable delivery.
delivery — sent to the user
delivery — sent to the user
The reply is delivered to the channel. Attributes commonly include the channel.
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:
Best Practices
Keep OpenTelemetry out of core
Keep OpenTelemetry out of core
The seam is dependency-free on purpose. Core holds only the protocol and the no-op default, so there is no OTel import and no hot-path overhead when tracing is off. The heavy
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
Pass the inbound turn’s
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
An exception propagating out of the
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.Choosing an Integration Path
Related
Gateway Overview
Bot gateway architecture and core concepts
Bot Gateway
Multi-bot WebSocket gateway — the host whose pipeline these spans wrap
Gateway Metrics
Prometheus counters — the other observability rail alongside traces
Correlation IDs
The join key that ties spans, logs, and metrics to one turn
Gateway Forensics
Crash and shutdown forensics, keyed on the same correlation id
Observability Hooks
Lifecycle hooks for logging, metrics, and tracing
Gateway Observability
The gateway observability section that links here

