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,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