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