newrelic_rpm 9.7.0 → 9.16.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +376 -2
  3. data/README.md +17 -18
  4. data/Rakefile +1 -1
  5. data/lib/boot/strap.rb +101 -0
  6. data/lib/new_relic/agent/agent.rb +4 -1
  7. data/lib/new_relic/agent/agent_helpers/connect.rb +10 -8
  8. data/lib/new_relic/agent/agent_helpers/start_worker_thread.rb +1 -1
  9. data/lib/new_relic/agent/agent_helpers/startup.rb +2 -1
  10. data/lib/new_relic/agent/agent_logger.rb +3 -1
  11. data/lib/new_relic/agent/aws.rb +68 -0
  12. data/lib/new_relic/agent/configuration/default_source.rb +519 -23
  13. data/lib/new_relic/agent/configuration/environment_source.rb +14 -2
  14. data/lib/new_relic/agent/configuration/high_security_source.rb +1 -0
  15. data/lib/new_relic/agent/configuration/manager.rb +51 -8
  16. data/lib/new_relic/agent/configuration/security_policy_source.rb +11 -0
  17. data/lib/new_relic/agent/configuration/yaml_source.rb +2 -0
  18. data/lib/new_relic/agent/connect/request_builder.rb +1 -1
  19. data/lib/new_relic/agent/custom_event_aggregator.rb +27 -1
  20. data/lib/new_relic/agent/database/obfuscation_helpers.rb +11 -11
  21. data/lib/new_relic/agent/database/obfuscator.rb +1 -0
  22. data/lib/new_relic/agent/database.rb +39 -0
  23. data/lib/new_relic/agent/distributed_tracing/distributed_trace_payload.rb +1 -5
  24. data/lib/new_relic/agent/error_collector.rb +39 -10
  25. data/lib/new_relic/agent/harvester.rb +1 -1
  26. data/lib/new_relic/agent/instrumentation/active_merchant.rb +0 -13
  27. data/lib/new_relic/agent/instrumentation/active_record.rb +1 -8
  28. data/lib/new_relic/agent/instrumentation/active_record_helper.rb +3 -0
  29. data/lib/new_relic/agent/instrumentation/active_record_subscriber.rb +1 -12
  30. data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger/instrumentation.rb +7 -3
  31. data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger.rb +0 -2
  32. data/lib/new_relic/agent/instrumentation/active_support_logger.rb +0 -2
  33. data/lib/new_relic/agent/instrumentation/async_http.rb +4 -3
  34. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda/chain.rb +33 -0
  35. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda/instrumentation.rb +93 -0
  36. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda/prepend.rb +23 -0
  37. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda.rb +23 -0
  38. data/lib/new_relic/agent/instrumentation/aws_sqs/chain.rb +37 -0
  39. data/lib/new_relic/agent/instrumentation/aws_sqs/instrumentation.rb +67 -0
  40. data/lib/new_relic/agent/instrumentation/aws_sqs/prepend.rb +21 -0
  41. data/lib/new_relic/agent/instrumentation/aws_sqs.rb +23 -0
  42. data/lib/new_relic/agent/instrumentation/bunny/instrumentation.rb +14 -0
  43. data/lib/new_relic/agent/instrumentation/bunny.rb +3 -4
  44. data/lib/new_relic/agent/instrumentation/concurrent_ruby.rb +1 -2
  45. data/lib/new_relic/agent/instrumentation/curb.rb +3 -4
  46. data/lib/new_relic/agent/instrumentation/delayed_job_instrumentation.rb +0 -23
  47. data/lib/new_relic/agent/instrumentation/dynamodb/chain.rb +27 -0
  48. data/lib/new_relic/agent/instrumentation/dynamodb/instrumentation.rb +64 -0
  49. data/lib/new_relic/agent/instrumentation/dynamodb/prepend.rb +19 -0
  50. data/lib/new_relic/agent/instrumentation/dynamodb.rb +23 -0
  51. data/lib/new_relic/agent/instrumentation/elasticsearch/instrumentation.rb +58 -8
  52. data/lib/new_relic/agent/instrumentation/elasticsearch.rb +0 -2
  53. data/lib/new_relic/agent/instrumentation/ethon.rb +0 -4
  54. data/lib/new_relic/agent/instrumentation/excon.rb +0 -16
  55. data/lib/new_relic/agent/instrumentation/fiber.rb +0 -2
  56. data/lib/new_relic/agent/instrumentation/grape.rb +1 -1
  57. data/lib/new_relic/agent/instrumentation/grpc/client/instrumentation.rb +0 -1
  58. data/lib/new_relic/agent/instrumentation/grpc_server.rb +1 -1
  59. data/lib/new_relic/agent/instrumentation/httpclient.rb +0 -1
  60. data/lib/new_relic/agent/instrumentation/httprb.rb +0 -1
  61. data/lib/new_relic/agent/instrumentation/httpx.rb +0 -4
  62. data/lib/new_relic/agent/instrumentation/logger.rb +1 -3
  63. data/lib/new_relic/agent/instrumentation/logstasher/chain.rb +21 -0
  64. data/lib/new_relic/agent/instrumentation/logstasher/instrumentation.rb +24 -0
  65. data/lib/new_relic/agent/instrumentation/logstasher/prepend.rb +13 -0
  66. data/lib/new_relic/agent/instrumentation/logstasher.rb +25 -0
  67. data/lib/new_relic/agent/instrumentation/memcache.rb +0 -1
  68. data/lib/new_relic/agent/instrumentation/net_http/instrumentation.rb +6 -0
  69. data/lib/new_relic/agent/instrumentation/opensearch/chain.rb +21 -0
  70. data/lib/new_relic/agent/instrumentation/opensearch/instrumentation.rb +66 -0
  71. data/lib/new_relic/agent/instrumentation/opensearch/prepend.rb +13 -0
  72. data/lib/new_relic/agent/instrumentation/opensearch.rb +23 -0
  73. data/lib/new_relic/agent/instrumentation/padrino.rb +3 -3
  74. data/lib/new_relic/agent/instrumentation/rack/instrumentation.rb +3 -0
  75. data/lib/new_relic/agent/instrumentation/rails_notifications/action_controller.rb +9 -5
  76. data/lib/new_relic/agent/instrumentation/rake.rb +0 -1
  77. data/lib/new_relic/agent/instrumentation/rdkafka/chain.rb +72 -0
  78. data/lib/new_relic/agent/instrumentation/rdkafka/instrumentation.rb +70 -0
  79. data/lib/new_relic/agent/instrumentation/rdkafka/prepend.rb +67 -0
  80. data/lib/new_relic/agent/instrumentation/rdkafka.rb +25 -0
  81. data/lib/new_relic/agent/instrumentation/redis/cluster_middleware.rb +26 -0
  82. data/lib/new_relic/agent/instrumentation/redis/instrumentation.rb +14 -11
  83. data/lib/new_relic/agent/instrumentation/redis/middleware.rb +3 -0
  84. data/lib/new_relic/agent/instrumentation/redis.rb +11 -5
  85. data/lib/new_relic/agent/instrumentation/resque.rb +0 -4
  86. data/lib/new_relic/agent/instrumentation/roda.rb +4 -4
  87. data/lib/new_relic/agent/instrumentation/ruby_kafka/chain.rb +55 -0
  88. data/lib/new_relic/agent/instrumentation/ruby_kafka/instrumentation.rb +67 -0
  89. data/lib/new_relic/agent/instrumentation/ruby_kafka/prepend.rb +60 -0
  90. data/lib/new_relic/agent/instrumentation/ruby_kafka.rb +25 -0
  91. data/lib/new_relic/agent/instrumentation/ruby_openai/chain.rb +36 -0
  92. data/lib/new_relic/agent/instrumentation/ruby_openai/instrumentation.rb +196 -0
  93. data/lib/new_relic/agent/instrumentation/ruby_openai/prepend.rb +20 -0
  94. data/lib/new_relic/agent/instrumentation/ruby_openai.rb +35 -0
  95. data/lib/new_relic/agent/instrumentation/sidekiq.rb +0 -14
  96. data/lib/new_relic/agent/instrumentation/sinatra.rb +3 -19
  97. data/lib/new_relic/agent/instrumentation/stripe_subscriber.rb +22 -1
  98. data/lib/new_relic/agent/instrumentation/thread.rb +0 -2
  99. data/lib/new_relic/agent/instrumentation/tilt.rb +0 -4
  100. data/lib/new_relic/agent/instrumentation/typhoeus.rb +0 -1
  101. data/lib/new_relic/agent/instrumentation/view_component/instrumentation.rb +13 -6
  102. data/lib/new_relic/agent/instrumentation/view_component.rb +0 -2
  103. data/lib/new_relic/agent/javascript_instrumentor.rb +2 -3
  104. data/lib/new_relic/agent/llm/chat_completion_message.rb +25 -0
  105. data/lib/new_relic/agent/llm/chat_completion_summary.rb +66 -0
  106. data/lib/new_relic/agent/llm/embedding.rb +60 -0
  107. data/lib/new_relic/agent/llm/llm_event.rb +95 -0
  108. data/lib/new_relic/agent/llm/response_headers.rb +80 -0
  109. data/lib/new_relic/agent/llm.rb +49 -0
  110. data/lib/new_relic/agent/local_log_decorator.rb +8 -1
  111. data/lib/new_relic/agent/log_event_aggregator.rb +120 -44
  112. data/lib/new_relic/agent/messaging.rb +11 -5
  113. data/lib/new_relic/agent/new_relic_service.rb +12 -2
  114. data/lib/new_relic/agent/serverless_handler.rb +400 -0
  115. data/lib/new_relic/agent/serverless_handler_event_sources.json +155 -0
  116. data/lib/new_relic/agent/serverless_handler_event_sources.rb +49 -0
  117. data/lib/new_relic/agent/span_event_primitive.rb +8 -10
  118. data/lib/new_relic/agent/system_info.rb +14 -0
  119. data/lib/new_relic/agent/threading/agent_thread.rb +1 -2
  120. data/lib/new_relic/agent/tracer.rb +5 -5
  121. data/lib/new_relic/agent/transaction/abstract_segment.rb +1 -1
  122. data/lib/new_relic/agent/transaction/external_request_segment.rb +0 -10
  123. data/lib/new_relic/agent/transaction/request_attributes.rb +13 -1
  124. data/lib/new_relic/agent/transaction/trace_context.rb +1 -1
  125. data/lib/new_relic/agent/transaction/tracing.rb +2 -2
  126. data/lib/new_relic/agent/transaction.rb +2 -6
  127. data/lib/new_relic/agent/transaction_error_primitive.rb +23 -19
  128. data/lib/new_relic/agent.rb +198 -10
  129. data/lib/new_relic/constants.rb +2 -0
  130. data/lib/new_relic/control/frameworks/grape.rb +14 -0
  131. data/lib/new_relic/control/frameworks/padrino.rb +14 -0
  132. data/lib/new_relic/control/frameworks/rails4.rb +1 -3
  133. data/lib/new_relic/control/instance_methods.rb +8 -0
  134. data/lib/new_relic/control/private_instance_methods.rb +4 -0
  135. data/lib/new_relic/control/security_interface.rb +57 -0
  136. data/lib/new_relic/control.rb +1 -1
  137. data/lib/new_relic/dependency_detection.rb +10 -5
  138. data/lib/new_relic/environment_report.rb +2 -2
  139. data/lib/new_relic/helper.rb +15 -0
  140. data/lib/new_relic/language_support.rb +3 -1
  141. data/lib/new_relic/local_environment.rb +14 -10
  142. data/lib/new_relic/rack/browser_monitoring.rb +28 -12
  143. data/lib/new_relic/supportability_helper.rb +2 -0
  144. data/lib/new_relic/thread_local_storage.rb +31 -0
  145. data/lib/new_relic/version.rb +2 -2
  146. data/lib/sequel/extensions/new_relic_instrumentation.rb +3 -2
  147. data/lib/tasks/config.rake +8 -3
  148. data/lib/tasks/gha.rake +31 -0
  149. data/lib/tasks/helpers/config.html.erb +3 -2
  150. data/lib/tasks/helpers/format.rb +1 -1
  151. data/lib/tasks/helpers/newrelicyml.rb +76 -13
  152. data/lib/tasks/instrumentation_generator/instrumentation.thor +31 -22
  153. data/lib/tasks/instrumentation_generator/templates/chain.tt +0 -1
  154. data/lib/tasks/instrumentation_generator/templates/chain_method.tt +0 -1
  155. data/lib/tasks/instrumentation_generator/templates/dependency_detection.tt +11 -8
  156. data/lib/tasks/instrumentation_generator/templates/newrelic.yml.tt +1 -1
  157. data/newrelic.yml +387 -143
  158. data/newrelic_rpm.gemspec +2 -0
  159. data/test/agent_helper.rb +17 -2
  160. metadata +80 -3
