newrelic_rpm 9.8.0 → 9.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fac159d955f3aaf1b926d68335c05f8bbea9fa3089bed68bee11f9bb50243361
4
- data.tar.gz: 783c82413e45080741cbc9fd9fe0488b59cede5b64d359af46cf524d871b7763
3
+ metadata.gz: ad2c3da566c3fda3369af14dd1d25b82a582c3db88c910474634b1b8169648fc
4
+ data.tar.gz: 1ebe2637f6cef4b3b3e70afb157b91516cc6bf37511f1acb1f3ec5e1caf917e5
5
5
  SHA512:
6
- metadata.gz: ae3deb3425be91f81d30fd76f6af95ec2ff573f2941e17474cbc96cadd70baa7eda534fcb8886ed039b71eba202d81ef9098600e307dcdf27919f84612eea15d
7
- data.tar.gz: 4000a2bd73ea4eee6853878630d777226c39af0b46aafa8946bc167495728dc754136731162e660e6fbc895d2df1ca12709e3d2e60be4df8ae5f09035e072cfe
6
+ metadata.gz: d86c1de9e30e3952b7d5dbec62845693795e6c3b0273b5f34a19a0a550e208314774d3da08246c26c8e903657c36b71b91ae170911869f3e801879e8c8ee4e05
7
+ data.tar.gz: bb5732481d984d24366be27b1186e35b753b7dff7613774b8f4f50749e516db6556edd93b1017e656535cf83ef306e31199e44b03d677ea8c249f472f7f30222
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # New Relic Ruby Agent Release Notes
2
2
 
