Parseable

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 the TracerProvider and 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 Parseable

Each 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 openlit
pip install openai \
  openinference-instrumentation-openai \
  opentelemetry-sdk \
  opentelemetry-exporter-otlp-proto-http

Instrument 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

FieldMeaning
gen_ai.provider.nameAlways openai for this integration
gen_ai.request.modelThe model requested by the application
gen_ai.response.modelThe model version that actually served the request
gen_ai.operation.nameThe GenAI operation, such as chat
gen_ai.input.messagesThe request messages, including system/user/tool roles
gen_ai.output.messagesThe response messages and finish reason
gen_ai.usage.input_tokensInput token count
gen_ai.usage.output_tokensOutput token count
gen_ai.usage.costComputed request cost (OpenLIT computes this; OpenInference may not, depending on version)
gen_ai.server.time_to_first_tokenTime to first token
span_status_codeWhether the span completed successfully (1 = OK)
span_trace_id / span_parent_span_idUse 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 OpenAI client 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=True in openlit.init(). OpenInference: call provider.force_flush() or provider.shutdown() before the process exits. BatchSpanProcessor batches on a timer by default.

  • Prompt or response text appears in telemetry and that's a concern

    OpenLIT: pass capture_message_content=False to openlit.init(). OpenInference: check the instrumentor's config for a content-masking option, or set OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=false (or the instrumentor-specific env var) before instrumenting.

See also

Was this page helpful?

On this page