@@ -392,11 +392,11 @@ module NewRelic
392
392
  #
393
393
  # If ever exposed, this requires additional synchronization
394
394
  def state_for(thread)
395
- state = thread[:newrelic_tracer_state]
395
+ state = ThreadLocalStorage.get(thread, :newrelic_tracer_state)
396
396
 
397
397
  if state.nil?
398
398
  state = Tracer::State.new
399
- thread[:newrelic_tracer_state] = state
399
+ ThreadLocalStorage.set(thread, :newrelic_tracer_state, state)
400
400
  end
401
401
 
402
402
  state
@@ -405,7 +405,7 @@ module NewRelic
405
405
  alias_method :tl_state_for, :state_for
406
406
 
407
407
  def clear_state
408
- Thread.current[:newrelic_tracer_state] = nil
408
+ ThreadLocalStorage[:newrelic_tracer_state] = nil
409
409
  end
410
410
 
411
411
  alias_method :tl_clear, :clear_state
@@ -420,12 +420,12 @@ module NewRelic
420
420
 
421
421
  def thread_block_with_current_transaction(segment_name: nil, parent: nil, &block)
422
422
  parent ||= current_segment
423
- current_txn = ::Thread.current[:newrelic_tracer_state]&.current_transaction if ::Thread.current[:newrelic_tracer_state]&.is_execution_traced?
423
+ current_txn = ThreadLocalStorage[:newrelic_tracer_state]&.current_transaction if ThreadLocalStorage[:newrelic_tracer_state]&.is_execution_traced?
424
424
  proc do |*args|
