Architecture
Detailed architecture of the Rhesis tracing system.
Component Architecture
SDK Components
OpenTelemetry Integration
The TracerProvider and BatchSpanProcessor are set up once, in the shared packages/rhesis/src/rhesis/telemetry/provider.py package (imported by both the SDK and the backend):
Span Creation
@observe (sdk/src/rhesis/sdk/decorators/observe.py) wraps sync, async, and generator functions, starting a span on each call and setting attributes passed to the decorator. @endpoint (sdk/src/rhesis/sdk/decorators/endpoint.py) does the same and additionally registers the function for remote invocation over WebSocket.
Span names are validated server-side (packages/rhesis/src/rhesis/telemetry/schemas.py) against the pattern ai.<domain>.<action> (e.g. ai.llm.invoke) or function.<name>. The domains chain, workflow, and pipeline are rejected in favor of primitive operations; agent is allowed via the dedicated ai.agent.invoke / ai.agent.handoff operations for multi-agent tracing.
OTLP Exporter
RhesisOTLPExporter (packages/rhesis/src/rhesis/telemetry/exporter.py) subclasses OpenTelemetry’s OTLPSpanExporter:
export() converts OTEL spans to the shared OTELSpan schema, splits batches larger than max_chunk_size (100) into separate requests, and retries transient failures (connection errors, timeouts, 408/429/5xx) with exponential-jitter backoff bounded by a wall-clock deadline. Root spans are always placed in the first chunk. After 5 consecutive export failures it logs a warning with the endpoint and success rate.
Backend Components
Ingestion Endpoint
ingest_trace() in routers/telemetry.py (POST /telemetry/traces):
- Resolves
project_id— prefers the span’sproject_id, falls back to the request’s project scope, otherwise returns 422 - Validates the OTLP payload (Pydantic
OTELTraceBatch, including span-name conventions) - Stores spans via
crud.create_trace_spans(), commits, and releases the DB session - Dispatches
post_ingest_link.delay(...)— fire-and-forget
If the broker is unreachable, the request still returns 200 — linking and enrichment for that batch simply do not run. This differs from a separate code path used by direct SDK-endpoint invocation (services/invokers/tracing.py), which enqueues enrichment via AsyncService.execute_with_fallback: it tries the async Celery dispatch first and only falls back to running enrichment synchronously, in-process, when the broker raises a connection error.
The post_ingest_link task itself (tasks/telemetry/post_ingest.py) performs test-result linking, conversation-id linking, input-file linking, and dispatches an enrichment → evaluation chain per root span. See Worker: Trace Ingestion Pipeline for the full breakdown, including metric evaluation.
Linking Service
TraceLinkingService (services/telemetry/linking_service.py) has two entry points that share one implementation:
Enrichment
TraceEnricher (services/telemetry/enrichment/processor.py) computes costs, anomalies, and metadata from a trace’s spans:
- Costs —
calculate_token_costs()useslitellm.cost_per_token()on spans withai.operation.type = llm.invoke, in USD and EUR - Anomalies —
detect_anomalies()flags spans over 10 seconds (slow_span), LLM spans over 10,000 total tokens (high_token_usage), and error-status spans (error) - Metadata —
extract_metadata()collects unique models, tools, and operation types, plus the root span’s name
The result is cached in the enriched_data JSONB column:
Re-enrichment is skipped once every span in the trace has a non-null processed_at — new child spans (later LLM calls, tool calls) trigger a re-run so multi-turn traces stay up to date as they arrive.
Query API
Trace Retrieval
GET /telemetry/traces/{trace_id} (project_id is a required query param) returns the span tree plus trace-level rollups and linked entities:
| Field | Description |
|---|---|
root_spans | Spans arranged as a parent-child tree |
span_count, error_count, total_tokens, total_cost_usd | Computed across all spans in the trace |
trace_metrics_status, trace_reviews | LLM-based metric evaluation status and human review state |
project, endpoint, test_run, test_result, test | Linked entities, populated where applicable |
Configuration
BatchSpanProcessor
Environment Variables
See Environment Variables for the full reference.
Celery Workers
Traces are processed by the same worker(s) that run test execution — see Worker: Background Tasks for queue and concurrency configuration.