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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 78007efa0d29f0f16fa12ee0437cb6214a64c7d43d6cd1bde7ca9286d44d110b
4
+ data.tar.gz: e9fe587611d3f346b98e632ab88772769246d351b06ce46cc42f015fb15899e2
5
+ SHA512:
6
+ metadata.gz: bb2ed43b514b1b50278c370c32b159f8f58bed0cd3e105b91f9ff0afb74a6ba66f184f515a294c2520cca7ad9b102506534f58fd4f3a778daeec02688900b437
7
+ data.tar.gz: 5ce0c0474f40d316dfa1ec0eddc13d156886c93b211bebd7963ae795c7b1eeca72fbc696d3a0bb948d3ad8ad106eab60d1b0774602217a794c2f92f42bac2938
@@ -0,0 +1,10 @@
1
+ require 'aws-xray-sdk/recorder'
2
+
3
+ module XRay
4
+ @recorder = Recorder.new
5
+
6
+ # providing the default global recorder
7
+ def self.recorder
8
+ @recorder
9
+ end
10
+ end
@@ -0,0 +1,158 @@
1
+ require 'aws-xray-sdk/exceptions'
2
+ require 'aws-xray-sdk/patcher'
3
+ require 'aws-xray-sdk/emitter/default_emitter'
4
+ require 'aws-xray-sdk/context/default_context'
5
+ require 'aws-xray-sdk/sampling/default_sampler'
6
+ require 'aws-xray-sdk/streaming/default_streamer'
7
+ require 'aws-xray-sdk/segment_naming/dynamic_naming'
8
+ require 'aws-xray-sdk/plugins/ec2'
9
+ require 'aws-xray-sdk/plugins/ecs'
10
+ require 'aws-xray-sdk/plugins/elastic_beanstalk'
11
+ require 'aws-xray-sdk/logger'
12
+
13
+ module XRay
14
+ # This class stores all configurations for X-Ray recorder
15
+ # and should be initialized only once.
16
+ class Configuration
17
+ include Patcher
18
+
19
+ SEGMENT_NAME_KEY = 'AWS_XRAY_TRACING_NAME'.freeze
20
+ CONFIG_KEY = %I[logger name sampling plugins daemon_address segment_naming
21
+ naming_pattern emitter streamer context context_missing
22
+ sampling_rules stream_threshold patch].freeze
23
+
24
+ def initialize
25
+ @name = ENV[SEGMENT_NAME_KEY]
26
+ @sampling = true
27
+ @emitter = DefaultEmitter.new
28
+ @context = DefaultContext.new
29
+ @sampler = DefaultSampler.new
30
+ @streamer = DefaultStreamer.new
31
+ @segment_naming = DynamicNaming.new fallback: @name
32
+ @plugins = []
33
+ end
34
+
35
+ # @param [String] v The default segment name.
36
+ # Environment vairable takes higher precedence.
37
+ def name=(v)
38
+ @name = ENV[SEGMENT_NAME_KEY] || v
39
+ end
40
+
41
+ # proxy method to the emitter's daemon_address config.
42
+ def daemon_address=(v)
43
+ emitter.daemon_address = v
44
+ end
45
+
46
+ # proxy method to the context's context_missing config.
47
+ def context_missing=(v)
48
+ context.context_missing = v
49
+ end
50
+
51
+ # proxy method to the sampler's sampling rule config.
52
+ def sampling_rules=(v)
53
+ sampler.sampling_rules = v
54
+ end
55
+
56
+ # proxy method to the streamer's stream threshold config.
57
+ def stream_threshold=(v)
58
+ streamer.stream_threshold = v
59
+ end
60
+
61
+ # proxy method to the dynamic naming's pattern config.
62
+ def naming_pattern=(v)
63
+ segment_naming.pattern = v
64
+ end
65
+
66
+ # makes a sampling decision based on internal configure, e.g.
67
+ # if sampling enabled and the default sampling rule.
68
+ def sample?
69
+ return true unless sampling
70
+ sampler.sample?
71
+ end
72
+
73
+ # @param [Hash] user_config The user configuration overrides.
74
+ def configure(user_config)
75
+ raise InvalidConfigurationError.new('User config must be a Hash.') unless user_config.is_a?(Hash)
76
+ return if user_config.empty?
77
+
78
+ user_config.each_key do |key|
79
+ case key
80
+ when :logger
81
+ XRay::Logging.logger = user_config[key]
82
+ when :name
83
+ self.name = user_config[key]
84
+ when :context
85
+ self.context = user_config[key]
86
+ when :context_missing
87
+ self.context_missing = user_config[key]
88
+ when :sampler
89
+ self.sampler = user_config[key]
90
+ when :sampling_rules
91
+ self.sampling_rules = user_config[key]
92
+ when :sampling
93
+ self.sampling = user_config[key]
94
+ when :emitter
95
+ self.emitter = user_config[key]
96
+ when :daemon_address
97
+ self.daemon_address = user_config[key]
98
+ when :segment_naming
99
+ self.segment_naming = user_config[key]
100
+ when :naming_pattern
101
+ self.naming_pattern = user_config[key]
102
+ when :streamer
103
+ self.streamer = user_config[key]
104
+ when :stream_threshold
105
+ self.stream_threshold = user_config[key]
106
+ when :plugins
107
+ self.plugins = load_plugins(user_config[key])
108
+ when :patch
109
+ patch(user_config[key])
110
+ else
111
+ raise InvalidConfigurationError.new(%(Invalid config key #{key}.))
112
+ end
113
+ end
114
+ end
115
+
116
+ attr_accessor :emitter
117
+
118
+ attr_accessor :context
119
+
120
+ attr_accessor :sampler
121
+
122
+ attr_accessor :streamer
123
+
124
+ attr_accessor :segment_naming
125
+
126
+ attr_accessor :plugins
127
+
128
+ attr_accessor :sampling
129
+
130
+ # @return [String] The default segment name.
131
+ attr_reader :name
132
+
133
+ # The global logger used across the X-Ray SDK.
134
+ # @return [Logger]
135
+ attr_reader :logger
136
+
137
+ private
138
+
139
+ def load_plugins(symbols)
140
+ plugins = []
141
+ symbols.each do |symbol|
142
+ case symbol
143
+ when :ec2
144
+ plugins << XRay::Plugins::EC2
145
+ when :ecs
146
+ plugins << XRay::Plugins::ECS
147
+ when :elastic_beanstalk
148
+ plugins << XRay::Plugins::ElasticBeanstalk
149
+ else
150
+ raise InvalidConfigurationError.new(%(Unsupported plugin #{symbol}.))
151
+ end
152
+ end
153
+ # eager loads aws metadata to eliminate impact on first incoming request
154
+ plugins.each(&:aws)
155
+ plugins
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,26 @@
1
+ module XRay
2
+ # The interface of context management for the X-Ray recorder.
3
+ module Context
4
+ # @param [Entity] entity The entity to be stored in the context.
5
+ def store_entity(entity:)
6
+ raise 'Not implemented'
7
+ end
8
+
9
+ def current_entity
10
+ raise 'Not implemented'
11
+ end
12
+
13
+ def clear!
14
+ raise 'Not implemented'
15
+ end
16
+
17
+ # Put current active entity to the new context storage.
18
+ def inject_context(entity, target_ctx: nil)
19
+ raise 'Not implemented'
20
+ end
21
+
22
+ def handle_context_missing
23
+ raise 'Not implemented'
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,81 @@
1
+ require 'aws-xray-sdk/logger'
2
+ require 'aws-xray-sdk/context/context'
3
+ require 'aws-xray-sdk/exceptions'
4
+
5
+ module XRay
6
+ # The default context storage management used by
7
+ # the X-Ray recorder. It uses thread local to store
8
+ # segments and subsegments.
9
+ class DefaultContext
10
+ include Context
11
+ include Logging
12
+
13
+ LOCAL_KEY = '_aws_xray_entity'.freeze
14
+ CONTEXT_MISSING_KEY = 'AWS_XRAY_CONTEXT_MISSING'.freeze
15
+ SUPPORTED_STRATEGY = %w[RUNTIME_ERROR LOG_ERROR].freeze
16
+ DEFAULT_STRATEGY = SUPPORTED_STRATEGY[0]
17
+
18
+ attr_reader :context_missing
19
+
20
+ def initialize
21
+ strategy = ENV[CONTEXT_MISSING_KEY] || DEFAULT_STRATEGY
22
+ @context_missing = sanitize_strategy(strategy)
23
+ end
24
+
25
+ # @param [Entity] entity The entity to be stored in the context.
26
+ def store_entity(entity:)
27
+ Thread.current[LOCAL_KEY] = entity
28
+ end
29
+
30
+ # @return [Entity] The current active entity(could be segment or subsegment).
31
+ def current_entity
32
+ if entity = Thread.current[LOCAL_KEY]
33
+ entity
34
+ else
35
+ handle_context_missing
36
+ end
37
+ end
38
+
39
+ # Clear the current thread local storage on X-Ray related entities.
40
+ def clear!
41
+ Thread.current[LOCAL_KEY] = nil
42
+ end
43
+
44
+ # @param [Entity] entity The entity to inject.
45
+ # @param [Thread] target_ctx Put the provided entity to the new thread.
46
+ def inject_context(entity, target_ctx: nil)
47
+ target_ctx ||= Thread.current
48
+ target_ctx[LOCAL_KEY] = entity if entity
49
+ end
50
+
51
+ # When the current entity needs to be accessed but there is none,
52
+ # it handles the missing context based on the configuration.
53
+ # On `RUNTIME_ERROR` it raises `ContextMissingError`.
54
+ # On 'LOG_ERROR' it logs an error message and return `nil`.
55
+ def handle_context_missing
56
+ case context_missing
57
+ when 'RUNTIME_ERROR'
58
+ raise ContextMissingError
59
+ when 'LOG_ERROR'
60
+ logger.error %(can not find the current context.)
61
+ end
62
+ nil
63
+ end
64
+
65
+ def context_missing=(v)
66
+ strategy = ENV[CONTEXT_MISSING_KEY] || v
67
+ @context_missing = sanitize_strategy(strategy)
68
+ end
69
+
70
+ private
71
+
72
+ def sanitize_strategy(v)
73
+ if SUPPORTED_STRATEGY.include?(v)
74
+ v
75
+ else
76
+ logger.warn %(context missing #{v} is not supported, switch to default #{DEFAULT_STRATEGY}.)
77
+ DEFAULT_STRATEGY
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,53 @@
1
+ require 'socket'
2
+ require 'aws-xray-sdk/logger'
3
+ require 'aws-xray-sdk/emitter/emitter'
4
+ require 'aws-xray-sdk/exceptions'
5
+
6
+ module XRay
7
+ # The default emitter the X-Ray recorder uses to send segments/subsegments
8
+ # to the X-Ray daemon over UDP using a non-blocking socket.
9
+ class DefaultEmitter
10
+ include Emitter
11
+ include Logging
12
+
13
+ attr_reader :address
14
+
15
+ def initialize
16
+ @socket = UDPSocket.new
17
+ @address = ENV[DAEMON_ADDRESS_KEY] || '127.0.0.1:2000'
18
+ configure_socket(@address)
19
+ end
20
+
21
+ # Serializes a segment/subsegment and sends it to the X-Ray daemon
22
+ # over UDP. It is no-op for non-sampled entity.
23
+ # @param [Entity] entity The entity to send
24
+ def send_entity(entity:)
25
+ return nil unless entity.sampled
26
+ begin
27
+ payload = %(#{@@protocol_header}#{@@protocol_delimiter}#{entity.to_json})
28
+ logger.debug %(sending payload #{payload} to daemon at #{address}.)
29
+ @socket.send payload, 0
30
+ rescue StandardError => e
31
+ logger.warn %(failed to send payload due to #{e.message})
32
+ end
33
+ end
34
+
35
+ def daemon_address=(v)
36
+ v = ENV[DAEMON_ADDRESS_KEY] || v
37
+ @address = v
38
+ configure_socket(v)
39
+ end
40
+
41
+ private
42
+
43
+ def configure_socket(v)
44
+ begin
45
+ addr = v.split(':')
46
+ host, ip = addr[0], addr[1].to_i
47
+ @socket.connect(host, ip)
48
+ rescue StandardError
49
+ raise InvalidDaemonAddressError, %(Invalid X-Ray daemon address specified: #{v}.)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,24 @@
1
+ require 'json'
2
+
3
+ module XRay
4
+ # The emitter interface the X-Ray recorder uses to send segments/subsegments
5
+ # to the X-Ray daemon over UDP.
6
+ module Emitter
7
+ DAEMON_ADDRESS_KEY = 'AWS_XRAY_DAEMON_ADDRESS'.freeze
8
+
9
+ @@protocol_header = {
10
+ format: 'json',
11
+ version: 1
12
+ }.to_json
13
+ @@protocol_delimiter = "\n"
14
+
15
+ # @param [Entity] entity Entity to send.
16
+ def send_entity(entity:)
17
+ raise 'Not implemented'
18
+ end
19
+
20
+ def daemon_address=(v)
21
+ raise 'Not implemented'
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ module XRay
2
+ # All custom exception thrown by the SDK should subclass AwsXRayError.
3
+ class AwsXRaySdkError < ::StandardError; end
4
+
5
+ class EntityClosedError < AwsXRaySdkError
6
+ def initialize
7
+ super('Segment or subsegment already ended.')
8
+ end
9
+ end
10
+
11
+ class ContextMissingError < AwsXRaySdkError
12
+ def initialize
13
+ super('Can not find any active segment or subsegment.')
14
+ end
15
+ end
16
+
17
+ class SegmentNameMissingError < AwsXRaySdkError
18
+ end
19
+
20
+ class InvalidDaemonAddressError < AwsXRaySdkError
21
+ end
22
+
23
+ class InvalidSamplingConfigError < AwsXRaySdkError
24
+ end
25
+
26
+ class InvalidConfigurationError < AwsXRaySdkError
27
+ end
28
+
29
+ class UnsupportedPatchingTargetError < AwsXRaySdkError
30
+ end
31
+ end
@@ -0,0 +1,127 @@
1
+ require 'aws-sdk-core'
2
+ require 'aws-xray-sdk/facets/helper'
3
+ require 'aws-xray-sdk/facets/resources/aws_params_whitelist'
4
+ require 'aws-xray-sdk/facets/resources/aws_services_whitelist'
5
+
6
+ module XRay
7
+ class AwsSDKPlugin < Seahorse::Client::Plugin
8
+ option :xray_recorder, default: XRay.recorder
9
+
10
+ def add_handlers(handlers, config)
11
+ # run before Seahorse::Client::Plugin::ParamValidator (priority 50)
12
+ handlers.add Handler, step: :validate, priority: 49
13
+ end
14
+
15
+ # Handler to capture AWS API calls as subsegments
16
+ class Handler < Seahorse::Client::Handler
17
+ include XRay::Facets::Helper
18
+
19
+ def call(context)
20
+ recorder = Aws.config[:xray_recorder]
21
+ operation = context.operation_name
22
+ service_name = context.client.class.api.metadata['serviceAbbreviation'] ||
23
+ context.client.class.to_s.split('::')[1]
24
+ recorder.capture service_name, namespace: 'aws' do |subsegment|
25
+ # inject header string before calling downstream AWS services
26
+ context.http_request.headers[TRACE_HEADER] = prep_header_str entity: subsegment
27
+ response = @handler.call(context)
28
+ http_response = context.http_response
29
+ resp_meta = {
30
+ status: http_response.status_code,
31
+ content_length: http_response.headers['content-length'].to_i
32
+ }
33
+ aws = {
34
+ # XRay back-end right now has strict operation name matching
35
+ operation: sanitize_op_name(operation),
36
+ region: context.client.config.region,
37
+ retries: context.retries,
38
+ request_id: http_response.headers['x-amzn-requestid']
39
+ }
40
+ # S3 returns special request id in response headers
41
+ if service_name == 'S3'
42
+ aws[:id_2] = http_response.headers['x-amz-id-2']
43
+ end
44
+
45
+ operation_h = AwsParams.whitelist[:services]
46
+ .fetch(service_name.to_sym, {})
47
+ .fetch(:operations, {})[operation]
48
+ unless operation_h.nil?
49
+ params_capture req_params: context.params, resp_params: response.to_h,
50
+ capture: operation_h, meta: aws
51
+ end
52
+ subsegment.aws = aws
53
+ if err = response.error
54
+ subsegment.add_exception exception: err, remote: true
55
+ end
56
+ subsegment.merge_http_response response: resp_meta
57
+ response
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def inject_headers(request:, entity:)
64
+ request.headers[TRACE_HEADER] = prep_header_str entity: entity
65
+ end
66
+
67
+ def sanitize_op_name(opname)
68
+ opname.to_s.split('_').collect(&:capitalize).join if opname
69
+ end
70
+
71
+ def params_capture(req_params:, resp_params:, capture:, meta:)
72
+ if norm = capture[:request_parameters]
73
+ capture_normal params: req_params, capture: norm, meta: meta
74
+ end
75
+
76
+ if norm = capture[:response_parameters]
77
+ capture_normal params: resp_params, capture: norm, meta: meta
78
+ end
79
+
80
+ if spec = capture[:request_descriptors]
81
+ capture_special params: req_params, capture: spec, meta: meta
82
+ end
83
+
84
+ if spec = capture[:response_descriptors]
85
+ capture_special params: resp_params, capture: spec, meta: meta
86
+ end
87
+ end
88
+
89
+ def capture_normal(params:, capture:, meta:)
90
+ params.each_key do |key|
91
+ meta[key] = params[key] if capture.include?(key)
92
+ end
93
+ end
94
+
95
+ def capture_special(params:, capture:, meta:)
96
+ params.each_key do |key|
97
+ process_descriptor(target: params[key], descriptor: capture[key], meta: meta) if capture.include?(key)
98
+ end
99
+ end
100
+
101
+ def process_descriptor(target:, descriptor:, meta:)
102
+ # "get_count" = true
103
+ v = target.length if descriptor[:get_count]
104
+ # "get_keys" = true
105
+ v = target.keys if descriptor[:get_keys]
106
+ meta[descriptor[:rename_to]] = v
107
+ end
108
+ end
109
+ end
110
+
111
+ # Add X-Ray plugin to AWS SDK clients
112
+ module AwsSDKPatcher
113
+ def self.patch(services: nil, recorder: XRay.recorder)
114
+ force = services.nil?
115
+ services ||= AwsServices.whitelist
116
+ services.each do |s|
117
+ begin
118
+ Aws.const_get(%(#{s}::Client)).add_plugin XRay::AwsSDKPlugin
119
+ Aws.config.update xray_recorder: recorder
120
+ rescue NameError
121
+ # swallow the error if no explicit user config
122
+ raise unless force
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end