newrelic_rpm 9.7.1 → 9.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +1 -1
  4. data/lib/new_relic/agent/configuration/default_source.rb +37 -1
  5. data/lib/new_relic/agent/configuration/high_security_source.rb +1 -0
  6. data/lib/new_relic/agent/configuration/manager.rb +6 -3
  7. data/lib/new_relic/agent/configuration/security_policy_source.rb +11 -0
  8. data/lib/new_relic/agent/custom_event_aggregator.rb +4 -4
  9. data/lib/new_relic/agent/error_collector.rb +2 -0
  10. data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger/instrumentation.rb +7 -3
  11. data/lib/new_relic/agent/instrumentation/concurrent_ruby.rb +1 -0
  12. data/lib/new_relic/agent/instrumentation/net_http/instrumentation.rb +6 -0
  13. data/lib/new_relic/agent/instrumentation/ruby_openai/chain.rb +36 -0
  14. data/lib/new_relic/agent/instrumentation/ruby_openai/instrumentation.rb +197 -0
  15. data/lib/new_relic/agent/instrumentation/ruby_openai/prepend.rb +20 -0
  16. data/lib/new_relic/agent/instrumentation/ruby_openai.rb +35 -0
  17. data/lib/new_relic/agent/llm/chat_completion_message.rb +25 -0
  18. data/lib/new_relic/agent/llm/chat_completion_summary.rb +66 -0
  19. data/lib/new_relic/agent/llm/embedding.rb +60 -0
  20. data/lib/new_relic/agent/llm/llm_event.rb +95 -0
  21. data/lib/new_relic/agent/llm/response_headers.rb +80 -0
  22. data/lib/new_relic/agent/llm.rb +49 -0
  23. data/lib/new_relic/agent/threading/agent_thread.rb +1 -2
  24. data/lib/new_relic/agent/tracer.rb +5 -5
  25. data/lib/new_relic/agent/transaction/abstract_segment.rb +1 -1
  26. data/lib/new_relic/agent/transaction/tracing.rb +2 -2
  27. data/lib/new_relic/agent.rb +91 -0
  28. data/lib/new_relic/rack/browser_monitoring.rb +8 -4
  29. data/lib/new_relic/supportability_helper.rb +2 -0
  30. data/lib/new_relic/thread_local_storage.rb +31 -0
  31. data/lib/new_relic/version.rb +2 -2
  32. data/newrelic.yml +21 -1
  33. metadata +13 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bdcea3ab1fea41b672c672c931a6005966bef1500709deb9d85681912cf7d33
4
- data.tar.gz: 7d2bcd8db6e959d909e26dd4c3d8e8a99c7cec6572204a1c0283ce65521a5d6a
3
+ metadata.gz: fac159d955f3aaf1b926d68335c05f8bbea9fa3089bed68bee11f9bb50243361
4
+ data.tar.gz: 783c82413e45080741cbc9fd9fe0488b59cede5b64d359af46cf524d871b7763
5
5
  SHA512:
6
- metadata.gz: '053846613ea0a66e691e15cd2159c3ada613f3f66dcfc9232f723569a54d3659f84b8b16ed178e7eb46691a36dc182addaf187370b15862e3f05d3e9335f50b3'
7
- data.tar.gz: 588be3aa5b6e29b2cb1c79e89d4b3bd4af5fc5e41e819f1916c52107aca1ba41f8ca9d573ccf7d8def38d9fb166a2cd73ae2bf9733de2bc03472cd517cdf1801
6
+ metadata.gz: ae3deb3425be91f81d30fd76f6af95ec2ff573f2941e17474cbc96cadd70baa7eda534fcb8886ed039b71eba202d81ef9098600e307dcdf27919f84612eea15d
7
+ data.tar.gz: 4000a2bd73ea4eee6853878630d777226c39af0b46aafa8946bc167495728dc754136731162e660e6fbc895d2df1ca12709e3d2e60be4df8ae5f09035e072cfe
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # New Relic Ruby Agent Release Notes
2
2
 