425
425
  begin
426
426
  if current_txn && !current_txn.finished?
427
427
  NewRelic::Agent::Tracer.state.current_transaction = current_txn
428
- ::Thread.current[:newrelic_thread_span_parent] = parent
428
+ ThreadLocalStorage[:newrelic_thread_span_parent] = parent
429
429
  current_txn.async = true
430
430
  segment_name = "#{segment_name}/Thread#{::Thread.current.object_id}/Fiber#{::Fiber.current.object_id}" if NewRelic::Agent.config[:'thread_ids_enabled']
431
431
  segment = NewRelic::Agent::Tracer.start_segment(name: segment_name, parent: parent) if segment_name
@@ -20,7 +20,7 @@ module NewRelic
20
20
  # calculation in all other cases.
21
21
  #
22
22
  attr_reader :start_time, :end_time, :duration, :exclusive_duration, :guid, :starting_segment_key
23
- attr_accessor :name, :parent, :children_time, :transaction, :transaction_name
23
+ attr_accessor :name, :parent, :children_time, :transaction, :transaction_name, :llm_event
24
24
  attr_writer :record_metrics, :record_scoped_metric, :record_on_finish
25
25
  attr_reader :noticed_error
26
26
 
@@ -23,7 +23,6 @@ module NewRelic
23
23
  MISSING_STATUS_CODE = 'MissingHTTPStatusCode'
