newrelic_rpm 9.12.0 → 9.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +217 -1
  3. data/CONTRIBUTING.md +2 -2
  4. data/README.md +16 -20
  5. data/lib/boot/strap.rb +4 -3
  6. data/lib/new_relic/agent/agent.rb +4 -0
  7. data/lib/new_relic/agent/agent_helpers/connect.rb +3 -0
  8. data/lib/new_relic/agent/agent_helpers/harvest.rb +3 -0
  9. data/lib/new_relic/agent/agent_helpers/shutdown.rb +3 -0
  10. data/lib/new_relic/agent/agent_helpers/start_worker_thread.rb +1 -0
  11. data/lib/new_relic/agent/agent_helpers/startup.rb +7 -0
  12. data/lib/new_relic/agent/aws.rb +6 -0
  13. data/lib/new_relic/agent/configuration/default_source.rb +363 -31
  14. data/lib/new_relic/agent/configuration/environment_source.rb +5 -1
  15. data/lib/new_relic/agent/configuration/manager.rb +23 -0
  16. data/lib/new_relic/agent/configuration/yaml_source.rb +6 -1
  17. data/lib/new_relic/agent/database/obfuscation_helpers.rb +11 -11
  18. data/lib/new_relic/agent/database.rb +41 -1
  19. data/lib/new_relic/agent/distributed_tracing.rb +2 -2
  20. data/lib/new_relic/agent/health_check.rb +136 -0
  21. data/lib/new_relic/agent/instrumentation/active_merchant.rb +0 -13
  22. data/lib/new_relic/agent/instrumentation/active_record.rb +1 -8
  23. data/lib/new_relic/agent/instrumentation/active_record_helper.rb +5 -1
  24. data/lib/new_relic/agent/instrumentation/active_record_subscriber.rb +9 -16
  25. data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger.rb +0 -2
  26. data/lib/new_relic/agent/instrumentation/active_support_logger.rb +0 -2
  27. data/lib/new_relic/agent/instrumentation/async_http.rb +1 -2
  28. data/lib/new_relic/agent/instrumentation/aws_sdk_firehose/chain.rb +21 -0
  29. data/lib/new_relic/agent/instrumentation/aws_sdk_firehose/instrumentation.rb +66 -0
  30. data/lib/new_relic/agent/instrumentation/aws_sdk_firehose/prepend.rb +15 -0
  31. data/lib/new_relic/agent/instrumentation/aws_sdk_firehose.rb +22 -0
  32. data/lib/new_relic/agent/instrumentation/aws_sdk_kinesis/chain.rb +21 -0
  33. data/lib/new_relic/agent/instrumentation/aws_sdk_kinesis/instrumentation.rb +91 -0
  34. data/lib/new_relic/agent/instrumentation/aws_sdk_kinesis/prepend.rb +15 -0
  35. data/lib/new_relic/agent/instrumentation/aws_sdk_kinesis.rb +22 -0
  36. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda/chain.rb +33 -0
  37. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda/instrumentation.rb +93 -0
  38. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda/prepend.rb +23 -0
  39. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda.rb +23 -0
  40. data/lib/new_relic/agent/instrumentation/aws_sqs.rb +0 -2
  41. data/lib/new_relic/agent/instrumentation/bunny.rb +3 -4
  42. data/lib/new_relic/agent/instrumentation/concurrent_ruby.rb +0 -2
  43. data/lib/new_relic/agent/instrumentation/curb.rb +3 -4
  44. data/lib/new_relic/agent/instrumentation/delayed_job_instrumentation.rb +0 -23
  45. data/lib/new_relic/agent/instrumentation/dynamodb/instrumentation.rb +1 -1
  46. data/lib/new_relic/agent/instrumentation/dynamodb.rb +0 -2
  47. data/lib/new_relic/agent/instrumentation/elasticsearch.rb +0 -2
  48. data/lib/new_relic/agent/instrumentation/ethon.rb +0 -4
  49. data/lib/new_relic/agent/instrumentation/excon.rb +0 -16
  50. data/lib/new_relic/agent/instrumentation/fiber.rb +0 -2
  51. data/lib/new_relic/agent/instrumentation/grape/instrumentation.rb +0 -3
  52. data/lib/new_relic/agent/instrumentation/grape.rb +1 -1
  53. data/lib/new_relic/agent/instrumentation/httpclient.rb +0 -1
  54. data/lib/new_relic/agent/instrumentation/httprb.rb +0 -1
  55. data/lib/new_relic/agent/instrumentation/httpx.rb +0 -4
  56. data/lib/new_relic/agent/instrumentation/logger.rb +1 -3
  57. data/lib/new_relic/agent/instrumentation/logstasher.rb +0 -2
  58. data/lib/new_relic/agent/instrumentation/memcache.rb +0 -1
  59. data/lib/new_relic/agent/instrumentation/opensearch/chain.rb +21 -0
  60. data/lib/new_relic/agent/instrumentation/opensearch/instrumentation.rb +66 -0
  61. data/lib/new_relic/agent/instrumentation/opensearch/prepend.rb +13 -0
  62. data/lib/new_relic/agent/instrumentation/opensearch.rb +23 -0
  63. data/lib/new_relic/agent/instrumentation/padrino.rb +3 -3
  64. data/lib/new_relic/agent/instrumentation/rake.rb +0 -1
  65. data/lib/new_relic/agent/instrumentation/rdkafka/chain.rb +72 -0
  66. data/lib/new_relic/agent/instrumentation/rdkafka/instrumentation.rb +70 -0
  67. data/lib/new_relic/agent/instrumentation/rdkafka/prepend.rb +67 -0
  68. data/lib/new_relic/agent/instrumentation/rdkafka.rb +25 -0
  69. data/lib/new_relic/agent/instrumentation/redis.rb +7 -6
  70. data/lib/new_relic/agent/instrumentation/resque.rb +7 -5
  71. data/lib/new_relic/agent/instrumentation/roda.rb +4 -4
  72. data/lib/new_relic/agent/instrumentation/ruby_kafka/chain.rb +55 -0
  73. data/lib/new_relic/agent/instrumentation/ruby_kafka/instrumentation.rb +67 -0
  74. data/lib/new_relic/agent/instrumentation/ruby_kafka/prepend.rb +60 -0
  75. data/lib/new_relic/agent/instrumentation/ruby_kafka.rb +25 -0
  76. data/lib/new_relic/agent/instrumentation/sidekiq/extensions/delayed_class.rb +1 -1
  77. data/lib/new_relic/agent/instrumentation/sidekiq.rb +0 -14
  78. data/lib/new_relic/agent/instrumentation/sinatra.rb +3 -19
  79. data/lib/new_relic/agent/instrumentation/thread.rb +0 -2
  80. data/lib/new_relic/agent/instrumentation/tilt.rb +0 -4
  81. data/lib/new_relic/agent/instrumentation/typhoeus.rb +0 -1
  82. data/lib/new_relic/agent/instrumentation/view_component/instrumentation.rb +11 -5
  83. data/lib/new_relic/agent/instrumentation/view_component.rb +0 -2
  84. data/lib/new_relic/agent/javascript_instrumentor.rb +2 -3
  85. data/lib/new_relic/agent/local_log_decorator.rb +12 -2
  86. data/lib/new_relic/agent/log_event_aggregator.rb +28 -2
  87. data/lib/new_relic/agent/messaging.rb +11 -5
  88. data/lib/new_relic/agent/new_relic_service.rb +8 -2
  89. data/lib/new_relic/agent/serverless_handler.rb +241 -12
  90. data/lib/new_relic/agent/serverless_handler_event_sources.json +155 -0
  91. data/lib/new_relic/agent/serverless_handler_event_sources.rb +49 -0
  92. data/lib/new_relic/agent/span_event_primitive.rb +4 -2
  93. data/lib/new_relic/agent/system_info.rb +14 -0
  94. data/lib/new_relic/agent/threading/backtrace_node.rb +10 -1
  95. data/lib/new_relic/agent/transaction/message_broker_segment.rb +3 -0
  96. data/lib/new_relic/agent/transaction/request_attributes.rb +13 -1
  97. data/lib/new_relic/agent/transaction/trace_context.rb +1 -1
  98. data/lib/new_relic/agent.rb +95 -2
  99. data/lib/new_relic/control/frameworks/grape.rb +14 -0
  100. data/lib/new_relic/control/frameworks/padrino.rb +14 -0
  101. data/lib/new_relic/control/frameworks/rails4.rb +1 -3
  102. data/lib/new_relic/dependency_detection.rb +11 -13
  103. data/lib/new_relic/environment_report.rb +2 -2
  104. data/lib/new_relic/helper.rb +15 -0
  105. data/lib/new_relic/language_support.rb +3 -1
  106. data/lib/new_relic/local_environment.rb +1 -4
  107. data/lib/new_relic/version.rb +1 -1
  108. data/lib/sequel/extensions/new_relic_instrumentation.rb +1 -1
  109. data/lib/tasks/helpers/newrelicyml.rb +73 -11
  110. data/lib/tasks/instrumentation_generator/instrumentation.thor +1 -1
  111. data/lib/tasks/instrumentation_generator/templates/dependency_detection.tt +11 -8
  112. data/newrelic.yml +224 -79
  113. data/test/agent_helper.rb +8 -1
  114. metadata +32 -6
