aws-xray-sdk 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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