24
24
 
25
25
  attr_reader :library, :uri, :procedure, :http_status_code
26
- attr_writer :record_agent_attributes
27
26
 
28
27
  def initialize(library, uri, procedure, start_time = nil) # :nodoc:
29
28
  @library = library
@@ -32,7 +31,6 @@ module NewRelic
32
31
  @host_header = nil
33
32
  @app_data = nil
34
33
  @http_status_code = nil
35
- @record_agent_attributes = false
36
34
  super(nil, nil, start_time)
37
35
  end
38
36
 
@@ -44,14 +42,6 @@ module NewRelic
44
42
  @host_header || uri.host
45
43
  end
46
44
 
47
- # By default external request segments only have errors and the http
48
- # url recorded as agent attributes. To have all the agent attributes
49
- # recorded, use the attr_writer like so `segment.record_agent_attributes = true`
50
- # See: SpanEventPrimitive#for_external_request_segment
51
- def record_agent_attributes?
52
- @record_agent_attributes
53
- end
54
-
55
45
  # This method adds New Relic request headers to a given request made to an
56
46
  # external API and checks to see if a host header is used for the request.
57
47
  # If a host header is used, it updates the segment name to match the host
@@ -24,7 +24,7 @@ module NewRelic
24
24
  @referer = referer_from_request(request)
25
25
  @accept = attribute_from_env(request, HTTP_ACCEPT_HEADER_KEY)
26
26
  @content_length = content_length_from_request(request)
27
- @content_type = attribute_from_request(request, :content_type)
27
+ @content_type = content_type_attribute_from_request(request)
28
28
  @host = attribute_from_request(request, :host)
29
29
  @port = port_from_request(request)
30
30
  @user_agent = attribute_from_request(request, :user_agent)
@@ -127,6 +127,18 @@ module NewRelic
127
127
  end
128
128
  end
129
129
 
130
+ def content_type_attribute_from_request(request)
131
+ # Rails 7.0 changed the behavior of `content_type`. We want just the MIME type, so use `media_type` if available.
132
+ # https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#actiondispatch-request-content-type-now-returns-content-type-header-as-it-is
133
+ content_type = if request.respond_to?(:media_type)
134
+ :media_type
135
+ elsif request.respond_to?(:content_type)
136
+ :content_type
137
+ end
138
+
139
+ request.send(content_type) if content_type
140
+ end
141
+
130
142
  def attribute_from_env(request, key)
131
143
  if env = attribute_from_request(request, :env)
132
144
  env[key]
@@ -95,7 +95,7 @@ module NewRelic
95
95
 
96
96
  def create_trace_state_payload
97
97
  unless Agent.config[:'distributed_tracing.enabled']
98
- NewRelic::Agent.logger.warn('Not configured to create WC3 trace context payload')
98
+ NewRelic::Agent.logger.warn('Not configured to create W3C trace context payload')
99
99
  return
100
100
  end
101
101
 
@@ -41,11 +41,11 @@ module NewRelic
41
41
 
42
42
  def thread_starting_span
43
43
  # if the previous current segment was in another thread, use the thread local parent
44
- if ::Thread.current[:newrelic_thread_span_parent] &&
44
+ if ThreadLocalStorage[:newrelic_thread_span_parent] &&
45
45
  current_segment &&
46
46
  current_segment.starting_segment_key != NewRelic::Agent::Tracer.current_segment_key
47
47
 
48
- ::Thread.current[:newrelic_thread_span_parent]
48
+ ThreadLocalStorage[:newrelic_thread_span_parent]
49
49
  end
50
50
  end
51
51
 
@@ -816,13 +816,9 @@ module NewRelic
816
816
  end
817
817
 
818
818
  def had_error_affecting_apdex?
819
- @exceptions.each do |exception, options|
820
- ignored = NewRelic::Agent.instance.error_collector.error_is_ignored?(exception)
821
- expected = options[:expected]
822
-
823
- return true unless ignored || expected
819
+ @exceptions.each.any? do |exception, options|
820
+ NewRelic::Agent.instance.error_collector.error_affects_apdex?(exception, options)
824
821
  end
825
- false
826
822
  end
827
823
 
828
824
  def apdex_bucket(duration, current_apdex_t)
@@ -16,26 +16,27 @@ module NewRelic
16
16
  module TransactionErrorPrimitive
17
17
  extend self
18
18
 
