newrelic_rpm 9.7.1 → 9.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -1
  3. data/README.md +1 -1
  4. data/lib/new_relic/agent/agent.rb +4 -1
  5. data/lib/new_relic/agent/agent_helpers/connect.rb +10 -8
  6. data/lib/new_relic/agent/agent_helpers/start_worker_thread.rb +1 -1
  7. data/lib/new_relic/agent/agent_helpers/startup.rb +2 -1
  8. data/lib/new_relic/agent/agent_logger.rb +2 -1
  9. data/lib/new_relic/agent/configuration/default_source.rb +67 -3
  10. data/lib/new_relic/agent/configuration/environment_source.rb +9 -1
  11. data/lib/new_relic/agent/configuration/high_security_source.rb +1 -0
  12. data/lib/new_relic/agent/configuration/manager.rb +28 -8
  13. data/lib/new_relic/agent/configuration/security_policy_source.rb +11 -0
  14. data/lib/new_relic/agent/configuration/yaml_source.rb +2 -0
  15. data/lib/new_relic/agent/connect/request_builder.rb +1 -1
  16. data/lib/new_relic/agent/custom_event_aggregator.rb +4 -4
  17. data/lib/new_relic/agent/distributed_tracing/distributed_trace_payload.rb +1 -5
  18. data/lib/new_relic/agent/error_collector.rb +2 -0
  19. data/lib/new_relic/agent/harvester.rb +1 -1
  20. data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger/instrumentation.rb +7 -3
  21. data/lib/new_relic/agent/instrumentation/concurrent_ruby.rb +1 -0
  22. data/lib/new_relic/agent/instrumentation/elasticsearch/instrumentation.rb +6 -1
  23. data/lib/new_relic/agent/instrumentation/net_http/instrumentation.rb +6 -0
  24. data/lib/new_relic/agent/instrumentation/ruby_openai/chain.rb +36 -0
  25. data/lib/new_relic/agent/instrumentation/ruby_openai/instrumentation.rb +196 -0
  26. data/lib/new_relic/agent/instrumentation/ruby_openai/prepend.rb +20 -0
  27. data/lib/new_relic/agent/instrumentation/ruby_openai.rb +35 -0
  28. data/lib/new_relic/agent/llm/chat_completion_message.rb +25 -0
  29. data/lib/new_relic/agent/llm/chat_completion_summary.rb +66 -0
  30. data/lib/new_relic/agent/llm/embedding.rb +60 -0
  31. data/lib/new_relic/agent/llm/llm_event.rb +95 -0
  32. data/lib/new_relic/agent/llm/response_headers.rb +80 -0
  33. data/lib/new_relic/agent/llm.rb +49 -0
  34. data/lib/new_relic/agent/log_event_aggregator.rb +1 -16
  35. data/lib/new_relic/agent/new_relic_service.rb +12 -2
  36. data/lib/new_relic/agent/serverless_handler.rb +171 -0
  37. data/lib/new_relic/agent/threading/agent_thread.rb +1 -2
  38. data/lib/new_relic/agent/tracer.rb +5 -5
  39. data/lib/new_relic/agent/transaction/abstract_segment.rb +1 -1
  40. data/lib/new_relic/agent/transaction/tracing.rb +2 -2
  41. data/lib/new_relic/agent/transaction_error_primitive.rb +23 -19
  42. data/lib/new_relic/agent.rb +102 -8
  43. data/lib/new_relic/constants.rb +2 -0
  44. data/lib/new_relic/control/instance_methods.rb +7 -0
  45. data/lib/new_relic/local_environment.rb +13 -6
  46. data/lib/new_relic/rack/browser_monitoring.rb +8 -4
  47. data/lib/new_relic/supportability_helper.rb +2 -0
  48. data/lib/new_relic/thread_local_storage.rb +31 -0
  49. data/lib/new_relic/version.rb +2 -2
  50. data/lib/tasks/config.rake +2 -1
  51. data/newrelic.yml +27 -1
  52. metadata +14 -2
@@ -143,6 +143,9 @@ module NewRelic
143
143
  end
144
144
 
145
145
  def metric_data(stats_hash)
146
+ # let the serverless handler handle serialization
147
+ return NewRelic::Agent.agent.serverless_handler.metric_data(stats_hash) if NewRelic::Agent.agent.serverless?
148
+
146
149
  timeslice_start = stats_hash.started_at