@@ -25,6 +25,7 @@ module NewRelic
25
25
  METRICS_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Metrics/Ruby/%s'.freeze
26
26
  FORWARDING_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Forwarding/Ruby/%s'.freeze
27
27
  DECORATING_SUPPORTABILITY_FORMAT = 'Supportability/Logging/LocalDecorating/Ruby/%s'.freeze
28
+ LABELS_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Labels/Ruby/%s'.freeze
28
29
  MAX_BYTES = 32768 # 32 * 1024 bytes (32 kibibytes)
29
30
 
30
31
  named :LogEventAggregator
@@ -38,6 +39,7 @@ module NewRelic
38
39
  METRICS_ENABLED_KEY = :'application_logging.metrics.enabled'
39
40
  FORWARDING_ENABLED_KEY = :'application_logging.forwarding.enabled'
40
41
  DECORATING_ENABLED_KEY = :'application_logging.local_decorating.enabled'
42
+ LABELS_ENABLED_KEY = :'application_logging.forwarding.labels.enabled'
41
43
  LOG_LEVEL_KEY = :'application_logging.forwarding.log_level'
42
44
  CUSTOM_ATTRIBUTES_KEY = :'application_logging.forwarding.custom_attributes'
43
45
 
@@ -51,6 +53,7 @@ module NewRelic
51
53
  @high_security = NewRelic::Agent.config[:high_security]
