Skip to content

Conversation

@xuan-cao-swi
Copy link
Contributor

Description

This PR introduces OpenTelemetry instrumentation for the openai Ruby gem from official OpenAI Ruby SDK, enabling automatic tracing for OpenAI API calls including chat completions, streaming responses, and embeddings. The main idea is to monkey-patch the request function which every openai api call use this function with different path.

The captured content is the content of message from user and assistants. It will only log the content if it's enabled.

request function takes all different openai ruby gem api call, but for now it only support chat (with stream) and embeddings

Chat Completion

client = OpenAI::Client.new(api_key: ENV['OPENAI_API_KEY'])

response = client.chat.completions.create(
  model: 'gpt-5-nano',
  messages: [
    { role: 'system', content: 'You are a helpful assistant.' },
    { role: 'user', content: 'Explain what Ruby is in one sentence.' }
  ],
  max_completion_tokens: 1000
)

# Result and Captured Content (if enabled):
I, [2025-11-22T00:04:48.965241 #99703]  INFO -- : {"event":"gen_ai.system.message","attributes":{"gen_ai.provider.name":"openai"},"body":{"content":"You are a helpful assistant."}}
I, [2025-11-22T00:04:48.965263 #99703]  INFO -- : {"event":"gen_ai.user.message","attributes":{"gen_ai.provider.name":"openai"},"body":{"content":"Explain what Ruby is in one sentence."}}
I, [2025-11-22T00:04:52.589295 #99703]  INFO -- : {"event":"gen_ai.choice","attributes":{"gen_ai.provider.name":"openai"},"body":{"index":0,"finish_reason":"stop","message":{"role":"assistant","content":"Ruby is a dynamic, open-source programming language designed for simplicity and productivity, with an elegant, human-friendly syntax."}}}

"Ruby is a dynamic, open-source programming language designed for simplicity and productivity, with an elegant, human-friendly syntax."

# Captured Span Attributes
-> gen_ai.operation.name: Str(chat)
-> gen_ai.provider.name: Str(openai)
-> gen_ai.request.model: Str(gpt-5-nano)
-> server.address: Str(api.openai.com)
-> server.port: Int(443)
-> http.request.method: Str(POST)
-> url.path: Str(chat/completions)
-> gen_ai.output.type: Str(text)
-> gen_ai.request.max_tokens: Int(1000)
-> gen_ai.response.model: Str(gpt-5-nano-2025-08-07)
-> gen_ai.response.id: Str(chatcmpl-CeVRmR6ubrhgLJOvFPvoDzm7uq0zm)
-> openai.response.service_tier: Str(default)
-> gen_ai.usage.input_tokens: Int(24)
-> gen_ai.usage.output_tokens: Int(232)
-> gen_ai.usage.total_tokens: Int(256)
-> gen_ai.response.finish_reasons: Slice(["stop"])

Streaming Chat Completion

stream = client.chat.completions.stream(
  model: 'gpt-5-nano',
  messages: [{ role: 'user', content: 'Count from 1 to 5 and say done.' }],
  max_completion_tokens: 1000
)

# Result and Captured Content (if enabled):
I, [2025-11-22T00:07:30.620543 #1730]  INFO -- : {"event":"gen_ai.system.message","attributes":{"gen_ai.provider.name":"openai"},"body":{"content":"You are a helpful assistant."}}
I, [2025-11-22T00:07:30.620575 #1730]  INFO -- : {"event":"gen_ai.user.message","attributes":{"gen_ai.provider.name":"openai"},"body":{"content":"Count from 1 to 5 and say done."}}
1, 2, 3, 4, 5. Done.
[Finished: stop]
I, [2025-11-22T00:07:33.966164 #1730]  INFO -- : {"event":"gen_ai.choice","attributes":{"gen_ai.provider.name":"openai"},"body":{"index":0,"finish_reason":"stop","message":{"role":"assistant","content":"1, 2, 3, 4, 5. Done."}}}

# Captured Span Attributes
-> gen_ai.operation.name: Str(chat)
-> gen_ai.provider.name: Str(openai)
-> gen_ai.request.model: Str(gpt-5-nano)
-> server.address: Str(api.openai.com)
-> server.port: Int(443)
-> http.request.method: Str(POST)
-> url.path: Str(chat/completions)
-> gen_ai.output.type: Str(text)
-> gen_ai.request.max_tokens: Int(1000)
-> gen_ai.response.model: Str(gpt-5-nano-2025-08-07)
-> gen_ai.response.id: Str(chatcmpl-CeVRzNeMmotPgE4f76N9DKAfS6z9g)
-> gen_ai.usage.input_tokens: Int(27)
-> gen_ai.usage.output_tokens: Int(217)
-> gen_ai.response.finish_reasons: Slice(["stop"])
-> openai.response.service_tier: Str(default)

Text Embeddings

response = client.embeddings.create(
  model: 'text-embedding-3-small',
  input: 'Ruby is a dynamic, open source programming language.'
)

# Result and Captured Content (if enabled):
I, [2025-11-22T00:08:40.827714 #2458]  INFO -- : {"event":"gen_ai.user.message","attributes":{"gen_ai.provider.name":"openai"},"body":{"content":"Ruby is a dynamic, open source programming language."}}
First 5 values: [-0.0451, 0.0145, -0.0182, -0.0009, 0.0348]
Usage: {:prompt_tokens=>10, :total_tokens=>10}
Embedding dimensions: 1536

# Captured Span Attributes
-> gen_ai.operation.name: Str(embeddings)
-> gen_ai.provider.name: Str(openai)
-> gen_ai.request.model: Str(text-embedding-3-small)
-> server.address: Str(api.openai.com)
-> server.port: Int(443)
-> http.request.method: Str(POST)
-> url.path: Str(embeddings)
-> gen_ai.output.type: Str(json)
-> gen_ai.response.model: Str(text-embedding-3-small)
-> gen_ai.usage.input_tokens: Int(10)
-> gen_ai.usage.total_tokens: Int(10)
-> gen_ai.embeddings.dimension.count: Int(1536)

@arielvalentin
Copy link
Contributor

@xuan-cao-swi did you generate this using instrumentation_generator ? There are a few changes missing like the toys release metadata, adding it to CI, patching the all gem. etc...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants