newrelic_rpm 8.7.0 → 8.8.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: 5346c26b9d9c2f9200860b562f1dadb5e79e3eb5d8904850d015c3096cc27d5a
4
- data.tar.gz: acf374f8f16b88746f6a8451255bb0c6c571fbb0a3875c967648b5a8c5286217
3
+ metadata.gz: 0eb3871a3ff66d8707dd01f40025d812d34904c02631d3a83af046bdbaeacdeb
4
+ data.tar.gz: 4e73dfe935bcd268ad0969a2c076e83313e75c3f3f0b711a7cba8c3af1327677
5
5
  SHA512:
6
- metadata.gz: fb227c6b6d69c8900b5693567b03cc2b4f3d95e4059953e7c45287e51892cbb9ae70e6fb9b9ad6015e5eab1b162281fcc384db07ae4367174617726fca086ad0
7
- data.tar.gz: e0bca3ab2559e65d9e2f900ddf69a1882190bc7034948760f87829772cfac441f8b14a4028c441316de2126c85b9dc18bf5acafc7587f10deae7beeb7ba0f386
6
+ metadata.gz: 07566af3aff20a1fe5951fb36e34086f665e8788ecb7edfd2664213e73877e7a1c58d4447f2dd85efb75beeda0ce2f484c513d4a7a09c01bcbe4c20e8a5cba1a
7
+ data.tar.gz: 577944d3712cdd9908d4a564346e7f4bd6fd5772fa00069d73acdb3d8a033621042c72346ddd4ca22585910f035116701f0f904dd8f40c8f0b1e9934bd787705
data/.rubocop.yml CHANGED
@@ -819,9 +819,6 @@ Lint/UselessAccessModifier:
819
819
  Lint/UselessAssignment:
820
820
  Enabled: false
821
821
 
822
- Lint/UselessElseWithoutRescue:
823
- Enabled: true
824
-
825
822
  Lint/UselessMethodDefinition:
826
823
  Enabled: false
827
824
 
@@ -967,6 +964,9 @@ Performance/CollectionLiteralInLoop:
967
964
  Performance/CompareWithBlock:
968
965
  Enabled: true
969
966
 
967
+ Performance/ConcurrentMonotonicTime:
968
+ Enabled: true
969
+
970
970
  # Disabling for now
971
971
  Performance/ConstantRegexp:
972
972
  Enabled: false
@@ -1072,6 +1072,9 @@ Performance/Squeeze:
1072
1072
  Performance/StartWith:
1073
1073
  Enabled: false
1074
1074
 
1075
+ Performance/StringIdentifierArgument:
1076
+ Enabled: true
1077
+
1075
1078
  Performance/StringInclude:
1076
1079
  Enabled: false
1077
1080
 
data/.yardopts CHANGED
@@ -1,6 +1,7 @@
1
1
  --non-transitive-tag api
2
2
  --api public
3
3
  lib/new_relic/agent.rb
4
+ lib/new_relic/traced_thread.rb
4
5
  lib/new_relic/agent/method_tracer.rb
5
6
  lib/new_relic/agent/distributed_tracing.rb
6
7
  lib/new_relic/agent/distributed_trace_payload.rb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # New Relic Ruby Agent Release Notes #
2
2
 
