newrelic_rpm 9.7.0 → 9.16.1

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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +376 -2
  3. data/README.md +17 -18
  4. data/Rakefile +1 -1
  5. data/lib/boot/strap.rb +101 -0
  6. data/lib/new_relic/agent/agent.rb +4 -1
  7. data/lib/new_relic/agent/agent_helpers/connect.rb +10 -8
  8. data/lib/new_relic/agent/agent_helpers/start_worker_thread.rb +1 -1
  9. data/lib/new_relic/agent/agent_helpers/startup.rb +2 -1
  10. data/lib/new_relic/agent/agent_logger.rb +3 -1
  11. data/lib/new_relic/agent/aws.rb +68 -0
  12. data/lib/new_relic/agent/configuration/default_source.rb +519 -23
  13. data/lib/new_relic/agent/configuration/environment_source.rb +14 -2
  14. data/lib/new_relic/agent/configuration/high_security_source.rb +1 -0
  15. data/lib/new_relic/agent/configuration/manager.rb +51 -8
  16. data/lib/new_relic/agent/configuration/security_policy_source.rb +11 -0
  17. data/lib/new_relic/agent/configuration/yaml_source.rb +2 -0
  18. data/lib/new_relic/agent/connect/request_builder.rb +1 -1
  19. data/lib/new_relic/agent/custom_event_aggregator.rb +27 -1
  20. data/lib/new_relic/agent/database/obfuscation_helpers.rb +11 -11
  21. data/lib/new_relic/agent/database/obfuscator.rb +1 -0
  22. data/lib/new_relic/agent/database.rb +39 -0
  23. data/lib/new_relic/agent/distributed_tracing/distributed_trace_payload.rb +1 -5
  24. data/lib/new_relic/agent/error_collector.rb +39 -10
  25. data/lib/new_relic/agent/harvester.rb +1 -1
  26. data/lib/new_relic/agent/instrumentation/active_merchant.rb +0 -13
  27. data/lib/new_relic/agent/instrumentation/active_record.rb +1 -8
  28. data/lib/new_relic/agent/instrumentation/active_record_helper.rb +3 -0
  29. data/lib/new_relic/agent/instrumentation/active_record_subscriber.rb +1 -12
  30. data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger/instrumentation.rb +7 -3
  31. data/lib/new_relic/agent/instrumentation/active_support_broadcast_logger.rb +0 -2
  32. data/lib/new_relic/agent/instrumentation/active_support_logger.rb +0 -2
  33. data/lib/new_relic/agent/instrumentation/async_http.rb +4 -3
  34. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda/chain.rb +33 -0
  35. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda/instrumentation.rb +93 -0
  36. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda/prepend.rb +23 -0
  37. data/lib/new_relic/agent/instrumentation/aws_sdk_lambda.rb +23 -0
  38. data/lib/new_relic/agent/instrumentation/aws_sqs/chain.rb +37 -0
  39. data/lib/new_relic/agent/instrumentation/aws_sqs/instrumentation.rb +67 -0
  40. data/lib/new_relic/agent/instrumentation/aws_sqs/prepend.rb +21 -0
  41. data/lib/new_relic/agent/instrumentation/aws_sqs.rb +23 -0
  42. data/lib/new_relic/agent/instrumentation/bunny/instrumentation.rb +14 -0
  43. data/lib/new_relic/agent/instrumentation/bunny.rb +3 -4
  44. data/lib/new_relic/agent/instrumentation/concurrent_ruby.rb +1 -2
  45. data/lib/new_relic/agent/instrumentation/curb.rb +3 -4
  46. data/lib/new_relic/agent/instrumentation/delayed_job_instrumentation.rb +0 -23
  47. data/lib/new_relic/agent/instrumentation/dynamodb/chain.rb +27 -0
  48. data/lib/new_relic/agent/instrumentation/dynamodb/instrumentation.rb +64 -0
  49. data/lib/new_relic/agent/instrumentation/dynamodb/prepend.rb +19 -0
  50. data/lib/new_relic/agent/instrumentation/dynamodb.rb +23 -0
  51. data/lib/new_relic/agent/instrumentation/elasticsearch/instrumentation.rb +58 -8
  52. data/lib/new_relic/agent/instrumentation/elasticsearch.rb +0 -2
  53. data/lib/new_relic/agent/instrumentation/ethon.rb +0 -4
  54. data/lib/new_relic/agent/instrumentation/excon.rb +0 -16
  55. data/lib/new_relic/agent/instrumentation/fiber.rb +0 -2
  56. data/lib/new_relic/agent/instrumentation/grape.rb +1 -1
  57. data/lib/new_relic/agent/instrumentation/grpc/client/instrumentation.rb +0 -1
  58. data/lib/new_relic/agent/instrumentation/grpc_server.rb +1 -1
  59. data/lib/new_relic/agent/instrumentation/httpclient.rb +0 -1
  60. data/lib/new_relic/agent/instrumentation/httprb.rb +0 -1
  61. data/lib/new_relic/agent/instrumentation/httpx.rb +0 -4
  62. data/lib/new_relic/agent/instrumentation/logger.rb +1 -3
  63. data/lib/new_relic/agent/instrumentation/logstasher/chain.rb +21 -0
  64. data/lib/new_relic/agent/instrumentation/logstasher/instrumentation.rb +24 -0
  65. data/lib/new_relic/agent/instrumentation/logstasher/prepend.rb +13 -0
  66. data/lib/new_relic/agent/instrumentation/logstasher.rb +25 -0
  67. data/lib/new_relic/agent/instrumentation/memcache.rb +0 -1
  68. data/lib/new_relic/agent/instrumentation/net_http/instrumentation.rb +6 -0
  69. data/lib/new_relic/agent/instrumentation/opensearch/chain.rb +21 -0
  70. data/lib/new_relic/agent/instrumentation/opensearch/instrumentation.rb +66 -0
  71. data/lib/new_relic/agent/instrumentation/opensearch/prepend.rb +13 -0
  72. data/lib/new_relic/agent/instrumentation/opensearch.rb +23 -0
  73. data/lib/new_relic/agent/instrumentation/padrino.rb +3 -3
  74. data/lib/new_relic/agent/instrumentation/rack/instrumentation.rb +3 -0
  75. data/lib/new_relic/agent/instrumentation/rails_notifications/action_controller.rb +9 -5
  76. data/lib/new_relic/agent/instrumentation/rake.rb +0 -1
  77. data/lib/new_relic/agent/instrumentation/rdkafka/chain.rb +72 -0
  78. data/lib/new_relic/agent/instrumentation/rdkafka/instrumentation.rb +70 -0
  79. data/lib/new_relic/agent/instrumentation/rdkafka/prepend.rb +67 -0
  80. data/lib/new_relic/agent/instrumentation/rdkafka.rb +25 -0
  81. data/lib/new_relic/agent/instrumentation/redis/cluster_middleware.rb +26 -0
  82. data/lib/new_relic/agent/instrumentation/redis/instrumentation.rb +14 -11
  83. data/lib/new_relic/agent/instrumentation/redis/middleware.rb +3 -0
  84. data/lib/new_relic/agent/instrumentation/redis.rb +11 -5
  85. data/lib/new_relic/agent/instrumentation/resque.rb +0 -4
  86. data/lib/new_relic/agent/instrumentation/roda.rb +4 -4
  87. data/lib/new_relic/agent/instrumentation/ruby_kafka/chain.rb +55 -0
  88. data/lib/new_relic/agent/instrumentation/ruby_kafka/instrumentation.rb +67 -0
  89. data/lib/new_relic/agent/instrumentation/ruby_kafka/prepend.rb +60 -0
  90. data/lib/new_relic/agent/instrumentation/ruby_kafka.rb +25 -0
  91. data/lib/new_relic/agent/instrumentation/ruby_openai/chain.rb +36 -0
  92. data/lib/new_relic/agent/instrumentation/ruby_openai/instrumentation.rb +196 -0
  93. data/lib/new_relic/agent/instrumentation/ruby_openai/prepend.rb +20 -0
  94. data/lib/new_relic/agent/instrumentation/ruby_openai.rb +35 -0
  95. data/lib/new_relic/agent/instrumentation/sidekiq.rb +0 -14
  96. data/lib/new_relic/agent/instrumentation/sinatra.rb +3 -19
  97. data/lib/new_relic/agent/instrumentation/stripe_subscriber.rb +22 -1
  98. data/lib/new_relic/agent/instrumentation/thread.rb +0 -2
  99. data/lib/new_relic/agent/instrumentation/tilt.rb +0 -4
  100. data/lib/new_relic/agent/instrumentation/typhoeus.rb +0 -1
  101. data/lib/new_relic/agent/instrumentation/view_component/instrumentation.rb +13 -6
  102. data/lib/new_relic/agent/instrumentation/view_component.rb +0 -2
  103. data/lib/new_relic/agent/javascript_instrumentor.rb +2 -3
  104. data/lib/new_relic/agent/llm/chat_completion_message.rb +25 -0
  105. data/lib/new_relic/agent/llm/chat_completion_summary.rb +66 -0
  106. data/lib/new_relic/agent/llm/embedding.rb +60 -0
  107. data/lib/new_relic/agent/llm/llm_event.rb +95 -0
  108. data/lib/new_relic/agent/llm/response_headers.rb +80 -0
  109. data/lib/new_relic/agent/llm.rb +49 -0
  110. data/lib/new_relic/agent/local_log_decorator.rb +8 -1
  111. data/lib/new_relic/agent/log_event_aggregator.rb +120 -44
  112. data/lib/new_relic/agent/messaging.rb +11 -5
  113. data/lib/new_relic/agent/new_relic_service.rb +12 -2
  114. data/lib/new_relic/agent/serverless_handler.rb +400 -0
  115. data/lib/new_relic/agent/serverless_handler_event_sources.json +155 -0
  116. data/lib/new_relic/agent/serverless_handler_event_sources.rb +49 -0
  117. data/lib/new_relic/agent/span_event_primitive.rb +8 -10
  118. data/lib/new_relic/agent/system_info.rb +14 -0
  119. data/lib/new_relic/agent/threading/agent_thread.rb +1 -2
  120. data/lib/new_relic/agent/tracer.rb +5 -5
  121. data/lib/new_relic/agent/transaction/abstract_segment.rb +1 -1
  122. data/lib/new_relic/agent/transaction/external_request_segment.rb +0 -10
  123. data/lib/new_relic/agent/transaction/request_attributes.rb +13 -1
  124. data/lib/new_relic/agent/transaction/trace_context.rb +1 -1
  125. data/lib/new_relic/agent/transaction/tracing.rb +2 -2
  126. data/lib/new_relic/agent/transaction.rb +2 -6
  127. data/lib/new_relic/agent/transaction_error_primitive.rb +23 -19
  128. data/lib/new_relic/agent.rb +198 -10
  129. data/lib/new_relic/constants.rb +2 -0
  130. data/lib/new_relic/control/frameworks/grape.rb +14 -0
  131. data/lib/new_relic/control/frameworks/padrino.rb +14 -0
  132. data/lib/new_relic/control/frameworks/rails4.rb +1 -3
  133. data/lib/new_relic/control/instance_methods.rb +8 -0
  134. data/lib/new_relic/control/private_instance_methods.rb +4 -0
  135. data/lib/new_relic/control/security_interface.rb +57 -0
  136. data/lib/new_relic/control.rb +1 -1
  137. data/lib/new_relic/dependency_detection.rb +10 -5
  138. data/lib/new_relic/environment_report.rb +2 -2
  139. data/lib/new_relic/helper.rb +15 -0
  140. data/lib/new_relic/language_support.rb +3 -1
  141. data/lib/new_relic/local_environment.rb +14 -10
  142. data/lib/new_relic/rack/browser_monitoring.rb +28 -12
  143. data/lib/new_relic/supportability_helper.rb +2 -0
  144. data/lib/new_relic/thread_local_storage.rb +31 -0
  145. data/lib/new_relic/version.rb +2 -2
  146. data/lib/sequel/extensions/new_relic_instrumentation.rb +3 -2
  147. data/lib/tasks/config.rake +8 -3
  148. data/lib/tasks/gha.rake +31 -0
  149. data/lib/tasks/helpers/config.html.erb +3 -2
  150. data/lib/tasks/helpers/format.rb +1 -1
  151. data/lib/tasks/helpers/newrelicyml.rb +76 -13
  152. data/lib/tasks/instrumentation_generator/instrumentation.thor +31 -22
  153. data/lib/tasks/instrumentation_generator/templates/chain.tt +0 -1
  154. data/lib/tasks/instrumentation_generator/templates/chain_method.tt +0 -1
  155. data/lib/tasks/instrumentation_generator/templates/dependency_detection.tt +11 -8
  156. data/lib/tasks/instrumentation_generator/templates/newrelic.yml.tt +1 -1
  157. data/newrelic.yml +387 -143
  158. data/newrelic_rpm.gemspec +2 -0
  159. data/test/agent_helper.rb +17 -2
  160. metadata +80 -3
@@ -0,0 +1,400 @@
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
+ require 'uri'
8
+
9
+ require_relative 'serverless_handler_event_sources'
10
+
11
+ module NewRelic
12
+ module Agent
13
+ class ServerlessHandler
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
+ DIGIT = /\d/
26
+ EVENT_SOURCES = NewRelic::Agent::ServerlessHandlerEventSources.to_hash
27
+
28
+ def self.env_var_set?
29
+ ENV.key?(LAMBDA_ENVIRONMENT_VARIABLE)
30
+ end
31
+
32
+ def initialize
33
+ @event = nil
34
+ @context = nil
35
+ @payloads = {}
36
+ end
37
+
38
+ def invoke_lambda_function_with_new_relic(event:, context:, method_name:, namespace: nil)
39
+ NewRelic::Agent.increment_metric(SUPPORTABILITY_METRIC)
40
+
41
+ @event, @context = event, context
42
+
43
+ NewRelic::Agent::Tracer.in_transaction(category: category, name: function_name) do
44
+ prep_transaction
45
+
46
+ process_response(NewRelic::LanguageSupport.constantize(namespace)
47
+ .send(method_name, event: event, context: context))
48
+ end
49
+ ensure
50
+ harvest!
51
+ write_output
52
+ reset!
53
+ end
54
+
55
+ def store_payload(method, payload)
56
+ return if METHOD_BLOCKLIST.include?(method)
57
+
58
+ @payloads[method] = payload
59
+ end
60
+
61
+ def metric_data(stats_hash)
62
+ payload = [nil,
63
+ stats_hash.started_at,
64
+ (stats_hash.harvested_at || Process.clock_gettime(Process::CLOCK_REALTIME)),
65
+ []]
66
+ stats_hash.each do |metric_spec, stats|
67
+ next if stats.is_reset?
68
+
69
+ hash = {name: metric_spec.name}
70
+ hash[:scope] = metric_spec.scope unless metric_spec.scope.empty?
71
+
72
+ payload.last.push([hash, [
73
+ stats.call_count,
74
+ stats.total_call_time,
75
+ stats.total_exclusive_time,
76
+ stats.min_call_time,
77
+ stats.max_call_time,
78
+ stats.sum_of_squares
79
+ ]])
80
+ end
81
+
82
+ return if payload.last.empty?
83
+
84
+ store_payload(:metric_data, payload)
85
+ end
86
+
87
+ def error_data(errors)
88
+ store_payload(:error_data, [nil, errors.map(&:to_collector_array)])
89
+ end
90
+
91
+ private
92
+
93
+ def prep_transaction
94
+ process_api_gateway_info
95
+ process_headers
96
+ add_agent_attributes
97
+ end
98
+
99
+ def harvest!
100
+ NewRelic::Agent.instance.harvest_and_send_analytic_event_data
101
+ NewRelic::Agent.instance.harvest_and_send_custom_event_data
102
+ NewRelic::Agent.instance.harvest_and_send_data_types
103
+ end
104
+
105
+ def metadata
106
+ m = {arn: @context.invoked_function_arn,
107
+ protocol_version: NewRelic::Agent::NewRelicService::PROTOCOL_VERSION,
108
+ function_version: @context.function_version,
109
+ execution_environment: EXECUTION_ENVIRONMENT,
110
+ agent_version: NewRelic::VERSION::STRING}
111
+ if PAYLOAD_VERSION >= 2
112
+ m[:metadata_version] = PAYLOAD_VERSION
113
+ m[:agent_language] = NewRelic::LANGUAGE
114
+ end
115
+ m
116
+ end
117
+
118
+ def function_name
119
+ ENV.fetch(LAMBDA_ENVIRONMENT_VARIABLE, FUNCTION_NAME)
120
+ end
121
+
122
+ def category
123
+ @category ||=
124
+ @event&.dig('requestContext', 'http', 'method') || @event&.fetch('httpMethod', nil) ? :web : :other
125
+ end
126
+
127
+ def write_output
128
+ string = PAYLOAD_VERSION == 1 ? payload_v1 : payload_v2
129
+
130
+ return puts string unless use_named_pipe?
131
+
132
+ File.write(NAMED_PIPE, string)
133
+
134
+ NewRelic::Agent.logger.debug "Wrote serverless payload to #{NAMED_PIPE}\n" \
135
+ "BEGIN PAYLOAD>>>\n#{string}\n<<<END PAYLOAD"
136
+ end
137
+
138
+ def payload_v1 # New Relic serverless payload v1
139
+ payload_hash = {'metadata' => metadata, 'data' => @payloads}
140
+ json = NewRelic::Agent.agent.service.marshaller.dump(payload_hash)
141
+ gzipped = NewRelic::Agent::NewRelicService::Encoders::Compressed::Gzip.encode(json)
142
+ base64_encoded = NewRelic::Base64.strict_encode64(gzipped)
143
+ array = [PAYLOAD_VERSION, LAMBDA_MARKER, base64_encoded]
144
+ ::JSON.dump(array)
145
+ end
146
+
147
+ def payload_v2 # New Relic serverless payload v2
148
+ json = NewRelic::Agent.agent.service.marshaller.dump(@payloads)
149
+ gzipped = NewRelic::Agent::NewRelicService::Encoders::Compressed::Gzip.encode(json)
150
+ base64_encoded = NewRelic::Base64.strict_encode64(gzipped)
151
+ array = [PAYLOAD_VERSION, LAMBDA_MARKER, metadata, base64_encoded]
152
+ ::JSON.dump(array)
153
+ end
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
+
237
+ def use_named_pipe?
238
+ return @use_named_pipe if defined?(@use_named_pipe)
239
+
240
+ @use_named_pipe = File.exist?(NAMED_PIPE) && File.writable?(NAMED_PIPE)
241
+ end
242
+
243
+ def add_agent_attributes
244
+ return unless NewRelic::Agent::Tracer.current_transaction
245
+
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
365
+ end
366
+
367
+ def add_agent_attribute(attribute, value)
368
+ NewRelic::Agent::Tracer.current_transaction.add_agent_attribute(attribute, value, AGENT_ATTRIBUTE_DESTINATIONS)
369
+ end
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
+
382
+ def cold?
383
+ return @cold if defined?(@cold)
384
+
385
+ @cold = false
386
+ true
387
+ end
388
+
389
+ def reset!
390
+ @event = nil
391
+ @category = nil
392
+ @context = nil
393
+ @headers = nil
394
+ @http_method = nil
395
+ @http_uri = nil
396
+ @payloads.replace({})
397
+ end
398
+ end
399
+ end
400
+ 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
@@ -52,6 +52,8 @@ module NewRelic
52
52
  DATASTORE_CATEGORY = 'datastore'
