opentelemetry-instrumentation-ruby_llm 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e43dc7d2e1f91e06c28aa8237a6393d565605f92635edf69e2948015e5d5c4b
4
- data.tar.gz: b7eb8e02952582e6d8af04f8ee04407ac5e880b9f992ccdd4e6e353e3c060af0
3
+ metadata.gz: 25a2701b37bc9acecc70d4ff326230cbf5e073454f47c453744ced08d468b2f8
4
+ data.tar.gz: a5885383024ecf52deb132bdf0ccb86b1cb9a0169561a714e6d3cabec8e3a64a
5
5
  SHA512:
6
- metadata.gz: b600d97f321e3626ffa9de8785e67762d3f391b60e5d11a5d3f97657cbd4801e8cb7d1dd86e632bc260873732772176371b75a1fe17f6325fc2815fb23dc4b9b
7
- data.tar.gz: 76384730ef9cfe8cfe7f1d5b68109c6cbd4bedb897f18393dc717e0e5089d3ec7cbd81f21f728db4e82bc73ff72f08765b75b97e980ef665d5662ad5321759c5
6
+ metadata.gz: 023c166358bee510b9cd75f41764179e019eba7f3df1153c8d3806a8ce7488b7781688b708a2b315b4a2207306d822cf063c2e2125874cbbe3fc912176897edd
7
+ data.tar.gz: ef4d632a49a4d22d66d751648a2c5edf6ba13d0223dc1d42901193f675e13eff065ea8dbf713bac7f29450b2af6fd510126e4bbf5a713138bf16b9a74152c642
@@ -16,6 +16,7 @@ jobs:
16
16
  - '3.4'
17
17
  - '3.3'
18
18
  - '3.2'
19
+ - '3.1'
19
20
 
20
21
  steps:
21
22
  - uses: actions/checkout@v4
@@ -15,7 +15,9 @@ module OpenTelemetry
15
15
 
16
16
  install do |_config|
17
17
  require_relative "patches/chat"
18
+ require_relative "patches/embedding"
18
19
  ::RubyLLM::Chat.prepend(Patches::Chat)
20
+ ::RubyLLM::Embedding.singleton_class.prepend(Patches::Embedding)
19
21
  end
20
22
  end
21
23
  end
@@ -5,7 +5,7 @@ module OpenTelemetry
5
5
  module RubyLLM
6
6
  module Patches
7
7
  module Chat
8
- def ask(message, &block)
8
+ def complete(&)
9
9
  provider = @model&.provider || "unknown"
10
10
  model_id = @model&.id || "unknown"
11
11
 
@@ -47,9 +47,6 @@ module OpenTelemetry
47
47
 
48
48
  result
49
49
  end
50
- rescue StandardError => e
51
- OpenTelemetry.handle_error(exception: e)
52
- super
53
50
  end
54
51
 
55
52
  def execute_tool(tool_call)
@@ -66,9 +63,6 @@ module OpenTelemetry
66
63
  span.set_attribute("gen_ai.tool.call.result", result_str[0..500])
67
64
  result
68
65
  end
69
- rescue StandardError => e
70
- OpenTelemetry.handle_error(exception: e)
71
- super
72
66
  end
73
67
 
74
68
  private
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenTelemetry
4
+ module Instrumentation
5
+ module RubyLLM
6
+ module Patches
7
+ module Embedding
8
+ def embed(text, model: nil, provider: nil, assume_model_exists: false, context: nil, dimensions: nil)
9
+ config = context&.config || ::RubyLLM.config
10
+ resolved_model = model || config.default_embedding_model
11
+ model_obj, _provider_instance = ::RubyLLM::Models.resolve(
12
+ resolved_model, provider: provider, assume_exists: assume_model_exists, config: config
13
+ )
14
+ model_id = model_obj.id
15
+ provider_name = model_obj.provider || "unknown"
16
+
17
+ attributes = {
18
+ "gen_ai.operation.name" => "embeddings",
19
+ "gen_ai.provider.name" => provider_name,
20
+ "gen_ai.request.model" => model_id
21
+ }
22
+
23
+ tracer.in_span("embeddings #{model_id}", attributes: attributes, kind: OpenTelemetry::Trace::SpanKind::CLIENT) do |span|
24
+ begin
25
+ result = super
26
+ rescue => e
27
+ span.record_exception(e)
28
+ span.status = OpenTelemetry::Trace::Status.error(e.message)
29
+ span.set_attribute("error.type", e.class.name)
30
+ raise
31
+ end
32
+
33
+ span.set_attribute("gen_ai.response.model", result.model) if result.model
34
+ span.set_attribute("gen_ai.usage.input_tokens", result.input_tokens) if result.input_tokens&.positive?
35
+
36
+ if result.vectors.is_a?(Array)
37
+ first = result.vectors.first
38
+ vector = first.is_a?(Array) ? first : result.vectors
39
+ span.set_attribute("gen_ai.embeddings.dimension.count", vector.length) if vector.is_a?(Array)
40
+ end
41
+
42
+ result
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def tracer
49
+ RubyLLM::Instrumentation.instance.tracer
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -3,7 +3,7 @@
3
3
  module OpenTelemetry