147
150
  timeslice_end = stats_hash.harvested_at || Process.clock_gettime(Process::CLOCK_REALTIME)
148
151
  metric_data_array = build_metric_data_array(stats_hash)
@@ -154,6 +157,9 @@ module NewRelic
154
157
  end
155
158
 
156
159
  def error_data(unsent_errors)
160
+ # let the serverless handler handle serialization
161
+ return NewRelic::Agent.agent.serverless_handler.error_data(unsent_errors) if NewRelic::Agent.agent.serverless?
162
+
157
163
  invoke_remote(:error_data, [@agent_id, unsent_errors],
158
164
  :item_count => unsent_errors.size)
159
165
  end
@@ -554,6 +560,8 @@ module NewRelic
554
560
  # enough to be worth compressing, and handles any errors the
555
561
  # server may return
556
562
  def invoke_remote(method, payload = [], options = {})
563
+ return NewRelic::Agent.agent.serverless_handler.store_payload(method, payload) if NewRelic::Agent.agent.serverless?
564
+
557
565
  start_ts = Process.clock_gettime(Process::CLOCK_MONOTONIC)
558
566
  request_send_ts, response_check_ts = nil
559
567
  data, encoding, size, serialize_finish_ts = marshal_payload(method, payload, options)
@@ -561,8 +569,10 @@ module NewRelic
561
569
  response, request_send_ts, response_check_ts = invoke_remote_send_request(method, payload, data, encoding)
562
570
  @marshaller.load(decompress_response(response))
563
571
  ensure
564
- record_timing_supportability_metrics(method, start_ts, serialize_finish_ts, request_send_ts, response_check_ts)
565
- record_size_supportability_metrics(method, size, options[:item_count]) if size
572
+ unless NewRelic::Agent.agent.serverless?
573
+ record_timing_supportability_metrics(method, start_ts, serialize_finish_ts, request_send_ts, response_check_ts)
574
+ record_size_supportability_metrics(method, size, options[:item_count]) if size
575
+ end
566
576
  end
567
577
 
568
578
  def handle_serialization_error(method, e)
