aws-xray-sdk 0.9.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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/lib/aws-xray-sdk.rb +10 -0
  3. data/lib/aws-xray-sdk/configuration.rb +158 -0
  4. data/lib/aws-xray-sdk/context/context.rb +26 -0
  5. data/lib/aws-xray-sdk/context/default_context.rb +81 -0
  6. data/lib/aws-xray-sdk/emitter/default_emitter.rb +53 -0
  7. data/lib/aws-xray-sdk/emitter/emitter.rb +24 -0
  8. data/lib/aws-xray-sdk/exceptions.rb +31 -0
  9. data/lib/aws-xray-sdk/facets/aws_sdk.rb +127 -0
  10. data/lib/aws-xray-sdk/facets/helper.rb +61 -0
  11. data/lib/aws-xray-sdk/facets/net_http.rb +61 -0
  12. data/lib/aws-xray-sdk/facets/rack.rb +87 -0
  13. data/lib/aws-xray-sdk/facets/rails/active_record.rb +66 -0
  14. data/lib/aws-xray-sdk/facets/rails/ex_middleware.rb +24 -0
  15. data/lib/aws-xray-sdk/facets/rails/railtie.rb +23 -0
  16. data/lib/aws-xray-sdk/facets/resources/aws_params_whitelist.rb +340 -0
  17. data/lib/aws-xray-sdk/facets/resources/aws_services_whitelist.rb +147 -0
  18. data/lib/aws-xray-sdk/logger.rb +19 -0
  19. data/lib/aws-xray-sdk/model/annotations.rb +97 -0
  20. data/lib/aws-xray-sdk/model/cause.rb +70 -0
  21. data/lib/aws-xray-sdk/model/dummy_entities.rb +72 -0
  22. data/lib/aws-xray-sdk/model/entity.rb +187 -0
  23. data/lib/aws-xray-sdk/model/metadata.rb +77 -0
  24. data/lib/aws-xray-sdk/model/segment.rb +63 -0
  25. data/lib/aws-xray-sdk/model/subsegment.rb +67 -0
  26. data/lib/aws-xray-sdk/model/trace_header.rb +54 -0
  27. data/lib/aws-xray-sdk/patcher.rb +21 -0
  28. data/lib/aws-xray-sdk/plugins/ec2.rb +39 -0
  29. data/lib/aws-xray-sdk/plugins/ecs.rb +23 -0
  30. data/lib/aws-xray-sdk/plugins/elastic_beanstalk.rb +25 -0
  31. data/lib/aws-xray-sdk/recorder.rb +209 -0
  32. data/lib/aws-xray-sdk/sampling/default_sampler.rb +105 -0
  33. data/lib/aws-xray-sdk/sampling/reservoir.rb +35 -0
  34. data/lib/aws-xray-sdk/sampling/sampler.rb +27 -0
  35. data/lib/aws-xray-sdk/sampling/sampling_rule.rb +57 -0
  36. data/lib/aws-xray-sdk/search_pattern.rb +82 -0
  37. data/lib/aws-xray-sdk/segment_naming/dynamic_naming.rb +26 -0
  38. data/lib/aws-xray-sdk/segment_naming/segment_naming.rb +10 -0
  39. data/lib/aws-xray-sdk/streaming/default_streamer.rb +53 -0
  40. data/lib/aws-xray-sdk/streaming/streamer.rb +17 -0
  41. data/lib/aws-xray-sdk/version.rb +3 -0
  42. metadata +224 -0