52
54
  @instrumentation_logger_enabled = NewRelic::Agent::Instrumentation::Logger.enabled?
53
55
  @attributes = NewRelic::Agent::LogEventAttributes.new
56
+
54
57
  register_for_done_configuring(events)
55
58
  end
56
59
 
@@ -186,6 +189,10 @@ module NewRelic
186
189
  attributes.add_custom_attributes(custom_attributes)
187
190
  end
188
191
 
192
+ def labels
193
+ @labels ||= create_labels
194
+ end
195
+
189
196
  # Because our transmission format (MELT) is different than historical
190
197
  # agent payloads, extract the munging here to keep the service focused
191
198
  # on the general harvest + transmit instead of the format.
@@ -201,8 +208,9 @@ module NewRelic
201
208
  # To save on unnecessary data transmission, trim the entity.type
202
209
  # sent by classic logs-in-context
203
210
  common_attributes.delete(ENTITY_TYPE_KEY)
204
-
205
- common_attributes.merge!(NewRelic::Agent.agent.log_event_aggregator.attributes.custom_attributes)
211
+ aggregator = NewRelic::Agent.agent.log_event_aggregator
212
+ common_attributes.merge!(aggregator.attributes.custom_attributes)
213
+ common_attributes.merge!(aggregator.labels)
206
214
 
207
215
  _, items = data
208
216
  payload = [{
@@ -247,6 +255,7 @@ module NewRelic
247
255
  record_configuration_metric(METRICS_SUPPORTABILITY_FORMAT, METRICS_ENABLED_KEY)
248
256
  record_configuration_metric(FORWARDING_SUPPORTABILITY_FORMAT, FORWARDING_ENABLED_KEY)
249
257
  record_configuration_metric(DECORATING_SUPPORTABILITY_FORMAT, DECORATING_ENABLED_KEY)
258
+ record_configuration_metric(LABELS_SUPPORTABILITY_FORMAT, LABELS_ENABLED_KEY)
250
259
 
251
260
  add_custom_attributes(NewRelic::Agent.config[CUSTOM_ATTRIBUTES_KEY])
252
261
  end
@@ -327,6 +336,23 @@ module NewRelic
327
336
 
328
337
  Logger::Severity.const_get(severity_constant) < Logger::Severity.const_get(configured_log_level_constant)
329
338
  end
339
+
340
+ def create_labels
341
+ return NewRelic::EMPTY_HASH unless NewRelic::Agent.config[LABELS_ENABLED_KEY]
342
+
343
+ downcased_exclusions = NewRelic::Agent.config[:'application_logging.forwarding.labels.exclude'].map(&:downcase)
344
+ log_labels = {}
345
+
346
+ NewRelic::Agent.config.parsed_labels.each do |parsed_label|
347
+ next if downcased_exclusions.include?(parsed_label['label_type'].downcase)
348
+
349
+ # labels are referred to as tags in the UI, so prefix the
350
+ # label-related attributes with 'tags.*'
351
+ log_labels["tags.#{parsed_label['label_type']}"] = parsed_label['label_value']
352
+ end
353
+
354
+ log_labels
355
+ end
330
356
  end
331
357
  end
332
358
  end
@@ -117,7 +117,8 @@ module NewRelic
117
117
  queue_name: nil,
118
118
  exchange_type: nil,
119
119
  reply_to: nil,
120
- correlation_id: nil)
120
+ correlation_id: nil,
121
+ action: nil)
121
122
 
122
123
  state = Tracer.state
123
124
  return yield if state.current_transaction
@@ -125,12 +126,12 @@ module NewRelic
125
126
  txn = nil
126
127
 
127
128
  begin
128
- txn_name = transaction_name(library, destination_type, destination_name)
129
+ txn_name = transaction_name(library, destination_type, destination_name, action)
129
130
 
130
131
  txn = Tracer.start_transaction(name: txn_name, category: :message)
131
-
132
132
  if headers