@@ -0,0 +1,171 @@
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 'json'
6
+ require 'new_relic/base64'
7
+
8
+ module NewRelic
9
+ module Agent
10
+ class ServerlessHandler
11
+ ATTRIBUTE_ARN = 'aws.lambda.arn'
12
+ ATTRIBUTE_COLD_START = 'aws.lambda.coldStart'
13
+ ATTRIBUTE_REQUEST_ID = 'aws.requestId'
14
+ AGENT_ATTRIBUTE_DESTINATIONS = NewRelic::Agent::AttributeFilter::DST_TRANSACTION_TRACER |
15
+ NewRelic::Agent::AttributeFilter::DST_TRANSACTION_EVENTS
16
+ EXECUTION_ENVIRONMENT = "AWS_Lambda_ruby#{RUBY_VERSION.rpartition('.').first}".freeze
17
+ LAMBDA_MARKER = 'NR_LAMBDA_MONITORING'
18
+ LAMBDA_ENVIRONMENT_VARIABLE = 'AWS_LAMBDA_FUNCTION_NAME'
19
+ METHOD_BLOCKLIST = %i[agent_command_results connect get_agent_commands preconnect profile_data
20
+ shutdown].freeze
21
+ NAMED_PIPE = '/tmp/newrelic-telemetry'
22
+ SUPPORTABILITY_METRIC = 'Supportability/AWSLambda/HandlerInvocation'
23
+ FUNCTION_NAME = 'lambda_function'
24
+ PAYLOAD_VERSION = ENV.fetch('NEW_RELIC_SERVERLESS_PAYLOAD_VERSION', 2)
25
+
26
+ def self.env_var_set?
27
+ ENV.key?(LAMBDA_ENVIRONMENT_VARIABLE)
28
+ end
29
+
30
+ def initialize
31
+ @context = nil
32
+ @payloads = {}
33
+ end
34
+
35
+ def invoke_lambda_function_with_new_relic(event:, context:, method_name:, namespace: nil)
36
+ NewRelic::Agent.increment_metric(SUPPORTABILITY_METRIC)
37
+
38
+ @context = context
39
+
40
+ NewRelic::Agent::Tracer.in_transaction(category: :other, name: function_name) do
41
+ add_agent_attributes
42
+
43
+ NewRelic::LanguageSupport.constantize(namespace).send(method_name, event: event, context: context)
44
+ end
45
+ ensure
46
+ harvest!
47
+ write_output
48
+ reset!
49
+ end
50
+
51
+ def store_payload(method, payload)
52
+ return if METHOD_BLOCKLIST.include?(method)
53
+
54
+ @payloads[method] = payload
55
+ end
56
+
57
+ def metric_data(stats_hash)
58
+ payload = [nil,
59
+ stats_hash.started_at,
60
+ (stats_hash.harvested_at || Process.clock_gettime(Process::CLOCK_REALTIME)),
61
+ []]
62
+ stats_hash.each do |metric_spec, stats|
63
+ next if stats.is_reset?
64
+
65
+ hash = {name: metric_spec.name}
66
+ hash[:scope] = metric_spec.scope unless metric_spec.scope.empty?
67
+
68
+ payload.last.push([hash, [
69
+ stats.call_count,
70
+ stats.total_call_time,
71
+ stats.total_exclusive_time,
72
+ stats.min_call_time,
73
+ stats.max_call_time,
74
+ stats.sum_of_squares
75
+ ]])
76
+ end
77
+
78
+ return if payload.last.empty?
79
+
80
+ store_payload(:metric_data, payload)
81
+ end
82
+
83
+ def error_data(errors)
84
+ store_payload(:error_data, [nil, errors.map(&:to_collector_array)])
85
+ end
86
+
87
+ private
88
+
89
+ def harvest!
90
+ NewRelic::Agent.instance.harvest_and_send_analytic_event_data
91
+ NewRelic::Agent.instance.harvest_and_send_custom_event_data
92
+ NewRelic::Agent.instance.harvest_and_send_data_types
93
+ end
94
+
95
+ def metadata
96
+ m = {arn: @context.invoked_function_arn,
97
+ protocol_version: NewRelic::Agent::NewRelicService::PROTOCOL_VERSION,
98
+ function_version: @context.function_version,
99
+ execution_environment: EXECUTION_ENVIRONMENT,
100
+ agent_version: NewRelic::VERSION::STRING}
101
+ if PAYLOAD_VERSION >= 2
102
+ m[:metadata_version] = PAYLOAD_VERSION
103
+ m[:agent_language] = NewRelic::LANGUAGE
104
+ end
105
+ m
106
+ end
107
+
108
+ def function_name
109
+ ENV.fetch(LAMBDA_ENVIRONMENT_VARIABLE, FUNCTION_NAME)
110
+ end
111
+
112
+ def write_output
113
+ string = PAYLOAD_VERSION == 1 ? payload_v1 : payload_v2
114
+
115
+ return puts string unless use_named_pipe?
116
+
117
+ File.write(NAMED_PIPE, string)
118
+
119
+ NewRelic::Agent.logger.debug "Wrote serverless payload to #{NAMED_PIPE}\n" \
120
+ "BEGIN PAYLOAD>>>\n#{string}\n<<<END PAYLOAD"
121
+ end
122
+
123
+ def payload_v1
124
+ payload_hash = {'metadata' => metadata, 'data' => @payloads}
125
+ json = NewRelic::Agent.agent.service.marshaller.dump(payload_hash)
126
+ gzipped = NewRelic::Agent::NewRelicService::Encoders::Compressed::Gzip.encode(json)
127
+ base64_encoded = NewRelic::Base64.strict_encode64(gzipped)
128
+ array = [PAYLOAD_VERSION, LAMBDA_MARKER, base64_encoded]
129
+ ::JSON.dump(array)
130
+ end
131
+
132
+ def payload_v2
133
+ json = NewRelic::Agent.agent.service.marshaller.dump(@payloads)
134
+ gzipped = NewRelic::Agent::NewRelicService::Encoders::Compressed::Gzip.encode(json)
135
+ base64_encoded = NewRelic::Base64.strict_encode64(gzipped)
136
+ array = [PAYLOAD_VERSION, LAMBDA_MARKER, metadata, base64_encoded]
137
+ ::JSON.dump(array)
138
+ end
139
+
140
+ def use_named_pipe?
141
+ return @use_named_pipe if defined?(@use_named_pipe)
142
+
143
+ @use_named_pipe = File.exist?(NAMED_PIPE) && File.writable?(NAMED_PIPE)
144
+ end
145
+
146
+ def add_agent_attributes
147
+ return unless NewRelic::Agent::Tracer.current_transaction
148
+
149
+ add_agent_attribute(ATTRIBUTE_COLD_START, true) if cold?
150
+ add_agent_attribute(ATTRIBUTE_ARN, @context.invoked_function_arn)
151
+ add_agent_attribute(ATTRIBUTE_REQUEST_ID, @context.aws_request_id)
152
+ end
153
+
154
+ def add_agent_attribute(attribute, value)
155
+ NewRelic::Agent::Tracer.current_transaction.add_agent_attribute(attribute, value, AGENT_ATTRIBUTE_DESTINATIONS)
156
+ end
157
+
158
+ def cold?
159
+ return @cold if defined?(@cold)
160
+
161
+ @cold = false
162
+ true
163
+ end
164
+
165
+ def reset!
166
+ @context = nil
167
+ @payloads.replace({})
168
+ end
169
+ end
170
+ end
171
+ end
@@ -9,8 +9,7 @@ module NewRelic
9
9
  def self.create(label, &blk)