53
53
  CLIENT = 'client'
54
54
 
55
+ DB_STATEMENT_MAX_BYTES = 4096
56
+
55
57
  # Builds a Hash of error attributes as well as the Span ID when
56
58
  # an error is present. Otherwise, returns nil when no error present.
57
59
  def error_attributes(segment)
@@ -79,17 +81,13 @@ module NewRelic
79
81
  intrinsics[SPAN_KIND_KEY] = CLIENT
80
82
  intrinsics[SERVER_ADDRESS_KEY] = segment.uri.host
81
83
  intrinsics[SERVER_PORT_KEY] = segment.uri.port
82
- agent_attributes = error_attributes(segment) || {}
84
+ agent_attributes = {}
83
85
 
84
86
  if allowed?(HTTP_URL_KEY)
85
87
  agent_attributes[HTTP_URL_KEY] = truncate(segment.uri)
86
88
  end
87
89
 
88
- if segment.respond_to?(:record_agent_attributes?) && segment.record_agent_attributes?
89
- agent_attributes.merge!(agent_attributes(segment))
90
- end
91
-
92
- [intrinsics, custom_attributes(segment), agent_attributes]
90
+ [intrinsics, custom_attributes(segment), agent_attributes.merge(agent_attributes(segment))]
93
91
  end