4
4
  module Instrumentation
5
5
  module RubyLLM
6
- VERSION = "0.2.0"
6
+ VERSION = "0.4.0"
7
7
  end
8
8
  end
9
9
  end
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.description = "Adds OpenTelemetry tracing to RubyLLM chat operations"
12
12
  spec.homepage = "https://github.com/thoughtbot/opentelemetry-instrumentation-ruby_llm"
13
13
 
14
- spec.required_ruby_version = ">= 3.2.0"
14
+ spec.required_ruby_version = ">= 3.1.3"
15
15
 
16
16
  spec.metadata["homepage_uri"] = spec.homepage
17
17
 
@@ -67,7 +67,7 @@ class InstrumentationTest < Minitest::Test
67
67
  assert_equal OpenTelemetry::Trace::Status::ERROR, span.status.code
68
68
  end
69
69
 
70
- def test_ask_still_works_when_instrumentation_fails
70
+ def test_instruments_complete_called_directly
71
71
  stub_request(:post, "https://api.openai.com/v1/chat/completions")
72
72
  .to_return(
73
73
  status: 200,
@@ -78,7 +78,7 @@ class InstrumentationTest < Minitest::Test
78
78
  model: "gpt-4o-mini",
79
79
  choices: [{
80
80
  index: 0,
81
- message: { role: "assistant", content: "Hello!" },
81
+ message: { role: "assistant", content: "Hello, world!" },
82
82
  finish_reason: "stop"
83
83
  }],
84
84
  usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
@@ -86,10 +86,18 @@ class InstrumentationTest < Minitest::Test
86
86
  )
87
87
 
88
88
  chat = RubyLLM.chat(model: "gpt-4o-mini")
89
- chat.define_singleton_method(:tracer) { raise StandardError, "instrumentation bug" }
89
+ chat.add_message(role: :user, content: "Hi")
90
+ chat.complete
91
+
92
+ spans = EXPORTER.finished_spans
93
+ assert_equal 1, spans.length
90
94
 
91
- response = chat.ask("Hi")
92
- assert_equal "Hello!", response.content
95
+ span = spans.first
96
+ assert_equal "chat gpt-4o-mini", span.name
97
+ assert_equal "chat", span.attributes["gen_ai.operation.name"]
98
+ assert_equal "openai", span.attributes["gen_ai.provider.name"]
99
+ assert_equal 10, span.attributes["gen_ai.usage.input_tokens"]
100
+ assert_equal 5, span.attributes["gen_ai.usage.output_tokens"]
93
101
  end
94
102
 
95
103
  def test_creates_span_for_tool_call
@@ -155,7 +163,7 @@ class InstrumentationTest < Minitest::Test
155
163
  chat_spans = spans.select { |s| s.name.include?("chat ") }
156
164
 
157
165
  assert_equal 1, tool_spans.length
158
- assert_equal 1, chat_spans.length
166
+ assert_equal 2, chat_spans.length
159
167
 
160
168
  tool_span = tool_spans.first
161
169
  assert_equal OpenTelemetry::Trace::SpanKind::INTERNAL, tool_span.kind
@@ -167,68 +175,6 @@ class InstrumentationTest < Minitest::Test
167
175
  assert_equal "function", tool_span.attributes["gen_ai.tool.type"]
168
176
  end
169
177
 
