Using a coding agent? Paste this prompt directly in your voice agent codebase to jumpstart your Cekura OTel integration.
Overview
OpenTelemetry (OTel) tracing gives you deep visibility into your voice agent’s execution - every LLM call, TTS request, STT transcription, and tool invocation captured as spans with timing, token usage, and metadata.
Once traces are flowing into Cekura, you can:
- View span timelines and waterfall diagrams for each call
- Identify latency bottlenecks (slow LLM responses, TTS delays)
- Track token usage and model performance across calls
- Debug tool call failures with full input/output context
Using Pipecat? The Cekura Python SDK instruments your pipeline automatically - no manual setup needed. See the Pipecat Tracing guide.
Prerequisites
- A Cekura account with an API key
- An Agent ID or Project ID from the Cekura dashboard
- Python 3.8+ (examples use Python, but any OTel-compatible language works)
Install Dependencies
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
Endpoints
| Protocol | Endpoint | When to use |
|---|
| gRPC | otel.cekura.ai:443 | Recommended for most use cases. Lower overhead, better for high-volume tracing. |
| HTTP | otel-http.cekura.ai:443 | Use when gRPC is not available (e.g. environments that don’t support HTTP/2). |
Authentication
Every request to the OTel endpoint requires two headers:
| Header | Required | Description |
|---|
x-cekura-api-key | Yes | Your Cekura API key |
x-cekura-agent-id | Yes* | The agent ID to associate traces with |
x-cekura-project-id | Yes* | Alternative to agent ID - use project-level scoping |
* Provide either x-cekura-agent-id or x-cekura-project-id.
These are passed as exporter headers (HTTP) or gRPC metadata, not as span attributes.
Setup
Configure the OTel exporter
Set up the exporter with your Cekura credentials:from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
# Create the exporter with Cekura auth headers
exporter = OTLPSpanExporter(
endpoint="otel.cekura.ai:443",
headers=(
("x-cekura-api-key", "your-api-key"),
("x-cekura-agent-id", "your-agent-id"),
),
)
# Set up the tracer provider
resource = Resource.create({"service.name": "my-voice-agent"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(exporter))
# Get a tracer
tracer = provider.get_tracer("my-voice-agent")
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
# Create the exporter with Cekura auth headers
exporter = OTLPSpanExporter(
endpoint="https://otel-http.cekura.ai/v1/traces",
headers={
"x-cekura-api-key": "your-api-key",
"x-cekura-agent-id": "your-agent-id",
},
)
# Set up the tracer provider
resource = Resource.create({"service.name": "my-voice-agent"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(exporter))
# Get a tracer
tracer = provider.get_tracer("my-voice-agent")
Instrument your agent code
Create spans around each service call in your agent. Use the span names from the naming conventions for best results in the Cekura UI.def handle_conversation_turn(user_audio):
# Speech-to-text
with tracer.start_as_current_span("stt") as stt_span:
transcript = stt_service.transcribe(user_audio)
stt_span.set_attribute("stt.provider", "deepgram")
stt_span.set_attribute("stt.transcript", transcript)
# LLM call
with tracer.start_as_current_span("llm") as llm_span:
response = llm_service.chat(transcript)
llm_span.set_attribute("gen_ai.system", "openai")
llm_span.set_attribute("gen_ai.request.model", "gpt-4o")
llm_span.set_attribute("gen_ai.usage.input_tokens", response.usage.prompt_tokens)
llm_span.set_attribute("gen_ai.usage.output_tokens", response.usage.completion_tokens)
# Text-to-speech
with tracer.start_as_current_span("tts") as tts_span:
audio = tts_service.synthesize(response.text)
tts_span.set_attribute("tts.provider", "elevenlabs")
tts_span.set_attribute("tts.characters", len(response.text))
return audio
Capture the trace ID
Extract the trace ID from your root span - you’ll need this to correlate the trace with the call log in Cekura.from opentelemetry.trace import format_trace_id
# Start a root span for the entire call
with tracer.start_as_current_span("conversation") as root_span:
# Get the trace ID (32-character hex string)
trace_id = format_trace_id(root_span.get_span_context().trace_id)
# Run your agent conversation loop
while call_active:
handle_conversation_turn(user_audio)
# trace_id is now something like "4bf92f3577b34da6a3ce929d0e0e4736"
Send the trace ID with your call log
When sending the call log to Cekura via the Send Calls API, include the trace_id field. This links the OTel trace to the call in the dashboard.import requests
requests.post(
"https://api.cekura.ai/observability/v1/observe/",
headers={
"x-cekura-api-key": "your-api-key",
},
json={
"agent": your_agent_id,
"call_id": "unique-call-id",
"trace_id": trace_id, # 32-char hex from step 3
"transcript_json": transcript_data,
# ... other fields
},
)
Once the call log is submitted with a trace_id, the trace will be visible in the call details page in the Cekura dashboard.
Span Naming Conventions
Cekura recognizes these span names and renders them with specialized UI:
| Span Name | Purpose | Recommended Attributes |
|---|
stt | Speech-to-text | stt.provider, stt.transcript, stt.confidence |
llm | LLM / model call | gen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens |
tts | Text-to-speech | tts.provider, tts.characters |
tool_call | Tool / function call | function.name, function.input, function.output |
Custom span names work too - they just won’t have specialized styling in the UI.
Example: Full Voice Agent
Here’s a complete example showing a voice agent with OTel tracing and Cekura integration:
import os
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.trace import format_trace_id
CEKURA_API_KEY = os.getenv("CEKURA_API_KEY")
AGENT_ID = os.getenv("CEKURA_AGENT_ID")
# 1. Set up OTel tracing
exporter = OTLPSpanExporter(
endpoint="otel.cekura.ai:443",
headers=(
("x-cekura-api-key", CEKURA_API_KEY),
("x-cekura-agent-id", AGENT_ID),
),
)
provider = TracerProvider(
resource=Resource.create({"service.name": "my-voice-agent"})
)
provider.add_span_processor(BatchSpanProcessor(exporter))
tracer = provider.get_tracer("my-voice-agent")
# 2. Agent conversation with tracing
def run_call(call_id, user_audio_stream):
with tracer.start_as_current_span("conversation") as root_span:
trace_id = format_trace_id(root_span.get_span_context().trace_id)
transcript = []
for audio_chunk in user_audio_stream:
# STT
with tracer.start_as_current_span("stt") as span:
text = stt.transcribe(audio_chunk)
span.set_attribute("stt.provider", "deepgram")
span.set_attribute("stt.transcript", text)
transcript.append({"role": "user", "content": text})
# LLM
with tracer.start_as_current_span("llm") as span:
response = llm.chat(transcript)
span.set_attribute("gen_ai.request.model", "gpt-4o")
span.set_attribute("gen_ai.usage.input_tokens", response.usage.prompt_tokens)
span.set_attribute("gen_ai.usage.output_tokens", response.usage.completion_tokens)
transcript.append({"role": "assistant", "content": response.text})
# TTS
with tracer.start_as_current_span("tts") as span:
audio = tts.synthesize(response.text)
span.set_attribute("tts.provider", "elevenlabs")
yield audio
# 3. Flush traces and send call log to Cekura
provider.force_flush()
requests.post(
"https://api.cekura.ai/observability/v1/observe/",
headers={"x-cekura-api-key": CEKURA_API_KEY},
json={
"agent": int(AGENT_ID),
"call_id": call_id,
"trace_id": trace_id,
"transcript_json": transcript,
},
)
Next Steps
- Pipecat Tracing - auto-instrumentation for Pipecat agents
- Send Calls API - full API reference for the observe endpoint
- Custom Metrics - evaluate agent performance from your call data
- Cekura MCP Server - use MCP tools to create tests, run simulations, and monitor production calls directly from your IDE or AI assistant