94
92
 
95
93
  def for_datastore_segment(segment) # rubocop:disable Metrics/AbcSize
@@ -99,7 +97,7 @@ module NewRelic
99
97
  intrinsics[SPAN_KIND_KEY] = CLIENT
100
98
  intrinsics[CATEGORY_KEY] = DATASTORE_CATEGORY
101
99
 
102
- agent_attributes = error_attributes(segment) || {}
100
+ agent_attributes = {}
103
101
 
104
102
  if segment.database_name && allowed?(DB_INSTANCE_KEY)
105
103
  agent_attributes[DB_INSTANCE_KEY] = truncate(segment.database_name)
@@ -118,12 +116,12 @@ module NewRelic
118
116
  agent_attributes[DB_SYSTEM_KEY] = segment.product if allowed?(DB_SYSTEM_KEY)
119
117
 
120
118
  if segment.sql_statement && allowed?(DB_STATEMENT_KEY)
121
- agent_attributes[DB_STATEMENT_KEY] = truncate(segment.sql_statement.safe_sql, 2000)
119
+ agent_attributes[DB_STATEMENT_KEY] = truncate(segment.sql_statement.safe_sql, DB_STATEMENT_MAX_BYTES)
122
120
  elsif segment.nosql_statement && allowed?(DB_STATEMENT_KEY)