170
- def test_execute_tool_still_works_when_instrumentation_fails
171
- calculator = Class.new(RubyLLM::Tool) do
172
- def self.name = "calculator"
173
- description "Performs math"
174
- param :expression, type: "string", desc: "Math expression"
175
-
176
- def execute(expression:)
177
- eval(expression).to_s
178
- end
179
- end
180
-
181
- stub_request(:post, "https://api.openai.com/v1/chat/completions")
182
- .to_return(
183
- {
184
- status: 200,
185
- headers: { "Content-Type" => "application/json" },
186
- body: {
187
- id: "chatcmpl-123",
188
- object: "chat.completion",
189
- model: "gpt-4o-mini",
190
- choices: [{
191
- index: 0,
192
- message: {
193
- role: "assistant",
194
- content: nil,
195
- tool_calls: [{
196
- id: "call_abc123",
197
- type: "function",
198
- function: { name: "calculator", arguments: '{"expression":"2+2"}' }
199
- }]
200
- },
201
- finish_reason: "tool_calls"
202
- }],
203
- usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
204
- }.to_json
205
- },
206
- {
207
- status: 200,
208
- headers: { "Content-Type" => "application/json" },
209
- body: {
210
- id: "chatcmpl-456",
211
- object: "chat.completion",
212
- model: "gpt-4o-mini",
213
- choices: [{
214
- index: 0,
215
- message: { role: "assistant", content: "The answer is 4" },
216
- finish_reason: "stop"
217
- }],
218
- usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 }
219
- }.to_json
220
- }
221
- )
222
-
223
- chat = RubyLLM.chat(model: "gpt-4o-mini")
224
- chat.with_tool(calculator)
225
-
226
- chat.define_singleton_method(:tracer) { raise StandardError, "instrumentation bug" }
227
-
228
- response = chat.ask("What is 2+2?")
229
- assert_equal "The answer is 4", response.content
230
- end
231
-
232
178
  def test_does_not_capture_content_by_default
233
179
  stub_request(:post, "https://api.openai.com/v1/chat/completions")
234
180
  .to_return(
@@ -299,6 +245,53 @@ class InstrumentationTest < Minitest::Test
299
245
  OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:capture_content] = false
300
246
  end
301
247
 
248
+ def test_creates_span_for_embedding
249
+ stub_request(:post, "https://api.openai.com/v1/embeddings")
250
+ .to_return(
251
+ status: 200,
252
+ headers: { "Content-Type" => "application/json" },
253
+ body: {
254
+ object: "list",
255
+ model: "text-embedding-3-small",
256
+ data: [
257
+ { object: "embedding", index: 0, embedding: [0.1, 0.2, 0.3] }
258
+ ],
259
+ usage: { prompt_tokens: 8, total_tokens: 8 }
260
+ }.to_json
261
+ )
262
+
263
+ RubyLLM.embed("Hello, world!", model: "text-embedding-3-small")
264
+
265
+ spans = EXPORTER.finished_spans
266
+ assert_equal 1, spans.length
267
+
268
+ span = spans.first
269
+ assert_equal OpenTelemetry::Trace::SpanKind::CLIENT, span.kind
270
+ assert_equal "embeddings text-embedding-3-small", span.name
271
+ assert_equal "embeddings", span.attributes["gen_ai.operation.name"]
272
+ assert_equal "openai", span.attributes["gen_ai.provider.name"]
273
+ assert_equal "text-embedding-3-small", span.attributes["gen_ai.request.model"]
274
+ assert_equal "text-embedding-3-small", span.attributes["gen_ai.response.model"]
275
+ assert_equal 8, span.attributes["gen_ai.usage.input_tokens"]
276
+ assert_equal 3, span.attributes["gen_ai.embeddings.dimension.count"]
277
+ end
278
+
279
+ def test_records_error_on_embedding_api_failure
280
+ stub_request(:post, "https://api.openai.com/v1/embeddings")
281
+ .to_return(status: 500, body: "Internal Server Error")
282
+
283
+ assert_raises do
284
+ RubyLLM.embed("Hello", model: "text-embedding-3-small")
285
+ end
286
+
287
+ spans = EXPORTER.finished_spans
288
+ span = spans.last
289
+
290
+ assert_equal "embeddings text-embedding-3-small", span.name
291
+ assert span.attributes["error.type"]
292
+ assert_equal OpenTelemetry::Trace::Status::ERROR, span.status.code
293
+ end
294
+
302
295
  def test_captures_content_when_enabled_via_env_var
303
296
  ENV["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = "true"
304
297
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opentelemetry-instrumentation-ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clarissa Borges
@@ -62,6 +62,7 @@ files:
62
62
  - lib/opentelemetry-instrumentation-ruby_llm.rb
63
63
  - lib/opentelemetry/instrumentation/ruby_llm/instrumentation.rb
64
64
  - lib/opentelemetry/instrumentation/ruby_llm/patches/chat.rb
65
+ - lib/opentelemetry/instrumentation/ruby_llm/patches/embedding.rb
65
66
  - lib/opentelemetry/instrumentation/ruby_llm/version.rb
66
67
  - opentelemetry-instrumentation-ruby_llm.gemspec
67
68
  - test/instrumentation_test.rb
@@ -78,7 +79,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
78
79
  requirements:
79
80
  - - ">="
80
81
  - !ruby/object:Gem::Version
81
- version: 3.2.0
82
+ version: 3.1.3
82
83
  required_rubygems_version: !ruby/object:Gem::Requirement
83
84
  requirements:
84
85
  - - ">="