133
- txn.distributed_tracer.consume_message_headers(headers, state, RABBITMQ_TRANSPORT_TYPE)
133
+ NewRelic::Agent::DistributedTracing::accept_distributed_trace_headers(headers, library) # to handle the new w3c headers
134
+ txn.distributed_tracer.consume_message_headers(headers, state, library) # to do the expected old things
134
135
  CrossAppTracing.reject_messaging_cat_headers(headers).each do |k, v|
135
136
  txn.add_agent_attribute(:"message.headers.#{k}", v, AttributeFilter::DST_NONE) unless v.nil?
136
137
  end
@@ -327,12 +328,17 @@ module NewRelic
327
328
  NewRelic::Agent.config[:'message_tracer.segment_parameters.enabled']
328
329
  end
329
330
 
330
- def transaction_name(library, destination_type, destination_name)
331
+ def transaction_name(library, destination_type, destination_name, action = nil)
331
332
  transaction_name = Transaction::MESSAGE_PREFIX + library
332
333
  transaction_name << NewRelic::SLASH
333
334
  transaction_name << Transaction::MessageBrokerSegment::TYPES[destination_type]
334
335
  transaction_name << NewRelic::SLASH
335
336
 
337
+ if action == :consume
338
+ transaction_name << 'Consume'
339
+ transaction_name << NewRelic::SLASH
340
+ end
341
+
336
342
  case destination_type
337
343
  when :queue
338
344
  transaction_name << Transaction::MessageBrokerSegment::NAMED
@@ -455,6 +455,8 @@ module NewRelic
455
455
  end
456
456
 
457
457
  def handle_error_response(response, endpoint)
458
+ NewRelic::Agent.agent.health_check.update_status(NewRelic::Agent::HealthCheck::HTTP_ERROR, [response.code, endpoint])
459
+
458
460
  case response
459
461
  when Net::HTTPRequestTimeOut,
460
462
  Net::HTTPTooManyRequests,
@@ -637,9 +639,13 @@ module NewRelic
637
639
  def send_request(opts)
638
640
  request = prep_request(opts)
639
641
  response = relay_request(request, opts)
640
- return response if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPAccepted)
641
642
 
642
- handle_error_response(response, opts[:endpoint])
643
+ if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPAccepted)
644
+ NewRelic::Agent.agent.health_check.update_status(NewRelic::Agent::HealthCheck::HEALTHY)
645
+ response
646
+ else
647
+ handle_error_response(response, opts[:endpoint])
648
+ end
643
649
  end
644
650
 
645
651
  def log_response(response)
@@ -4,13 +4,13 @@
4
4
 
5
5
  require 'json'
6
6
  require 'new_relic/base64'
7
+ require 'uri'
8
+
9
+ require_relative 'serverless_handler_event_sources'
7
10
 
8
11
  module NewRelic
9
12
  module Agent
10
13
  class ServerlessHandler
11
- ATTRIBUTE_ARN = 'aws.lambda.arn'
12
- ATTRIBUTE_COLD_START = 'aws.lambda.coldStart'
13
- ATTRIBUTE_REQUEST_ID = 'aws.requestId'
14
14
  AGENT_ATTRIBUTE_DESTINATIONS = NewRelic::Agent::AttributeFilter::DST_TRANSACTION_TRACER |
15
15
  NewRelic::Agent::AttributeFilter::DST_TRANSACTION_EVENTS
16
16
  EXECUTION_ENVIRONMENT = "AWS_Lambda_ruby#{RUBY_VERSION.rpartition('.').first}".freeze
@@ -22,12 +22,15 @@ module NewRelic
22
22
  SUPPORTABILITY_METRIC = 'Supportability/AWSLambda/HandlerInvocation'
23
23
  FUNCTION_NAME = 'lambda_function'
24
24
  PAYLOAD_VERSION = ENV.fetch('NEW_RELIC_SERVERLESS_PAYLOAD_VERSION', 2)
25
+ DIGIT = /\d/
26
+ EVENT_SOURCES = NewRelic::Agent::ServerlessHandlerEventSources.to_hash
25
27
 
26
28
  def self.env_var_set?
27
29
  ENV.key?(LAMBDA_ENVIRONMENT_VARIABLE)
28
30
  end
29
31
 
30
32
  def initialize
33
+ @event = nil
31
34
  @context = nil
32
35
  @payloads = {}
33
36
  end
@@ -35,12 +38,13 @@ module NewRelic
35
38
  def invoke_lambda_function_with_new_relic(event:, context:, method_name:, namespace: nil)
36
39
  NewRelic::Agent.increment_metric(SUPPORTABILITY_METRIC)
37
40
 
38
- @context = context
41
+ @event, @context = event, context
39
42
 
40
- NewRelic::Agent::Tracer.in_transaction(category: :other, name: function_name) do
41
- add_agent_attributes
43
+ NewRelic::Agent::Tracer.in_transaction(category: category, name: function_name) do
44
+ prep_transaction
42
45
 