3
+ ## v8.8.0
4
+
5
+ * **Support Makara database adapters with ActiveRecord**
6
+
7
+ Thanks to a community submission from @lucasklaassen with [PR #1177](https://github.com/newrelic/newrelic-ruby-agent/pull/1177), the Ruby agent will now correctly work well with the [Makara gem](https://github.com/instacart/makara). Functionality such as SQL obfuscation should now work when Makara database adapters are used with Active Record.
8
+
9
+ * **Lowered the minimum payload size to compress**
10
+
11
+ Previously the Ruby agent used a particularly large payload size threshold of 64KiB that would need to be met before the agent would compress data en route to New Relic's collector. The original value stems from segfault issues that very old Rubies (< 2.2) used to encounter when compressing smaller payloads. This value has been lowered to 2KiB (2048 bytes), which should provide a more optimal balance between the CPU cycles spent on compression and the bandwidth savings gained from it.
12
+
13
+ * **Provide Code Level Metrics for New Relic CodeStream**
14
+
15
+ For Ruby on Rails applications and/or those with manually traced methods, the agent is now capable of reporting metrics with Ruby method-level granularity. When the new `code_level_metrics.enabled` configuration parameter is set to a `true` value, the agent will associate source-code-related metadata with the metrics for things such as Rails controller methods. Then, when the corresponding Ruby class file that defines the methods is loaded up in a [New Relic CodeStream](https://www.codestream.com/)-powered IDE, [the four golden signals](https://sre.google/sre-book/monitoring-distributed-systems/) for each method will be presented to the developer directly.
16
+
17
+ * **Supportability Metrics will always report uncompressed payload size**
18
+
19
+ New Relic's agent specifications call for Supportability Metrics to always reference the uncompressed payload byte size. Previously, the Ruby agent was calculating the byte size after compression. Furthermore, compression is only performed on payloads of a certain size. This means that sometimes the value could have represented a compressed size and sometimes an uncompressed one. Now the uncompressed value is always used, bringing consistency for comparing two instances of the same metric and alignment with the New Relic agent specifications.
20
+
21
+
3
22
  ## v8.7.0
4
23
 
5
24
  * **APM logs-in-context log forwarding on by default**
data/CONTRIBUTING.md CHANGED
@@ -128,7 +128,7 @@ This will run the unit tests in standalone mode. You can run against a specific
128
128
  by passing the version name (which should match the name of a subdirectory in test/environments)
129
129
  as an argument to the test:env rake task, like this:
130
130
 
131
- bundle exec rake test:env[rails60]
131
+ bundle exec rake 'test:env[rails60]'
132
132
 
133
133
  These tests are setup to run automatically in
134
134
  [GitHub Actions](https://github.com/newrelic/newrelic-ruby-agent/actions) under several
data/Rakefile CHANGED
@@ -11,15 +11,6 @@ task :test => ['test:newrelic']
11
11
  namespace :test do
12
12
  desc "Run all tests"
13
13
  task :all => %w[newrelic multiverse all_compatible_envs]
14
-
15
- begin
16
- require 'test_bisect'
17
- TestBisect::BisectTask.new do |t|
18
- t.test_task_name = 'test:newrelic'
19
- end
20
- rescue LoadError
21
- end
22
-
23
14
  agent_home = File.expand_path(File.dirname(__FILE__))
24
15
 
25
16
  desc "Run agent performance tests"
@@ -116,24 +107,38 @@ task :update_ca_bundle do |t|
116
107
  end
117
108
 
118
109
  namespace :cross_agent_tests do
119
- cross_agent_tests_upstream_path = File.expand_path(File.join(File.dirname(__FILE__), '..', 'cross_agent_tests'))
120
- cross_agent_tests_local_path = File.expand_path(File.join(File.dirname(__FILE__), 'test', 'fixtures', 'cross_agent_tests'))
110
+ CROSS_AGENT_TESTS_UPSTREAM_PATH = File.expand_path(File.join('..', 'cross_agent_tests')).freeze
111
+ CROSS_AGENT_TESTS_LOCAL_PATH = File.expand_path(File.join('test', 'fixtures', 'cross_agent_tests')).freeze
112
+
113
+ def prompt_to_continue(command, destination = 'local')
114
+ puts "The following rsync command will be executed to update the #{destination} copy of the specs:"
115
+ puts
116
+ puts command
117
+ puts
118
+ puts "CAUTION: Any unsaved changes that exist within the #{destination} content will be OVERWRITTEN!"
119
+ if destination.eql?('local')
120
+ puts 'CAUTION 2: Before continuing, make sure your local repo is on the correct, synced branch!'
121
+ end
122
+ puts
123
+ print "Do you wish to continue? ('y' to continue, return to cancel) [n] "
124
+ continue = STDIN.gets.chomp
125
+ if continue.downcase.eql?('y')
126
+ system(command)
127
+ else
128
+ puts 'Cancelled'
129
+ end
130
+ end
121
131
 
122
- # Note: before you pull, make sure your local repo is on the correct, synced branch!
123
132
  desc 'Pull latest changes from cross_agent_tests repo'
124
133
  task :pull do
125
- puts "Updating embedded cross_agent_tests from #{cross_agent_tests_upstream_path}..."
126
- cmd = "rsync -avu --exclude .git #{cross_agent_tests_upstream_path}/ #{cross_agent_tests_local_path}/"
127
- puts cmd
128
- system(cmd)
134
+ command = " rsync -av --exclude .git #{CROSS_AGENT_TESTS_UPSTREAM_PATH}/ #{CROSS_AGENT_TESTS_LOCAL_PATH}/"
135
+ prompt_to_continue(command)
129
136
  end
130
137
 
131
138
  desc 'Copy changes from embedded cross_agent_tests to official repo working copy'
132
139
  task :push do
133
- puts "Copying changes from embedded cross_agent_tests to #{cross_agent_tests_upstream_path}..."
134
- cmd = "rsync -avu #{cross_agent_tests_local_path}/ #{cross_agent_tests_upstream_path}/"
135
- puts cmd
136
- system(cmd)
140
+ command = "rsync -av #{CROSS_AGENT_TESTS_LOCAL_PATH}/ #{CROSS_AGENT_TESTS_UPSTREAM_PATH}/"
141
+ prompt_to_continue(command, 'remote (agent spec repo)')
137
142
  end
138
143
  end
139
144
 
@@ -2010,6 +2010,13 @@ A map of error classes to a list of messages. When an error of one of the classe
2010
2010
  :allowed_from_server => false,
2011
2011
  :description => 'If `true`, the agent decorates logs with metadata to link to entities, hosts, traces, and spans.'
2012
2012
  },
2013
+ :'code_level_metrics.enabled' => {
2014
+ :default => false,
2015
+ :public => true,
2016
+ :type => Boolean,
2017
+ :allowed_from_server => true,
2018
+ :description => 'If `true`, the agent will report source code level metrics for traced methods.'
2019
+ },
2013
2020
  :'instrumentation.active_support_logger' => {
2014
2021
  :default => instrumentation_value_from_boolean(:'application_logging.enabled'),
2015
2022
  :documentation_default => 'auto',
@@ -131,15 +131,15 @@ module NewRelic
131
131
  'execute',
132
132
  'call'
133
133
  ]
134
-
134
+ OTHER_OPERATION = 'other'.freeze
135
135
  SQL_COMMENT_REGEX = Regexp.new('/\*.*?\*/', Regexp::MULTILINE).freeze
136
136
 
137
137
  def parse_operation_from_query(sql)
138
138
  sql = Helper.correctly_encoded(sql).gsub(SQL_COMMENT_REGEX, NewRelic::EMPTY_STR)
139
- if sql =~ /(\w+)/
140
- op = $1.downcase
141
- return op if KNOWN_OPERATIONS.include?(op)
142
- end
139
+ return unless sql =~ /(\w+)/
140
+
141
+ op = Regexp.last_match(1).downcase
142
+ KNOWN_OPERATIONS.include?(op) ? op : OTHER_OPERATION
143
143
  end
144
144
 
145
145
  class ConnectionManager
@@ -101,7 +101,9 @@ module NewRelic
101
101
  end
102
102
 
103
103
  def self.operation_from_sql(sql)
104
- NewRelic::Agent::Database.parse_operation_from_query(sql) || OTHER
104
+ operation = NewRelic::Agent::Database.parse_operation_from_query(sql)
105
+ operation = OTHER if operation.eql?(NewRelic::Agent::Database::OTHER_OPERATION)
106
+ operation
105
107
  end
106
108
 
107
109
  # Allow Transaction#with_database_metric_name to override our
@@ -50,27 +50,30 @@ module NewRelic
50
50
  Tracer.start_transaction_or_segment(
51
51
  name: format_metric_name(payload[:action], controller_class),
52
52
  category: :controller,
53
- options: {
54
- request: request,
55
- filtered_params: NewRelic::Agent::ParameterFiltering.filter_using_rails(
56
- payload[:params],
57
- Rails.application.config.filter_parameters
58
- ),
59
- apdex_start_time: queue_start(request),
60
- ignore_apdex: NewRelic::Agent::Instrumentation::IgnoreActions.is_filtered?(
61
- ControllerInstrumentation::NR_IGNORE_APDEX_KEY,
62
- controller_class,
63
- payload[:action]
64
- ),
65
- ignore_enduser: NewRelic::Agent::Instrumentation::IgnoreActions.is_filtered?(
66
- ControllerInstrumentation::NR_IGNORE_ENDUSER_KEY,
67
- controller_class,
68
- payload[:action]
69
- )
70
- }
53
+ options: tracer_options(payload, request, controller_class)
71
54
  )
72
55
  end
73
56
 
57
+ def tracer_options(payload, request, controller_class)
58
+ {
59
+ request: request,
60
+ filtered_params: filtered_params(payload[:params]),
61
+ apdex_start_time: queue_start(request),
62
+ ignore_apdex: ignore?(payload[:action], ControllerInstrumentation::NR_IGNORE_APDEX_KEY, controller_class),
63
+ ignore_enduser: ignore?(payload[:action],
64
+ ControllerInstrumentation::NR_IGNORE_ENDUSER_KEY,
65
+ controller_class)
66
+ }.merge(NewRelic::Agent::MethodTracerHelpers.code_information(controller_class, payload[:action]))
67
+ end
68
+
69
+ def filtered_params(params)
70
+ NewRelic::Agent::ParameterFiltering.filter_using_rails(params, Rails.application.config.filter_parameters)
71
+ end
72
+
73
+ def ignore?(action, key, controller_class)
74
+ NewRelic::Agent::Instrumentation::IgnoreActions.is_filtered?(key, controller_class, action)
75
+ end
76
+
74
77
  def format_metric_name(metric_action, controller)
75
78
  controller_class = controller.is_a?(Class) ? controller : Object.const_get(controller)
76
79
  "Controller/#{controller_class.controller_path}/#{metric_action}"
@@ -58,14 +58,21 @@ module NewRelic
58
58
  end
59
59
 
60
60
  def self.run_in_trace(job, block, event)
61
- trace_execution_scoped("MessageBroker/#{adapter}/Queue/#{event}/Named/#{job.queue_name}") do
61
+ trace_execution_scoped("MessageBroker/#{adapter}/Queue/#{event}/Named/#{job.queue_name}",
62
+ code_information: code_information_for_job(job)) do
62
63
  block.call
63
64
  end
64
65
  end
65
66
 
66
67
  def self.run_in_transaction(job, block)
68
+ options = code_information_for_job(job)
69
+ options = {} if options.frozen? # the hash will be added to later
67
70
  ::NewRelic::Agent::Tracer.in_transaction(name: transaction_name_for_job(job),
68
- category: :other, &block)
71
+ category: :other, options: options, &block)
72
+ end
73
+
74
+ def self.code_information_for_job(job)
75
+ NewRelic::Agent::MethodTracerHelpers.code_information(job.class, :perform)
69
76
  end
70
77
 
71
78
  def self.transaction_category
@@ -93,9 +93,21 @@ module NewRelic
93
93
 
94
94
  ACTIVE_RECORD = "ActiveRecord".freeze
95
95
  OTHER = "other".freeze
96
+ MAKARA_SUFFIX = "_makara".freeze
97
+
98
+ # convert vendor (makara, etc.) wrapper names to their bare names
99
+ # ex: postgresql_makara -> postgresql
100
+ def bare_adapter_name(adapter_name)
101
+ # TODO: OLD RUBIES - RUBY_VERSION < 2.5
102
+ # With Ruby 2.5+ we could use #delete_suffix instead of #chomp for a
103
+ # potential speed boost
104
+ return adapter_name.chomp(MAKARA_SUFFIX) if adapter_name && adapter_name.end_with?(MAKARA_SUFFIX)
105
+
106
+ adapter_name
107
+ end
96
108
 
97
109
  def product_operation_collection_for name, sql, adapter_name
98
- product = map_product(adapter_name)
110
+ product = map_product(bare_adapter_name(adapter_name))
99
111
  splits = split_name(name)
100
112
  model = model_from_splits(splits)
101
113
  operation = operation_from_splits(splits, sql)
@@ -194,8 +206,7 @@ module NewRelic
194
206
  ACTIVE_RECORD_DEFAULT_PRODUCT_NAME = "ActiveRecord".freeze
195
207
 
196
208
  def map_product(adapter_name)
197
- PRODUCT_NAMES.fetch(adapter_name,
198
- ACTIVE_RECORD_DEFAULT_PRODUCT_NAME)
209
+ PRODUCT_NAMES.fetch(adapter_name, ACTIVE_RECORD_DEFAULT_PRODUCT_NAME)
199
210
  end
200
211
 
201
212
  module InstanceIdentification
@@ -221,11 +232,16 @@ module NewRelic
221
232
  SLASH = "/".freeze
222
233
  LOCALHOST = "localhost".freeze
223
234
 
235
+ def adapter_from_config(config)
236
+ bare_name = NewRelic::Agent::Instrumentation::ActiveRecordHelper.bare_adapter_name(config[:adapter])
237
+ PRODUCT_SYMBOLS[bare_name]
238
+ end
239
+
224
240
  def host(config)
225
241
  return UNKNOWN unless config
226
242
 
227
243
  configured_value = config[:host]
228
- adapter = PRODUCT_SYMBOLS[config[:adapter]]
244
+ adapter = adapter_from_config(config)
229
245
  if configured_value.nil? ||
230
246
  postgres_unix_domain_socket_case?(configured_value, adapter)
231
247
 
@@ -243,7 +259,7 @@ module NewRelic
243
259
  def port_path_or_id(config)
244
260
  return UNKNOWN unless config
245
261
 
246
- adapter = PRODUCT_SYMBOLS[config[:adapter]]
262
+ adapter = adapter_from_config(config)
247
263
  if config[:socket]
248
264
  config[:socket].empty? ? UNKNOWN : config[:socket]
249
265
  elsif postgres_unix_domain_socket_case?(config[:host], adapter) || mysql_default_case?(config, adapter)
@@ -263,7 +279,7 @@ module NewRelic
263
279
  SUPPORTED_ADAPTERS = [:mysql, :postgres].freeze
264
280
 
265
281
  def supported_adapter? config
266
- config && SUPPORTED_ADAPTERS.include?(PRODUCT_SYMBOLS[config[:adapter]])
282
+ config && SUPPORTED_ADAPTERS.include?(adapter_from_config(config))
267
283
  end
268
284
 
269
285
  private
@@ -91,17 +91,26 @@ module NewRelic
91
91
  end
92
92
 
93
93
  return unless connection_id = payload[:connection_id]
94
- connection = nil
95
94
 
96
95
  ::ActiveRecord::Base.connection_handler.connection_pool_list.each do |handler|
97
- connection = handler.connections.detect do |conn|
98
- conn.object_id == connection_id
99
- end
100
-
101
- break if connection
96
+ connection = handler.connections.detect { |conn| conn.object_id == connection_id }
97
+ return connection.instance_variable_get(:@config) if connection
98
+
99
+ # when using makara, handler.connections will be empty, so use the
100
+ # spec config instead.
101
+ # https://github.com/newrelic/newrelic-ruby-agent/issues/507
102
+ # thank you @lucasklaassen
103
+ return handler.spec.config if use_spec_config?(handler)
102
104
  end
103
105
 
104
- connection.instance_variable_get(:@config) if connection
106
+ nil
107
+ end
108
+
109
+ def use_spec_config?(handler)
110
+ handler.respond_to?(:spec) &&
111
+ handler.spec &&
112
+ handler.spec.config &&
113
+ handler.spec.config[:adapter].end_with?('makara')
105
114
  end
106
115
 
107
116
  def start_segment(config, payload)
@@ -165,10 +165,6 @@ module NewRelic
165
165
  def add_transaction_tracer(method, options = {})
166
166
  NewRelic::Agent.record_api_supportability_metric(:add_transaction_tracer)
167
167
 
168
- # The metric path:
169
- options[:name] ||= method.to_s
170
-
171
- argument_list = generate_argument_list(options)
172
168
  traced_method, punctuation = parse_punctuation(method)
173
169
  with_method_name, without_method_name = build_method_names(traced_method, punctuation)
174
170
 
@@ -177,6 +173,12 @@ module NewRelic
177
173
  return
178
174
  end
179
175
 
176
+ # The metric path:
177
+ options[:name] ||= method.to_s
178
+
179
+ code_info = NewRelic::Agent::MethodTracerHelpers.code_information(self, method)
180
+ argument_list = generate_argument_list(options.merge(code_info))
181
+
180
182
  class_eval <<-EOC
181
183
  def #{with_method_name}(*args, &block)
182
184
  perform_action_with_newrelic_trace(#{argument_list.join(',')}) do
@@ -441,6 +443,9 @@ module NewRelic
441
443
  txn_options[:apdex_start_time] = queue_start_time
442
444
  txn_options[:ignore_apdex] = ignore_apdex?
443
445
  txn_options[:ignore_enduser] = ignore_enduser?
446
+ NewRelic::Agent::MethodTracerHelpers::SOURCE_CODE_INFORMATION_PARAMETERS.each do |parameter|
447
+ txn_options[parameter] = trace_options[parameter]
448
+ end
444
449
  txn_options
445
450
  end
446
451
 
@@ -9,7 +9,7 @@ module NewRelic
9
9
  module Agent
10
10
  class LogEventAggregator < EventAggregator
11
11
  # Per-message keys
12
- LEVEL_KEY = "log.level".freeze
12
+ LEVEL_KEY = "level".freeze
13
13
  MESSAGE_KEY = "message".freeze
14
14
  TIMESTAMP_KEY = "timestamp".freeze
15
15
  PRIORITY_KEY = "priority".freeze
@@ -100,7 +100,7 @@ module NewRelic
100
100
  module ClassMethods
101
101
  # contains methods refactored out of the #add_method_tracer method
102
102
  module AddMethodTracer
103
- ALLOWED_KEYS = [:metric, :push_scope, :code_header, :code_footer].freeze
103
+ ALLOWED_KEYS = [:metric, :push_scope, :code_header, :code_information, :code_footer].freeze
104
104
 
105
105
  DEFAULT_SETTINGS = {:push_scope => true, :metric => true, :code_header => "", :code_footer => ""}.freeze
106
106
 
@@ -274,7 +274,8 @@ module NewRelic
274
274
 
275
275
  _nr_define_traced_method(method_name, scoped_metric: scoped_metric, unscoped_metrics: unscoped_metrics,
276
276
  code_header: options[:code_header], code_footer: options[:code_footer],
277
- record_metrics: options[:metric], visibility: visibility)
277
+ record_metrics: options[:metric], visibility: visibility,
278
+ code_information: options[:code_information])
278
279
 
279
280
  prepend(_nr_traced_method_module)
280
281
 
@@ -298,7 +299,8 @@ module NewRelic
298
299
 
299
300
  def _nr_define_traced_method(method_name, scoped_metric: nil, unscoped_metrics: [],
300
301
  code_header: nil, code_footer: nil, record_metrics: true,
301
- visibility: :public)
302
+ visibility: :public, code_information: {})
303
+
302
304
  _nr_traced_method_module.module_eval do