19
- SAMPLE_TYPE = 'TransactionError'.freeze
20
- TYPE_KEY = 'type'.freeze
21
- ERROR_CLASS_KEY = 'error.class'.freeze
22
- ERROR_MESSAGE_KEY = 'error.message'.freeze
23
- ERROR_EXPECTED_KEY = 'error.expected'.freeze
24
- TIMESTAMP_KEY = 'timestamp'.freeze
25
- PORT_KEY = 'port'.freeze
26
- NAME_KEY = 'transactionName'.freeze
27
- DURATION_KEY = 'duration'.freeze
28
- SAMPLED_KEY = 'sampled'.freeze
29
- GUID_KEY = 'nr.transactionGuid'.freeze
30
- REFERRING_TRANSACTION_GUID_KEY = 'nr.referringTransactionGuid'.freeze
31
- SYNTHETICS_RESOURCE_ID_KEY = 'nr.syntheticsResourceId'.freeze
32
- SYNTHETICS_JOB_ID_KEY = 'nr.syntheticsJobId'.freeze
33
- SYNTHETICS_MONITOR_ID_KEY = 'nr.syntheticsMonitorId'.freeze
19
+ SAMPLE_TYPE = 'TransactionError'
20
+ TYPE_KEY = 'type'
21
+ ERROR_CLASS_KEY = 'error.class'
22
+ ERROR_MESSAGE_KEY = 'error.message'
23
+ ERROR_EXPECTED_KEY = 'error.expected'
24
+ TIMESTAMP_KEY = 'timestamp'
25
+ PORT_KEY = 'port'
26
+ NAME_KEY = 'transactionName'
27
+ DURATION_KEY = 'duration'
28
+ SAMPLED_KEY = 'sampled'
29
+ CAT_GUID_KEY = 'nr.transactionGuid'
30
+ CAT_REFERRING_TRANSACTION_GUID_KEY = 'nr.referringTransactionGuid'
31
+ SYNTHETICS_RESOURCE_ID_KEY = 'nr.syntheticsResourceId'
32
+ SYNTHETICS_JOB_ID_KEY = 'nr.syntheticsJobId'
33
+ SYNTHETICS_MONITOR_ID_KEY = 'nr.syntheticsMonitorId'
34
34
  SYNTHETICS_TYPE_KEY = 'nr.syntheticsType'
35
35
  SYNTHETICS_INITIATOR_KEY = 'nr.syntheticsInitiator'
36
36
  SYNTHETICS_KEY_PREFIX = 'nr.synthetics'
37
- PRIORITY_KEY = 'priority'.freeze
38
- SPAN_ID_KEY = 'spanId'.freeze
37
+ PRIORITY_KEY = 'priority'
38
+ SPAN_ID_KEY = 'spanId'
39
+ GUID_KEY = 'guid'
39
40
 
40
41
  SYNTHETICS_PAYLOAD_EXPECTED = [:synthetics_resource_id, :synthetics_job_id, :synthetics_monitor_id, :synthetics_type, :synthetics_initiator]
41
42
 
@@ -57,7 +58,10 @@ module NewRelic
57
58
  }
58
59
 
59
60
  attrs[SPAN_ID_KEY] = span_id if span_id
61
+ # don't use safe navigation - leave off keys with missing values
62
+ # instead of using nil
60
63
  attrs[PORT_KEY] = noticed_error.request_port if noticed_error.request_port
64
+ attrs[GUID_KEY] = noticed_error.transaction_id if noticed_error.transaction_id
61
65
 
62
66
  if payload
63
67
  attrs[NAME_KEY] = payload[:name]
@@ -93,8 +97,8 @@ module NewRelic
93
97
  end
94
98
 
95
99
  def append_cat(payload, sample)
96
- sample[GUID_KEY] = payload[:guid] if payload[:guid]
97
- sample[REFERRING_TRANSACTION_GUID_KEY] = payload[:referring_transaction_guid] if payload[:referring_transaction_guid]
100
+ sample[CAT_GUID_KEY] = payload[:guid] if payload[:guid]
101
+ sample[CAT_REFERRING_TRANSACTION_GUID_KEY] = payload[:referring_transaction_guid] if payload[:referring_transaction_guid]
98
102
  end
99
103
  end
100
104
  end
@@ -31,6 +31,7 @@ module NewRelic
31
31
  require 'new_relic/noticed_error'
32
32
  require 'new_relic/agent/noticeable_error'
33
33
  require 'new_relic/supportability_helper'
34
+ require 'new_relic/thread_local_storage'
34
35
 
35
36
  require 'new_relic/agent/encoding_normalizer'
36
37
  require 'new_relic/agent/stats'
@@ -62,6 +63,8 @@ module NewRelic
62
63
  require 'new_relic/agent/attribute_processing'
63
64
  require 'new_relic/agent/linking_metadata'
64
65
  require 'new_relic/agent/local_log_decorator'
66
+ require 'new_relic/agent/llm'
67
+ require 'new_relic/agent/aws'
65
68
 
66
69
  require 'new_relic/agent/instrumentation/controller_instrumentation'
67
70
 
@@ -82,6 +85,10 @@ module NewRelic
82
85
  # An exception that forces an agent to stop reporting until its mongrel is restarted.
83
86
  class ForceDisconnectException < StandardError; end
84
87
 
88
+ # Error handling for the automated custom instrumentation tracer logic
89
+ class AutomaticTracerParseException < StandardError; end
90
+ class AutomaticTracerTraceException < StandardError; end
91
+
85
92
  # An exception that forces an agent to restart.
86
93
  class ForceRestartException < StandardError
87
94
  def message
@@ -105,11 +112,17 @@ module NewRelic
105
112
 
106
113
  # placeholder name used when we cannot determine a transaction's name
107
114
  UNKNOWN_METRIC = '(unknown)'.freeze
115
+ LLM_FEEDBACK_MESSAGE = 'LlmFeedbackMessage'
116
+ # give the observed app time to load the code that automatic tracers have
117
+ # been configured for
118
+ AUTOMATIC_TRACER_MAX_ATTEMPTS = 60 # 60 = try about twice a second for 30 seconds
108
119
 