3
+ ## v9.9.0
4
+
5
+ Version 9.9.0 introduces support for AWS Lambda serverless function observability, adds support for Elasticsearch 8.13.0, and adds the 'request.temperature' attribute to chat completion summaries in ruby-openai instrumentation.
6
+
7
+ - **Feature: Serverless Mode for AWS Lambda**
8
+
9
+ The Ruby agent is now capable of operating in a quick and light serverless mode suitable for observing AWS Lambda function invocations. For serverless use, the agent is delivered by a New Relic Lambda [layer](https://github.com/newrelic/newrelic-lambda-layers) that can be associated with a Lambda function. All reported data will appear in New Relic's dedicated serverless UI views. Only AWS based Lambda functions are supported for now, though support for other cloud hosted serverless offerings may be added in future depending on Ruby customer demand. The serverless functionality is only intended for use with the official New Relic Ruby layers for Lambda. Any existing workflows that involve the manual use of the Ruby agent in an AWS Lambda context without a New Relic layer should not be impacted.
10
+
11
+ - **Feature: Add support for Elasticsearch 8.13.0**
12
+
13
+ Elasticsearch 8.13.0 increased the number of arguments used in the method the agent instruments, `Elastic::Transport::Client#perform_request`. Now, the agent supports a variable number of arguments for the instrumented method to prevent future `ArgumentError`s.
14
+
15
+ - **Bugfix: Add 'request.temperature' to ruby-openai chat completion summaries**
16
+
17
+ Previously, the agent was not reporting the `request.temperature` attribute on `LlmChatCompletionSummary` events through ruby-openai instrumentation. We are now reporting this attribute.
18
+
3
19
  ## v9.8.0
4
20
 
5
21
  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.
@@ -15,7 +31,7 @@ Version 9.8.0 introduces instrumentation for ruby-openai, adds the option to sto
15
31
  This version introduces two new APIs that allow users to record additional information on LLM events:
16
32
  * `NewRelic::Agent.record_llm_feedback_event` - Records user feedback events.
17
33
  * `NewRelic::Agent.set_llm_token_count_callback` - Sets a callback proc for calculating `token_count` attributes for embedding and chat completion message events.
18
-
34
+
19
35
  Visit [RubyDoc](https://rubydoc.info/github/newrelic/newrelic-ruby-agent/) for more information on each of these APIs.
20
36
 
21
37
  - **Feature: Store tracer state on thread-level**
@@ -49,7 +65,7 @@ Version 9.7.0 introduces ViewComponent instrumentation, changes the endpoint use
49
65
 
50
66
  - **Feature: ViewComponent instrumentation**
51
67
 
52
- [ViewComponent](https://viewcomponent.org/) is a now an instrumented library. [PR#2367](https://github.com/newrelic/newrelic-ruby-agent/pull/2367)
68
+ [ViewComponent](https://viewcomponent.org/) is a now an instrumented library. [PR#2367](https://github.com/newrelic/newrelic-ruby-agent/pull/2367)
53
69
 
54
70
  - **Feature: Use root path to access Elasticsearch cluster name**
55
71
 
@@ -34,6 +34,7 @@ require 'new_relic/agent/utilization_data'
34
34
  require 'new_relic/environment_report'
35
35
  require 'new_relic/agent/attribute_filter'
36
36
  require 'new_relic/agent/adaptive_sampler'
37
+ require 'new_relic/agent/serverless_handler'
37
38
  require 'new_relic/agent/connect/request_builder'
38
39
  require 'new_relic/agent/connect/response_handler'
39
40
 
@@ -96,6 +97,7 @@ module NewRelic
96
97
  @monotonic_gc_profiler = VM::MonotonicGCProfiler.new
97
98
  @adaptive_sampler = AdaptiveSampler.new(Agent.config[:sampling_target],
98
99
  Agent.config[:sampling_target_period_in_seconds])
100
+ @serverless_handler = ServerlessHandler.new
99
101
  end
100
102
 
101
103
  def init_event_handlers
@@ -172,6 +174,7 @@ module NewRelic
172
174
  attr_reader :transaction_event_recorder
173
175
  attr_reader :attribute_filter
174
176
  attr_reader :adaptive_sampler
177
+ attr_reader :serverless_handler
175
178
 
176
179
  def transaction_event_aggregator
177
180
  @transaction_event_recorder.transaction_event_aggregator
@@ -307,7 +310,7 @@ module NewRelic
307
310
  @stats_engine = StatsEngine.new
308
311
  end
309
312
 
310
- def flush_pipe_data
313
+ def flush_pipe_data # used only by resque
311
314
  if connected? && @service.is_a?(PipeService)
312
315
  transmit_data_types
313
316
  end
@@ -27,9 +27,13 @@ module NewRelic
27
27
  @connect_state == :disconnected
28
28
  end
29
29
 
30
- # Don't connect if we're already connected, or if we tried to connect
31
- # and were rejected with prejudice because of a license issue, unless
32
- # we're forced to by force_reconnect.
30
+ def serverless?
31
+ Agent.config[:'serverless_mode.enabled']
32
+ end
33
+
34
+ # Don't connect if we're already connected, if we're in serverless mode,
35
+ # or if we tried to connect and were rejected with prejudice because of
36
+ # a license issue, unless we're forced to by force_reconnect.
33
37
  def should_connect?(force = false)
34
38
  force || (!connected? && !disconnected?)
35
39
  end
@@ -62,10 +66,8 @@ module NewRelic
62
66
  # no longer try to connect to the server, saving the
63
67
  # application and the server load
64
68
  def handle_license_error(error)
65
- ::NewRelic::Agent.logger.error( \
66
- error.message, \
67
- 'Visit NewRelic.com to obtain a valid license key, or to upgrade your account.'
68
- )
69
+ ::NewRelic::Agent.logger.error(error.message,
70
+ 'Visit newrelic.com to obtain a valid license key, or to upgrade your account.')
69
71
  disconnect
70
72
  end
71
73
 
@@ -94,7 +96,7 @@ module NewRelic
94
96
  # connects, then configures the agent using the response from
95
97
  # the connect service
96
98
  def connect_to_server
97
- request_builder = ::NewRelic::Agent::Connect::RequestBuilder.new( \
99
+ request_builder = ::NewRelic::Agent::Connect::RequestBuilder.new(
98
100
  @service,
99
101
  Agent.config,
100
102
  event_harvest_config,
@@ -128,7 +128,7 @@ module NewRelic
128
128
  catch_errors do
129
129
  NewRelic::Agent.disable_all_tracing do
130
130
  connect(connection_options)
131
- if connected?
131
+ if NewRelic::Agent.instance.connected?
132
132
  create_and_run_event_loop
133
133
  # never reaches here unless there is a problem or
134
134
  # the agent is exiting
@@ -124,6 +124,8 @@ module NewRelic
124
124
  # Warn the user if they have configured their agent not to
125
125
  # send data, that way we can see this clearly in the log file
126
126
  def monitoring?
127
+ return false if Agent.config[:'serverless_mode.enabled']
128
+
127
129
  if Agent.config[:monitor_mode]
128
130
  true
129
131
  else
@@ -146,7 +148,6 @@ module NewRelic
146
148
  end
147
149
  end
148
150
 
149
- # A correct license key exists and is of the proper length
150
151
  def has_correct_license_key?
151
152
  has_license_key? && correct_license_length
152
153
  end
@@ -132,7 +132,8 @@ module NewRelic
132
132
  end
133
133
 
134
134
  def wants_stdout?
135
- ::NewRelic::Agent.config[:log_file_path].casecmp(NewRelic::STANDARD_OUT) == 0
135
+ ::NewRelic::Agent.config[:log_file_path].casecmp(NewRelic::STANDARD_OUT) == 0 ||
136
+ ::NewRelic::Agent.config[:'serverless_mode.enabled']
136
137
  end
137
138
 
138
139
  def find_or_create_file_path(path_setting, root)
@@ -52,9 +52,24 @@ module NewRelic
52
52
  result
53
53
  end
54
54
 
55
+ def self.default_settings(key)
56
+ ::NewRelic::Agent::Configuration::DEFAULTS[key]
57
+ end
58
+
59
+ def self.value_from_defaults(key, subkey)
60
+ default_settings(key)&.send(:[], subkey)
61
+ end
62
+
63
+ def self.allowlist_for(key)
64
+ value_from_defaults(key, :allowlist)
65
+ end
66
+
67
+ def self.default_for(key)
68
+ value_from_defaults(key, :default)
69
+ end
70
+
55
71
  def self.transform_for(key)
56
- default_settings = ::NewRelic::Agent::Configuration::DEFAULTS[key]
57
- default_settings[:transform] if default_settings
72
+ value_from_defaults(key, :transform)
58
73
  end
59
74
 
60
75
  def self.config_search_paths # rubocop:disable Metrics/AbcSize
@@ -800,6 +815,7 @@ module NewRelic
800
815
  :public => true,
801
816
  :type => String,
802
817
  :allowed_from_server => false,
818
+ :allowlist => %w[debug info warn error fatal unknown DEBUG INFO WARN ERROR FATAL UNKNOWN],
803
819
  :description => <<~DESCRIPTION
804
820
  Sets the minimum level a log event must have to be forwarded to New Relic.
805
821
 
@@ -1844,6 +1860,17 @@ module NewRelic
1844
1860
  :transform => DefaultSource.method(:convert_to_regexp_list),
1845
1861
  :description => 'Define transactions you want the agent to ignore, by specifying a list of patterns matching the URI you want to ignore. For more detail, see [the docs on ignoring specific transactions](/docs/agents/ruby-agent/api-guides/ignoring-specific-transactions/#config-ignoring).'
1846
1862
  },
1863
+ # Serverless
1864
+ :'serverless_mode.enabled' => {
1865
+ :default => false,
1866
+ :public => true,
1867
+ :type => Boolean,
1868
+ :allowed_from_server => false,
1869
+ :transform => proc { |bool| NewRelic::Agent::ServerlessHandler.env_var_set? || bool },
1870
+ :description => 'If `true`, the agent will operate in a streamlined mode suitable for use with short-lived ' \
1871
+ 'serverless functions. NOTE: Only AWS Lambda functions are supported currently and this ' \
1872
+ "option is not intended for use without [New Relic's Ruby Lambda layer](https://docs.newrelic.com/docs/serverless-function-monitoring/aws-lambda-monitoring/get-started/monitoring-aws-lambda-serverless-monitoring/) offering."
1873
+ },
1847
1874
  # Sidekiq
1848
1875
  :'sidekiq.args.include' => {
1849
1876
  default: NewRelic::EMPTY_ARRAY,
@@ -2252,6 +2279,7 @@ module NewRelic
2252
2279
  :public => true,
2253
2280
  :type => Symbol,
2254
2281
  :allowed_from_server => false,
2282
+ :allowlist => %i[none low medium high],
2255
2283
  :external => :infinite_tracing,
2256
2284
  :description => <<~DESC
2257
2285
  Configure the compression level for data sent to the trace observer.
@@ -99,7 +99,7 @@ module NewRelic
99
99
  elsif !value.nil?
100
100
  self[config_key] = true
101
101
  end
102
- else
102
+ elsif !serverless?
103
103
  ::NewRelic::Agent.logger.info("#{environment_key} does not have a corresponding configuration setting (#{config_key} does not exist).")
104
104
  ::NewRelic::Agent.logger.info('Run `rake newrelic:config:docs` or visit https://docs.newrelic.com/docs/apm/agents/ruby-agent/configuration/ruby-agent-configuration to see a list of available configuration settings.')
105
105
  self[config_key] = value
@@ -114,6 +114,14 @@ module NewRelic
114
114
  def collect_new_relic_environment_variable_keys
115
115
  ENV.keys.select { |key| key.match(SUPPORTED_PREFIXES) }
116
116
  end
117
+
118
+ # we can't rely on the :'serverless_mode.enabled' config parameter being
119
+ # set yet to signify serverless mode given that we're in the midst of
120
+ # building the config but we can always rely on the env var being set
121
+ # by the Lambda layer
122
+ def serverless?
123
+ NewRelic::Agent::ServerlessHandler.env_var_set?
124
+ end
117
125
  end
118
126
  end
119
127
  end
@@ -138,7 +138,11 @@ module NewRelic
138
138
  end
139
139
 
140
140
  def evaluate_and_apply_transformations(key, value)
141
- apply_transformations(key, evaluate_procs(value))
141
+ evaluated = evaluate_procs(value)
142
+ default = enforce_allowlist(key, evaluated)
143
+ return default if default
144
+
145
+ apply_transformations(key, evaluated)
142
146
  end
143
147
 
144
148
  def apply_transformations(key, value)
@@ -146,7 +150,7 @@ module NewRelic
146
150
  begin
147
151
  transform.call(value)
148
152
  rescue => e
149
- ::NewRelic::Agent.logger.error("Error applying transformation for #{key}, pre-transform value was: #{value}.", e)
153
+ NewRelic::Agent.logger.error("Error applying transformation for #{key}, pre-transform value was: #{value}.", e)
150
154
  raise e
151
155
  end
152
156
  else
@@ -154,8 +158,21 @@ module NewRelic
154
158
  end
155
159
  end
156
160
 
161
+ def enforce_allowlist(key, value)
162
+ return unless allowlist = default_source.allowlist_for(key)
163
+ return if allowlist.include?(value)
164
+
165
+ default = default_source.default_for(key)
166
+ NewRelic::Agent.logger.warn "Invalid value '#{value}' for #{key}, applying default value of '#{default}'"
167
+ default
168
+ end
169
+
157
170
  def transform_from_default(key)
158
- ::NewRelic::Agent::Configuration::DefaultSource.transform_for(key)
171
+ default_source.transform_for(key)
172
+ end
173
+
174
+ def default_source
175
+ NewRelic::Agent::Configuration::DefaultSource
159
176
  end
160
177
 
161
178
  def register_callback(key, &proc)
@@ -214,7 +231,7 @@ module NewRelic
214
231
  begin
215
232
  thawed_layer[k] = instance_eval(&v) if v.respond_to?(:call)
216
233
  rescue => e
217
- ::NewRelic::Agent.logger.debug("#{e.class.name} : #{e.message} - when accessing config key #{k}")
234
+ NewRelic::Agent.logger.debug("#{e.class.name} : #{e.message} - when accessing config key #{k}")
218
235
  thawed_layer[k] = nil
219
236
  end
220
237
  thawed_layer.delete(:config)
@@ -383,7 +400,7 @@ module NewRelic
383
400
  # is expensive enough that we don't want to do it unless we're
384
401
  # actually going to be logging the message based on our current log
385
402
  # level, so use a `do` block.
386
- ::NewRelic::Agent.logger.debug do
403
+ NewRelic::Agent.logger.debug do
387
404
  hash = flattened.delete_if { |k, _h| DEFAULTS.fetch(k, {}).fetch(:exclude_from_reported_settings, false) }
388
405
  "Updating config (#{direction}) from #{source.class}. Results: #{hash.inspect}"
389
406
  end
@@ -53,6 +53,8 @@ module NewRelic
53
53
  protected
54
54
 
55
55
  def validate_config_file_path(path)
56
+ return if NewRelic::Agent.config[:'serverless_mode.enabled']
57
+
56
58
  expanded_path = File.expand_path(path)
57
59
 
58
60
  if path.empty? || !File.exist?(expanded_path)
@@ -24,7 +24,7 @@ module NewRelic
24
24
  :host => local_host,
25
25
  :display_host => Agent.config[:'process_host.display_name'],
26
26
  :app_name => Agent.config[:app_name],
27
- :language => 'ruby',
27
+ :language => LANGUAGE,
28
28
  :labels => Agent.config.parsed_labels,
29
29
  :agent_version => NewRelic::VERSION::STRING,
30
30
  :environment => @environment_report,
@@ -35,7 +35,7 @@ module NewRelic
35
35
 
36
36
  class << self
37
37
  def for_transaction(transaction)
38
- return nil unless connected?
38
+ return nil unless Agent.instance.connected?
39
39
 
40
40
  payload = new
41
41
  payload.version = VERSION
@@ -101,10 +101,6 @@ module NewRelic
101
101
  transaction.current_segment.guid
102
102
  end
103
103
  end
104
-
105
- def connected?
106
- Agent.instance.connected?
107
- end
108
104
  end
109
105
 
110
106
  attr_accessor :version,
@@ -38,7 +38,7 @@ module NewRelic
38
38
  end
39
39
 
40
40
  def harvest_thread_enabled?
41
- !NewRelic::Agent.config[:disable_harvest_thread]
41
+ !NewRelic::Agent.config[:disable_harvest_thread] && !NewRelic::Agent.config[:'serverless_mode.enabled']
42
42
  end
43
43
 
44
44
  def restart_harvest_thread
@@ -10,7 +10,11 @@ module NewRelic::Agent::Instrumentation
10
10
  OPERATION = 'perform_request'
11
11
  INSTRUMENTATION_NAME = NewRelic::Agent.base_name(name)
12
12
 
13
- def perform_request_with_tracing(method, path, params = {}, body = nil, headers = nil)
13
+ # We need the positional arguments `params` and `body`
14
+ # to capture the nosql statement
15
+ # *args protects the instrumented method if new arguments are added to
16
+ # perform_request
17
+ def perform_request_with_tracing(_method, _path, params = {}, body = nil, _headers = nil, *_args)
14
18
  return yield unless NewRelic::Agent::Tracer.tracing_enabled?
15
19
 
16
20
  NewRelic::Agent.record_instrumentation_invocation(INSTRUMENTATION_NAME)
@@ -22,6 +26,7 @@ module NewRelic::Agent::Instrumentation
22
26
  port_path_or_id: nr_hosts[:port],
23
27
  database_name: nr_cluster_name
24
28
  )
29
+
25
30
  begin
26
31
  NewRelic::Agent::Tracer.capture_segment_error(segment) { yield }
27
32
  ensure
@@ -68,7 +68,7 @@ module NewRelic::Agent::Instrumentation
68
68
  vendor: VENDOR,
69
69
  request_max_tokens: (parameters[:max_tokens] || parameters['max_tokens'])&.to_i,
70
70
  request_model: parameters[:model] || parameters['model'],
71
- temperature: (parameters[:temperature] || parameters['temperature'])&.to_f,
71
+ request_temperature: (parameters[:temperature] || parameters['temperature'])&.to_f,
72
72
  metadata: llm_custom_attributes
73
73
  )
74
74
  end
@@ -128,7 +128,6 @@ module NewRelic::Agent::Instrumentation
128
128
  def update_chat_completion_messages(messages, response, summary)
129
129
  messages += create_chat_completion_response_messages(response, messages.size, summary.id)
130
130
  response_id = response['id'] || NewRelic::Agent::GuidGenerator.generate_guid
131
-
132
131
  messages.each do |message|
133
132
  message.id = "#{response_id}-#{message.sequence}"
134
133
  message.request_id = summary.request_id
@@ -247,21 +247,6 @@ module NewRelic
247
247
  message.byteslice(0...MAX_BYTES)
248
248
  end
249
249
 
250
- def minimum_log_level
251
- if Logger::Severity.constants.include?(configured_log_level_constant)
252
- configured_log_level_constant
253
- else
254
- NewRelic::Agent.logger.log_once(
255
- :error,
256
- 'Invalid application_logging.forwarding.log_level ' \
257
- "'#{NewRelic::Agent.config[LOG_LEVEL_KEY]}' specified! " \
258
- "Must be one of #{Logger::Severity.constants.join('|')}. " \
259
- "Using default level of 'debug'"
260
- )
261
- :DEBUG
262
- end
263
- end
264
-
265
250
  def configured_log_level_constant
266
251
  format_log_level_constant(NewRelic::Agent.config[LOG_LEVEL_KEY])
267
252
  end
@@ -275,7 +260,7 @@ module NewRelic
275
260
  # always record custom log levels
276
261
  return false unless Logger::Severity.constants.include?(severity_constant)
277
262
 
278
- Logger::Severity.const_get(severity_constant) < Logger::Severity.const_get(minimum_log_level)
263
+ Logger::Severity.const_get(severity_constant) < Logger::Severity.const_get(configured_log_level_constant)
279
264
  end
280
265
  end
281
266
  end
@@ -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
@@ -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
@@ -709,16 +709,19 @@ module NewRelic
709
709
  def add_custom_attributes(params) # THREAD_LOCAL_ACCESS
710
710
  record_api_supportability_metric(:add_custom_attributes)
711
711
 
712
- if params.is_a?(Hash)
713
- Transaction.tl_current&.add_custom_attributes(params)
714
-
715
- segment = ::NewRelic::Agent::Tracer.current_segment
716
- if segment
717
- add_new_segment_attributes(params, segment)
718
- end
719
- else
712
+ unless params.is_a?(Hash)
720
713
  ::NewRelic::Agent.logger.warn("Bad argument passed to #add_custom_attributes. Expected Hash but got #{params.class}")
714
+ return
721
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
722
725
  end
723
726
 
724
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
@@ -6,7 +6,7 @@
6
6
  module NewRelic
7
7
  module VERSION # :nodoc:
8
8
  MAJOR = 9
9
- MINOR = 8
9
+ MINOR = 9
10
10
  TINY = 0
11
11
 
12
12
  STRING = "#{MAJOR}.#{MINOR}.#{TINY}"
@@ -22,7 +22,8 @@ namespace :newrelic do
22
22
  'error_collector' => "The agent collects and reports all uncaught exceptions by default. These configuration options allow you to customize the error collection.\n\nFor information on ignored and expected errors, [see this page on Error Analytics in APM](/docs/agents/manage-apm-agents/agent-data/manage-errors-apm-collect-ignore-or-mark-expected/). To set expected errors via the `NewRelic::Agent.notice_error` Ruby method, [consult the Ruby agent API](/docs/agents/ruby-agent/api-guides/sending-handled-errors-new-relic/).",
23
23
  'browser_monitoring' => "The browser monitoring [page load timing](/docs/browser/new-relic-browser/page-load-timing/page-load-timing-process) feature (sometimes referred to as real user monitoring or RUM) gives you insight into the performance real users are experiencing with your website. This is accomplished by measuring the time it takes for your users' browsers to download and render your web pages by injecting a small amount of JavaScript code into the header and footer of each page.",
24
24
  'application_logging' => "The Ruby agent supports [APM logs in context](/docs/apm/new-relic-apm/getting-started/get-started-logs-context). For some tips on configuring logs for the Ruby agent, see [Configure Ruby logs in context](/docs/logs/logs-context/configure-logs-context-ruby).\n\nAvailable logging-related config options include:",
25
- 'analytics_events' => '[New Relic dashboards](/docs/query-your-data/explore-query-data/dashboards/introduction-new-relic-one-dashboards) is a resource to gather and visualize data about your software and what it says about your business. With it you can quickly and easily create real-time dashboards to get immediate answers about end-user experiences, clickstreams, mobile activities, and server transactions.'
25
+ 'analytics_events' => '[New Relic dashboards](/docs/query-your-data/explore-query-data/dashboards/introduction-new-relic-one-dashboards) is a resource to gather and visualize data about your software and what it says about your business. With it you can quickly and easily create real-time dashboards to get immediate answers about end-user experiences, clickstreams, mobile activities, and server transactions.',
26
+ 'ai_monitoring' => "This section includes Ruby agent configurations for setting up AI monitoring.\n\n<Callout variant='important'>You need to enable distributed tracing to capture trace and feedback data. It is turned on by default in Ruby agents 8.0.0 and higher.</Callout>"
26
27
  }
27
28
 
28
29
  NAME_OVERRIDES = {
data/newrelic.yml CHANGED
@@ -629,6 +629,12 @@ common: &default_settings
629
629
  # before shutting down.
630
630
  # send_data_on_exit: true
631
631
 
632
+ # If true, the agent will operate in a streamlined mode suitable for use with
633
+ # short-lived serverless functions. NOTE: Only AWS Lambda functions are supported
634
+ # currently and this option is not intended for use without New Relic's Ruby
635
+ # Lambda layer offering.
636
+ # serverless_mode.enabled: false
637
+
632
638
  # An array of strings that will collectively serve as a denylist for filtering
633
639
  # which Sidekiq job arguments get reported to New Relic. To capture any Sidekiq
634
640
  # arguments, 'job.sidekiq.args.*' must be added to the separate
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: newrelic_rpm
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.8.0
4
+ version: 9.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tanna McClure
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2024-03-26 00:00:00.000000000 Z
14
+ date: 2024-04-17 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: bundler
@@ -562,6 +562,7 @@ files:
562
562
  - lib/new_relic/agent/samplers/memory_sampler.rb
563
563
  - lib/new_relic/agent/samplers/object_sampler.rb
564
564
  - lib/new_relic/agent/samplers/vm_sampler.rb
565
+ - lib/new_relic/agent/serverless_handler.rb
565
566
  - lib/new_relic/agent/span_event_aggregator.rb
566
567
  - lib/new_relic/agent/span_event_primitive.rb
567
568
  - lib/new_relic/agent/sql_sampler.rb