123
- agent_attributes[DB_STATEMENT_KEY] = truncate(segment.nosql_statement, 2000)
121
+ agent_attributes[DB_STATEMENT_KEY] = truncate(segment.nosql_statement, DB_STATEMENT_MAX_BYTES)
124
122
  end
125
123
 
126
- [intrinsics, custom_attributes(segment), agent_attributes]
124
+ [intrinsics, custom_attributes(segment), agent_attributes.merge(agent_attributes(segment))]
127
125
  end
128
126
 
129
127
  private
@@ -19,6 +19,20 @@ module NewRelic
19
19
  RbConfig::CONFIG['target_os']
20
20
  end
21
21
 
22
+ def self.os_distribution
23
+ case
24
+ when darwin? then :darwin
25
+ when linux? then :linux
26
+ when bsd? then :bsd
27
+ when windows? then :windows
28
+ else ruby_os_identifier
29
+ end
30
+ end
31
+
32
+ def self.windows?
33
+ !!(ruby_os_identifier[/mingw|mswin/i])
34
+ end
35
+
22
36
  def self.darwin?
23
37
  !!(ruby_os_identifier =~ /darwin/i)
24
38
  end
@@ -9,8 +9,7 @@ module NewRelic
9
9
  def self.create(label, &blk)
10
10
  ::NewRelic::Agent.logger.debug("Creating AgentThread: #{label}")
11
11
  wrapped_blk = proc do
12
- if ::Thread.current[:newrelic_tracer_state] && Thread.current[:newrelic_tracer_state].current_transaction
13
- txn = ::Thread.current[:newrelic_tracer_state].current_transaction
12
+ if (txn = ::NewRelic::ThreadLocalStorage[:newrelic_tracer_state]&.current_transaction)
14
13
  ::NewRelic::Agent.logger.warn("AgentThread created with current transaction #{txn.best_name}")
15
14
  end
16
15
  begin