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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e85590961ea3d4ac30828dbc035d692040a2a66758ba6dab9decab2114af3f3
4
- data.tar.gz: a8a701fa288ec93109bfb6a4d3fdcc5962c40abc98d9a9269d12a9e430848f78
3
+ metadata.gz: 3e43dc7d2e1f91e06c28aa8237a6393d565605f92635edf69e2948015e5d5c4b
4
+ data.tar.gz: b7eb8e02952582e6d8af04f8ee04407ac5e880b9f992ccdd4e6e353e3c060af0
5
5
  SHA512:
6
- metadata.gz: 60672653bad15a0e24a97a60ea94433f82df8300370987f656f135a5eb1339b731afbf3644dddbe4a867b3f9970d2b757e06ca7862a4b7d0d36798dff3f9aa58
7
- data.tar.gz: d4536fbcba4b6439df8c8a4a30198072eea8cc61265ee1572a5c2b0dbd6f775260381d727d06ebc13604d755dc98f44f24f7ee552d218bb50b2695aec6ad3873
6
+ metadata.gz: b600d97f321e3626ffa9de8785e67762d3f391b60e5d11a5d3f97657cbd4801e8cb7d1dd86e632bc260873732772176371b75a1fe17f6325fc2815fb23dc4b9b
7
+ data.tar.gz: 76384730ef9cfe8cfe7f1d5b68109c6cbd4bedb897f18393dc717e0e5089d3ec7cbd81f21f728db4e82bc73ff72f08765b75b97e980ef665d5662ad5321759c5
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  *.gem
2
+ Gemfile.lock
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
@@ -7,6 +7,8 @@ module OpenTelemetry
7
7
  instrumentation_name "OpenTelemetry::Instrumentation::RubyLLM"
8
8
  instrumentation_version VERSION
9
9
 
10
+ option :capture_content, default: false, validate: :boolean
11
+
10
12
  present do
11
13
  defined?(::RubyLLM)
12
14
  end
@@ -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
@@ -3,7 +3,7 @@
3
3
  module OpenTelemetry
4
4
  module Instrumentation
5
5
  module RubyLLM
6
- VERSION = "0.1.0"
6
+ VERSION = "0.2.0"
7
7
  end
8
8
  end
9
9
  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.1.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