10
10
  ::NewRelic::Agent.logger.debug("Creating AgentThread: #{label}")
11
11
  wrapped_blk = proc do
12
- if ::Thread.current[:newrelic_tracer_state] && Thread.current[:newrelic_tracer_state].current_transaction
13
- txn = ::Thread.current[:newrelic_tracer_state].current_transaction
12
+ if (txn = ::NewRelic::ThreadLocalStorage[:newrelic_tracer_state]&.current_transaction)
14
13
  ::NewRelic::Agent.logger.warn("AgentThread created with current transaction #{txn.best_name}")
15
14
  end
16
15
  begin
@@ -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
 
@@ -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
 
@@ -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,7 @@ 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'
65
67
 
66
68
  require 'new_relic/agent/instrumentation/controller_instrumentation'
67
69
 
@@ -105,11 +107,14 @@ module NewRelic
105
107
 
106
108
  # placeholder name used when we cannot determine a transaction's name
107
109
  UNKNOWN_METRIC = '(unknown)'.freeze
110
+ LLM_FEEDBACK_MESSAGE = 'LlmFeedbackMessage'
108
111
 
109
112
  attr_reader :error_group_callback
113
+ attr_reader :llm_token_count_callback
110
114
 
111
115
  @agent = nil
112
116
  @error_group_callback = nil
117
+ @llm_token_count_callback = nil
113
118
  @logger = nil
114
119
  @tracer_lock = Mutex.new
115
120
  @tracer_queue = []
@@ -387,6 +392,92 @@ module NewRelic
387
392
  nil
388
393
  end
389
394
 
