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 +4 -4
- data/.github/workflows/main.yml +1 -0
- data/lib/opentelemetry/instrumentation/ruby_llm/instrumentation.rb +2 -0
- data/lib/opentelemetry/instrumentation/ruby_llm/patches/chat.rb +1 -7
- data/lib/opentelemetry/instrumentation/ruby_llm/patches/embedding.rb +55 -0
- data/lib/opentelemetry/instrumentation/ruby_llm/version.rb +1 -1
- data/opentelemetry-instrumentation-ruby_llm.gemspec +1 -1
- data/test/instrumentation_test.rb +61 -68
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25a2701b37bc9acecc70d4ff326230cbf5e073454f47c453744ced08d468b2f8
|
|
4
|
+
data.tar.gz: a5885383024ecf52deb132bdf0ccb86b1cb9a0169561a714e6d3cabec8e3a64a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 023c166358bee510b9cd75f41764179e019eba7f3df1153c8d3806a8ce7488b7781688b708a2b315b4a2207306d822cf063c2e2125874cbbe3fc912176897edd
|
|
7
|
+
data.tar.gz: ef4d632a49a4d22d66d751648a2c5edf6ba13d0223dc1d42901193f675e13eff065ea8dbf713bac7f29450b2af6fd510126e4bbf5a713138bf16b9a74152c642
|
data/.github/workflows/main.yml
CHANGED
|
@@ -5,7 +5,7 @@ module OpenTelemetry
|
|
|
5
5
|
module RubyLLM
|
|
6
6
|
module Patches
|
|
7
7
|
module Chat
|
|
8
|
-
def
|
|
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
|
|
@@ -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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
92
|
-
assert_equal "
|
|
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
|
|
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.
|
|
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.
|
|
82
|
+
version: 3.1.3
|
|
82
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
84
|
requirements:
|
|
84
85
|
- - ">="
|