43
- NewRelic::LanguageSupport.constantize(namespace).send(method_name, event: event, context: context)
46
+ process_response(NewRelic::LanguageSupport.constantize(namespace)
47
+ .send(method_name, event: event, context: context))
44
48
  end
45
49
  ensure
46
50
  harvest!
@@ -86,6 +90,12 @@ module NewRelic
86
90
 
87
91
  private
88
92
 
93
+ def prep_transaction
94
+ process_api_gateway_info
95
+ process_headers
96
+ add_agent_attributes
97
+ end
98
+
89
99
  def harvest!
90
100
  NewRelic::Agent.instance.harvest_and_send_analytic_event_data
91
101
  NewRelic::Agent.instance.harvest_and_send_custom_event_data
@@ -109,6 +119,11 @@ module NewRelic
109
119
  ENV.fetch(LAMBDA_ENVIRONMENT_VARIABLE, FUNCTION_NAME)
110
120
  end
111
121
 
122
+ def category
123
+ @category ||=
124
+ @event&.dig('requestContext', 'http', 'method') || @event&.fetch('httpMethod', nil) ? :web : :other
125
+ end
126
+
112
127
  def write_output
113
128
  string = PAYLOAD_VERSION == 1 ? payload_v1 : payload_v2
114
129
 
@@ -120,7 +135,7 @@ module NewRelic
120
135
  "BEGIN PAYLOAD>>>\n#{string}\n<<<END PAYLOAD"
121
136
  end
122
137
 
123
- def payload_v1
138
+ def payload_v1 # New Relic serverless payload v1
124
139
  payload_hash = {'metadata' => metadata, 'data' => @payloads}
125
140
  json = NewRelic::Agent.agent.service.marshaller.dump(payload_hash)
126
141
  gzipped = NewRelic::Agent::NewRelicService::Encoders::Compressed::Gzip.encode(json)
@@ -129,7 +144,7 @@ module NewRelic
129
144
  ::JSON.dump(array)
130
145
  end
131
146
 
132
- def payload_v2
147
+ def payload_v2 # New Relic serverless payload v2
133
148
  json = NewRelic::Agent.agent.service.marshaller.dump(@payloads)
134
149
  gzipped = NewRelic::Agent::NewRelicService::Encoders::Compressed::Gzip.encode(json)
135
150
  base64_encoded = NewRelic::Base64.strict_encode64(gzipped)
@@ -137,6 +152,88 @@ module NewRelic
137
152
  ::JSON.dump(array)
138
153
  end
139
154
 
155
+ def determine_api_gateway_version
156
+ return unless @event
157
+
158
+ version = @event.fetch('version', '')
159
+ if version.start_with?('2.')
160
+ return 2
161
+ elsif version.start_with?('1.')
162
+ return 1
163
+ end
164
+
165
+ headers = headers_from_event
166
+ return unless headers
167
+
168
+ if @event.dig('requestContext', 'http', 'path') && @event.dig('requestContext', 'http', 'method')
169
+ 2
170
+ elsif @event.fetch('path', nil) && @event.fetch('httpMethod', nil)
171
+ 1
172
+ end
173
+ end
174
+
175
+ def process_api_gateway_info
176
+ api_v = determine_api_gateway_version
177
+ return unless api_v
178
+
179
+ info = api_v == 2 ? info_for_api_gateway_v2 : info_for_api_gateway_v1
180
+ info[:query_parameters] = @event.fetch('queryStringParameters', nil)
181
+
182
+ @http_method = info[:method]
183
+ @http_uri = http_uri(info)
184
+ end
185
+
186
+ def http_uri(info)
187
+ return unless info[:host] && info[:path]
188
+
189
+ url_str = "https://#{info[:host]}"
190
+ url_str += ":#{info[:port]}" unless info[:host].match?(':')
191
+ url_str += "#{info[:path]}"
192
+
193
+ if info[:query_parameters]
194
+ qp = info[:query_parameters].map { |k, v| "#{k}=#{v}" }.join('&')
195
+ url_str += "?#{qp}"
196
+ end
197
+
198
+ URI.parse(url_str)
199
+ rescue StandardError => e
200
+ NewRelic::Agent.logger.error "ServerlessHandler failed to parse the source HTTP URI: #{e}"
201
+ end
202
+
203
+ def info_for_api_gateway_v2
204
+ ctx = @event.fetch('requestContext', nil)
205
+ return {} unless ctx
206
+
207
+ {method: ctx.dig('http', 'method'),
208
+ path: ctx.dig('http', 'path'),
209
+ host: ctx.fetch('domainName', @event.dig('headers', 'Host')),
210
+ port: @event.dig('headers', 'X-Forwarded-Port') || 443}
211
+ end
212
+
213
+ def info_for_api_gateway_v1
214
+ headers = headers_from_event
215
+ {method: @event.fetch('httpMethod', nil),
216
+ path: @event.fetch('path', nil),
217
+ host: headers.fetch('Host', nil),
218
+ port: headers.fetch('X-Forwarded-Port', 443)}
219
+ end
220
+
221
+ def process_headers
222
+ return unless ::NewRelic::Agent.config[:'distributed_tracing.enabled']
223
+
224
+ headers = headers_from_event
225
+ return unless headers && !headers.empty?
226
+
227
+ dt_headers = headers.fetch(NewRelic::NEWRELIC_KEY, nil)
228
+ return unless dt_headers
229
+
230
+ ::NewRelic::Agent::DistributedTracing::accept_distributed_trace_headers(dt_headers, 'Other')
231
+ end
232
+
233
+ def headers_from_event
234
+ @headers ||= @event&.dig('requestContext', 'http') || @event&.dig('headers')
235
+ end
236
+
140
237
  def use_named_pipe?
