opentelemetry-instrumentation-ruby_llm 0.1.0 → 0.2.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/.gitignore +1 -0
- data/README.md +31 -1
- data/example/trace_demonstration_with_langfuse.rb +2 -1
- data/example/trace_demonstration_with_langfuse_and_tools.rb +4 -1
- data/lib/opentelemetry/instrumentation/ruby_llm/instrumentation.rb +2 -0
- data/lib/opentelemetry/instrumentation/ruby_llm/patches/chat.rb +55 -10
- data/lib/opentelemetry/instrumentation/ruby_llm/version.rb +1 -1
- data/test/instrumentation_test.rb +104 -0
- metadata +1 -2
- data/Gemfile.lock +0 -105
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e43dc7d2e1f91e06c28aa8237a6393d565605f92635edf69e2948015e5d5c4b
|
|
4
|
+
data.tar.gz: b7eb8e02952582e6d8af04f8ee04407ac5e880b9f992ccdd4e6e353e3c060af0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b600d97f321e3626ffa9de8785e67762d3f391b60e5d11a5d3f97657cbd4801e8cb7d1dd86e632bc260873732772176371b75a1fe17f6325fc2815fb23dc4b9b
|
|
7
|
+
data.tar.gz: 76384730ef9cfe8cfe7f1d5b68109c6cbd4bedb897f18393dc717e0e5089d3ec7cbd81f21f728db4e82bc73ff72f08765b75b97e980ef665d5662ad5321759c5
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -30,6 +30,35 @@ OpenTelemetry::SDK.configure do |c|
|
|
|
30
30
|
end
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
### Content capture
|
|
36
|
+
|
|
37
|
+
By default, message content is **not captured**. To enable it:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
OpenTelemetry::SDK.configure do |c|
|
|
41
|
+
c.use 'OpenTelemetry::Instrumentation::RubyLLM', capture_content: true
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or set the environment variable:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
When enabled, the following attributes are added to chat spans:
|
|
52
|
+
|
|
53
|
+
| Attribute | Description |
|
|
54
|
+
|-----------|-------------|
|
|
55
|
+
| `gen_ai.system_instructions` | System instructions provided via `with_instructions` |
|
|
56
|
+
| `gen_ai.input.messages` | Input messages sent to the model |
|
|
57
|
+
| `gen_ai.output.messages` | Final output messages from the model |
|
|
58
|
+
|
|
59
|
+
> [!WARNING]
|
|
60
|
+
> Captured content may include sensitive or personally identifiable information (PII). Use with caution in production environments.
|
|
61
|
+
|
|
33
62
|
## What's traced?
|
|
34
63
|
|
|
35
64
|
| Feature | Status |
|
|
@@ -37,9 +66,10 @@ end
|
|
|
37
66
|
| Chat completions | Supported |
|
|
38
67
|
| Tool calls | Supported |
|
|
39
68
|
| Error handling | Supported |
|
|
69
|
+
| Opt-in input/output content capture | Supported |
|
|
40
70
|
| Conversation tracking (`gen_ai.conversation.id`) | Planned |
|
|
41
|
-
| Opt-in input/output content capture | Planned |
|
|
42
71
|
| System instructions capture | Planned |
|
|
72
|
+
| Custom attributes on traces and spans | Planned |
|
|
43
73
|
| Embeddings | Planned |
|
|
44
74
|
| Streaming | Planned |
|
|
45
75
|
|
|
@@ -26,7 +26,7 @@ OpenTelemetry::SDK.configure do |c|
|
|
|
26
26
|
)
|
|
27
27
|
)
|
|
28
28
|
)
|
|
29
|
-
c.use "OpenTelemetry::Instrumentation::RubyLLM"
|
|
29
|
+
c.use "OpenTelemetry::Instrumentation::RubyLLM", capture_content: true
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
RubyLLM.configure do |c|
|
|
@@ -35,6 +35,7 @@ RubyLLM.configure do |c|
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
chat = RubyLLM.chat
|
|
38
|
+
chat.with_instructions("You are a helpful assistant that provides concise answers.")
|
|
38
39
|
response = chat.ask("What is the meaning of life?")
|
|
39
40
|
puts "\nResponse: #{response.content}"
|
|
40
41
|
|
|
@@ -26,7 +26,7 @@ OpenTelemetry::SDK.configure do |c|
|
|
|
26
26
|
)
|
|
27
27
|
)
|
|
28
28
|
)
|
|
29
|
-
c.use "OpenTelemetry::Instrumentation::RubyLLM"
|
|
29
|
+
c.use "OpenTelemetry::Instrumentation::RubyLLM", capture_content: true
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
RubyLLM.configure do |c|
|
|
@@ -44,9 +44,12 @@ class Calculator < RubyLLM::Tool
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
chat = RubyLLM.chat
|
|
47
|
+
chat.with_instructions("You are a helpful assistant that provides concise answers.")
|
|
47
48
|
chat.with_tool(Calculator)
|
|
48
49
|
response = chat.ask("Use the calculator tool to compute 123 * 456")
|
|
49
50
|
puts "\nResponse: #{response.content}"
|
|
51
|
+
response = chat.ask("Use the tool again to compute 789 + 1011")
|
|
52
|
+
puts "\nResponse: #{response.content}"
|
|
50
53
|
|
|
51
54
|
# This line is only necessary in short-lived scripts. In a long-running application, spans will be flushed automatically.
|
|
52
55
|
OpenTelemetry.tracer_provider.force_flush
|
|
@@ -18,22 +18,34 @@ module OpenTelemetry
|
|
|
18
18
|
tracer.in_span("chat #{model_id}", attributes: attributes, kind: OpenTelemetry::Trace::SpanKind::CLIENT) do |span|
|
|
19
19
|
begin
|
|
20
20
|
result = super
|
|
21
|
-
|
|
22
|
-
if @messages.last
|
|
23
|
-
response = @messages.last
|
|
24
|
-
span.set_attribute("gen_ai.response.model", response.model_id) if response.model_id
|
|
25
|
-
span.set_attribute("gen_ai.usage.input_tokens", response.input_tokens) if response.input_tokens
|
|
26
|
-
span.set_attribute("gen_ai.usage.output_tokens", response.output_tokens) if response.output_tokens
|
|
27
|
-
span.set_attribute("gen_ai.request.temperature", @temperature) if @temperature
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
result
|
|
31
21
|
rescue => e
|
|
32
22
|
span.record_exception(e)
|
|
33
23
|
span.status = OpenTelemetry::Trace::Status.error(e.message)
|
|
34
24
|
span.set_attribute("error.type", e.class.name)
|
|
35
25
|
raise
|
|
36
26
|
end
|
|
27
|
+
|
|
28
|
+
if @messages.last
|
|
29
|
+
response = @messages.last
|
|
30
|
+
span.set_attribute("gen_ai.response.model", response.model_id) if response.model_id
|
|
31
|
+
span.set_attribute("gen_ai.usage.input_tokens", response.input_tokens) if response.input_tokens
|
|
32
|
+
span.set_attribute("gen_ai.usage.output_tokens", response.output_tokens) if response.output_tokens
|
|
33
|
+
span.set_attribute("gen_ai.request.temperature", @temperature) if @temperature
|
|
34
|
+
|
|
35
|
+
if capture_content?
|
|
36
|
+
system_messages = @messages.select { |m| m.role == :system }
|
|
37
|
+
input_messages = @messages[0..-2].reject { |m| m.role == :system }
|
|
38
|
+
|
|
39
|
+
unless system_messages.empty?
|
|
40
|
+
span.set_attribute("gen_ai.system_instructions", format_system_instructions(system_messages))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
span.set_attribute("gen_ai.input.messages", format_messages(input_messages))
|
|
44
|
+
span.set_attribute("gen_ai.output.messages", format_messages([response]))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
result
|
|
37
49
|
end
|
|
38
50
|
rescue StandardError => e
|
|
39
51
|
OpenTelemetry.handle_error(exception: e)
|
|
@@ -61,6 +73,39 @@ module OpenTelemetry
|
|
|
61
73
|
|
|
62
74
|
private
|
|
63
75
|
|
|
76
|
+
def capture_content?
|
|
77
|
+
env_value = ENV["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"]
|
|
78
|
+
return env_value.to_s.strip.casecmp("true").zero? unless env_value.nil?
|
|
79
|
+
|
|
80
|
+
RubyLLM::Instrumentation.instance.config[:capture_content]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def format_messages(messages)
|
|
84
|
+
messages.map { |m| format_message(m) }.to_json
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def format_message(message)
|
|
88
|
+
msg = { role: message.role.to_s, parts: [] }
|
|
89
|
+
|
|
90
|
+
if message.content
|
|
91
|
+
msg[:parts] << { type: "text", content: message.content.to_s }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if message.tool_calls&.any?
|
|
95
|
+
message.tool_calls.each_value do |tc|
|
|
96
|
+
msg[:parts] << { type: "tool_call", id: tc.id, name: tc.name, arguments: tc.arguments }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
msg[:tool_call_id] = message.tool_call_id if message.tool_call_id
|
|
101
|
+
|
|
102
|
+
msg
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def format_system_instructions(system_messages)
|
|
106
|
+
system_messages.map { |m| { type: "text", content: m.content.to_s } }.to_json
|
|
107
|
+
end
|
|
108
|
+
|
|
64
109
|
def tracer
|
|
65
110
|
RubyLLM::Instrumentation.instance.tracer
|
|
66
111
|
end
|
|
@@ -228,4 +228,108 @@ class InstrumentationTest < Minitest::Test
|
|
|
228
228
|
response = chat.ask("What is 2+2?")
|
|
229
229
|
assert_equal "The answer is 4", response.content
|
|
230
230
|
end
|
|
231
|
+
|
|
232
|
+
def test_does_not_capture_content_by_default
|
|
233
|
+
stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
|
234
|
+
.to_return(
|
|
235
|
+
status: 200,
|
|
236
|
+
headers: { "Content-Type" => "application/json" },
|
|
237
|
+
body: {
|
|
238
|
+
id: "chatcmpl-123",
|
|
239
|
+
object: "chat.completion",
|
|
240
|
+
model: "gpt-4o-mini",
|
|
241
|
+
choices: [{
|
|
242
|
+
index: 0,
|
|
243
|
+
message: { role: "assistant", content: "Hello, world!" },
|
|
244
|
+
finish_reason: "stop"
|
|
245
|
+
}],
|
|
246
|
+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
247
|
+
}.to_json
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
251
|
+
chat.with_instructions("You are helpful")
|
|
252
|
+
chat.ask("Hi")
|
|
253
|
+
|
|
254
|
+
span = EXPORTER.finished_spans.first
|
|
255
|
+
assert_nil span.attributes["gen_ai.system_instructions"]
|
|
256
|
+
assert_nil span.attributes["gen_ai.input.messages"]
|
|
257
|
+
assert_nil span.attributes["gen_ai.output.messages"]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def test_captures_content_when_enabled
|
|
261
|
+
OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:capture_content] = true
|
|
262
|
+
|
|
263
|
+
stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
|
264
|
+
.to_return(
|
|
265
|
+
status: 200,
|
|
266
|
+
headers: { "Content-Type" => "application/json" },
|
|
267
|
+
body: {
|
|
268
|
+
id: "chatcmpl-123",
|
|
269
|
+
object: "chat.completion",
|
|
270
|
+
model: "gpt-4o-mini",
|
|
271
|
+
choices: [{
|
|
272
|
+
index: 0,
|
|
273
|
+
message: { role: "assistant", content: "Hello, world!" },
|
|
274
|
+
finish_reason: "stop"
|
|
275
|
+
}],
|
|
276
|
+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
277
|
+
}.to_json
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
281
|
+
chat.with_instructions("You are helpful")
|
|
282
|
+
chat.ask("Hi")
|
|
283
|
+
|
|
284
|
+
span = EXPORTER.finished_spans.first
|
|
285
|
+
|
|
286
|
+
system_instructions = JSON.parse(span.attributes["gen_ai.system_instructions"])
|
|
287
|
+
assert_equal [{ "type" => "text", "content" => "You are helpful" }], system_instructions
|
|
288
|
+
|
|
289
|
+
input_messages = JSON.parse(span.attributes["gen_ai.input.messages"])
|
|
290
|
+
assert_equal 1, input_messages.length
|
|
291
|
+
assert_equal "user", input_messages[0]["role"]
|
|
292
|
+
assert_equal [{ "type" => "text", "content" => "Hi" }], input_messages[0]["parts"]
|
|
293
|
+
|
|
294
|
+
output_messages = JSON.parse(span.attributes["gen_ai.output.messages"])
|
|
295
|
+
assert_equal 1, output_messages.length
|
|
296
|
+
assert_equal "assistant", output_messages[0]["role"]
|
|
297
|
+
assert_equal [{ "type" => "text", "content" => "Hello, world!" }], output_messages[0]["parts"]
|
|
298
|
+
ensure
|
|
299
|
+
OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:capture_content] = false
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def test_captures_content_when_enabled_via_env_var
|
|
303
|
+
ENV["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = "true"
|
|
304
|
+
|
|
305
|
+
stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
|
306
|
+
.to_return(
|
|
307
|
+
status: 200,
|
|
308
|
+
headers: { "Content-Type" => "application/json" },
|
|
309
|
+
body: {
|
|
310
|
+
id: "chatcmpl-123",
|
|
311
|
+
object: "chat.completion",
|
|
312
|
+
model: "gpt-4o-mini",
|
|
313
|
+
choices: [{
|
|
314
|
+
index: 0,
|
|
315
|
+
message: { role: "assistant", content: "Hello, world!" },
|
|
316
|
+
finish_reason: "stop"
|
|
317
|
+
}],
|
|
318
|
+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
319
|
+
}.to_json
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
323
|
+
chat.ask("Hi")
|
|
324
|
+
|
|
325
|
+
span = EXPORTER.finished_spans.first
|
|
326
|
+
|
|
327
|
+
input_messages = JSON.parse(span.attributes["gen_ai.input.messages"])
|
|
328
|
+
assert_equal "user", input_messages[0]["role"]
|
|
329
|
+
|
|
330
|
+
output_messages = JSON.parse(span.attributes["gen_ai.output.messages"])
|
|
331
|
+
assert_equal "assistant", output_messages[0]["role"]
|
|
332
|
+
ensure
|
|
333
|
+
ENV.delete("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT")
|
|
334
|
+
end
|
|
231
335
|
end
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Clarissa Borges
|
|
@@ -51,7 +51,6 @@ files:
|
|
|
51
51
|
- CODEOWNERS
|
|
52
52
|
- CODE_OF_CONDUCT.md
|
|
53
53
|
- Gemfile
|
|
54
|
-
- Gemfile.lock
|
|
55
54
|
- LICENSE
|
|
56
55
|
- README.md
|
|
57
56
|
- Rakefile
|
data/Gemfile.lock
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
PATH
|
|
2
|
-
remote: .
|
|
3
|
-
specs:
|
|
4
|
-
opentelemetry-instrumentation-ruby_llm (0.1.0)
|
|
5
|
-
opentelemetry-api (~> 1.0)
|
|
6
|
-
opentelemetry-instrumentation-base (~> 0.23)
|
|
7
|
-
|
|
8
|
-
GEM
|
|
9
|
-
remote: https://rubygems.org/
|
|
10
|
-
specs:
|
|
11
|
-
addressable (2.8.8)
|
|
12
|
-
public_suffix (>= 2.0.2, < 8.0)
|
|
13
|
-
base64 (0.3.0)
|
|
14
|
-
bigdecimal (4.0.1)
|
|
15
|
-
crack (1.0.1)
|
|
16
|
-
bigdecimal
|
|
17
|
-
rexml
|
|
18
|
-
event_stream_parser (1.0.0)
|
|
19
|
-
faraday (2.14.0)
|
|
20
|
-
faraday-net_http (>= 2.0, < 3.5)
|
|
21
|
-
json
|
|
22
|
-
logger
|
|
23
|
-
faraday-multipart (1.2.0)
|
|
24
|
-
multipart-post (~> 2.0)
|
|
25
|
-
faraday-net_http (3.4.2)
|
|
26
|
-
net-http (~> 0.5)
|
|
27
|
-
faraday-retry (2.4.0)
|
|
28
|
-
faraday (~> 2.0)
|
|
29
|
-
google-protobuf (4.33.4)
|
|
30
|
-
bigdecimal
|
|
31
|
-
rake (>= 13)
|
|
32
|
-
google-protobuf (4.33.4-arm64-darwin)
|
|
33
|
-
bigdecimal
|
|
34
|
-
rake (>= 13)
|
|
35
|
-
googleapis-common-protos-types (1.22.0)
|
|
36
|
-
google-protobuf (~> 4.26)
|
|
37
|
-
hashdiff (1.2.1)
|
|
38
|
-
json (2.18.0)
|
|
39
|
-
logger (1.7.0)
|
|
40
|
-
marcel (1.1.0)
|
|
41
|
-
minitest (6.0.1)
|
|
42
|
-
prism (~> 1.5)
|
|
43
|
-
multipart-post (2.4.1)
|
|
44
|
-
net-http (0.9.1)
|
|
45
|
-
uri (>= 0.11.1)
|
|
46
|
-
opentelemetry-api (1.7.0)
|
|
47
|
-
opentelemetry-common (0.23.0)
|
|
48
|
-
opentelemetry-api (~> 1.0)
|
|
49
|
-
opentelemetry-exporter-otlp (0.31.1)
|
|
50
|
-
google-protobuf (>= 3.18)
|
|
51
|
-
googleapis-common-protos-types (~> 1.3)
|
|
52
|
-
opentelemetry-api (~> 1.1)
|
|
53
|
-
opentelemetry-common (~> 0.20)
|
|
54
|
-
opentelemetry-sdk (~> 1.10)
|
|
55
|
-
opentelemetry-semantic_conventions
|
|
56
|
-
opentelemetry-instrumentation-base (0.25.0)
|
|
57
|
-
opentelemetry-api (~> 1.7)
|
|
58
|
-
opentelemetry-common (~> 0.21)
|
|
59
|
-
opentelemetry-registry (~> 0.1)
|
|
60
|
-
opentelemetry-registry (0.4.0)
|
|
61
|
-
opentelemetry-api (~> 1.1)
|
|
62
|
-
opentelemetry-sdk (1.10.0)
|
|
63
|
-
opentelemetry-api (~> 1.1)
|
|
64
|
-
opentelemetry-common (~> 0.20)
|
|
65
|
-
opentelemetry-registry (~> 0.2)
|
|
66
|
-
opentelemetry-semantic_conventions
|
|
67
|
-
opentelemetry-semantic_conventions (1.36.0)
|
|
68
|
-
opentelemetry-api (~> 1.0)
|
|
69
|
-
prism (1.9.0)
|
|
70
|
-
public_suffix (7.0.2)
|
|
71
|
-
rake (13.3.1)
|
|
72
|
-
rexml (3.4.4)
|
|
73
|
-
ruby_llm (1.11.0)
|
|
74
|
-
base64
|
|
75
|
-
event_stream_parser (~> 1)
|
|
76
|
-
faraday (>= 1.10.0)
|
|
77
|
-
faraday-multipart (>= 1)
|
|
78
|
-
faraday-net_http (>= 1)
|
|
79
|
-
faraday-retry (>= 1)
|
|
80
|
-
marcel (~> 1.0)
|
|
81
|
-
ruby_llm-schema (~> 0.2.1)
|
|
82
|
-
zeitwerk (~> 2)
|
|
83
|
-
ruby_llm-schema (0.2.5)
|
|
84
|
-
uri (1.1.1)
|
|
85
|
-
webmock (3.26.1)
|
|
86
|
-
addressable (>= 2.8.0)
|
|
87
|
-
crack (>= 0.3.2)
|
|
88
|
-
hashdiff (>= 0.4.0, < 2.0.0)
|
|
89
|
-
zeitwerk (2.7.4)
|
|
90
|
-
|
|
91
|
-
PLATFORMS
|
|
92
|
-
arm64-darwin-24
|
|
93
|
-
ruby
|
|
94
|
-
|
|
95
|
-
DEPENDENCIES
|
|
96
|
-
minitest
|
|
97
|
-
opentelemetry-exporter-otlp
|
|
98
|
-
opentelemetry-instrumentation-ruby_llm!
|
|
99
|
-
opentelemetry-sdk
|
|
100
|
-
rake
|
|
101
|
-
ruby_llm
|
|
102
|
-
webmock
|
|
103
|
-
|
|
104
|
-
BUNDLED WITH
|
|
105
|
-
2.7.1
|