303
305
  define_method(method_name) do |*args, &block|
304
306
  return super(*args, &block) unless NewRelic::Agent.tl_is_execution_traced?
@@ -325,7 +327,10 @@ module NewRelic
325
327
  # If tracing multiple metrics on this method, nest one unscoped trace inside the scoped trace.
326
328
  begin
327
329
  if scoped_metric_eval
328
- ::NewRelic::Agent::MethodTracer.trace_execution_scoped(scoped_metric_eval, metric: record_metrics, internal: true) do
330
+ ::NewRelic::Agent::MethodTracer.trace_execution_scoped(scoped_metric_eval,
331
+ metric: record_metrics,
332
+ internal: true,
333
+ code_information: code_information) do
329
334
  if unscoped_metrics_eval.empty?
330
335
  super(*args, &block)
331
336
  else
@@ -5,6 +5,13 @@
5
5
  module NewRelic
6
6
  module Agent
7
7
  module MethodTracerHelpers
8
+ # These are code level metrics (CLM) attributes. For Ruby, they map like so:
9
+ # filepath: full path to an .rb file on disk
10
+ # lineno: the line number a Ruby method is defined on within a given .rb file
11
+ # function: the name of the Ruby method
12
+ # namespace: the Ruby class' namespace as a string, ex: 'MyModule::MyClass'
13
+ SOURCE_CODE_INFORMATION_PARAMETERS = %i[filepath lineno function namespace].freeze
14
+ SOURCE_CODE_INFORMATION_FAILURE_METRIC = "Supportabiltiy/CodeLevelMetrics/Ruby/Failure".freeze
8
15
  MAX_ALLOWED_METRIC_DURATION = 1_000_000_000 # roughly 31 years