395
+ # Records user feedback events for LLM applications. This API must pass
396
+ # the current trace id as a parameter, which can be obtained using:
397
+ #
398
+ # NewRelic::Agent::Tracer.current_trace_id
399
+ #
400
+ # @param [String] ID of the trace where the chat completion(s) related
401
+ # to the feedback occurred.
402
+ #
403
+ # @param [String or Integer] Rating provided by an end user
404
+ # (ex: “Good", "Bad”, 1, 2, 5, 8, 10).
405
+ #
406
+ # @param [optional, String] Category of the feedback as provided by the
407
+ # end user (ex: “informative”, “inaccurate”).
408
+ #
409
+ # @param start_time [optional, String] Freeform text feedback from an
410
+ # end user.
411
+ #
412
+ # @param [optional, Hash] Set of key-value pairs to store any other
413
+ # desired data to submit with the feedback event.
414
+ #
415
+ # @api public
416
+ #
417
+ def record_llm_feedback_event(trace_id:,
418
+ rating:,
419
+ category: nil,
420
+ message: nil,
421
+ metadata: NewRelic::EMPTY_HASH)
422
+
423
+ record_api_supportability_metric(:record_llm_feedback_event)
424
+ unless NewRelic::Agent.config[:'distributed_tracing.enabled']
425
+ return NewRelic::Agent.logger.error('Distributed tracing must be enabled to record LLM feedback')
426
+ end
427
+
428
+ feedback_message_event = {
429
+ 'trace_id': trace_id,
430
+ 'rating': rating,
431
+ 'category': category,
432
+ 'message': message,
433
+ 'id': NewRelic::Agent::GuidGenerator.generate_guid,
434
+ 'ingest_source': NewRelic::Agent::Llm::LlmEvent::INGEST_SOURCE
435
+ }
436
+ feedback_message_event.merge!(metadata) unless metadata.empty?
437
+
438
+ NewRelic::Agent.record_custom_event(LLM_FEEDBACK_MESSAGE, feedback_message_event)
439
+ rescue ArgumentError
440
+ raise
441
+ rescue => exception
442
+ NewRelic::Agent.logger.error('record_llm_feedback_event', exception)
443
+ end
444
+
445
+ # @!endgroup
446
+
447
+ # @!group LLM callbacks
448
+
449
+ # Set a callback proc for calculating `token_count` attributes for
450
+ # LlmEmbedding and LlmChatCompletionMessage events
451
+ #
452
+ # @param callback_proc [Proc] the callback proc
453
+ #
454
+ # This method should be called only once to set a callback for
455
+ # use with all LLM token calculations. If it is called multiple times, each
456
+ # new callback will replace the old one.
457
+ #
458
+ # The proc will be called with a single hash as its input argument and
459
+ # must return an Integer representing the number of tokens used for that
460
+ # particular prompt, completion message, or embedding. Values less than or
461
+ # equal to 0 will not be attached to an event.
462
+ #
463
+ # The hash has the following keys:
464
+ #
465
+ # :model => [String] The name of the LLM model
466
+ # :content => [String] The message content or prompt
467
+ #
468
+ # @api public
469
+ #
470
+ def set_llm_token_count_callback(callback_proc)
471
+ unless callback_proc.is_a?(Proc)
472
+ NewRelic::Agent.logger.error("#{self}.#{__method__}: expected an argument of type Proc, " \
473
+ "got #{callback_proc.class}")
474
+ return
475
+ end
476
+
477
+ record_api_supportability_metric(:set_llm_token_count_callback)
478
+ @llm_token_count_callback = callback_proc
479
+ end
480
+
390
481
  # @!endgroup
391
482
 
392
483
  # @!group Manual agent configuration and startup/shutdown
@@ -618,16 +709,19 @@ module NewRelic
618
709
  def add_custom_attributes(params) # THREAD_LOCAL_ACCESS
619
710
  record_api_supportability_metric(:add_custom_attributes)
620
711
 
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
712
+ unless params.is_a?(Hash)
629
713
  ::NewRelic::Agent.logger.warn("Bad argument passed to #add_custom_attributes. Expected Hash but got #{params.class}")
714
+ return
630
715
  end
716
+
717
+ if NewRelic::Agent.agent&.serverless?
718
+ ::NewRelic::Agent.logger.warn('Custom attributes are not supported in serverless mode')
719
+ return
720
+ end
721
+
722
+ Transaction.tl_current&.add_custom_attributes(params)
723
+ segment = ::NewRelic::Agent::Tracer.current_segment
724
+ add_new_segment_attributes(params, segment) if segment
631
725
  end
632
726
 
633
727
  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'
@@ -76,6 +76,7 @@ module NewRelic
76
76
  end
77
77
 
78
78
  def determine_env(options)
79
+ options[:env] = :serverless if local_env.discovered_dispatcher == :serverless
79
80
  env = options[:env] || self.env
80
81
  env = env.to_s
81
82
 
@@ -95,6 +96,12 @@ module NewRelic
95
96
  def configure_agent(env, options)
96
97
  manual = Agent::Configuration::ManualSource.new(options)
97
98
  Agent.config.replace_or_add_config(manual)
99
+
100
+ # if manual config sees serverless mode enabled, then the proc
101
+ # must have returned 'true'. don't bother with YAML and high security
102
+ # in a serverless context
103
+ return if Agent.config[:'serverless_mode.enabled']
104
+
98
105
  yaml_source = Agent::Configuration::YamlSource.new(config_file_path, env)
99
106
  log_yaml_source_failures(yaml_source) if yaml_source.failed?
100
107
  Agent.config.replace_or_add_config(yaml_source)
@@ -60,21 +60,22 @@ module NewRelic
60
60
 
61
61
  def discover_dispatcher