109
120
  attr_reader :error_group_callback
121
+ attr_reader :llm_token_count_callback
110
122
 
111
123
  @agent = nil
112
124
  @error_group_callback = nil
125
+ @llm_token_count_callback = nil
113
126
  @logger = nil
114
127
  @tracer_lock = Mutex.new
115
128
  @tracer_queue = []
@@ -119,8 +132,8 @@ module NewRelic
119
132
  def agent # :nodoc:
120
133
  return @agent if @agent
121
134
 
122
- NewRelic::Agent.logger.warn("Agent unavailable as it hasn't been started.")
123
- NewRelic::Agent.logger.warn(caller.join("\n"))
135
+ NewRelic::Agent.logger.debug("Agent unavailable as it hasn't been started.")
136
+ NewRelic::Agent.logger.debug(caller.join("\n"))
124
137
  nil
125
138
  end
126
139
 
@@ -157,6 +170,92 @@ module NewRelic
157
170
  end
158
171
  end
159
172
 
173
+ # @api private
174
+ def self.add_automatic_method_tracers(arr)
175
+ return unless arr
176
+ return arr if arr.respond_to?(:empty?) && arr.empty?
177
+
178
+ arr = arr.split(/\s*,\s*/) if arr.is_a?(String)
179
+
180
+ add_tracers_once_methods_are_defined(arr.dup)
181
+
182
+ arr
183
+ end
184
+
185
+ # spawn a thread that will attempt to establish a tracer for each of the
186
+ # configured methods. the thread will continue to keep trying with each
187
+ # tracer until one of the following happens:
188
+ # - the tracer is successfully established
189
+ # - the configured method string couldn't be parsed
190
+ # - establishing a tracer for a successfully parsed string failed
191
+ # - the maximum number of attempts has been reached
192
+ # the thread will only be spawned once per agent initialization, to account
193
+ # for configuration reloading scenarios.
194
+ #
195
+ # @api private
196
+ def self.add_tracers_once_methods_are_defined(notations)
197
+ # this class method can be invoked multiple times at agent startup, so
198
+ # we return asap here instead of using a traditional memoization of
199
+ # waiting for the method's body to finish being executed
200
+ if defined?(@add_tracers_once_methods_are_defined)
201
+ return
202
+ else
203
+ @add_tracers_once_methods_are_defined = true
204
+ end
205
+
206
+ Thread.new do
207
+ AUTOMATIC_TRACER_MAX_ATTEMPTS.times do
208
+ notations.delete_if { |notation| prep_tracer_for(notation) }
209
+
210
+ break if notations.empty?
211
+
212
+ sleep 0.5
213
+ end
214
+ end
215
+ end
216
+
217
+ # returns `true` if the notation string has either been successfully
218
+ # processed or raised an error during processing. returns `false` if the
219
+ # string seems good but the (customer) code to be traced has not yet been
220
+ # loaded into the Ruby VM
221
+ #
222
+ # @api private
223
+ def self.prep_tracer_for(fully_qualified_method_notation)
224
+ delimiters = fully_qualified_method_notation.scan(/\.|#/)
225
+ raise AutomaticTracerParseException.new("Expected exactly one '.' or '#' delimiter.") unless delimiters.size == 1
226
+
227
+ delimiter = delimiters.first
228
+ namespace, method_name = fully_qualified_method_notation.split(delimiter)
229
+ unless namespace && !namespace.empty?
230
+ raise AutomaticTracerParseException.new("Nothing found to the left of the #{delimiter} delimiter.")
231
+ end
232
+ unless method_name && !method_name.empty?
233
+ raise AutomaticTracerParseException.new("Nothing found to the right of the #{delimiter} delimiter.")
234
+ end
235
+
236
+ begin
237
+ klass = ::NewRelic::LanguageSupport.constantize(namespace)
238
+ return false unless klass
239
+
240
+ klass_to_trace = delimiter.eql?('.') ? klass.singleton_class : klass
241
+ add_or_defer_method_tracer(klass_to_trace, method_name, nil, {})
242
+ rescue StandardError => e
243
+ raise AutomaticTracerTraceException.new("#{e.class} - #{e.message}")
244
+ end
245
+
246
+ true
247
+ rescue AutomaticTracerParseException => e
248
+ NewRelic::Agent.logger.error('Unable to parse out a usable method name to trace. Expected a valid, fully ' \
249
+ "qualified method notation. Got: '#{fully_qualified_method_notation}'. " \
250
+ "Error: #{e.message}")
251
+ true
252
+ rescue AutomaticTracerTraceException => e
253
+ NewRelic::Agent.logger.error('Unable to automatically apply a tracer to method ' \
254
+ "'#{fully_qualified_method_notation}'. Error: #{e.message}")
255
+ true
256
+ end
257
+
258
+ # @api private
160
259
  def add_deferred_method_tracers_now
161
260
  @tracer_lock.synchronize do
162
261
  @tracer_queue.each do |receiver, method_name, metric_name, options|
@@ -387,6 +486,92 @@ module NewRelic
387
486
  nil
388
487
  end
389
488
 
489
+ # Records user feedback events for LLM applications. This API must pass
490
+ # the current trace id as a parameter, which can be obtained using:
491
+ #
492
+ # NewRelic::Agent::Tracer.current_trace_id
493
+ #
494
+ # @param [String] ID of the trace where the chat completion(s) related
495
+ # to the feedback occurred.
496
+ #
497
+ # @param [String or Integer] Rating provided by an end user
498
+ # (ex: “Good", "Bad”, 1, 2, 5, 8, 10).
499
+ #
500
+ # @param [optional, String] Category of the feedback as provided by the
501
+ # end user (ex: “informative”, “inaccurate”).
502
+ #
503
+ # @param start_time [optional, String] Freeform text feedback from an
504
+ # end user.
505
+ #
506
+ # @param [optional, Hash] Set of key-value pairs to store any other
507
+ # desired data to submit with the feedback event.
508
+ #
509
+ # @api public
510
+ #
511
+ def record_llm_feedback_event(trace_id:,
512
+ rating:,
513
+ category: nil,
514
+ message: nil,
515
+ metadata: NewRelic::EMPTY_HASH)
516
+
517
+ record_api_supportability_metric(:record_llm_feedback_event)
518
+ unless NewRelic::Agent.config[:'distributed_tracing.enabled']
519
+ return NewRelic::Agent.logger.error('Distributed tracing must be enabled to record LLM feedback')
520
+ end
521
+
522
+ feedback_message_event = {
523
+ 'trace_id': trace_id,
524
+ 'rating': rating,
525
+ 'category': category,
526
+ 'message': message,
527
+ 'id': NewRelic::Agent::GuidGenerator.generate_guid,
528
+ 'ingest_source': NewRelic::Agent::Llm::LlmEvent::INGEST_SOURCE
529
+ }
530
+ feedback_message_event.merge!(metadata) unless metadata.empty?
531
+
532
+ NewRelic::Agent.record_custom_event(LLM_FEEDBACK_MESSAGE, feedback_message_event)
533
+ rescue ArgumentError
534
+ raise
535
+ rescue => exception
536
+ NewRelic::Agent.logger.error('record_llm_feedback_event', exception)
537
+ end
538
+
539
+ # @!endgroup
540
+
541
+ # @!group LLM callbacks
542
+
543
+ # Set a callback proc for calculating `token_count` attributes for
544
+ # LlmEmbedding and LlmChatCompletionMessage events
545
+ #
546
+ # @param callback_proc [Proc] the callback proc
547
+ #
548
+ # This method should be called only once to set a callback for
549
+ # use with all LLM token calculations. If it is called multiple times, each
550
+ # new callback will replace the old one.
551
+ #
552
+ # The proc will be called with a single hash as its input argument and
553
+ # must return an Integer representing the number of tokens used for that
554
+ # particular prompt, completion message, or embedding. Values less than or
555
+ # equal to 0 will not be attached to an event.
556
+ #
557
+ # The hash has the following keys:
558
+ #
559
+ # :model => [String] The name of the LLM model
560
+ # :content => [String] The message content or prompt
561
+ #
562
+ # @api public
563
+ #
564
+ def set_llm_token_count_callback(callback_proc)
565
+ unless callback_proc.is_a?(Proc)
566
+ NewRelic::Agent.logger.error("#{self}.#{__method__}: expected an argument of type Proc, " \
567
+ "got #{callback_proc.class}")
568
+ return
569
+ end
570
+
571
+ record_api_supportability_metric(:set_llm_token_count_callback)
572
+ @llm_token_count_callback = callback_proc
573
+ end
574
+
390
575
  # @!endgroup
391
576
 
392
577
  # @!group Manual agent configuration and startup/shutdown
@@ -618,16 +803,19 @@ module NewRelic
618
803
  def add_custom_attributes(params) # THREAD_LOCAL_ACCESS
619
804
  record_api_supportability_metric(:add_custom_attributes)
620
805
 
621
- if params.is_a?(Hash)
622
- Transaction.tl_current&.add_custom_attributes(params)
623
-
624
- segment = ::NewRelic::Agent::Tracer.current_segment
625
- if segment
626
- add_new_segment_attributes(params, segment)
627
- end
628
- else
806
+ unless params.is_a?(Hash)
629
807
  ::NewRelic::Agent.logger.warn("Bad argument passed to #add_custom_attributes. Expected Hash but got #{params.class}")
808
+ return
630
809
  end
810
+
811
+ if NewRelic::Agent.agent&.serverless?
812
+ ::NewRelic::Agent.logger.warn('Custom attributes are not supported in serverless mode')
813
+ return
814
+ end
815
+
816
+ Transaction.tl_current&.add_custom_attributes(params)
817
+ segment = ::NewRelic::Agent::Tracer.current_segment
818
+ add_new_segment_attributes(params, segment) if segment
631
819
  end
632
820
 
633
821
  def add_new_segment_attributes(params, segment)
@@ -11,6 +11,8 @@ module NewRelic
11
11
  EMPTY_HASH = {}.freeze
12
12
  EMPTY_STR = ''
13
13
 
14
+ LANGUAGE = 'ruby'
15
+
14
16
  HTTP = 'HTTP'
15
17
  HTTPS = 'HTTPS'
16
18
  UNKNOWN = 'Unknown'
@@ -0,0 +1,14 @@
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 'new_relic/control/frameworks/ruby'
6
+ module NewRelic
7
+ class Control
8
+ module Frameworks
9
+ # Contains basic control logic for Grape
10
+ class Grape < NewRelic::Control::Frameworks::Ruby
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
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 'new_relic/control/frameworks/sinatra'
6
+ module NewRelic
7
+ class Control
8
+ module Frameworks
9
+ # Contains basic control logic for Padrino
10
+ class Padrino < NewRelic::Control::Frameworks::Sinatra
11
+ end
12
+ end
13
+ end
14
+ end
@@ -9,9 +9,7 @@ module NewRelic
9
9
  module Frameworks
10
10
  class Rails4 < NewRelic::Control::Frameworks::Rails3
11
11
  def rails_gem_list
12
- Bundler.rubygems.all_specs.map do |gem|
13
- "#{gem.name} (#{gem.version})"
14
- end
12
+ NewRelic::Helper.rubygems_specs.map { |gem| "#{gem.name} (#{gem.version})" }
15
13
  end
16
14
 
17
15
  def append_plugin_list
@@ -73,9 +73,11 @@ module NewRelic
73
73
  init_config(options)
74
74
  NewRelic::Agent.agent = NewRelic::Agent::Agent.instance
75
75
  init_instrumentation
76
+ init_security_agent
76
77
  end
77
78
 
78
79
  def determine_env(options)
80
+ options[:env] = :serverless if local_env.discovered_dispatcher == :serverless
79
81
  env = options[:env] || self.env
80
82
  env = env.to_s
81
83
 
@@ -95,6 +97,12 @@ module NewRelic
95
97
  def configure_agent(env, options)
96
98
  manual = Agent::Configuration::ManualSource.new(options)
97
99
  Agent.config.replace_or_add_config(manual)
100
+
101
+ # if manual config sees serverless mode enabled, then the proc
102
+ # must have returned 'true'. don't bother with YAML and high security
103
+ # in a serverless context
104
+ return if Agent.config[:'serverless_mode.enabled']
105
+
98
106
  yaml_source = Agent::Configuration::YamlSource.new(config_file_path, env)
99
107
  log_yaml_source_failures(yaml_source) if yaml_source.failed?
100
108
  Agent.config.replace_or_add_config(yaml_source)
@@ -43,6 +43,10 @@ module NewRelic
43
43
  DependencyDetection.detect!
44
44
  end
45
45
  end
46
+
47
+ def init_security_agent
48
+ SecurityInterface.instance.init_agent
49
+ end
46
50
  end
47
51
  end
48
52
  end
@@ -0,0 +1,57 @@
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 'singleton'
6
+
7
+ module NewRelic
8
+ class Control
9
+ class SecurityInterface
10
+ include Singleton
11
+
12
+ attr_accessor :wait
13
+
14
+ SUPPORTABILITY_PREFIX_SECURITY = 'Supportability/Ruby/SecurityAgent/Enabled/'
15
+ SUPPORTABILITY_PREFIX_SECURITY_AGENT = 'Supportability/Ruby/SecurityAgent/Agent/Enabled/'
16
+ ENABLED = 'enabled'
17
+ DISABLED = 'disabled'
18
+
19
+ def agent_started?
20
+ (@agent_started ||= false) == true
21
+ end
22
+
23
+ def waiting?
24
+ (@wait ||= false) == true
25
+ end
26
+
27
+ def init_agent
28
+ return if agent_started? || waiting?
29
+
30
+ record_supportability_metrics
31
+
32
+ if Agent.config[:'security.agent.enabled'] && !Agent.config[:high_security]
33
+ Agent.logger.info('Invoking New Relic security module')
34
+ require 'newrelic_security'
35
+
36
+ @agent_started = true
37
+ else
38
+ Agent.logger.info('New Relic Security is completely disabled by one of the user-provided configurations: `security.agent.enabled` or `high_security`. Not loading security capabilities.')
39
+ Agent.logger.info("high_security = #{Agent.config[:high_security]}")
40
+ Agent.logger.info("security.agent.enabled = #{Agent.config[:'security.agent.enabled']}")
41
+ end
42
+ rescue LoadError
43
+ Agent.logger.info('New Relic security agent not found - skipping')
44
+ rescue StandardError => exception
45
+ Agent.logger.error("Exception in New Relic security module loading: #{exception} #{exception.backtrace}")
46
+ end
47
+
48
+ def record_supportability_metrics
49
+ Agent.config[:'security.agent.enabled'] ? security_agent_metric(ENABLED) : security_agent_metric(DISABLED)
50
+ end
51
+
52
+ def security_agent_metric(setting)
53
+ NewRelic::Agent.record_metric_once(SUPPORTABILITY_PREFIX_SECURITY_AGENT + setting)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -8,7 +8,6 @@ require 'new_relic/local_environment'
8
8
  require 'new_relic/language_support'
9
9
  require 'new_relic/helper'
10
10
 
11
- require 'singleton'
12
11
  require 'erb'
13
12
  require 'socket'
14
13
  require 'net/https'
@@ -18,6 +17,7 @@ require 'new_relic/control/server_methods'
18
17
  require 'new_relic/control/instrumentation'
19
18
  require 'new_relic/control/class_methods'
20
19
  require 'new_relic/control/instance_methods'
20
+ require 'new_relic/control/security_interface'
21
21
 
22
22
  require 'new_relic/agent'
23
23
  require 'new_relic/delayed_job_injection'