141
238
  return @use_named_pipe if defined?(@use_named_pipe)
142
239
 
@@ -146,15 +243,142 @@ module NewRelic
146
243
  def add_agent_attributes
147
244
  return unless NewRelic::Agent::Tracer.current_transaction
148
245
 
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)
246
+ add_agent_attribute('aws.lambda.coldStart', true) if cold?
247
+ add_agent_attribute('aws.lambda.arn', @context.invoked_function_arn)
248
+ add_agent_attribute('aws.requestId', @context.aws_request_id)
249
+
250
+ add_event_source_attributes
251
+ add_http_attributes if api_gateway_event?
252
+ end
253
+
254
+ def add_http_attributes
255
+ return unless category == :web
256
+
257
+ if @http_uri
258
+ add_agent_attribute('uri.host', @http_uri.host)
259
+ add_agent_attribute('uri.port', @http_uri.port)
260
+ if NewRelic::Agent.instance.attribute_filter.allows_key?('http.url', AttributeFilter::DST_SPAN_EVENTS)
261
+ add_agent_attribute('http.url', @http_uri.to_s)
262
+ end
263
+ end
264
+
265
+ if @http_method
266
+ add_agent_attribute('http.method', @http_method)
267
+ add_agent_attribute('http.request.method', @http_method)
268
+ end
269
+ end
270
+
271
+ def api_gateway_event?
272
+ return false unless @event
273
+
274
+ # '1.0' for API Gateway V1, '2.0' for API Gateway V2
275
+ return true if @event.fetch('version', '').start_with?(DIGIT)
276
+
277
+ return false unless headers_from_event
278
+
279
+ # API Gateway V1 - look for toplevel 'path' and 'httpMethod' keys if a version is unset
280
+ return true if @event.fetch('path', nil) && @event.fetch('httpMethod', nil)
281
+
282
+ # API Gateway V2 - look for 'requestContext/http' inner nested 'path' and 'method' keys if a version is unset
283
+ return true if @event.dig('requestContext', 'http', 'path') && @event.dig('requestContext', 'http', 'method')
284
+
285
+ false
286
+ end
287
+
288
+ def add_event_source_attributes
289
+ arn = event_source_arn
290
+ add_agent_attribute('aws.lambda.eventSource.arn', arn) if arn
291
+
292
+ info = event_source_event_info
293
+ return unless info
294
+
295
+ add_agent_attribute('aws.lambda.eventSource.eventType', info['name'])
296
+
297
+ info['attributes'].each do |name, elements|
298
+ next if elements.empty?
299
+
300
+ size = false
301
+ if elements.last.eql?('#size')
302
+ elements = elements.dup
303
+ elements.pop
304
+ size = true
305
+ end
306
+ value = @event.dig(*elements)
307
+ value = value.size if size
308
+ next unless value
309
+
310
+ add_agent_attribute(name, value)
311
+ end
312
+ end
313
+
314
+ def event_source_arn
315
+ return unless @event
316
+
317
+ # SQS/Kinesis Stream/DynamoDB/CodeCommit/S3/SNS
318
+ return event_source_arn_for_records if @event.fetch('Records', nil)
319
+
320
+ # Kinesis Firehose
321
+ ds_arn = @event.fetch('deliveryStreamArn', nil) if @event.fetch('records', nil)
322
+ return ds_arn if ds_arn
323
+
324
+ # ELB
325
+ elb_arn = @event.dig('requestContext', 'elb', 'targetGroupArn')
326
+ return elb_arn if elb_arn
327
+
328
+ # (other)
329
+ es_arn = @event.dig('resources', 0)
330
+ return es_arn if es_arn
331
+
332
+ NewRelic::Agent.logger.debug 'Unable to determine an event source arn'
333
+
334
+ nil
335
+ end
336
+
337
+ def event_source_event_info
338
+ return unless @event
339
+
340
+ # if every required key for a source is found, consider that source
341
+ # to be a match
342
+ EVENT_SOURCES.each_value do |info|
343
+ return info unless info['required_keys'].detect { |r| @event.dig(*r).nil? }
344
+ end
345
+
346
+ nil
347
+ end
348
+
349
+ def event_source_arn_for_records
350
+ record = @event['Records'].first
351
+ unless record
352
+ NewRelic::Agent.logger.debug "Unable to find any records in the event's 'Records' array"
353
+ return
354
+ end
355
+
356
+ arn = record.fetch('eventSourceARN', nil) || # SQS/Kinesis Stream/DynamoDB/CodeCommit
357
+ record.dig('s3', 'bucket', 'arn') || # S3
358
+ record.fetch('EventSubscriptionArn', nil) # SNS
359
+
360
+ unless arn
361
+ NewRelic::Agent.logger.debug "Unable to determine an event source arn from the event's 'Records' array"
362
+ end
363
+
364
+ arn
152
365
  end