3
+ ## v9.8.0
4
+
5
+ Version 9.8.0 introduces instrumentation for ruby-openai, adds the option to store tracer state on the thread-level, hardens the browser agent insertion logic to better proactively anticipate errors, and prevents excpetions from being raised in the Active Support Broadcast logger instrumentation.
6
+
7
+ - **Feature: Add instrumentation for ruby-openai**
8
+
9
+ Instrumentation has been added for the [ruby-openai](https://github.com/alexrudall/ruby-openai) gem, supporting versions 3.4.0 and higher [(PR#2442)](https://github.com/newrelic/newrelic-ruby-agent/pull/2442). While ruby-openai instrumentation is enabled by default, the configuration option `ai_monitoring.enabled` is disabled by default and controls all AI monitoring. `ai_monitoring.enabled` must be set to `true` in order to receive ruby-openai instrumentation. High-Security Mode must be disabled in order to receive AI monitoring.
10
+
11
+ Calls to embedding and chat completion endpoints are automatically traced. These events can be enhanced with the introduction of two new APIs. Custom attributes can also be added to LLM events using the API `NewRelic::Agent.add_custom_attributes`, but they must be prefixed with `llm.`. For example, `NewRelic::Agent.add_custom_attributes({'llm.user_id': user_id})`.
12
+
13
+ - **Feature: Add AI monitoring APIs**
14
+
15
+ This version introduces two new APIs that allow users to record additional information on LLM events:
16
+ * `NewRelic::Agent.record_llm_feedback_event` - Records user feedback events.
17
+ * `NewRelic::Agent.set_llm_token_count_callback` - Sets a callback proc for calculating `token_count` attributes for embedding and chat completion message events.
18
+
19
+ Visit [RubyDoc](https://rubydoc.info/github/newrelic/newrelic-ruby-agent/) for more information on each of these APIs.
20
+
21
+ - **Feature: Store tracer state on thread-level**
22
+
23
+ A new configuration option, `thread_local_tracer_state`, stores New Relic's tracer state on the thread-level, as opposed to the default fiber-level storage. This configuration is turned off by default. Our thanks go to community member [@markiz](https://github.com/markiz) who contributed the idea, code, configuration option, and tests for this new feature! [PR#2475](https://github.com/newrelic/newrelic-ruby-agent/pull/2475).
24
+
25
+ - **Bugfix: Harden the browser agent insertion logic**
26
+
27
+ With [Issue#2462](https://github.com/newrelic/newrelic-ruby-agent/issues/2462), community member [@miry](https://github.com/miry) explained that it was possible for an HTTP response headers hash to have symbols for values. Not only would these symbols prevent the inclusion of the New Relic browser agent tag in the response body, but more importantly they would cause an exception that would bubble up to the monitored web application itself. With [PR#2465](https://github.com/newrelic/newrelic-ruby-agent/pull/2465) symbol based values are now supported and all other potential future exceptions are now handled. Additionally, the refactor to support symbols has been shown through benchmarking to give the processing of string and mixed type hashes a slight speed boost too.
28
+
29
+ - **Bugfix: Prevent Exception in Active Support Broadcast logger instrumentation**
30
+
31
+ Previously, in certain situations the agent could cause an exception to be raised when attempting to interact with a broadcast log event. This has been fixed. Thanks to [@nathan-appere](https://github.com/nathan-appere) for reporting this issue and providing a fix! [PR#2510](https://github.com/newrelic/newrelic-ruby-agent/pull/2510)
32
+
3
33
 
4
34
  ## v9.7.1
5
35
 
data/README.md CHANGED
@@ -119,7 +119,7 @@ If you have any questions, or to execute our corporate CLA (required if your con
119
119
 
120
120
  As noted in our [security policy](https://github.com/newrelic/newrelic-ruby-agent/security/policy), New Relic is committed to the privacy and security of our customers and their data. We believe that providing coordinated disclosure by security researchers and engaging with the security community are important means to achieve our security goals.
121
121
 
122
- If you believe you have found a security vulnerability in this project or any of New Relic's products or websites, we welcome and greatly appreciate you reporting it to New Relic through [HackerOne](https://hackerone.com/newrelic).
122
+ If you believe you have found a security vulnerability in this project or any of New Relic's products or websites, we welcome and greatly appreciate you reporting it to New Relic through [our bug bounty program](https://docs.newrelic.com/docs/security/security-privacy/information-security/report-security-vulnerabilities/).
123
123
 
124
124
  If you would like to contribute to this project, please review [these guidelines](https://github.com/newrelic/newrelic-ruby-agent/blob/main/CONTRIBUTING.md).
125
125
 
@@ -360,6 +360,26 @@ module NewRelic
360
360
  - a.third.event
361
361
  DESCRIPTION
362
362
  },
363
+ :'ai_monitoring.enabled' => {
364
+ :default => false,
365
+ :public => true,
366
+ :type => Boolean,
367
+ :allowed_from_server => false,
368
+ :description => 'If `false`, all LLM instrumentation (OpenAI only for now) will be disabled and no metrics, events, or spans will be sent. AI Monitoring is automatically disabled if `high_security` mode is enabled.'
369
+ },
370
+ :'ai_monitoring.record_content.enabled' => {
371
+ :default => true,
372
+ :public => true,
373
+ :type => Boolean,
374
+ :allowed_from_server => false,
375
+ :description => <<~DESCRIPTION
376
+ If `false`, LLM instrumentation (OpenAI only for now) will not capture input and output content on specific LLM events.
377
+
378
+ The excluded attributes include:
379
+ * `content` from LlmChatCompletionMessage events
380
+ * `input` from LlmEmbedding events
381
+ DESCRIPTION
382
+ },
363
383
  # this is only set via server side config
364
384
  :apdex_t => {
365
385
  :default => 0.5,
@@ -554,6 +574,13 @@ module NewRelic
554
574
  :allowed_from_server => false,
555
575
  :description => 'When set to `true`, forces a synchronous connection to the New Relic [collector](/docs/using-new-relic/welcome-new-relic/get-started/glossary/#collector) during application startup. For very short-lived processes, this helps ensure the New Relic agent has time to report.'
556
576
  },
577
+ :thread_local_tracer_state => {
578
+ :default => false,
579
+ :public => true,
580
+ :type => Boolean,
581
+ :allowed_from_server => false,
582
+ :description => 'If `true`, tracer state storage is thread-local, otherwise, fiber-local'
583
+ },
557
584
  :timeout => {
558
585
  :default => 2 * 60, # 2 minutes
559
586
  :public => true,
@@ -720,7 +747,7 @@ module NewRelic
720
747
  :public => true,
721
748
  :type => Integer,
722
749
  :allowed_from_server => false,
723
- :description => 'Defines the maximum number of frames in an error backtrace. Backtraces over this amount are truncated at the beginning and end.'
750
+ :description => 'Defines the maximum number of frames in an error backtrace. Backtraces over this amount are truncated in the middle, preserving the beginning and the end of the stack trace.'
724
751
  },
725
752
  :'error_collector.max_event_samples_stored' => {
726
753
  :default => 100,
@@ -1570,6 +1597,15 @@ module NewRelic
1570
1597
  :allowed_from_server => false,
1571
1598
  :description => 'Controls auto-instrumentation of `Net::HTTP` at start-up. May be one of: `auto`, `prepend`, `chain`, `disabled`.'
1572
1599
  },
1600
+ :'instrumentation.ruby_openai' => {
1601
+ :default => 'auto',
1602
+ :documentation_default => 'auto',
1603
+ :public => true,
1604
+ :type => String,
1605
+ :dynamic_name => true,
1606
+ :allowed_from_server => false,
1607
+ :description => 'Controls auto-instrumentation of the ruby-openai gem at start-up. May be one of: `auto`, `prepend`, `chain`, `disabled`.'
1608
+ },
1573
1609
  :'instrumentation.puma_rack' => {
1574
1610
  :default => value_of(:'instrumentation.rack'),
1575
1611
  :documentation_default => 'auto',
@@ -19,6 +19,7 @@ module NewRelic
19
19
  :'elasticsearch.obfuscate_queries' => true,
20
20
  :'transaction_tracer.record_redis_arguments' => false,
21
21
 
22
+ :'ai_monitoring.enabled' => false,
22
23
  :'custom_insights_events.enabled' => false,
23
24
  :'strip_exception_messages.enabled' => true
24
25
  })
@@ -34,6 +34,7 @@ module NewRelic
34
34
  def initialize
35
35
  reset_to_defaults
36
36
  @callbacks = Hash.new { |hash, key| hash[key] = [] }
37
+ @lock = Mutex.new
37
38
  end
38
39
 
39
40
  def add_config_for_testing(source, level = 0)
@@ -364,9 +365,11 @@ module NewRelic
364
365
  def reset_cache
365
366
  return new_cache unless defined?(@cache) && @cache
366
367
 
367
- preserved = @cache.dup.select { |_k, v| DEPENDENCY_DETECTION_VALUES.include?(v) }
368
- new_cache
369
- preserved.each { |k, v| @cache[k] = v }
368
+ @lock.synchronize do
369
+ preserved = @cache.dup.select { |_k, v| DEPENDENCY_DETECTION_VALUES.include?(v) }
370
+ new_cache
371
+ preserved.each { |k, v| @cache[k] = v }
372
+ end
370
373
 
371
374
  @cache
372
375
  end
@@ -7,6 +7,8 @@ require 'new_relic/agent/configuration/dotted_hash'
7
7
  module NewRelic
8
8
  module Agent
9
9
  module Configuration
10
+ # The Language Security Policy Source gives customers the ability to
11
+ # configure high security mode settings.
10
12
  class SecurityPolicySource < DottedHash
11
13
  class << self
12
14
  def enabled?(option)
@@ -147,6 +149,15 @@ module NewRelic
147
149
  permitted_fn: nil
148
150
  }
149
151
  ],
152
+ 'ai_monitoring' => [
153
+ {
154
+ option: :'ai_monitoring.enabled',
155
+ supported: true,
156
+ enabled_fn: method(:enabled?),
157
+ disabled_value: false,
158
+ permitted_fn: nil
159
+ }
160
+ ],
150
161
  'allow_raw_exception_messages' => [
151
162
  {
152
163
  option: :'strip_exception_messages.enabled',
@@ -52,11 +52,11 @@ module NewRelic
52
52
  {TYPE => type,
53
53
  TIMESTAMP => Process.clock_gettime(Process::CLOCK_REALTIME).to_i,
54
54
  PRIORITY => priority},
55
- create_custom_event_attributes(attributes)
55
+ create_custom_event_attributes(type, attributes)
56
56
  ]
57
57
  end
58
58
 
59
- def create_custom_event_attributes(attributes)
59
+ def create_custom_event_attributes(type, attributes)
60
60
  result = AttributeProcessing.flatten_and_coerce(attributes)
61
61
 
62
62
  if result.size > MAX_ATTRIBUTE_COUNT
@@ -70,9 +70,9 @@ module NewRelic
70
70
  key = key[0, MAX_NAME_SIZE]
71
71
  end
72
72
 
73
- # value is limited to 4095
73
+ # value is limited to 4095 except for LLM content-related events
74
74
  if val.is_a?(String) && val.length > MAX_ATTRIBUTE_SIZE
75
- val = val[0, MAX_ATTRIBUTE_SIZE]
75
+ val = val[0, MAX_ATTRIBUTE_SIZE] unless NewRelic::Agent::LLM.exempt_event_attribute?(type, key)
76
76
  end
77
77
 
78
78
  new_result[key] = val
@@ -216,6 +216,8 @@ module NewRelic
216
216
  def notice_segment_error(segment, exception, options = {})
217
217
  return if skip_notice_error?(exception)
218
218
 
219
+ options.merge!(segment.llm_event.error_attributes(exception)) if segment.llm_event
220
+
219
221
  segment.set_noticed_error(create_noticed_error(exception, options))
220
222
  exception
221
223
  rescue => e
@@ -5,9 +5,13 @@
5
5
  module NewRelic::Agent::Instrumentation
6
6
  module ActiveSupportBroadcastLogger
7
7
  def record_one_broadcast_with_new_relic(*args)
8
- broadcasts[1..-1].each { |broadcasted_logger| broadcasted_logger.instance_variable_set(:@skip_instrumenting, true) }
9
- yield
10
- broadcasts.each { |broadcasted_logger| broadcasted_logger.instance_variable_set(:@skip_instrumenting, false) }
8
+ if broadcasts && broadcasts[1..-1]
9
+ broadcasts[1..-1].each { |broadcasted_logger| broadcasted_logger.instance_variable_set(:@skip_instrumenting, true) }
10
+ yield
11
+ broadcasts.each { |broadcasted_logger| broadcasted_logger.instance_variable_set(:@skip_instrumenting, false) }
12
+ else
13
+ NewRelic::Agent.logger.error('Error recording broadcasted logger')
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -11,6 +11,7 @@ DependencyDetection.defer do
11
11
 
12
12
  depends_on do
13
13
  defined?(Concurrent) &&
14
+ defined?(Concurrent::VERSION) &&
14
15
  Gem::Version.new(Concurrent::VERSION) >= Gem::Version.new('1.1.5')
15
16
  end
16
17
 
@@ -32,7 +32,13 @@ module NewRelic
32
32
  end
33
33
 
34
34
  wrapped_response = NewRelic::Agent::HTTPClients::NetHTTPResponse.new(response)
35
+
36
+ if NewRelic::Agent::LLM.openai_parent?(segment)
37
+ NewRelic::Agent::LLM.populate_openai_response_headers(wrapped_response, segment.parent)
38
+ end
39
+
35
40
  segment.process_response_headers(wrapped_response)
41
+
36
42
  response
37
43
  ensure
38
44
  segment&.finish
@@ -0,0 +1,36 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ module NewRelic::Agent::Instrumentation
6
+ module OpenAI::Chain
7
+ def self.instrument!
8
+ ::OpenAI::Client.class_eval do
9
+ include NewRelic::Agent::Instrumentation::OpenAI
10
+
11
+ alias_method(:json_post_without_new_relic, :json_post)
12
+
13
+ # In versions 4.0.0+ json_post is an instance method
14
+ # defined in the OpenAI::HTTP module, included by the
15
+ # OpenAI::Client class
16
+ def json_post(**kwargs)
17
+ json_post_with_new_relic(**kwargs) do
18
+ json_post_without_new_relic(**kwargs)
19
+ end
20
+ end
21
+
22
+ # In versions below 4.0.0 json_post is a class method
23
+ # on OpenAI::Client
24
+ class << self
25
+ alias_method(:json_post_without_new_relic, :json_post)
26
+
27
+ def json_post(**kwargs)
28
+ json_post_with_new_relic(**kwargs) do
29
+ json_post_without_new_relic(**kwargs)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,197 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ module NewRelic::Agent::Instrumentation
6
+ module OpenAI
7
+ VENDOR = 'openAI' # AIM expects this capitalization style for the UI
8
+ INSTRUMENTATION_NAME = NewRelic::Agent.base_name(name)
9
+ EMBEDDINGS_PATH = '/embeddings'
10
+ CHAT_COMPLETIONS_PATH = '/chat/completions'
11
+ EMBEDDINGS_SEGMENT_NAME = 'Llm/embedding/OpenAI/embeddings'
12
+ CHAT_COMPLETIONS_SEGMENT_NAME = 'Llm/completion/OpenAI/chat'
13
+
14
+ def json_post_with_new_relic(path:, parameters:)
15
+ return yield unless path == EMBEDDINGS_PATH || path == CHAT_COMPLETIONS_PATH
16
+
17
+ NewRelic::Agent.record_instrumentation_invocation(INSTRUMENTATION_NAME)
18
+ NewRelic::Agent::Llm::LlmEvent.set_llm_agent_attribute_on_transaction
19
+ record_openai_metric
20
+
21
+ if path == EMBEDDINGS_PATH
22
+ embeddings_instrumentation(parameters) { yield }
23
+ elsif path == CHAT_COMPLETIONS_PATH
24
+ chat_completions_instrumentation(parameters) { yield }
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def embeddings_instrumentation(parameters)
31
+ segment = NewRelic::Agent::Tracer.start_segment(name: EMBEDDINGS_SEGMENT_NAME)
32
+ event = create_embeddings_event(parameters)
33
+ segment.llm_event = event
34
+ begin
35
+ response = NewRelic::Agent::Tracer.capture_segment_error(segment) { yield }
36
+ # TODO: Remove !response.include?('error') when we drop support for versions below 4.0.0
37
+ add_embeddings_response_params(response, event) if response && !response.include?('error')
38
+
39
+ response
40
+ ensure
41
+ finish(segment, event)
42
+ end
43
+ end
44
+
45
+ def chat_completions_instrumentation(parameters)
46
+ segment = NewRelic::Agent::Tracer.start_segment(name: CHAT_COMPLETIONS_SEGMENT_NAME)
47
+ event = create_chat_completion_summary(parameters)
48
+ segment.llm_event = event
49
+ messages = create_chat_completion_messages(parameters, event.id)
50
+
51
+ begin
52
+ response = NewRelic::Agent::Tracer.capture_segment_error(segment) { yield }
53
+ # TODO: Remove !response.include?('error') when we drop support for versions below 4.0.0
54
+ if response && !response.include?('error')
55
+ add_chat_completion_response_params(parameters, response, event)
56
+ messages = update_chat_completion_messages(messages, response, event)
57
+ end
58
+
59
+ response
60
+ ensure
61
+ finish(segment, event)
62
+ messages&.each { |m| m.record }
63
+ end
64
+ end
65
+
66
+ def create_chat_completion_summary(parameters)
67
+ NewRelic::Agent::Llm::ChatCompletionSummary.new(
68
+ vendor: VENDOR,
69
+ request_max_tokens: (parameters[:max_tokens] || parameters['max_tokens'])&.to_i,
70
+ request_model: parameters[:model] || parameters['model'],
71
+ temperature: (parameters[:temperature] || parameters['temperature'])&.to_f,
72
+ metadata: llm_custom_attributes
73
+ )
74
+ end
75
+
76
+ def create_embeddings_event(parameters)
77
+ event = NewRelic::Agent::Llm::Embedding.new(
78
+ vendor: VENDOR,
79
+ request_model: parameters[:model] || parameters['model'],
80
+ metadata: llm_custom_attributes
81
+ )
82
+ add_input(event, (parameters[:input] || parameters['input']))
83
+
84
+ event
85
+ end
86
+
87
+ def add_chat_completion_response_params(parameters, response, event)
88
+ event.response_number_of_messages = (parameters[:messages] || parameters['messages']).size + response['choices'].size
89
+ # The response hash always returns keys as strings, so we don't need to run an || check here
90
+ event.response_model = response['model']
91
+ event.response_choices_finish_reason = response['choices'][0]['finish_reason']
92
+ end
93
+
94
+ def add_embeddings_response_params(response, event)
95
+ event.response_model = response['model']
96
+ event.token_count = calculate_token_count(event.request_model, event.input)
97
+ end
98
+
99
+ def create_chat_completion_messages(parameters, summary_id)
100
+ (parameters[:messages] || parameters['messages']).map.with_index do |message, index|
101
+ msg = NewRelic::Agent::Llm::ChatCompletionMessage.new(
102
+ role: message[:role] || message['role'],
103
+ sequence: index,
104
+ completion_id: summary_id,
105
+ vendor: VENDOR
106
+ )
107
+ add_content(msg, (message[:content] || message['content']))
108
+
109
+ msg
110
+ end
111
+ end
112
+
113
+ def create_chat_completion_response_messages(response, sequence_origin, summary_id)
114
+ response['choices'].map.with_index(sequence_origin) do |choice, index|
115
+ msg = NewRelic::Agent::Llm::ChatCompletionMessage.new(
116
+ role: choice['message']['role'],
117
+ sequence: index,
118
+ completion_id: summary_id,
119
+ vendor: VENDOR,
120
+ is_response: true
121
+ )
122
+ add_content(msg, choice['message']['content'])
123
+
124
+ msg
125
+ end
126
+ end
127
+
128
+ def update_chat_completion_messages(messages, response, summary)
129
+ messages += create_chat_completion_response_messages(response, messages.size, summary.id)
130
+ response_id = response['id'] || NewRelic::Agent::GuidGenerator.generate_guid
131
+
132
+ messages.each do |message|
133
+ message.id = "#{response_id}-#{message.sequence}"
134
+ message.request_id = summary.request_id
135
+ message.response_model = response['model']
136
+ message.metadata = llm_custom_attributes
137
+
138
+ model = message.is_response ? message.response_model : summary.request_model
139
+
140
+ message.token_count = calculate_token_count(model, message.content)
141
+ end
142
+ end
143
+
144
+ def calculate_token_count(model, content)
145
+ return unless NewRelic::Agent.llm_token_count_callback
146
+
147
+ begin
148
+ count = NewRelic::Agent.llm_token_count_callback.call({model: model, content: content})
149
+ rescue => e
150
+ NewRelic::Agent.logger.warn("Error calculating token count using the provided proc. Error: #{e}'")
151
+ end
152
+
153
+ count if count.is_a?(Integer) && count > 0
154
+ end
155
+
156
+ def record_content_enabled?
157
+ NewRelic::Agent.config[:'ai_monitoring.record_content.enabled']
158
+ end
159
+
160
+ def add_content(message, content)
161
+ message.content = content if record_content_enabled?
162
+ end
163
+
164
+ def add_input(event, input)
165
+ event.input = input if record_content_enabled?
166
+ end
167
+
168
+ def llm_custom_attributes
169
+ NewRelic::Agent::Tracer.current_transaction&.attributes&.custom_attributes&.select { |k| k.to_s.match(/llm.*/) }
170
+ end
171
+
172
+ def record_openai_metric
173
+ NewRelic::Agent.record_metric(nr_supportability_metric, 0.0)
174
+ end
175
+
176
+ def segment_noticed_error?(segment)
177
+ segment&.noticed_error
178
+ end
179
+
180
+ def nr_supportability_metric
181
+ @nr_supportability_metric ||= "Supportability/Ruby/ML/OpenAI/#{::OpenAI::VERSION}"
182
+ end
183
+
184
+ def finish(segment, event)
185
+ segment&.finish
186
+
187
+ return unless event
188
+
189
+ if segment
190
+ event.error = true if segment_noticed_error?(segment)
191
+ event.duration = segment.duration
192
+ end
193
+
194
+ event.record
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,20 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ module NewRelic::Agent::Instrumentation
6
+ module OpenAI::Prepend
7
+ include NewRelic::Agent::Instrumentation::OpenAI
8
+
9
+ # In versions 4.0.0+ json_post is an instance method defined in the
10
+ # OpenAI::HTTP module, included by the OpenAI::Client class.
11
+ #
12
+ # In versions below 4.0.0 json_post is a class method on OpenAI::Client.
13
+ #
14
+ # Dependency detection will apply the instrumentation to the correct scope,
15
+ # so we don't need to change the code here.
16
+ def json_post(**kwargs)
17
+ json_post_with_new_relic(**kwargs) { super }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'ruby_openai/instrumentation'
6
+ require_relative 'ruby_openai/chain'
7
+ require_relative 'ruby_openai/prepend'
8
+
9
+ DependencyDetection.defer do
10
+ named :'ruby_openai'
11
+
12
+ depends_on do
13
+ NewRelic::Agent.config[:'ai_monitoring.enabled'] &&
14
+ defined?(OpenAI) && defined?(OpenAI::Client) &&
15
+ Gem::Version.new(OpenAI::VERSION) >= Gem::Version.new('3.4.0')
16
+ end
17
+
18
+ executes do
19
+ if use_prepend?
20
+ # TODO: Remove condition when we drop support for versions below 5.0.0
21
+ if Gem::Version.new(OpenAI::VERSION) >= Gem::Version.new('5.0.0')
22
+ prepend_instrument OpenAI::Client,
23
+ NewRelic::Agent::Instrumentation::OpenAI::Prepend,
24
+ NewRelic::Agent::Instrumentation::OpenAI::VENDOR
25
+ else
26
+ prepend_instrument OpenAI::Client.singleton_class,
27
+ NewRelic::Agent::Instrumentation::OpenAI::Prepend,
28
+ NewRelic::Agent::Instrumentation::OpenAI::VENDOR
29
+ end
30
+ else
31
+ chain_instrument NewRelic::Agent::Instrumentation::OpenAI::Chain,
32
+ NewRelic::Agent::Instrumentation::OpenAI::VENDOR
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ module NewRelic
6
+ module Agent
7
+ module Llm
8
+ class ChatCompletionMessage < LlmEvent
9
+ ATTRIBUTES = %i[content role sequence completion_id token_count
10
+ is_response]
11
+ EVENT_NAME = 'LlmChatCompletionMessage'
12
+
13
+ attr_accessor(*ATTRIBUTES)
14
+
15
+ def attributes
16
+ LlmEvent::ATTRIBUTES + ATTRIBUTES
17
+ end
18
+
19
+ def event_name
20
+ EVENT_NAME
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ # This file is distributed under New Relic's license terms.
2
+ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'response_headers'
6
+
7
+ module NewRelic
8
+ module Agent
9
+ module Llm
10
+ class ChatCompletionSummary < LlmEvent
11
+ include ResponseHeaders
12
+
13
+ ATTRIBUTES = %i[request_max_tokens response_number_of_messages
14
+ request_model response_choices_finish_reason request_temperature
15
+ duration error]
16
+ ATTRIBUTE_NAME_EXCEPTIONS = {
17
+ response_number_of_messages: 'response.number_of_messages',
18
+ request_model: 'request.model',
19
+ response_choices_finish_reason: 'response.choices.finish_reason',
20
+ request_temperature: 'request.temperature'
21
+ }
22
+ ERROR_COMPLETION_ID = 'completion_id'
23
+ EVENT_NAME = 'LlmChatCompletionSummary'
24
+
25
+ attr_accessor(*ATTRIBUTES)
26
+
27
+ def attributes
28
+ LlmEvent::ATTRIBUTES + ResponseHeaders::ATTRIBUTES + ATTRIBUTES
29
+ end
30
+
31
+ def attribute_name_exceptions
32
+ # TODO: OLD RUBIES < 2.6
33
+ # Hash#merge accepts multiple arguments in 2.6
34
+ # Remove condition once support for Ruby <2.6 is dropped
35
+ if RUBY_VERSION >= '2.6.0'
36
+ LlmEvent::ATTRIBUTE_NAME_EXCEPTIONS.merge(ResponseHeaders::ATTRIBUTE_NAME_EXCEPTIONS, ATTRIBUTE_NAME_EXCEPTIONS)
37
+ else
38
+ LlmEvent::ATTRIBUTE_NAME_EXCEPTIONS.merge(ResponseHeaders::ATTRIBUTE_NAME_EXCEPTIONS).merge(ATTRIBUTE_NAME_EXCEPTIONS)
39
+ end
40
+ end
41
+
42
+ def event_name
43
+ EVENT_NAME
44
+ end
45
+
46
+ def error_attributes(exception)
47
+ attrs = {ERROR_COMPLETION_ID => id}
48
+
49
+ error_attributes_from_response(exception, attrs)
50
+ end
51
+
52
+ private
53
+
54
+ def error_attributes_from_response(exception, attrs)
55
+ return attrs unless exception.respond_to?(:response)
56
+
57
+ attrs[ERROR_ATTRIBUTE_STATUS_CODE] = exception.response.dig(:status)
58
+ attrs[ERROR_ATTRIBUTE_CODE] = exception.response.dig(:body, ERROR_STRING, CODE_STRING)
59
+ attrs[ERROR_ATTRIBUTE_PARAM] = exception.response.dig(:body, ERROR_STRING, PARAM_STRING)
60
+
61
+ attrs
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end