62
62
  dispatchers = %w[
63
+ serverless
64
+ puma
65
+ sidekiq
66
+ falcon
67
+ delayed_job
68
+ unicorn
63
69
  passenger
70
+ resque
64
71
  torquebox
65
72
  trinidad
66
73
  glassfish
67
- resque
68
- sidekiq
69
- delayed_job
70
- puma
71
74
  thin
72
75
  mongrel
73
76
  litespeed
74
- unicorn
75
77
  webrick
76
78
  fastcgi
77
- falcon
78
79
  ]
79
80
  while dispatchers.any? && @discovered_dispatcher.nil?
80
81
  send('check_for_' + (dispatchers.shift))
@@ -198,6 +199,12 @@ module NewRelic
198
199
  @discovered_dispatcher = :passenger
199
200
  end
200
201
 
202
+ def check_for_serverless
203
+ return unless NewRelic::Agent.config[:'serverless_mode.enabled']
204
+
205
+ @discovered_dispatcher = :serverless
206
+ end
207
+
201
208
  public
202
209
 
203
210
  # outputs a human-readable description
@@ -23,8 +23,8 @@ module NewRelic
23
23
  CONTENT_TYPE = 'Content-Type'.freeze
24
24
  CONTENT_DISPOSITION = 'Content-Disposition'.freeze
25
25
  CONTENT_LENGTH = 'Content-Length'.freeze
26
- ATTACHMENT = 'attachment'.freeze
27
- TEXT_HTML = 'text/html'.freeze
26
+ ATTACHMENT = /attachment/.freeze
27
+ TEXT_HTML = %r{text/html}.freeze
28
28
 
29
29
  BODY_START = '<body'.freeze
30
30
  HEAD_START = '<head'.freeze
@@ -65,6 +65,10 @@ module NewRelic
65
65
  html?(headers) &&
66
66
  !attachment?(headers) &&
67
67
  !streaming?(env, headers)
68
+ rescue StandardError => e
69
+ NewRelic::Agent.logger.error('RUM instrumentation applicability check failed on exception:' \
70
+ "#{e.class} - #{e.message}")
71
+ false
68
72
  end
69
73
 
70
74
  private
@@ -100,11 +104,11 @@ module NewRelic
100
104
 
101
105
  def html?(headers)
102
106
  # needs else branch coverage
103
- headers[CONTENT_TYPE] && headers[CONTENT_TYPE].include?(TEXT_HTML) # rubocop:disable Style/SafeNavigation
107
+ headers[CONTENT_TYPE]&.match?(TEXT_HTML)
104
108
  end
105
109
 
106
110
  def attachment?(headers)
107
- headers[CONTENT_DISPOSITION]&.include?(ATTACHMENT)
111
+ headers[CONTENT_DISPOSITION]&.match?(ATTACHMENT)
108
112
  end
109
113
 
110
114
  def streaming?(env, headers)
@@ -43,9 +43,11 @@ module NewRelic
43
43
  :process_response_metadata,
44
44
  :record_custom_event,
45
45
  :record_metric,
46
+ :record_llm_feedback_event,
46
47
  :recording_web_transaction?,
47
48
  :require_test_helper,
48
49
  :set_error_group_callback,
50
+ :set_llm_token_count_callback,
49
51
  :set_segment_callback,
50
52
  :set_sql_obfuscator,
51
53
  :set_transaction_name,
@@ -0,0 +1,31 @@
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 ThreadLocalStorage
7
+ def self.get(thread, key)
8
+ if Agent.config[:thread_local_tracer_state]
9
+ thread.thread_variable_get(key)
10
+ else
11
+ thread[key]
12
+ end
13
+ end
14
+
15
+ def self.set(thread, key, value)
16
+ if Agent.config[:thread_local_tracer_state]
17
+ thread.thread_variable_set(key, value)
18
+ else
19
+ thread[key] = value
20
+ end
21
+ end
22
+
23
+ def self.[](key)
24
+ get(::Thread.current, key)
25
+ end
26
+
27
+ def self.[]=(key, value)
28
+ set(::Thread.current, key, value)
29
+ end
30
+ end
31
+ end
@@ -6,8 +6,8 @@
6
6
  module NewRelic
7
7
  module VERSION # :nodoc:
8
8
  MAJOR = 9
9
- MINOR = 7
10
- TINY = 1
9
+ MINOR = 9
10
+ TINY = 0
11
11
 
12
12
  STRING = "#{MAJOR}.#{MINOR}.#{TINY}"
13
13
  end