9
16
 
10
17
  extend self
@@ -26,12 +33,85 @@ module NewRelic
26
33
  segment.record_metrics = false
27
34
  end
28
35
 
36
+ unless !options.key?(:code_information) || options[:code_information].nil? || options[:code_information].empty?
37
+ segment.code_information = options[:code_information]
38
+ end
39
+
29
40
  begin
30
41
  Tracer.capture_segment_error(segment) { yield }
31
42
  ensure
32
43
  segment.finish if segment
33
44
  end
34
45
  end
46
+
47
+ def code_information(object, method_name)
48
+ unless NewRelic::Agent.config[:'code_level_metrics.enabled'] && object && method_name
49
+ return NewRelic::EMPTY_HASH
50
+ end
51
+
52
+ @code_information ||= {}
53
+ cache_key = "#{object.object_id}#{method_name}"
54
+ return @code_information[cache_key] if @code_information.key?(cache_key)
55
+
56
+ namespace, location, is_class_method = namespace_and_location(object, method_name.to_sym)
57
+
58
+ @code_information[cache_key] = {filepath: location.first,
59
+ lineno: location.last,
60
+ function: "#{'self.' if is_class_method}#{method_name}",
61
+ namespace: namespace}
62
+ rescue => e
63
+ ::NewRelic::Agent.logger.warn("Unable to determine source code info for '#{object}', " \
64
+ "method '#{method_name}' - #{e.class}: #{e.message}")
65
+ ::NewRelic::Agent.increment_metric(SOURCE_CODE_INFORMATION_FAILURE_METRIC, 1)
66
+ ::NewRelic::EMPTY_HASH
67
+ end
68
+
69
+ private
70
+
71
+ # The string representation of a singleton class looks like
72
+ # '#<Class:MyModule::MyClass>'. Return the 'MyModule::MyClass' part of
73
+ # that string
74
+ def klass_name(object)
75
+ name = Regexp.last_match(1) if object.to_s =~ /^#<Class:(.*)>$/
76
+ return name if name
77
+
78
+ raise "Unable to glean a class name from string '#{object}'" unless name
79
+ end
80
+
81
+ # get at the underlying class from the singleton class
82
+ #
83
+ # note: even with the regex hit from klass_name(), `Object.const_get`
84
+ # is more performant than iterating through `ObjectSpace`
85
+ def klassify_singleton(object)
86
+ Object.const_get(klass_name(object))
87
+ end
88
+
89
+ # determine the namespace (class name including all module names in scope)
90
+ # and source code location (file path and line number) for the given
91
+ # object and method name
92
+ #
93
+ # traced class methods:
94
+ # * object is a singleton class, `#<Class::MyClass>`
95
+ # * get at the underlying non-singleton class
96
+ #
97
+ # traced instance methods and Rails controller methods:
98
+ # * object is a class, `MyClass`
99
+ #
100
+ # anonymous class based methods (`c = Class.new { def method; end; }`:
101
+ # * `#name` returns `nil`, so use '(Anonymous)' instead
102
+ #
103
+ def namespace_and_location(object, method_name)
104
+ klass = object.singleton_class? ? klassify_singleton(object) : object
105
+ name = klass.name || '(Anonymous)'
106
+ is_class_method = false
107
+ method = if klass.instance_methods.include?(method_name)
108
+ klass.instance_method(method_name)
109
+ else
110
+ is_class_method = true
111
+ klass.method(method_name)
112
+ end
113
+ [name, method.source_location, is_class_method]
114
+ end
35
115
  end
