OpenAI
Send OpenAI Python SDK traces to Parseable using OpenTelemetry
The OpenAI Python SDK does not emit OpenTelemetry data on its own. There is no built-in openai.instrument() method for plain SDK calls. The separate openai-agents package has its own tracing, but that is meant for applications built with the Agents SDK, not for instrumenting a regular openai client.
To get spans from a call such as client.chat.completions.create(), use an instrumentor that patches the OpenAI client before it makes requests. This page shows two common options:
- OpenLIT: a short
openlit.init()setup, its own OTLP exporter configuration and token cost calculation out of the box. This is also the pattern used by the CrewAI and LiteLLM SDK docs in this hub. - OpenInference: a standard OpenTelemetry instrumentor through
OpenAIInstrumentor().instrument(tracer_provider=...). You create theTracerProviderand exporter yourself, which makes it easier to combine with other OpenInference instrumentors on the same provider.
Both approaches emit GenAI semantic convention spans that can be stored in the same Parseable traces dataset. Pick one for a given OpenAI client. Running both together can double-instrument the same request.
How it works
Python application using OpenAI SDK
|
| OpenLIT or OpenInference patches the OpenAI client
|
| OTLP traces
v
Parseable
|
+--> openai-sdk-traces traces dataset in ParseableEach chat completion produces a chat <model> span with GenAI attributes such as prompt, response, token usage and cost when the instrumentor provides it. The trace also includes a child POST span for the underlying HTTP call to api.openai.com. If the model responds with tool calls, the follow-up request that sends tool results back is captured as another span in the same trace.
Prerequisites
Before you start, keep these ready:
- A running Parseable instance
- A Parseable API key with ingest access
- Python 3.10 or newer
- An
OPENAI_API_KEY
Set up OpenAI SDK with Parseable
Install dependencies
pip install openai openlitpip install openai \
openinference-instrumentation-openai \
opentelemetry-sdk \
opentelemetry-exporter-otlp-proto-httpInstrument the client before making requests
Whichever instrumentor you pick, it must run before you construct the OpenAI client, so the patch is in place when the client makes its first call.
openlit.init() sets up its own TracerProvider and OTLP exporter, so you do not need a separate OpenTelemetry setup for this path.
import os
import openlit
from openai import OpenAI
openlit.init(
otlp_endpoint=os.environ["PARSEABLE_URL"], # e.g. http://<parseable-host>:8010
otlp_headers={
"X-API-Key": os.environ["PARSEABLE_API_KEY"],
"X-P-Stream": "openai-sdk-traces",
"X-P-Log-Source": "otel-traces",
},
service_name="openai-sdk-demo",
environment="production",
disable_batch=True,
disable_metrics=True,
disable_events=True,
)
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(response.choices[0].message.content)disable_batch=True exports each span as soon as it finishes, which is useful for short-lived scripts. Remove it for long-running services so spans batch and export on a timer instead.
OpenInference is a standard OpenTelemetry instrumentor. You build the TracerProvider and exporter yourself, then pass that provider to OpenAIInstrumentor().instrument(...). This is the same pattern the CrewAI integration uses when it combines CrewAIInstrumentor and OpenAIInstrumentor on one provider.
import os
from openai import OpenAI
from openinference.instrumentation.openai import OpenAIInstrumentor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
provider = TracerProvider(
resource=Resource.create({"service.name": "openai-sdk-demo"})
)
exporter = OTLPSpanExporter(
endpoint=f"{os.environ['PARSEABLE_URL']}/v1/traces",
headers={
"X-API-Key": os.environ["PARSEABLE_API_KEY"],
"X-P-Stream": "openai-sdk-traces",
"X-P-Log-Source": "otel-traces",
},
)
provider.add_span_processor(BatchSpanProcessor(exporter))
OpenAIInstrumentor().instrument(tracer_provider=provider)
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(response.choices[0].message.content)OTLPSpanExporter builds the URL as {PARSEABLE_URL}/v1/traces explicitly. Unlike OpenLIT, it does not append the path for you. BatchSpanProcessor batches on a timer by default. For short scripts, call provider.force_flush() or provider.shutdown() before exit so spans are exported before the process ends.
The dataset named in X-P-Stream is created automatically on first ingest if it does not already exist.
Tool calls
Tool-calling requests use the same instrumentation path under both instrumentors. OpenAI SDK-level tool call and result messages are captured as part of the same trace.
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "What's the weather in Bengaluru?"}],
tools=tools,
tool_choice="auto",
)
message = response.choices[0].message
if message.tool_calls:
messages = [{"role": "user", "content": "What's the weather in Bengaluru?"}, message]
for call in message.tool_calls:
# ... execute the tool, then append its result ...
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": '{"city": "Bengaluru", "temp_c": 28}',
})
client.chat.completions.create(model="gpt-4o-mini", messages=messages)Send a few requests
Run the application a few times with different models and prompts, including at least one tool-calling request. This gives you enough data to inspect normal chat spans, HTTP spans and tool-call follow-up spans in Parseable.
What you get in Parseable
Open openai-sdk-traces from the Traces page. Each chat completion appears as a chat <model> span with GenAI attributes, plus a child POST span for the HTTP call. Multiple calls in one process share service.instance.id, and tool-call follow-up requests can be followed through the same trace identifiers.
Useful fields
| Field | Meaning |
|---|---|
gen_ai.provider.name | Always openai for this integration |
gen_ai.request.model | The model requested by the application |
gen_ai.response.model | The model version that actually served the request |
gen_ai.operation.name | The GenAI operation, such as chat |
gen_ai.input.messages | The request messages, including system/user/tool roles |
gen_ai.output.messages | The response messages and finish reason |
gen_ai.usage.input_tokens | Input token count |
gen_ai.usage.output_tokens | Output token count |
gen_ai.usage.cost | Computed request cost (OpenLIT computes this; OpenInference may not, depending on version) |
gen_ai.server.time_to_first_token | Time to first token |
span_status_code | Whether the span completed successfully (1 = OK) |
span_trace_id / span_parent_span_id | Use these to reconstruct the chat span and its child HTTP span |
Query examples
Total requests and tokens by model:
SELECT
"gen_ai.request.model" AS model,
COUNT(*) AS requests,
SUM(CAST("gen_ai.usage.input_tokens" AS BIGINT)) AS input_tokens,
SUM(CAST("gen_ai.usage.output_tokens" AS BIGINT)) AS output_tokens
FROM "openai-sdk-traces"
WHERE "gen_ai.operation.name" = 'chat'
GROUP BY model;Error rate by model:
SELECT
"gen_ai.request.model" AS model,
COUNT(*) AS total,
SUM(CASE WHEN span_status_code != 1 THEN 1 ELSE 0 END) AS errors
FROM "openai-sdk-traces"
WHERE "gen_ai.operation.name" = 'chat'
GROUP BY model;OpenLIT or OpenInference
Use OpenLIT when you want a short init() call, built-in cost calculation and a setup that does not require you to build the OpenTelemetry provider yourself.
Use OpenInference when you are already building an OpenTelemetry TracerProvider for other instrumentors, such as combining CrewAIInstrumentor and OpenAIInstrumentor on one provider, or when you want direct control over the exporter and processors.
Troubleshooting
-
No traces appear
Confirm the instrumentor runs before the
OpenAIclient is constructed. If the client is imported and instantiated at module load time before instrumentation runs, it cannot be patched. -
Traces appear late or not at all in short scripts
OpenLIT: set
disable_batch=Trueinopenlit.init(). OpenInference: callprovider.force_flush()orprovider.shutdown()before the process exits.BatchSpanProcessorbatches on a timer by default. -
Prompt or response text appears in telemetry and that's a concern
OpenLIT: pass
capture_message_content=Falsetoopenlit.init(). OpenInference: check the instrumentor's config for a content-masking option, or setOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=false(or the instrumentor-specific env var) before instrumenting.
See also
Was this page helpful?