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,77 @@
1
+ require 'oj'
2
+ require 'aws-xray-sdk/exceptions'
3
+
4
+ module XRay
5
+ # Metadata are key-value pairs with values of any type, including objects
6
+ # and lists, but that are not indexed. Use metadata to record data
7
+ # you want to store in the trace but don't need to use for searching traces.
8
+ class Metadata
9
+ def initialize(entity)
10
+ @data = {}
11
+ @entity = entity
12
+ end
13
+
14
+ def sub_meta(namespace)
15
+ @data[namespace] = SubMeta.new(@entity) unless @data[namespace]
16
+ @data[namespace]
17
+ end
18
+
19
+ def to_h
20
+ @data
21
+ end
22
+ end
23
+
24
+ # The actual class that stores all data under a certain namespace.
25
+ class SubMeta
26
+ def initialize(entity)
27
+ @data = {}
28
+ @entity = entity
29
+ end
30
+
31
+ def [](key)
32
+ @data[key]
33
+ end
34
+
35
+ def []=(k, v)
36
+ raise EntityClosedError if @entity.closed?
37
+ @data[k] = v
38
+ end
39
+
40
+ def update(h)
41
+ raise EntityClosedError if @entity.closed?
42
+ @data.merge!(h)
43
+ end
44
+
45
+ def to_h
46
+ @data
47
+ end
48
+
49
+ def to_json
50
+ @to_json ||= begin
51
+ Oj.dump to_h, mode: :compat, use_as_json: true
52
+ end
53
+ end
54
+ end
55
+
56
+ # Singleton facade metadata class doing no-op for performance
57
+ # in case of not sampled X-Ray entities.
58
+ module FacadeMetadata
59
+ class << self
60
+ def [](key)
61
+ # no-op
62
+ end
63
+
64
+ def []=(k, v)
65
+ # no-op
66
+ end
67
+
68
+ def update(h)
69
+ # no-op
70
+ end
71
+
72
+ def to_h
73
+ # no-op
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,63 @@
1
+ require 'aws-xray-sdk/model/entity'
2
+
3
+ module XRay
4
+ # The compute resources running your application logic send data
5
+ # about their work as segments. A segment provides the resource's name,
6
+ # details about the request, and details about the work done.
7
+ class Segment
8
+ include Entity
9
+ attr_accessor :ref_counter, :subsegment_size, :origin, :user
10
+
11
+ # @param [String] trace_id Manually crafted trace id.
12
+ # @param [String] name Must be specified either on object creation or
13
+ # on environment variable `AWS_TRACING_NAME`. The latter has higher precedence.
14
+ # @param [String] parent_id ID of the segment/subsegment representing the upstream caller.
15
+ def initialize(trace_id: nil, name: nil, parent_id: nil)
16
+ @trace_id = trace_id
17
+ @name = ENV['AWS_TRACING_NAME'] || name
18
+ @parent_id = parent_id
19
+ @start_time = Time.now.to_f
20
+ @ref_counter = 0
21
+ @subsegment_size = 0
22
+ @sampled = true
23
+ end
24
+
25
+ def trace_id
26
+ @trace_id ||= begin
27
+ %[1-#{Time.now.to_i.to_s(16)}-#{SecureRandom.hex(12)}]
28
+ end
29
+ end
30
+
31
+ def add_subsegment(subsegment:)
32
+ super subsegment: subsegment
33
+ @ref_counter += 1
34
+ @subsegment_size += 1
35
+ end
36
+
37
+ def remove_subsegment(subsegment:)
38
+ super subsegment: subsegment
39
+ @subsegment_size = subsegment_size - subsegment.all_children_count - 1
40
+ end
41
+
42
+ def decrement_ref_counter
43
+ @ref_counter -= 1
44
+ end
45
+
46
+ def ready_to_send?
47
+ closed? && ref_counter.zero?
48
+ end
49
+
50
+ def to_h
51
+ h = super
52
+ h[:trace_id] = trace_id
53
+ h[:origin] = origin if origin
54
+ h[:parent_id] = @parent_id if @parent_id
55
+ h[:user] = user if user
56
+ h
57
+ end
58
+
59
+ def segment
60
+ self
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,67 @@
1
+ require 'aws-xray-sdk/model/entity'
2
+
3
+ module XRay
4
+ # The work done in a single segment can be broke down into subsegments.
5
+ # Subsegments provide more granular timing information and details about
6
+ # downstream calls that your application made to fulfill the original request.
7
+ # A subsegment can contain additional details about a call to an AWS service,
8
+ # an external HTTP API, or an SQL database.
9
+ class Subsegment
10
+ include Entity
11
+
12
+ attr_reader :segment
13
+ attr_accessor :sql
14
+
15
+ # @param [String] name The subsegment name.
16
+ # @param [Segment] segment The root parent segment. This segment
17
+ # may not be its direct parent.
18
+ # @param [String] namespace Currently supported namespaces are
19
+ # 'remote', 'aws', 'local'.
20
+ def initialize(name:, segment:, namespace: 'local')
21
+ @name = name
22
+ @segment = segment
23
+ @namespace = namespace
24
+ @start_time = Time.now.to_f
25
+ @sampled = true
26
+ end
27
+
28
+ def add_subsegment(subsegment:)
29
+ super subsegment: subsegment
30
+ segment.ref_counter += 1
31
+ segment.subsegment_size += 1
32
+ end
33
+
34
+ def remove_subsegment(subsegment:)
35
+ super subsegment: subsegment
36
+ cur = segment.subsegment_size
37
+ segment.subsegment_size = cur - subsegment.all_children_count - 1
38
+ end
39
+
40
+ def close(end_time: nil)
41
+ super end_time: end_time
42
+ segment.decrement_ref_counter
43
+ end
44
+
45
+ def sql
46
+ @sql ||= {}
47
+ end
48
+
49
+ # Returns the number of its direct and indirect children.
50
+ # This is useful when we remove the reference to a subsegment
51
+ # and need to keep remaining subsegment size accurate.
52
+ def all_children_count
53
+ size = subsegments.count
54
+ subsegments.each { |v| size += v.all_children_count }
55
+ size
56
+ end
57
+
58
+ def to_h
59
+ h = super
60
+ h[:trace_id] = segment.trace_id
61
+ h[:sql] = sql unless sql.empty?
62
+ h[:type] = 'subsegment'
63
+ h[:namespace] = namespace if namespace
64
+ h
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,54 @@
1
+ require 'aws-xray-sdk/logger'
2
+
3
+ module XRay
4
+ # The sampling decision and trace ID are added to HTTP requests in
5
+ # tracing headers named ``X-Amzn-Trace-Id``. The first X-Ray-integrated
6
+ # service that the request hits adds a tracing header, which is read
7
+ # by the X-Ray SDK and included in the response.
8
+ class TraceHeader
9
+ include Logging
10
+ attr_accessor :root, :parent_id, :sampled
11
+
12
+ # @param [String] root Trace id.
13
+ # @param [String] parent_id The id of the parent segment or subsegment.
14
+ # @param [Integer] sampled 0 means not sampled.
15
+ def initialize(root:, parent_id:, sampled:)
16
+ @root = root
17
+ @parent_id = parent_id
18
+ @sampled = sampled.to_i if sampled
19
+ end
20
+
21
+ def self.from_header_string(header_str:)
22
+ empty_header if header_str.to_s.empty?
23
+ header = header_str.delete(' ').downcase
24
+ tmp = {}
25
+ begin
26
+ fields = header.split(';')
27
+ fields.each do |f|
28
+ pair = f.split('=')
29
+ tmp[pair[0].to_sym] = pair[1]
30
+ end
31
+ new root: tmp[:root], parent_id: tmp[:parent], sampled: tmp[:sampled]
32
+ rescue StandardError
33
+ logger.warn %(Invalid trace header #{header}. Ignored.)
34
+ empty_header
35
+ end
36
+ end
37
+
38
+ # @return [String] The heading string constructed based on this header object.
39
+ def header_string
40
+ return '' unless root
41
+ if !parent_id
42
+ %(Root=#{root};Sampled=#{sampled})
43
+ elsif !sampled
44
+ %(Root=#{root};Parent=#{parent_id})
45
+ else
46
+ %(Root=#{root};Parent=#{parent_id};Sampled=#{sampled})
47
+ end
48
+ end
49
+
50
+ def self.empty_header
51
+ new root: nil, parent_id: nil, sampled: nil
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ require 'aws-xray-sdk/exceptions'
2
+
3
+ module XRay
4
+ # Patching external libraries/frameworks to be traced by X-Ray recorder.
5
+ module Patcher
6
+ # @param [Array] targets A list of libraries/frameworks to patch.
7
+ def patch(targets)
8
+ targets.each do |l|
9
+ case l
10
+ when :net_http
11
+ require 'aws-xray-sdk/facets/net_http'
12
+ when :aws_sdk
13
+ require 'aws-xray-sdk/facets/aws_sdk'
14
+ XRay::AwsSDKPatcher.patch
15
+ else
16
+ raise UnsupportedPatchingTargetError.new(%(#{l} is not supported by X-Ray SDK.))
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ require 'open-uri'
2
+ require 'aws-xray-sdk/logger'
3
+
4
+ module XRay
5
+ module Plugins
6
+ # A plugin that gets the EC2 instance-id and AZ if running on an EC2 instance.
7
+ module EC2
8
+ include Logging
9
+
10
+ ORIGIN = 'AWS::EC2::Instance'.freeze
11
+ # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html#instancedata-data-retrieval
12
+ ID_ADDR = 'http://169.254.169.254/latest/meta-data/instance-id'.freeze
13
+ AZ_ADDR = 'http://169.254.169.254/latest/meta-data/placement/availability-zone'.freeze
14
+
15
+ def self.aws
16
+ @@aws ||= begin
17
+ instance_id = open(ID_ADDR, open_timeout: 1).read
18
+ az = open(AZ_ADDR, open_timeout: 1).read
19
+ {
20
+ ec2: {
21
+ instance_id: instance_id,
22
+ availability_zone: az
23
+ }
24
+ }
25
+ rescue StandardError => e
26
+ # Two attempts in total to get EC2 metadata
27
+ @retries ||= 0
28
+ if @retries < 1
29
+ @retries += 1
30
+ retry
31
+ else
32
+ @@aws = {}
33
+ Logging.logger.warn %(can not get the ec2 instance metadata due to: #{e.message}.)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ require 'socket'
2
+ require 'aws-xray-sdk/logger'
3
+
4
+ module XRay
5
+ module Plugins
6
+ # Due to lack of ECS container metadata service, the only host information
7
+ # available is the host name.
8
+ module ECS
9
+ include Logging
10
+
11
+ ORIGIN = 'AWS::ECS::Container'.freeze
12
+
13
+ def self.aws
14
+ @@aws ||= begin
15
+ { ecs: { container: Socket.gethostname } }
16
+ rescue StandardError => e
17
+ @@aws = {}
18
+ Logging.logger.warn %(can not get the ecs container hostname due to: #{e.message}.)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ require 'oj'
2
+ require 'aws-xray-sdk/logger'
3
+
4
+ module XRay
5
+ module Plugins
6
+ # A plugin that records information about the elastic beanstalk environment
7
+ # hosting your application.
8
+ module ElasticBeanstalk
9
+ include Logging
10
+
11
+ CONF_PATH = '/var/elasticbeanstalk/xray/environment.conf'.freeze
12
+ ORIGIN = 'AWS::ElasticBeanstalk::Environment'.freeze
13
+
14
+ def self.aws
15
+ @@aws ||= begin
16
+ file = File.open(CONF_PATH)
17
+ { elastic_beanstalk: Oj.load(file) }
18
+ rescue StandardError => e
19
+ @@aws = {}
20
+ Logging.logger.warn %(can not get the environment config due to: #{e.message}.)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,209 @@
1
+ require 'aws-xray-sdk/configuration'
2
+ require 'aws-xray-sdk/exceptions'
3
+ require 'aws-xray-sdk/model/segment'
4
+ require 'aws-xray-sdk/model/subsegment'
5
+ require 'aws-xray-sdk/model/dummy_entities'
6
+ require 'aws-xray-sdk/model/annotations'
7
+ require 'aws-xray-sdk/model/metadata'
8
+
9
+ module XRay
10
+ # A global AWS X-Ray recorder that will begin/end segments/subsegments
11
+ # and send them to the X-Ray daemon. It is also responsible for managing
12
+ # context.
13
+ class Recorder
14
+ attr_reader :config
15
+
16
+ def initialize(user_config: nil)
17
+ @config = Configuration.new
18
+ @config.configure(user_config) unless user_config.nil?
19
+ end
20
+
21
+ # Begin a segment for the current context. The recorder
22
+ # only keeps one segment at a time. Create a second one without
23
+ # closing existing one will overwrite the existing one.
24
+ # @return [Segment] thew newly created segment.
25
+ def begin_segment(name, trace_id: nil, parent_id: nil, sampled: nil)
26
+ seg_name = name || config.name
27
+ raise SegmentNameMissingError if seg_name.to_s.empty?
28
+
29
+ # sampling decision comes from outside has higher precedence.
30
+ sample = sampled.nil? ? config.sample? : sampled
31
+ if sample
32
+ segment = Segment.new name: seg_name, trace_id: trace_id, parent_id: parent_id
33
+ populate_runtime_context(segment)
34
+ else
35
+ segment = DummySegment.new name: seg_name, trace_id: trace_id, parent_id: parent_id
36
+ end
37
+ context.store_entity entity: segment
38
+ segment
39
+ end
40
+
41
+ # @return [Segment] the active segment tied to the current context.
42
+ # If the current context is under a subsegment, it returns its parent segment.
43
+ def current_segment
44
+ entity = current_entity
45
+ entity.segment if entity
46
+ end
47
+
48
+ # End the current segment and send it to X-Ray daemon if it is ready.
49
+ def end_segment(end_time: nil)
50
+ segment = current_segment
51
+ return unless segment
52
+ segment.close end_time: end_time
53
+ context.clear!
54
+ emitter.send_entity entity: segment if segment.ready_to_send?
55
+ end
56
+
57
+ # Begin a new subsegment and add it to be the child of the current active
58
+ # subsegment or segment. Also tie the new created subsegment to the current context.
59
+ # Its sampling decision will follow its parent.
60
+ # @return [Subsegment] the newly created subsegment.
61
+ def begin_subsegment(name, namespace: nil, segment: nil)
62
+ entity = segment || current_entity
63
+ return unless entity
64
+ if entity.sampled
65
+ subsegment = Subsegment.new name: name, segment: entity.segment, namespace: namespace
66
+ else
67
+ subsegment = DummySubsegment.new name: name, segment: entity.segment
68
+ end
69
+ # attach the new created subsegment under the current active entity
70
+ entity.add_subsegment subsegment: subsegment
71
+ # associate the new subsegment to the current context
72
+ context.store_entity entity: subsegment
73
+ subsegment
74
+ end
75
+
76
+ # @return [Subsegment] the active subsegment tied to the current context.
77
+ # Returns nil if the current context has no associated subsegment.
78
+ def current_subsegment
79
+ entity = context.current_entity
80
+ entity.is_a?(Subsegment) ? entity : nil
81
+ end
82
+
83
+ # End the current active subsegment. It also send the entire segment if
84
+ # this subsegment is the last one open or stream out subsegments of its
85
+ # parent segment if the stream threshold is breached.
86
+ def end_subsegment(end_time: nil)
87
+ entity = current_entity
88
+ return unless entity.is_a?(Subsegment)
89
+ entity.close end_time: end_time
90
+ # update current context
91
+ if entity.parent.closed?
92
+ context.clear!
93
+ else
94
+ context.store_entity entity: entity.parent
95
+ end
96
+ # check if the entire segment can be send.
97
+ # If not, stream subsegments when threshold is reached.
98
+ segment = entity.segment
99
+ if segment.ready_to_send?
100
+ emitter.send_entity entity: segment
101
+ elsif streamer.eligible? segment: segment
102
+ streamer.stream_subsegments root: segment, emitter: emitter
103
+ end
104
+ end
105
+
106
+ # Record the passed block as a subsegment.
107
+ def capture(name, namespace: nil, segment: nil)
108
+ subsegment = begin_subsegment name, namespace: namespace, segment: segment
109
+ begin
110
+ yield subsegment
111
+ rescue Exception => e
112
+ subsegment.add_exception exception: e
113
+ raise e
114
+ ensure
115
+ end_subsegment
116
+ end
117
+ end
118
+
119
+ # Returns current segment or subsegment that associated to the current context.
120
+ # This is a proxy method to Context class current_entity.
121
+ def current_entity
122
+ context.current_entity
123
+ end
124
+
125
+ def inject_context(entity, target_ctx: nil)
126
+ context.inject_context entity, target_ctx: target_ctx
127
+ return unless block_given?
128
+ yield
129
+ context.clear!
130
+ end
131
+
132
+ def clear_context
133
+ context.clear!
134
+ end
135
+
136
+ def sampled?
137
+ entity = current_entity
138
+ if block_given?
139
+ yield if entity && entity.sampled
140
+ else
141
+ entity && entity.sampled
142
+ end
143
+ end
144
+
145
+ # A proxy method to get the annotations from the current active entity.
146
+ def annotations
147
+ entity = current_entity
148
+ if entity
149
+ entity.annotations
150
+ else
151
+ FacadeAnnotations
152
+ end
153
+ end
154
+
155
+ # A proxy method to get the metadata under provided namespace
156
+ # from the current active entity.
157
+ def metadata(namespace: :default)
158
+ entity = current_entity
159
+ if entity
160
+ entity.metadata(namespace: namespace)
161
+ else
162
+ FacadeMetadata
163
+ end
164
+ end
165
+
166
+ # A proxy method to XRay::Configuration.configure
167
+ def configure(user_config)
168
+ config.configure(user_config)
169
+ end
170
+
171
+ def context
172
+ config.context
173
+ end
174
+
175
+ def sampler
176
+ config.sampler
177
+ end
178
+
179
+ def emitter
180
+ config.emitter
181
+ end
182
+
183
+ def streamer
184
+ config.streamer
185
+ end
186
+
187
+ def segment_naming
188
+ config.segment_naming
189
+ end
190
+
191
+ def sampling_enabled?
192
+ config.sampling
193
+ end
194
+
195
+ private_class_method
196
+
197
+ def populate_runtime_context(segment)
198
+ aws = {}
199
+ config.plugins.each do |p|
200
+ meta = p.aws
201
+ if meta.is_a?(Hash) && !meta.empty?
202
+ aws.merge! meta
203
+ segment.origin = p::ORIGIN
204
+ end
205
+ end
206
+ segment.aws = aws unless aws.empty?
207
+ end
208
+ end
209
+ end