153
366
 
154
367
  def add_agent_attribute(attribute, value)
155
368
  NewRelic::Agent::Tracer.current_transaction.add_agent_attribute(attribute, value, AGENT_ATTRIBUTE_DESTINATIONS)
156
369
  end
157
370
 
371
+ def process_response(response)
372
+ return response unless category == :web && response.respond_to?(:fetch)
373
+
374
+ http_status = response.fetch(:statusCode, response.fetch('statusCode', nil))
375
+ return unless http_status
376
+
377
+ add_agent_attribute('http.statusCode', http_status)
378
+
379
+ response
380
+ end
381
+
158
382
  def cold?
159
383
  return @cold if defined?(@cold)
160
384
 
@@ -163,7 +387,12 @@ module NewRelic
163
387
  end
164
388
 
165
389
  def reset!
390
+ @event = nil
391
+ @category = nil
166
392
  @context = nil
393
+ @headers = nil
394
+ @http_method = nil
395
+ @http_uri = nil
167
396
  @payloads.replace({})
168
397
  end
169
398
  end
@@ -0,0 +1,155 @@
1
+ {
2
+ "alb": {
3
+ "attributes": {},
4
+ "name": "alb",
5
+ "required_keys": [
6
+ "httpMethod",
7
+ "requestContext.elb"
8
+ ]
9
+ },
10
+
11
+ "apiGateway": {
12
+ "attributes": {
13
+ "aws.lambda.eventSource.accountId": "requestContext.accountId",
14
+ "aws.lambda.eventSource.apiId": "requestContext.apiId",
15
+ "aws.lambda.eventSource.resourceId": "requestContext.resourceId",
16
+ "aws.lambda.eventSource.resourcePath": "requestContext.resourcePath",
17
+ "aws.lambda.eventSource.stage": "requestContext.stage"
18
+ },
19
+ "name": "apiGateway",
20
+ "required_keys": [
21
+ "headers",
22
+ "httpMethod",
23
+ "path",
24
+ "requestContext",
25
+ "requestContext.stage"
26
+ ]
27
+ },
28
+
29
+ "apiGatewayV2": {
30
+ "attributes": {
31
+ "aws.lambda.eventSource.accountId": "requestContext.accountId",
32
+ "aws.lambda.eventSource.apiId": "requestContext.apiId",
33
+ "aws.lambda.eventSource.stage": "requestContext.stage"
34
+ },
35
+ "name": "apiGatewayV2",
36
+ "required_keys": [
37
+ "version",
38
+ "headers",
39
+ "requestContext.http",
40
+ "requestContext.http.path",
41
+ "requestContext.http.method",
42
+ "requestContext.stage"
43
+ ]
44
+ },
45
+
46
+ "cloudFront": {
47
+ "attributes": {},
48
+ "name": "cloudFront",
49
+ "required_keys": [
50
+ "Records[0].cf"
51
+ ]
52
+ },
53
+
54
+ "cloudWatchScheduled": {
55
+ "attributes": {
56
+ "aws.lambda.eventSource.account": "account",
57
+ "aws.lambda.eventSource.id": "id",
58
+ "aws.lambda.eventSource.region": "region",
59
+ "aws.lambda.eventSource.resource": "resources[0]",
60
+ "aws.lambda.eventSource.time": "time"
61
+ },
62
+ "name": "cloudWatch_scheduled",
63
+ "required_keys": [
64
+ "detail-type",
65
+ "source"
66
+ ]
67
+ },
68
+
69
+ "dynamoStreams": {
70
+ "attributes": {
71
+ "aws.lambda.eventSource.length": "Records.length"
72
+ },
73
+ "name": "dynamo_streams",
74
+ "required_keys": [
75
+ "Records[0].dynamodb"
76
+ ]
77
+ },
78
+
79
+ "firehose": {
80
+ "attributes": {
81
+ "aws.lambda.eventSource.length": "records.length",
82
+ "aws.lambda.eventSource.region": "region"
83
+ },
84
+ "name": "firehose",
85
+ "required_keys": [
86
+ "deliveryStreamArn",
87
+ "records[0].kinesisRecordMetadata"
88
+ ]
89
+ },
90
+
91
+ "kinesis": {
92
+ "attributes": {
93
+ "aws.lambda.eventSource.length": "Records.length",
94
+ "aws.lambda.eventSource.region": "Records[0].awsRegion"
95
+ },
96
+ "name": "kinesis",
97
+ "required_keys": [
98
+ "Records[0].kinesis"
99
+ ]
100
+ },
101
+
102
+ "s3": {
103
+ "attributes": {
104
+ "aws.lambda.eventSource.bucketName": "Records[0].s3.bucket.name",
105
+ "aws.lambda.eventSource.eventName": "Records[0].eventName",
106
+ "aws.lambda.eventSource.eventTime": "Records[0].eventTime",
107
+ "aws.lambda.eventSource.length": "Records.length",
108
+ "aws.lambda.eventSource.objectKey": "Records[0].s3.object.key",
109
+ "aws.lambda.eventSource.objectSequencer": "Records[0].s3.object.sequencer",
110
+ "aws.lambda.eventSource.objectSize": "Records[0].s3.object.size",
111
+ "aws.lambda.eventSource.region": "Records[0].awsRegion"
112
+ },
113
+ "name": "s3",
114
+ "required_keys": [
115
+ "Records[0].s3"
116
+ ]
117
+ },
118
+
119
+ "ses": {
120
+ "attributes": {
121
+ "aws.lambda.eventSource.date": "Records[0].ses.mail.commonHeaders.date",
122
+ "aws.lambda.eventSource.length": "Records.length",
123
+ "aws.lambda.eventSource.messageId": "Records[0].ses.mail.commonHeaders.messageId",
124
+ "aws.lambda.eventSource.returnPath": "Records[0].ses.mail.commonHeaders.returnPath"
125
+ },
126
+ "name": "ses",
127
+ "required_keys": [
128
+ "Records[0].ses"
129
+ ]
130
+ },
131
+
132
+ "sns": {
133
+ "attributes": {
134
+ "aws.lambda.eventSource.length": "Records.length",
135
+ "aws.lambda.eventSource.messageId": "Records[0].Sns.MessageId",
136
+ "aws.lambda.eventSource.timestamp": "Records[0].Sns.Timestamp",
137
+ "aws.lambda.eventSource.topicArn": "Records[0].Sns.TopicArn",
138
+ "aws.lambda.eventSource.type": "Records[0].Sns.Type"
139
+ },
140
+ "name": "sns",
141
+ "required_keys": [
142
+ "Records[0].Sns"
143
+ ]
144
+ },
145
+
146
+ "sqs": {
147
+ "attributes": {
148
+ "aws.lambda.eventSource.length": "Records.length"
149
+ },
150
+ "name": "sqs",
151
+ "required_keys": [
152
+ "Records[0].receiptHandle"
153
+ ]
154
+ }
155
+ }
@@ -0,0 +1,49 @@
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
+
7
+ module NewRelic
8
+ module Agent
9
+ # ServerlessHandlerEventSources - New Relic's language agent devs maintain
10
+ # a cross-agent JSON map of all AWS resources with the potential to invoke
11
+ # an AWS Lambda function by issuing it an event. This map is used to glean
12
+ # source specific attributes while instrumenting the function's invocation.
13
+ #
14
+ # Given that the event arrives as a Ruby hash argument to the AWS Lambda
15
+ # function, the JSON map's values need to be converted into arrays that can
16
+ # be passed to `Hash#dig`. So a value such as `'records[0].name'` needs to
17
+ # be converted to `['records', 0, 'name']`. This class's `.to_hash` method
18
+ # yields the converted data.
19
+ #
20
+ # Furthermore, `.length` calls are converted to Ruby `#size` notation to
21
+ # denote that a method call must be performed on the dug value.
22
+ class ServerlessHandlerEventSources
23
+ JSON_SOURCE = File.join(File.dirname(__FILE__), 'serverless_handler_event_sources.json').freeze
24
+ JSON_RAW = JSON.parse(File.read(JSON_SOURCE)).freeze
25
+
26
+ def self.to_hash
27
+ JSON_RAW.each_with_object({}) do |(type, info), hash|
28
+ hash[type] = {'attributes' => {},
29
+ 'name' => info['name'],
30
+ 'required_keys' => []}
31
+ info['attributes'].each { |attr, value| hash[type]['attributes'][attr] = transform(value) }
32
+ info['required_keys'].each { |key| hash[type]['required_keys'].push(transform(key)) }
33
+ end.freeze
34
+ end
35
+
36
+ def self.transform(value)
37
+ value.gsub(/\[(\d+)\]/, '.\1').split('.').map do |e|
38
+ if e.match?(/^\d+$/)
39
+ e.to_i
40
+ elsif e == 'length'
41
+ '#size'
42
+ else
43
+ e
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end