36
116
  end
37
117
  end
@@ -15,23 +15,23 @@ module NewRelic
15
15
  class NewRelicService
16
16
  # Specifies the version of the agent's communication protocol with
17
17
  # the NewRelic hosted site.
18
-
19
18
  PROTOCOL_VERSION = 17
20
19
 
21
- # 1f147a42: v10 (tag 3.5.3.17)
22
- # cf0d1ff1: v9 (tag 3.5.0)
23
- # 14105: v8 (tag 2.10.3)
24
- # (no v7)
25
- # 10379: v6 (not tagged)
26
- # 4078: v5 (tag 2.5.4)
27
- # 2292: v4 (tag 2.3.6)
28
- # 1754: v3 (tag 2.3.0)
29
- # 534: v2 (shows up in 2.1.0, our first tag)
30
-
31
20
  # These include Errno connection errors, and all indicate that the
32
21
  # underlying TCP connection may be in a bad state.
33
22
  CONNECTION_ERRORS = [Timeout::Error, EOFError, SystemCallError, SocketError].freeze
34
23
 
24
+ # Don't perform compression on the payload unless its uncompressed size is
25
+ # greater than or equal to this number of bytes. In testing with
26
+ # Ruby 2.2 - 3.1, we determined an absolute minimum value for ASCII to be
27
+ # 535 bytes to obtain at least a 10% savings in size. It is recommended
28
+ # that this value be kept above that 535 number. It is also important to
29
+ # consider the CPU cost involved with performing compression and to find
30
+ # a balance between CPU cycles spent and bandwidth saved. A good
31
+ # reasonable default here is 2048 bytes, which is a tried and true Apache
32
+ # Tomcat default (as of v8.5.78)
33
+ MIN_BYTE_SIZE_TO_COMPRESS = 2048
34
+
35
35
  attr_accessor :request_timeout