@@ -0,0 +1,61 @@
1
+ require 'aws-xray-sdk/model/trace_header'
2
+
3
+ module XRay
4
+ module Facets
5
+ # Hepler functions shared for all external frameworks/libraries
6
+ # like make sampling decisions from incoming http requests etc.
7
+ module Helper
8
+ TRACE_HEADER = 'X-Amzn-Trace-Id'.freeze
9
+ TRACE_HEADER_PROXY = 'HTTP_X_AMZN_TRACE_ID'.freeze
10
+
11
+ # Construct a `TraceHeader` object from headers
12
+ # of the incoming request. This method should always return
13
+ # a `TraceHeader` object regardless of tracing header's presence
14
+ # in the incoming request.
15
+ # @param [Hash] headers Hash that contains X-Ray trace header key.
16
+ # @return [TraceHeader] The new constructed trace header object.
17
+ def construct_header(headers:)
18
+ if v = headers[TRACE_HEADER_PROXY] || headers[TRACE_HEADER]
19
+ TraceHeader.from_header_string header_str: v
20
+ else
21
+ TraceHeader.empty_header
22
+ end
23
+ end
24
+
25
+ # The sampling decision coming from `trace_header` always has
26
+ # the highest precedence. If the `trace_header` doesn't contain
27
+ # sampling decision then it checks if sampling is enabled or not
28
+ # in the recorder. If not enbaled it returns 'true'. Otherwise it uses
29
+ # sampling rule to decide.
30
+ def should_sample?(header_obj:, recorder:,
31
+ host: nil, method: nil, path: nil,
32
+ **args)
33
+ # check outside decision
34
+ if i = header_obj.sampled
35
+ if i.zero?
36
+ false
37
+ else
38
+ true
39
+ end
40
+ # check sampling rules
41
+ elsif recorder.sampling_enabled?
42
+ recorder.sampler.sample_request?(service_name: host, url_path: path,
43
+ http_method: method)
44
+ # sample if no reason not to
45
+ else
46
+ true
47
+ end
48
+ end
49
+
50
+ # Prepares a X-Ray header string based on the provided Segment/Subsegment.
51
+ def prep_header_str(entity:)
52
+ return '' if entity.nil?
53
+ root = entity.segment.trace_id
54
+ parent_id = entity.id
55
+ sampled = entity.sampled ? 1 : 0
56
+ header = TraceHeader.new root: root, parent_id: parent_id, sampled: sampled
57
+ header.header_string
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,61 @@
1
+ require 'net/http'
2
+ require 'aws-xray-sdk/facets/helper'
3
+
4
+ module XRay
5
+ # Patch net/http to be traced by X-Ray
6
+ module NetHttp
7
+ # Class level interceptor to capture http requests as subsegments
8
+ module HTTPClassInterceptor
9
+ def new(*options)
10
+ o = super(*options)
11
+ o
12
+ end
13
+ end
14
+
15
+ # Instance level interceptor to capture http requests as subsegments
16
+ module HTTPInstanceInterceptor
17
+ include XRay::Facets::Helper
18
+
19
+ def initialize(*options)
20
+ super(*options)
21
+ end
22
+
23
+ def request(req, body = nil, &block)
24
+ entity = XRay.recorder.current_entity
25
+ capture = !(entity && entity.namespace && entity.namespace == 'aws'.freeze)
26
+ if started? && capture && entity
27
+ XRay.recorder.capture(address, namespace: 'remote') do |subsegment|
28
+ protocol = use_ssl? ? 'https'.freeze : 'http'.freeze
29
+ # avoid modifying original variable
30
+ iport = port.nil? ? nil : %(:#{port})
31
+ # do not capture query string
32
+ path = req.path.split('?')[0] if req.path
33
+ uri = %(#{protocol}://#{address}#{iport}#{path})
34
+ req_meta = {
35
+ url: uri,
36
+ method: req.method
37
+ }
38
+ subsegment.merge_http_request request: req_meta
39
+ req[TRACE_HEADER] = prep_header_str entity: subsegment
40
+ begin
41
+ res = super
42
+ res_meta = {
43
+ status: res.code.to_i,
44
+ content_length: res.content_length
45
+ }
46
+ subsegment.merge_http_response response: res_meta
47
+ rescue Exception => e
48
+ subsegment.add_exception exception: e
49
+ raise e
50
+ end
51
+ end
52
+ else
53
+ super
54
+ end
55
+ end
56
+ end
57
+
58
+ ::Net::HTTP.singleton_class.prepend HTTPClassInterceptor
59
+ ::Net::HTTP.prepend HTTPInstanceInterceptor
60
+ end
61
+ end
@@ -0,0 +1,87 @@
1
+ require 'rack/request'
2
+ require 'aws-xray-sdk'
3
+ require 'aws-xray-sdk/facets/helper'
4
+
5
+ module XRay
6
+ module Rack
7
+ # Rack middleware that generates a segment for each request/response cycle.
8
+ class Middleware
9
+ include XRay::Facets::Helper
10
+ X_FORWARD = 'HTTP_X_FORWARDED_FOR'.freeze
11
+
12
+ def initialize(app, recorder: nil)
13
+ @app = app
14
+ @recorder = recorder || XRay.recorder
15
+ end
16
+
17
+ def call(env)
18
+ header = construct_header(headers: env)
19
+ req = ::Rack::Request.new(env)
20
+
21
+ # params required for path based sampling
22
+ host = req.host
23
+ url_path = req.path
24
+ method = req.request_method
25
+
26
+ # get sampling decision
27
+ sampled = should_sample?(
28
+ header_obj: header, recorder: @recorder,
29
+ host: host, method: method, path: url_path
30
+ )
31
+
32
+ # get segment name from host header if applicable
33
+ seg_name = @recorder.segment_naming.provide_name(host: req.host)
34
+
35
+ # begin the segment
36
+ segment = @recorder.begin_segment seg_name, trace_id: header.root, parent_id: header.parent_id,
37
+ sampled: sampled
38
+
39
+ # add neccessary http request metadata to the segment
40
+ req_meta = extract_request_meta(req)
41
+ segment.merge_http_request request: req_meta unless req_meta.empty?
42
+ begin
43
+ status, headers, body = @app.call env
44
+ resp_meta = {}
45
+ resp_meta[:status] = status
46
+ # Don't set content_length if it is not available on headers.
47
+ resp_obj = ::Rack::Response.new body: body, status: status, headers: headers
48
+ if len = resp_obj.content_length
49
+ resp_meta[:content_length] = len
50
+ end
51
+ segment.merge_http_response response: resp_meta
52
+ [status, headers, body]
53
+ rescue Exception => e
54
+ segment.apply_status_code status: 500
55
+ segment.add_exception exception: e
56
+ raise e
57
+ ensure
58
+ @recorder.end_segment
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def extract_request_meta(req)
65
+ req_meta = {}
66
+ req_meta[:url] = req.url if req.url
67
+ req_meta[:user_agent] = req.user_agent if req.user_agent
68
+ req_meta[:method] = req.request_method if req.request_method
69
+ if req.has_header?(X_FORWARD)
70
+ req_meta[:client_ip] = get_ip(req.get_header(X_FORWARD))
71
+ req_meta[:x_forwarded_for] = true
72
+ elsif v = req.ip
73
+ req_meta[:client_ip] = v
74
+ end
75
+ req_meta
76
+ end
77
+
78
+ def get_ip(ips)
79
+ if ips.respond_to?(:length)
80
+ ips[ips.length - 1]
81
+ else
82
+ ips
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,66 @@
1
+ require 'active_record'
2
+
3
+ module XRay
4
+ module Rails
5
+ # Recording Rails database transactions as subsegments.
6
+ module ActiveRecord
7
+ class << self
8
+ IGNORE_OPS = ['SCHEMA', 'ActiveRecord::SchemaMigration Load',
9
+ 'ActiveRecord::InternalMetadata Load'].freeze
10
+ DB_TYPE_MAPPING = {
11
+ mysql2: 'MySQL',
12
+ postgresql: 'PostgreSQL'
13
+ }.freeze
14
+
15
+ def record(transaction)
16
+ payload = transaction.payload
17
+ pool, conn = get_pool_n_conn(payload[:connection_id])
18
+
19
+ return if IGNORE_OPS.include?(payload[:name]) || pool.nil? || conn.nil?
20
+ db_config = pool.spec.config
21
+ name, sql = build_name_sql_meta config: db_config, conn: conn
22
+ subsegment = XRay.recorder.begin_subsegment name, namespace: 'remote'
23
+ subsegment.start_time = transaction.time.to_f
24
+ subsegment.sql = sql
25
+ XRay.recorder.end_subsegment end_time: transaction.end.to_f
26
+ end
27
+
28
+ private
29
+
30
+ def build_name_sql_meta(config:, conn:)
31
+ # extract all available info
32
+ adapter = config[:adapter]
33
+ database = config[:database]
34
+ host = config[:host].nil? ? nil : %(@#{config[:host]})
35
+ port = config[:port].nil? ? nil : %(:#{config[:port]})
36
+ username = config[:username]
37
+
38
+ # assemble subsegment name
39
+ name = %(#{database}#{host})
40
+ # assemble sql meta
41
+ sql = {}
42
+ sql[:user] = username
43
+ sql[:url] = %(#{adapter}://#{username}#{host}#{port}/#{database})
44
+ sql[:database_type] = DB_TYPE_MAPPING[adapter.to_sym]
45
+ [name, sql]
46
+ end
47
+
48
+ def get_pool_n_conn(conn_id)
49
+ pool, conn = nil, nil
50
+ ::ActiveRecord::Base.connection_handler.connection_pool_list.each do |p|
51
+ conn = p.connections.select { |c| c.object_id == conn_id }
52
+ pool = p unless conn.nil?
53
+ end
54
+ [pool, conn]
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Add a hook on database transactions using Rails instrumentation API
62
+ ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
63
+ # We need the full event which has all the timing info
64
+ transaction = ActiveSupport::Notifications::Event.new(*args)
65
+ XRay::Rails::ActiveRecord.record(transaction)
66
+ end
@@ -0,0 +1,24 @@
1
+ require 'aws-xray-sdk'
2
+
3
+ module XRay
4
+ module Rails
5
+ # Middleware for capturing unhandled exceptions from views/controller.
6
+ # To properly capture exceptions this middleware needs to be placed
7
+ # after the default exception handling middleware. Otherwise they will
8
+ # be swallowed.
9
+ class ExceptionMiddleware
10
+ def initialize(app, recorder: nil)
11
+ @app = app
12
+ @recorder = recorder || XRay.recorder
13
+ end
14
+
15
+ def call(env)
16
+ @app.call(env)
17
+ rescue Exception => e
18
+ segment = @recorder.current_segment
19
+ segment.add_exception exception: e if segment
20
+ raise e
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ require 'aws-xray-sdk/facets/rack'
2
+ require 'aws-xray-sdk/facets/rails/ex_middleware'
3
+
4
+ module XRay
5
+ # configure X-Ray instrumentation for rails framework
6
+ class Railtie < ::Rails::Railtie
7
+ RAILS_OPTIONS = %I[active_record].freeze
8
+
9
+ initializer "aws-xray-sdk.rack_middleware" do |app|
10
+ app.middleware.insert 0, Rack::Middleware
11
+ app.middleware.use XRay::Rails::ExceptionMiddleware
12
+ end
13
+
14
+ config.after_initialize do |app|
15
+ if app.config.respond_to?('xray')
16
+ options = app.config.xray
17
+ require 'aws-xray-sdk/facets/rails/active_record' if options[:active_record]
18
+ general_options = options.reject { |k, v| RAILS_OPTIONS.include?(k) }
19
+ XRay.recorder.configure(general_options)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,340 @@
1
+ module XRay
2
+ # AWS SDK parameters whitelisted will be recorded
3
+ # as metadata on AWS subsegments
4
+ module AwsParams
5
+ @whitelist = {
6
+ services: {
7
+ DynamoDB: {
8
+ operations: {
9
+ batch_get_item: {
10
+ request_descriptors: {
11
+ request_items: {
12
+ map: true,
13
+ get_keys: true,
14
+ rename_to: :table_names
15
+ }
16
+ },
17
+ response_parameters: %I[
18
+ consumed_capacity
19
+ ]
20
+ },
21
+ batch_write_item: {
22
+ request_descriptors: {
23
+ request_items: {
24
+ map: true,
25
+ get_keys: true,
26
+ rename_to: :table_names
27
+ }
28
+ },
29
+ response_parameters: %I[
30
+ consumed_capacity
31
+ item_collection_metrics
32
+ ]
33
+ },
34
+ create_table: {
35
+ request_parameters: %I[
36
+ global_secondary_indexes
37
+ local_secondary_indexes
38
+ provisioned_throughput
39
+ table_name
40
+ ]
41
+ },
42
+ delete_item: {
43
+ request_parameters: %I[
44
+ table_name
45
+ ],
46
+ response_parameters: %I[
47
+ consumed_capacity
48
+ item_collection_metrics
49
+ ]
50
+ },
51
+ delete_table: {
52
+ request_parameters: %I[
53
+ table_name
54
+ ]
55
+ },
56
+ describe_table: {
57
+ request_parameters: %I[
58
+ table_name
59
+ ]
60
+ },
61
+ get_item: {
62
+ request_parameters: %I[
63
+ consistent_read
64
+ projection_expression
65
+ table_name
66
+ ],
67
+ response_parameters: %I[
68
+ consumed_capacity
69
+ ]
70
+ },
71
+ list_tables: {
72
+ request_parameters: %I[
73
+ exclusive_start_table_name
74
+ limit
75
+ ],
76
+ response_descriptors: {
77
+ table_names: {
78
+ list: true,
79
+ get_count: true,
80
+ rename_to: :table_count
81
+ }
82
+ }
83
+ },
84
+ put_item: {
85
+ request_parameters: %I[
86
+ table_name
87
+ ],
88
+ response_parameters: %I[
89
+ consumed_capacity
90
+ item_collection_metrics
91
+ ]
92
+ },
93
+ query: {
94
+ request_parameters: %I[
95
+ attributes_to_get
96
+ consistent_read
97
+ index_name
98
+ limit
99
+ projection_expression
100
+ scan_index_forward
101
+ select
102
+ table_name
103
+ ],
104
+ response_parameters: %I[
105
+ consumed_capacity
106
+ ]
107
+ },
108
+ scan: {
109
+ request_parameters: %I[
110
+ attributes_to_get
111
+ consistent_read
112
+ index_name
113
+ limit
114
+ projection_expression
115
+ segment
116
+ select
117
+ table_name
118
+ total_segments
119
+ ],
120
+ response_parameters: %I[
121
+ consumed_capacity
122
+ count
123
+ scanned_count
124
+ ]
125
+ },
126
+ update_item: {
127
+ request_parameters: %I[
128
+ table_name
129
+ ],
130
+ response_parameters: %I[
131
+ consumed_capacity
132
+ item_collection_metrics
133
+ ]
134
+ },
135
+ update_table: {
136
+ request_parameters: %I[
137
+ attribute_definitions
138
+ global_secondary_index_updates
139
+ provisioned_throughput
140
+ table_name
141
+ ]
142
+ }
143
+ }
144
+ },
145
+ SQS: {
146
+ operations: {
147
+ add_permission: {
148
+ request_parameters: %I[
149
+ label
150
+ queue_url
151
+ ]
152
+ },
153
+ change_message_visibility: {
154
+ request_parameters: %I[
155
+ queue_url
156
+ visibility_timeout
157
+ ]
158
+ },
159
+ change_message_visibility_batch: {
160
+ request_parameters: %I[
161
+ queue_url
162
+ ],
163
+ response_parameters: %I[
164
+ failed
165
+ ]
166
+ },
167
+ create_queue: {
168
+ request_parameters: %I[
169
+ attributes
170
+ queue_name
171
+ ]
172
+ },
173
+ delete_message: {
174
+ request_parameters: %I[
175
+ queue_urls
176
+ ]
177
+ },
178
+ delete_message_batch: {
179
+ request_parameters: %I[
180
+ queue_url
181
+ ],
182
+ response_parameters: %I[
183
+ failed
184
+ ]
185
+ },
186
+ delete_queue: {
187
+ request_parameters: %I[
188
+ queue_url
189
+ ]
190
+ },
191
+ get_queue_attributes: {
192
+ request_parameters: %I[
193
+ queue_url
194
+ ],
195
+ response_parameters: %I[
196
+ attributes
197
+ ]
198
+ },
199
+ get_queue_url: {
200
+ request_parameters: %I[
201
+ queue_name
202
+ queue_owner_aws_account_id
203
+ ],
204
+ response_parameters: %I[
205
+ queue_url
206
+ ]
207
+ },
208
+ list_dead_letter_source_queues: {
209
+ request_parameters: %I[
210
+ queue_url
211
+ ],
212
+ response_parameters: %I[
213
+ queue_urls
214
+ ]
215
+ },
216
+ list_queues: {
217
+ request_parameters: %I[
218
+ queue_name_prefix
219
+ ],
220
+ response_descriptors: {
221
+ queue_urls: {
222
+ list: true,
223
+ get_count: true,
224
+ rename_to: :queue_count
225
+ }
226
+ }
227
+ },
228
+ purge_queue: {
229
+ request_parameters: %I[
230
+ queue_url
231
+ ]
232
+ },
233
+ receive_message: {
234
+ request_parameters: %I[
235
+ attribute_names
236
+ max_number_of_messages
237
+ message_attribute_names
238
+ queue_url
239
+ visibility_timeout
240
+ wait_time_seconds
241
+ ],
242
+ response_descriptors: {
243
+ messages: {
244
+ list: true,
245
+ get_count: true,
246
+ rename_to: :message_count
247
+ }
248
+ }
249
+ },
250
+ remove_permission: {
251
+ request_parameters: %I[
252
+ queue_url
253
+ ]
254
+ },
255
+ send_message: {
256
+ request_parameters: %I[
257
+ delay_seconds
258
+ queue_url
259
+ ],
260
+ request_descriptors: {
261
+ message_attributes: {
262
+ map: true,
263
+ get_keys: true,
264
+ rename_to: :message_attribute_names
265
+ }
266
+ },
267
+ response_parameters: %I[
268
+ message_id
269
+ ]
270
+ },
271
+ send_message_batch: {
272
+ request_parameters: %I[
273
+ queue_url
274
+ ],
275
+ request_descriptors: {
276
+ entries: {
277
+ list: true,
278
+ get_count: true,
279
+ rename_to: :message_count
280
+ }
281
+ },
282
+ response_descriptors: {
283
+ failed: {
284
+ list: true,
285
+ get_count: true,
286
+ rename_to: :failed_count
287
+ },
288
+ successful: {
289
+ list: true,
290
+ get_count: true,
291
+ rename_to: :successful_count
292
+ }
293
+ }
294
+ },
295
+ set_queue_attributes: {
296
+ request_parameters: %I[
297
+ queue_url
298
+ ],
299
+ request_descriptors: {
300
+ attributes: {
301
+ map: true,
302
+ get_keys: true,
303
+ rename_to: :attribute_names
304
+ }
305
+ }
306
+ }
307
+ }
308
+ },
309
+ Lambda: {
310
+ operations: {
311
+ invoke: {
312
+ request_parameters: %I[
313
+ function_name
314
+ invocation_type
315
+ log_type
316
+ qualifier
317
+ ],
318
+ response_parameters: %I[
319
+ function_error
320
+ status_code
321
+ ]
322
+ },
323
+ invoke_async: {
324
+ request_parameters: %I[
325
+ function_name
326
+ ],
327
+ response_parameters: %I[
328
+ status
329
+ ]
330
+ }
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ def self.whitelist
337
+ @whitelist
338
+ end
339
+ end
340
+ end