Observability
orchcore provides optional OpenTelemetry integration via OrchcoreTelemetry. When enabled, it emits traces for pipelines, phases, and agents. When OpenTelemetry packages are not installed, all methods silently become no-ops.
Overview
OrchcoreTelemetry creates hierarchical spans that map to orchcore's execution model:
pipeline span
├── phase span (planning)
│ └── agent span (claude)
└── phase span (execution)
├── agent span (claude)
└── agent span (codex)
Setup
Install OpenTelemetry Dependencies
OpenTelemetry is not a core dependency. The recommended installation path is the bundled telemetry extra:
uv pip install orchcore[telemetry]
This installs the tested OpenTelemetry API, SDK, and OTLP exporters for both gRPC and HTTP.
If you need a narrower dependency set, manual installation still works:
uv pip install opentelemetry-api opentelemetry-sdk
uv pip install opentelemetry-exporter-otlp-proto-grpc
# or
uv pip install opentelemetry-exporter-otlp-proto-http
Initialize Telemetry
from orchcore.observability import OrchcoreTelemetry
telemetry = OrchcoreTelemetry(
enabled=True,
service_name="my-orchestrator", # Default: "orchcore"
otlp_endpoint="http://localhost:4317", # Optional OTLP collector
otlp_protocol="grpc", # "grpc" (default) or "http"
)
If enabled=False (default) or if OpenTelemetry packages are not installed, the object is safe to use — all methods become no-ops.
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
False |
Enable telemetry. When False, all methods are no-ops |
service_name |
str |
"orchcore" |
OpenTelemetry service name attribute |
otlp_endpoint |
str \| None |
None |
OTLP collector endpoint. When None, no exporter is configured |
otlp_protocol |
str |
"grpc" |
Exporter protocol: "grpc" or "http" |
Creating Spans
All span methods are context managers that yield the span object (or None when disabled):
# Pipeline-level span
with telemetry.pipeline_span("my-pipeline", task_slug="fix-auth-bug") as span:
# Phase-level span
with telemetry.phase_span("planning", agent="claude") as span:
# Agent-level span
with telemetry.agent_span("planning", "claude") as span:
...
Recording Cost
Record cost information on the current active span:
from decimal import Decimal
telemetry.record_cost("claude", Decimal("0.42"))
This sets orchcore.cost.claude on the current span. When called inside a
pipeline_span, it also sets orchcore.cost.total to the running pipeline total
at recording time.
Graceful Degradation
OrchcoreTelemetry is designed to never break your pipeline:
- If OpenTelemetry packages are not installed → constructor logs a debug message, all methods become no-ops
- If OTLP exporter packages are missing → tracing works locally but spans are not exported
- If
record_costfails (span ended, invalid type) → a debug message is logged, no exception raised
Integration Example
from orchcore.observability import OrchcoreTelemetry
telemetry = OrchcoreTelemetry(
enabled=True,
service_name="planora",
otlp_endpoint="http://jaeger:4317",
)
with telemetry.pipeline_span("planora-run", task_slug="implement-feature"):
for phase in phases:
with telemetry.phase_span(phase.name):
for agent_name in phase.agents:
with telemetry.agent_span(phase.name, agent_name):
result = await runner.run(agent, prompt, output_path)
telemetry.record_cost(agent_name, result.cost_usd)
Related
- Architecture Overview — how observability fits into the broader system