36
36
  attr_reader :collector, :marshaller, :agent_id
37
37
 
@@ -198,12 +198,9 @@ module NewRelic
198
198
  response
199
199
  end
200
200
 
201
- # We do not compress if content is smaller than 64kb. There are
202
- # problems with bugs in Ruby in some versions that expose us
203
- # to a risk of segfaults if we compress aggressively.
204
201
  def compress_request_if_needed(data, endpoint)
205
202
  encoding = 'identity'
206
- if data.size > 64 * 1024
203
+ if data.size >= MIN_BYTE_SIZE_TO_COMPRESS
207
204
  encoding = Agent.config[:compressed_content_encoding]
208
205
  data = if encoding == 'deflate'
209
206
  Encoders::Compressed::Deflate.encode(data)
@@ -429,8 +426,8 @@ module NewRelic
429
426
  end
430
427
  serialize_finish_ts = Process.clock_gettime(Process::CLOCK_MONOTONIC)
431
428
 
429
+ size = data.size # only the uncompressed size is reported
432
430
  data, encoding = compress_request_if_needed(data, method)
433
- size = data.size
434
431
 
435
432
  # Preconnect needs to always use the configured collector host, not the redirect host
436
433
  # We reset it here so we are always using the configured collector during our creation of the new connection
@@ -165,18 +165,21 @@ module NewRelic
165
165
  end
166
166
  end
167
167
 
168
- def merge_and_freeze_attributes agent_attributes, error_attributes
169
- return agent_attributes.freeze unless error_attributes
170
- return error_attributes.freeze if agent_attributes.equal?(NewRelic::EMPTY_HASH)
171
- agent_attributes.merge!(error_attributes).freeze
168
+ def merge_hashes(hash1, hash2)
169
+ return hash1 if hash2.nil? || hash2.empty?
170
+ return hash2 if hash1.nil? || hash1.empty?
171
+
172
+ hash1.merge!(hash2)
172
173
  end
