newrelic_rpm 9.7.0 → 9.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -2
- data/README.md +1 -1
- data/lib/new_relic/agent/configuration/default_source.rb +37 -1
- data/lib/new_relic/agent/configuration/high_security_source.rb +1 -0
- data/lib/new_relic/agent/configuration/manager.rb +6 -3
- data/lib/new_relic/agent/configuration/security_policy_source.rb +11 -0
- data/lib/new_relic/agent/custom_event_aggregator.rb +27 -1
- data/lib/new_relic/agent/error_collector.rb +2 -0
- data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger/instrumentation.rb +7 -3
- data/lib/new_relic/agent/instrumentation/async_http.rb +3 -1
- data/lib/new_relic/agent/instrumentation/concurrent_ruby.rb +1 -0
- data/lib/new_relic/agent/instrumentation/grpc_server.rb +1 -1
- data/lib/new_relic/agent/instrumentation/net_http/instrumentation.rb +6 -0
- data/lib/new_relic/agent/instrumentation/ruby_openai/chain.rb +36 -0
- data/lib/new_relic/agent/instrumentation/ruby_openai/instrumentation.rb +197 -0
- data/lib/new_relic/agent/instrumentation/ruby_openai/prepend.rb +20 -0
- data/lib/new_relic/agent/instrumentation/ruby_openai.rb +35 -0
- data/lib/new_relic/agent/instrumentation/view_component/instrumentation.rb +2 -1
- data/lib/new_relic/agent/llm/chat_completion_message.rb +25 -0
- data/lib/new_relic/agent/llm/chat_completion_summary.rb +66 -0
- data/lib/new_relic/agent/llm/embedding.rb +60 -0
- data/lib/new_relic/agent/llm/llm_event.rb +95 -0
- data/lib/new_relic/agent/llm/response_headers.rb +80 -0
- data/lib/new_relic/agent/llm.rb +49 -0
- data/lib/new_relic/agent/threading/agent_thread.rb +1 -2
- data/lib/new_relic/agent/tracer.rb +5 -5
- data/lib/new_relic/agent/transaction/abstract_segment.rb +1 -1
- data/lib/new_relic/agent/transaction/tracing.rb +2 -2
- data/lib/new_relic/agent.rb +91 -0
- data/lib/new_relic/rack/browser_monitoring.rb +8 -4
- data/lib/new_relic/supportability_helper.rb +2 -0
- data/lib/new_relic/thread_local_storage.rb +31 -0
- data/lib/new_relic/version.rb +1 -1
- data/lib/tasks/instrumentation_generator/instrumentation.thor +1 -1
- data/lib/tasks/instrumentation_generator/templates/chain.tt +0 -1
- data/lib/tasks/instrumentation_generator/templates/chain_method.tt +0 -1
- data/newrelic.yml +21 -1
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fac159d955f3aaf1b926d68335c05f8bbea9fa3089bed68bee11f9bb50243361
|
4
|
+
data.tar.gz: 783c82413e45080741cbc9fd9fe0488b59cede5b64d359af46cf524d871b7763
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae3deb3425be91f81d30fd76f6af95ec2ff573f2941e17474cbc96cadd70baa7eda534fcb8886ed039b71eba202d81ef9098600e307dcdf27919f84612eea15d
|
7
|
+
data.tar.gz: 4000a2bd73ea4eee6853878630d777226c39af0b46aafa8946bc167495728dc754136731162e660e6fbc895d2df1ca12709e3d2e60be4df8ae5f09035e072cfe
|
data/CHANGELOG.md
CHANGED
@@ -1,13 +1,55 @@
|
|
1
1
|
# New Relic Ruby Agent Release Notes
|
2
2
|
|
3
|
-
## v9.
|
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.
|
4
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
|
+
|
33
|
+
|
34
|
+
## v9.7.1
|
35
|
+
|
36
|
+
Version 9.7.1 fixes a ViewComponent instrumentation bug and enforces maximum size limits for custom event attributes.
|
37
|
+
|
38
|
+
- **Bugfix: Stop suppressing ViewComponent errors**
|
39
|
+
|
40
|
+
Previously, the agent suppressed ViewComponent render errors. The agent now reports these errors and allows them to raise. Thank you [@mjacobus](https://github.com/mjacobus) for reporting this bug and providing a fix! [PR#2410](https://github.com/newrelic/newrelic-ruby-agent/pull/2410)
|
41
|
+
|
42
|
+
- **Bugfix: Enforce maximum size limits for custom event attributes**
|
43
|
+
|
44
|
+
Previously, the agent would allow custom event attributes to be any size. This would lead to the New Relic backend dropping attributes larger than the maximum size. Now, the agent will truncate custom event attribute values to 4095 characters, attribute names to 255 characters, and the total count of attributes to 64. [PR#2401](https://github.com/newrelic/newrelic-ruby-agent/pull/2401)
|
45
|
+
|
46
|
+
## v9.7.0
|
5
47
|
|
6
48
|
Version 9.7.0 introduces ViewComponent instrumentation, changes the endpoint used to access the cluster name for Elasticsearch instrumentation, removes the creation of the Ruby/Thread and Ruby/Fiber spans, and adds support for Falcon.
|
7
49
|
|
8
50
|
- **Feature: ViewComponent instrumentation**
|
9
51
|
|
10
|
-
[ViewComponent](https://viewcomponent.org/) is a now an instrumented
|
52
|
+
[ViewComponent](https://viewcomponent.org/) is a now an instrumented library. [PR#2367](https://github.com/newrelic/newrelic-ruby-agent/pull/2367)
|
11
53
|
|
12
54
|
- **Feature: Use root path to access Elasticsearch cluster name**
|
13
55
|
|
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 [
|
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
|
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',
|
@@ -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
|
-
|
368
|
-
|
369
|
-
|
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',
|
@@ -14,6 +14,9 @@ module NewRelic
|
|
14
14
|
TIMESTAMP = 'timestamp'.freeze
|
15
15
|
PRIORITY = 'priority'.freeze
|
16
16
|
EVENT_TYPE_REGEX = /^[a-zA-Z0-9:_ ]+$/.freeze
|
17
|
+
MAX_ATTRIBUTE_COUNT = 64
|
18
|
+
MAX_ATTRIBUTE_SIZE = 4095
|
19
|
+
MAX_NAME_SIZE = 255
|
17
20
|
|
18
21
|
named :CustomEventAggregator
|
19
22
|
capacity_key :'custom_insights_events.max_samples_stored'
|
@@ -49,10 +52,33 @@ module NewRelic
|
|
49
52
|
{TYPE => type,
|
50
53
|
TIMESTAMP => Process.clock_gettime(Process::CLOCK_REALTIME).to_i,
|
51
54
|
PRIORITY => priority},
|
52
|
-
|
55
|
+
create_custom_event_attributes(type, attributes)
|
53
56
|
]
|
54
57
|
end
|
55
58
|
|
59
|
+
def create_custom_event_attributes(type, attributes)
|
60
|
+
result = AttributeProcessing.flatten_and_coerce(attributes)
|
61
|
+
|
62
|
+
if result.size > MAX_ATTRIBUTE_COUNT
|
63
|
+
NewRelic::Agent.logger.warn("Custom event attributes are limited to #{MAX_ATTRIBUTE_COUNT}. Discarding #{result.size - MAX_ATTRIBUTE_COUNT} attributes")
|
64
|
+
result = result.first(MAX_ATTRIBUTE_COUNT)
|
65
|
+
end
|
66
|
+
|
67
|
+
result.each_with_object({}) do |(key, val), new_result|
|
68
|
+
# name is limited to 255
|
69
|
+
if key.is_a?(String) && key.length > MAX_NAME_SIZE
|
70
|
+
key = key[0, MAX_NAME_SIZE]
|
71
|
+
end
|
72
|
+
|
73
|
+
# value is limited to 4095 except for LLM content-related events
|
74
|
+
if val.is_a?(String) && val.length > MAX_ATTRIBUTE_SIZE
|
75
|
+
val = val[0, MAX_ATTRIBUTE_SIZE] unless NewRelic::Agent::LLM.exempt_event_attribute?(type, key)
|
76
|
+
end
|
77
|
+
|
78
|
+
new_result[key] = val
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
56
82
|
def after_initialize
|
57
83
|
@type_strings = Hash.new { |hash, key| hash[key] = key.to_s.freeze }
|
58
84
|
end
|
@@ -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]
|
9
|
-
|
10
|
-
|
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
|
@@ -10,7 +10,9 @@ DependencyDetection.defer do
|
|
10
10
|
named :async_http
|
11
11
|
|
12
12
|
depends_on do
|
13
|
-
defined?(Async::HTTP) &&
|
13
|
+
defined?(Async::HTTP) &&
|
14
|
+
Gem::Version.new(Async::HTTP::VERSION) >= Gem::Version.new('0.59.0') &&
|
15
|
+
!defined?(Traces::Backend::NewRelic) # defined in the traces-backend-newrelic gem
|
14
16
|
end
|
15
17
|
|
16
18
|
executes do
|
@@ -14,7 +14,7 @@ DependencyDetection.defer do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
executes do
|
17
|
-
supportability_name = NewRelic::Agent::Instrumentation::GRPC::
|
17
|
+
supportability_name = NewRelic::Agent::Instrumentation::GRPC::Server::INSTRUMENTATION_NAME
|
18
18
|
if use_prepend?
|
19
19
|
prepend_instrument GRPC::RpcServer, NewRelic::Agent::Instrumentation::GRPC::Server::RpcServerPrepend, supportability_name
|
20
20
|
prepend_instrument GRPC::RpcDesc, NewRelic::Agent::Instrumentation::GRPC::Server::RpcDescPrepend, supportability_name
|
@@ -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
|