173
174
 
174
175
  def agent_attributes segment
175
176
  agent_attributes = segment.attributes
176
177
  .agent_attributes_for(NewRelic::Agent::AttributeFilter::DST_SPAN_EVENTS)
177
178
  error_attributes = error_attributes(segment)
178
- return NewRelic::EMPTY_HASH unless agent_attributes || error_attributes
179
- merge_and_freeze_attributes(agent_attributes, error_attributes)
179
+ code_attributes = segment.code_attributes
180
+ agent_attributes = merge_hashes(agent_attributes, error_attributes)
181
+ agent_attributes = merge_hashes(agent_attributes, code_attributes)
182
+ agent_attributes.freeze
180
183
  end
181
184
 
182
185
  def parent_guid segment
@@ -46,6 +46,10 @@ module NewRelic
46
46
  @record_scoped_metric = true
47
47
  @record_on_finish = false
48
48
  @noticed_error = nil
49
+ @code_filepath = nil
50
+ @code_function = nil
51
+ @code_lineno = nil
52
+ @code_namespace = nil
49
53
  end
50
54
 
51
55
  def start
@@ -57,6 +61,7 @@ module NewRelic
57
61
  def finish
58
62
  @end_time = Process.clock_gettime(Process::CLOCK_REALTIME)
59
63
  @duration = end_time - start_time
64
+
60
65
  return unless transaction
61
66
  run_complete_callbacks
62
67
  finalize if record_on_finish?
@@ -110,6 +115,28 @@ module NewRelic
110
115
  @concurrent_children
111
116
  end
112
117
 
118
+ def code_information=(info = {})
119
+ return unless info[:filepath]
120
+
121
+ @code_filepath = info[:filepath]
122
+ @code_function = info[:function]
123
+ @code_lineno = info[:lineno]
124
+ @code_namespace = info[:namespace]
125
+ end
126
+
127
+ def all_code_information_present?
128
+ @code_filepath && @code_function && @code_lineno && @code_namespace
129
+ end
130
+
131
+ def code_attributes
132
+ return ::NewRelic::EMPTY_HASH unless all_code_information_present?
133
+
134
+ @code_attributes ||= {'code.filepath' => @code_filepath,
135
+ 'code.function' => @code_function,
136
+ 'code.lineno' => @code_lineno,
137
+ 'code.namespace' => @code_namespace}
138
+ end
139
+
113
140
  INSPECT_IGNORE = [:@transaction, :@transaction_state].freeze
114
141
 
115
142
  def inspect
@@ -124,7 +124,7 @@ module NewRelic
124
124
  txn = Transaction.new(category, options)
125
125
  state.reset(txn)
126
126
  txn.state = state
127
- txn.start
127
+ txn.start(options)
128
128
  txn
129
129
  end
130
130
 
@@ -414,7 +414,7 @@ module NewRelic
414
414
  @frozen_name ? true : false
415
415
  end
416
416
 
417
- def start
417
+ def start(options = {})
418
418
  return if !state.is_execution_traced?
419
419
 
420
420
  sql_sampler.on_start_transaction(state, request_path)
@@ -423,7 +423,7 @@ module NewRelic
423
423
 
424
424
  ignore! if user_defined_rules_ignore?
425
425
 
426
- create_initial_segment
426
+ create_initial_segment(options)
427
427
  Segment.merge_untrusted_agent_attributes \
428
428
  @filtered_params,
429
429
  :'request.parameters',
@@ -434,12 +434,12 @@ module NewRelic
434
434
  segments.first
435
435
  end
436
436
 
437
- def create_initial_segment
438
- segment = create_segment @default_name
437
+ def create_initial_segment(options = {})
438
+ segment = create_segment @default_name, options
439
439
  segment.record_scoped_metric = false
440
440
  end
441
441
 
442
- def create_segment(name)
442
+ def create_segment(name, options = {})
443
443
  summary_metrics = nil
444
444
 
445
445
  if name.start_with?(MIDDLEWARE_PREFIX)
@@ -453,6 +453,10 @@ module NewRelic
453
453
  unscoped_metrics: summary_metrics
454
454
  )
455
455
 
456
+ # #code_information will glean the code info out of the options hash
457
+ # if it exists or noop otherwise
458
+ segment.code_information = options
459
+
456
460
  segment
457
461
  end
458
462
 
@@ -467,7 +471,8 @@ module NewRelic
467
471
 
468
472
  nest_initial_segment if segments.length == 1
469
473
  nested_name = self.class.nested_transaction_name options[:transaction_name]
470
- segment = create_segment nested_name
474
+
475
+ segment = create_segment nested_name, options
471
476
  set_default_transaction_name(options[:transaction_name], category)
472
477
  segment
473
478
  end
@@ -143,6 +143,8 @@ module NewRelic
143
143
  #
144
144
  def add_or_defer_method_tracer(receiver, method_name, metric_name, options)
145
145
  @tracer_lock.synchronize do
146
+ options[:code_information] = NewRelic::Agent::MethodTracerHelpers.code_information(receiver, method_name)
147
+
146
148
  if @agent
147
149
  receiver.send(:_nr_add_method_tracer_now, method_name, metric_name, options)
148
150
  else
@@ -7,19 +7,18 @@ module NewRelic
7
7
  #
8
8
  # This class allows the current transaction to be passed to a Thread so that nested segments can be created from the operations performed within the Thread's block.
9
9
  # To have the New Relic Ruby agent automatically trace all of your applications threads,
10
- # enable the `instrumentation.thread.tracing` configuration option in your newrelic.yml.
10
+ # enable the +instrumentation.thread.tracing+ configuration option in your newrelic.yml.
11
11
  #
12
- # Note: disabling the configuration option `instrumentation.thread` while using this class can cause incorrectly nested spans.
12
+ # Note: disabling the configuration option +instrumentation.thread+ while using this class can cause incorrectly nested spans.
13
13
  #
14
14
  # @api public
15
15
  class TracedThread < Thread
16
16
  #
17
17
  # Creates a new Thread whose work will be traced by New Relic.
18
18
  # Use this class as a replacement for the native Thread class.
19
- # Example: Instead of using `Thread.new`, use:
20
- # ```ruby
21
- # NewRelic::TracedThread.new { execute_some_code }
22
- # ```
19
+ # Example: Instead of using +Thread.new+, use:
20
+ #
21
+ # NewRelic::TracedThread.new { execute_some_code }
23
22
  #
24
23
  # @api public
25
24
  def initialize(*args, &block)
@@ -6,7 +6,7 @@
6
6
  module NewRelic
7
7
  module VERSION # :nodoc:
8
8
  MAJOR = 8
9
- MINOR = 7
9
+ MINOR = 8
10
10
  TINY = 0
11
11
 
12
12
  STRING = "#{MAJOR}.#{MINOR}.#{TINY}"
data/newrelic.yml CHANGED
@@ -42,6 +42,9 @@ common: &default_settings
42
42
  # If `true`, the agent decorates logs with metadata to link to entities, hosts, traces, and spans.
43
43
  # application_logging.local_decorating.enabled: false
44
44
 
45
+ # If `true`, the agent will report source code level metrics for traced methods
46
+ # code_level_metrics.enabled: false
47
+
45
48
  # If true, enables transaction event sampling.
46
49
  # transaction_events.enabled: true
47
50
 
data/newrelic_rpm.gemspec CHANGED
@@ -47,10 +47,6 @@ https://github.com/newrelic/newrelic-ruby-agent/
47
47
  s.require_paths = ["lib"]
48
48
  s.rubygems_version = Gem::VERSION
49
49
  s.summary = "New Relic Ruby Agent"
50
- s.post_install_message = 'Thanks for installing the latest version of newrelic_rpm. '\
51
- 'This version turns on application log forwarding by default. If you want to turn ' \
52
- 'off this feature, set `application_logging.forwarding.enabled: false`. ' \
53
- 'For more information, visit: https://docs.newrelic.com/docs/logs/logs-context/configure-logs-context-ruby'
54
50
  s.add_development_dependency 'rake', '12.3.3'
55
51
  s.add_development_dependency 'rb-inotify', '0.9.10' # locked to support < Ruby 2.3 (and listen 3.0.8)
56
52
  s.add_development_dependency 'listen', '3.0.8' # locked to support < Ruby 2.3
@@ -62,7 +58,6 @@ https://github.com/newrelic/newrelic-ruby-agent/
62
58
  s.add_development_dependency 'pry-stack_explorer', '~> 0.4.9'
63
59
  s.add_development_dependency 'guard', '~> 2.16.0'
64
60
  s.add_development_dependency 'guard-minitest', '~> 2.4.0'
65
- s.add_development_dependency 'hometown', '~> 0.2.5'
66
61
  s.add_development_dependency 'bundler'
67
62
  s.add_development_dependency 'rubocop'
68
63
  s.add_development_dependency 'rubocop-performance'
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: 8.7.0
4
+ version: 8.8.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: 2022-05-03 00:00:00.000000000 Z
14
+ date: 2022-06-02 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rake
@@ -167,20 +167,6 @@ dependencies:
167
167
  - - "~>"
168
168
  - !ruby/object:Gem::Version
169
169
  version: 2.4.0
170
- - !ruby/object:Gem::Dependency
171
- name: hometown
172
- requirement: !ruby/object:Gem::Requirement
173
- requirements:
174
- - - "~>"
175
- - !ruby/object:Gem::Version
176
- version: 0.2.5
177
- type: :development
178
- prerelease: false
179
- version_requirements: !ruby/object:Gem::Requirement
180
- requirements:
181
- - - "~>"
182
- - !ruby/object:Gem::Version
183
- version: 0.2.5
184
170
  - !ruby/object:Gem::Dependency
185
171
  name: bundler
186
172
  requirement: !ruby/object:Gem::Requirement
@@ -618,10 +604,7 @@ metadata:
618
604
  documentation_uri: https://docs.newrelic.com/docs/agents/ruby-agent
619
605
  source_code_uri: https://github.com/newrelic/newrelic-ruby-agent
620
606
  homepage_uri: https://newrelic.com/ruby
621
- post_install_message: 'Thanks for installing the latest version of newrelic_rpm. This
622
- version turns on application log forwarding by default. If you want to turn off
623
- this feature, set `application_logging.forwarding.enabled: false`. For more information,
624
- visit: https://docs.newrelic.com/docs/logs/logs-context/configure-logs-context-ruby'
607
+ post_install_message:
625
608
  rdoc_options: []
626
609
  require_paths:
627
610
  - lib