aws-xray-sdk 0.10.2 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/aws-xray-sdk/configuration.rb +10 -7
- data/lib/aws-xray-sdk/daemon_config.rb +59 -0
- data/lib/aws-xray-sdk/emitter/default_emitter.rb +10 -21
- data/lib/aws-xray-sdk/emitter/emitter.rb +1 -3
- data/lib/aws-xray-sdk/facets/aws_sdk.rb +14 -6
- data/lib/aws-xray-sdk/facets/helper.rb +4 -11
- data/lib/aws-xray-sdk/facets/net_http.rb +4 -0
- data/lib/aws-xray-sdk/facets/rack.rb +8 -6
- data/lib/aws-xray-sdk/facets/rails/railtie.rb +1 -1
- data/lib/aws-xray-sdk/model/cause.rb +2 -2
- data/lib/aws-xray-sdk/model/dummy_entities.rb +4 -0
- data/lib/aws-xray-sdk/model/entity.rb +2 -2
- data/lib/aws-xray-sdk/model/metadata.rb +2 -2
- data/lib/aws-xray-sdk/model/segment.rb +8 -1
- data/lib/aws-xray-sdk/plugins/elastic_beanstalk.rb +2 -2
- data/lib/aws-xray-sdk/recorder.rb +7 -4
- data/lib/aws-xray-sdk/sampling/connector.rb +72 -0
- data/lib/aws-xray-sdk/sampling/default_sampler.rb +72 -78
- data/lib/aws-xray-sdk/sampling/lead_poller.rb +72 -0
- data/lib/aws-xray-sdk/sampling/local/reservoir.rb +35 -0
- data/lib/aws-xray-sdk/sampling/local/sampler.rb +110 -0
- data/lib/aws-xray-sdk/sampling/local/sampling_rule.rb +63 -0
- data/lib/aws-xray-sdk/sampling/reservoir.rb +68 -24
- data/lib/aws-xray-sdk/sampling/rule_cache.rb +86 -0
- data/lib/aws-xray-sdk/sampling/rule_poller.rb +39 -0
- data/lib/aws-xray-sdk/sampling/sampler.rb +3 -3
- data/lib/aws-xray-sdk/sampling/sampling_decision.rb +8 -0
- data/lib/aws-xray-sdk/sampling/sampling_rule.rb +102 -35
- data/lib/aws-xray-sdk/version.rb +1 -1
- metadata +34 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91c311c0f94185bc3f4a17afbfde8f7a4f881503feeb7eb116a43631c6649efc
|
4
|
+
data.tar.gz: 4a2791948dc071dfa2e57f410eb60311ff812aa276f0968dfc85db28632a2565
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c9caa633a61621f6c17a281c416855aef63033f8c8119589c0c1bff70218c80f7b84234251f97be5a7cf5b3d39a1d0dc8c54fb8c619ec46c9ab37aabb323bf8
|
7
|
+
data.tar.gz: 42cce4783e4dfa257317a1c3b6bca7fd52e2b4b1521b313048b1c8e1037a9bb85596e2b70a15fca54b15777fc941b380114bf5deec452062a1ea5bc73ac4b4ef
|
@@ -2,6 +2,7 @@ require 'aws-xray-sdk/exceptions'
|
|
2
2
|
require 'aws-xray-sdk/patcher'
|
3
3
|
require 'aws-xray-sdk/emitter/default_emitter'
|
4
4
|
require 'aws-xray-sdk/context/default_context'
|
5
|
+
require 'aws-xray-sdk/daemon_config'
|
5
6
|
require 'aws-xray-sdk/sampling/default_sampler'
|
6
7
|
require 'aws-xray-sdk/streaming/default_streamer'
|
7
8
|
require 'aws-xray-sdk/segment_naming/dynamic_naming'
|
@@ -17,9 +18,9 @@ module XRay
|
|
17
18
|
include Patcher
|
18
19
|
|
19
20
|
SEGMENT_NAME_KEY = 'AWS_XRAY_TRACING_NAME'.freeze
|
20
|
-
CONFIG_KEY = %I[logger name sampling plugins daemon_address
|
21
|
-
naming_pattern emitter streamer context
|
22
|
-
sampling_rules stream_threshold patch].freeze
|
21
|
+
CONFIG_KEY = %I[logger name sampling plugins daemon_address
|
22
|
+
segment_naming naming_pattern emitter streamer context
|
23
|
+
context_missing sampling_rules stream_threshold patch].freeze
|
23
24
|
|
24
25
|
def initialize
|
25
26
|
@name = ENV[SEGMENT_NAME_KEY]
|
@@ -38,9 +39,12 @@ module XRay
|
|
38
39
|
@name = ENV[SEGMENT_NAME_KEY] || v
|
39
40
|
end
|
40
41
|
|
41
|
-
#
|
42
|
+
# setting daemon address for components communicate with X-Ray daemon.
|
42
43
|
def daemon_address=(v)
|
43
|
-
|
44
|
+
v = ENV[DaemonConfig::DAEMON_ADDRESS_KEY] || v
|
45
|
+
config = DaemonConfig.new(v)
|
46
|
+
emitter.daemon_config = config
|
47
|
+
sampler.daemon_config = config if sampler.respond_to?(:daemon_config=)
|
44
48
|
end
|
45
49
|
|
46
50
|
# proxy method to the context's context_missing config.
|
@@ -63,8 +67,7 @@ module XRay
|
|
63
67
|
segment_naming.pattern = v
|
64
68
|
end
|
65
69
|
|
66
|
-
# makes a sampling decision
|
67
|
-
# if sampling enabled and the default sampling rule.
|
70
|
+
# makes a sampling decision without incoming filters.
|
68
71
|
def sample?
|
69
72
|
return true unless sampling
|
70
73
|
sampler.sample?
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'aws-xray-sdk/exceptions'
|
2
|
+
|
3
|
+
module XRay
|
4
|
+
# The class that stores X-Ray daemon configuration about
|
5
|
+
# the ip address and port for UDP and TCP port. It gets the address
|
6
|
+
# string from `AWS_XRAY_DAEMON_ADDRESS` and then from recorder's
|
7
|
+
# configuration for `daemon_address`.
|
8
|
+
# A notation of `127.0.0.1:2000` or `tcp:127.0.0.1:2000 udp:127.0.0.2:2001`
|
9
|
+
# are both acceptable. The former one means UDP and TCP are running at
|
10
|
+
# the same address. By default it assumes a X-Ray daemon
|
11
|
+
# running at `127.0.0.1:2000` listening to both UDP and TCP traffic.
|
12
|
+
class DaemonConfig
|
13
|
+
DAEMON_ADDRESS_KEY = 'AWS_XRAY_DAEMON_ADDRESS'.freeze
|
14
|
+
attr_reader :tcp_ip, :tcp_port, :udp_ip, :udp_port
|
15
|
+
@@dafault_addr = '127.0.0.1:2000'
|
16
|
+
|
17
|
+
def initialize(addr: @@dafault_addr)
|
18
|
+
update_address(addr)
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_address(v)
|
22
|
+
v = ENV[DAEMON_ADDRESS_KEY] || v
|
23
|
+
update_addr(v)
|
24
|
+
rescue StandardError
|
25
|
+
raise InvalidDaemonAddressError, %(Invalid X-Ray daemon address specified: #{v}.)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def update_addr(v)
|
31
|
+
parts = v.split(' ')
|
32
|
+
if parts.length == 1 # format of '127.0.0.1:2000'
|
33
|
+
addr = parts[0].split(':')
|
34
|
+
raise InvalidDaemonAddressError unless addr.length == 2
|
35
|
+
@tcp_ip = addr[0]
|
36
|
+
@tcp_port = addr[1].to_i
|
37
|
+
@udp_ip = addr[0]
|
38
|
+
@udp_port = addr[1].to_i
|
39
|
+
else
|
40
|
+
set_tcp_udp(parts) # format of 'tcp:127.0.0.1:2000 udp:127.0.0.2:2001'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_tcp_udp(parts)
|
45
|
+
part1 = parts[0]
|
46
|
+
part2 = parts[1]
|
47
|
+
key1 = part1.split(':')[0]
|
48
|
+
key2 = part2.split(':')[0]
|
49
|
+
addr_h = {}
|
50
|
+
addr_h[key1] = part1.split(':')
|
51
|
+
addr_h[key2] = part2.split(':')
|
52
|
+
|
53
|
+
@tcp_ip = addr_h['tcp'][1]
|
54
|
+
@tcp_port = addr_h['tcp'][2].to_i
|
55
|
+
@udp_ip = addr_h['udp'][1]
|
56
|
+
@udp_port = addr_h['udp'][2].to_i
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -2,6 +2,7 @@ require 'socket'
|
|
2
2
|
require 'aws-xray-sdk/logger'
|
3
3
|
require 'aws-xray-sdk/emitter/emitter'
|
4
4
|
require 'aws-xray-sdk/exceptions'
|
5
|
+
require 'aws-xray-sdk/daemon_config'
|
5
6
|
|
6
7
|
module XRay
|
7
8
|
# The default emitter the X-Ray recorder uses to send segments/subsegments
|
@@ -10,12 +11,11 @@ module XRay
|
|
10
11
|
include Emitter
|
11
12
|
include Logging
|
12
13
|
|
13
|
-
attr_reader :
|
14
|
+
attr_reader :daemon_config
|
14
15
|
|
15
|
-
def initialize
|
16
|
+
def initialize(daemon_config: DaemonConfig.new)
|
16
17
|
@socket = UDPSocket.new
|
17
|
-
|
18
|
-
configure_socket(@address)
|
18
|
+
self.daemon_config = daemon_config
|
19
19
|
end
|
20
20
|
|
21
21
|
# Serializes a segment/subsegment and sends it to the X-Ray daemon
|
@@ -25,29 +25,18 @@ module XRay
|
|
25
25
|
return nil unless entity.sampled
|
26
26
|
begin
|
27
27
|
payload = %(#{@@protocol_header}#{@@protocol_delimiter}#{entity.to_json})
|
28
|
-
logger.debug %(sending payload #{payload} to daemon at #{address}.)
|
28
|
+
logger.debug %(sending payload #{payload} to daemon at #{@address}.)
|
29
29
|
@socket.send payload, 0
|
30
30
|
rescue StandardError => e
|
31
31
|
logger.warn %(failed to send payload due to #{e.message})
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
def
|
36
|
-
|
37
|
-
@
|
38
|
-
|
39
|
-
|
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
|
35
|
+
def daemon_config=(v)
|
36
|
+
@address = %(#{v.udp_ip}:#{v.udp_port})
|
37
|
+
@socket.connect(v.udp_ip, v.udp_port)
|
38
|
+
rescue StandardError
|
39
|
+
raise InvalidDaemonAddressError, %(Invalid X-Ray daemon address specified: #{v}.)
|
51
40
|
end
|
52
41
|
end
|
53
42
|
end
|
@@ -4,8 +4,6 @@ module XRay
|
|
4
4
|
# The emitter interface the X-Ray recorder uses to send segments/subsegments
|
5
5
|
# to the X-Ray daemon over UDP.
|
6
6
|
module Emitter
|
7
|
-
DAEMON_ADDRESS_KEY = 'AWS_XRAY_DAEMON_ADDRESS'.freeze
|
8
|
-
|
9
7
|
@@protocol_header = {
|
10
8
|
format: 'json',
|
11
9
|
version: 1
|
@@ -17,7 +15,7 @@ module XRay
|
|
17
15
|
raise 'Not implemented'
|
18
16
|
end
|
19
17
|
|
20
|
-
def
|
18
|
+
def daemon_config=(v)
|
21
19
|
raise 'Not implemented'
|
22
20
|
end
|
23
21
|
end
|
@@ -17,15 +17,19 @@ module XRay
|
|
17
17
|
include XRay::Facets::Helper
|
18
18
|
|
19
19
|
def call(context)
|
20
|
-
recorder = Aws.config[:xray_recorder]
|
21
|
-
if recorder.current_entity.nil?
|
22
|
-
super
|
23
|
-
end
|
24
|
-
|
25
20
|
operation = context.operation_name
|
26
21
|
service_name = context.client.class.api.metadata['serviceAbbreviation'] ||
|
27
22
|
context.client.class.to_s.split('::')[1]
|
28
|
-
|
23
|
+
if skip?(service_name, operation)
|
24
|
+
return super
|
25
|
+
end
|
26
|
+
|
27
|
+
recorder = Aws.config[:xray_recorder]
|
28
|
+
if recorder.nil? || recorder.current_entity.nil?
|
29
|
+
return super
|
30
|
+
end
|
31
|
+
|
32
|
+
recorder.capture(service_name, namespace: 'aws') do |subsegment|
|
29
33
|
# inject header string before calling downstream AWS services
|
30
34
|
context.http_request.headers[TRACE_HEADER] = prep_header_str entity: subsegment
|
31
35
|
response = @handler.call(context)
|
@@ -109,6 +113,10 @@ module XRay
|
|
109
113
|
v = target.keys if descriptor[:get_keys]
|
110
114
|
meta[descriptor[:rename_to]] = v
|
111
115
|
end
|
116
|
+
|
117
|
+
def skip?(service, op)
|
118
|
+
return service == 'XRay' && (op == :get_sampling_rules || op == :get_sampling_targets)
|
119
|
+
end
|
112
120
|
end
|
113
121
|
end
|
114
122
|
|
@@ -26,21 +26,14 @@ module XRay
|
|
26
26
|
# the highest precedence. If the `trace_header` doesn't contain
|
27
27
|
# sampling decision then it checks if sampling is enabled or not
|
28
28
|
# in the recorder. If not enbaled it returns 'true'. Otherwise it uses
|
29
|
-
# sampling
|
30
|
-
def should_sample?(header_obj:, recorder:,
|
31
|
-
host: nil, method: nil, path: nil,
|
32
|
-
**args)
|
29
|
+
# sampling rules to decide.
|
30
|
+
def should_sample?(header_obj:, recorder:, sampling_req:, **args)
|
33
31
|
# check outside decision
|
34
32
|
if i = header_obj.sampled
|
35
|
-
|
36
|
-
false
|
37
|
-
else
|
38
|
-
true
|
39
|
-
end
|
33
|
+
!i.zero?
|
40
34
|
# check sampling rules
|
41
35
|
elsif recorder.sampling_enabled?
|
42
|
-
recorder.sampler.sample_request?(
|
43
|
-
http_method: method)
|
36
|
+
recorder.sampler.sample_request?(sampling_req)
|
44
37
|
# sample if no reason not to
|
45
38
|
else
|
46
39
|
true
|
@@ -21,6 +21,10 @@ module XRay
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def request(req, body = nil, &block)
|
24
|
+
if req.path && (req.path == ('/GetSamplingRules') || req.path == ('/SamplingTargets'))
|
25
|
+
return super
|
26
|
+
end
|
27
|
+
|
24
28
|
entity = XRay.recorder.current_entity
|
25
29
|
capture = !(entity && entity.namespace && entity.namespace == 'aws'.freeze)
|
26
30
|
if started? && capture && entity
|
@@ -8,6 +8,7 @@ module XRay
|
|
8
8
|
class Middleware
|
9
9
|
include XRay::Facets::Helper
|
10
10
|
X_FORWARD = 'HTTP_X_FORWARDED_FOR'.freeze
|
11
|
+
SCHEME_SEPARATOR = "://".freeze
|
11
12
|
|
12
13
|
def initialize(app, recorder: nil)
|
13
14
|
@app = app
|
@@ -22,16 +23,15 @@ module XRay
|
|
22
23
|
host = req.host
|
23
24
|
url_path = req.path
|
24
25
|
method = req.request_method
|
26
|
+
# get segment name from host header if applicable
|
27
|
+
seg_name = @recorder.segment_naming.provide_name(host: req.host)
|
25
28
|
|
26
29
|
# get sampling decision
|
27
30
|
sampled = should_sample?(
|
28
|
-
header_obj: header, recorder: @recorder,
|
29
|
-
host: host,
|
31
|
+
header_obj: header, recorder: @recorder, sampling_req:
|
32
|
+
{ host: host, http_method: method, url_path: url_path, service: seg_name }
|
30
33
|
)
|
31
34
|
|
32
|
-
# get segment name from host header if applicable
|
33
|
-
seg_name = @recorder.segment_naming.provide_name(host: req.host)
|
34
|
-
|
35
35
|
# begin the segment
|
36
36
|
segment = @recorder.begin_segment seg_name, trace_id: header.root, parent_id: header.parent_id,
|
37
37
|
sampled: sampled
|
@@ -63,7 +63,9 @@ module XRay
|
|
63
63
|
|
64
64
|
def extract_request_meta(req)
|
65
65
|
req_meta = {}
|
66
|
-
req_meta[:url] = req.
|
66
|
+
req_meta[:url] = req.scheme + SCHEME_SEPARATOR if req.scheme
|
67
|
+
req_meta[:url] += req.host_with_port if req.host_with_port
|
68
|
+
req_meta[:url] += req.path if req.path
|
67
69
|
req_meta[:user_agent] = req.user_agent if req.user_agent
|
68
70
|
req_meta[:method] = req.request_method if req.request_method
|
69
71
|
if req.has_header?(X_FORWARD)
|
@@ -6,7 +6,7 @@ module XRay
|
|
6
6
|
class Railtie < ::Rails::Railtie
|
7
7
|
RAILS_OPTIONS = %I[active_record].freeze
|
8
8
|
|
9
|
-
initializer
|
9
|
+
initializer("aws-xray-sdk.rack_middleware") do |app|
|
10
10
|
app.middleware.insert 0, Rack::Middleware
|
11
11
|
app.middleware.use XRay::Rails::ExceptionMiddleware
|
12
12
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'multi_json'
|
2
2
|
|
3
3
|
module XRay
|
4
4
|
# Represents cause section in segment and subsegment document.
|
@@ -26,7 +26,7 @@ module XRay
|
|
26
26
|
|
27
27
|
def to_json
|
28
28
|
@to_json ||= begin
|
29
|
-
|
29
|
+
MultiJson.dump to_h
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'securerandom'
|
2
2
|
require 'bigdecimal'
|
3
|
-
require '
|
3
|
+
require 'multi_json'
|
4
4
|
require 'aws-xray-sdk/exceptions'
|
5
5
|
require 'aws-xray-sdk/model/cause'
|
6
6
|
require 'aws-xray-sdk/model/annotations'
|
@@ -168,7 +168,7 @@ module XRay
|
|
168
168
|
|
169
169
|
def to_json
|
170
170
|
@to_json ||= begin
|
171
|
-
|
171
|
+
MultiJson.dump(to_h)
|
172
172
|
end
|
173
173
|
end
|
174
174
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'multi_json'
|
2
2
|
require 'aws-xray-sdk/exceptions'
|
3
3
|
|
4
4
|
module XRay
|
@@ -48,7 +48,7 @@ module XRay
|
|
48
48
|
|
49
49
|
def to_json
|
50
50
|
@to_json ||= begin
|
51
|
-
|
51
|
+
MultiJson.dump to_h
|
52
52
|
end
|
53
53
|
end
|
54
54
|
end
|
@@ -6,7 +6,8 @@ module XRay
|
|
6
6
|
# details about the request, and details about the work done.
|
7
7
|
class Segment
|
8
8
|
include Entity
|
9
|
-
attr_accessor :ref_counter, :subsegment_size, :origin,
|
9
|
+
attr_accessor :ref_counter, :subsegment_size, :origin,
|
10
|
+
:user, :service
|
10
11
|
|
11
12
|
# @param [String] trace_id Manually crafted trace id.
|
12
13
|
# @param [String] name Must be specified either on object creation or
|
@@ -39,6 +40,12 @@ module XRay
|
|
39
40
|
@subsegment_size = subsegment_size - subsegment.all_children_count - 1
|
40
41
|
end
|
41
42
|
|
43
|
+
def sampling_rule_name=(v)
|
44
|
+
@aws ||= {}
|
45
|
+
@aws[:xray] ||= {}
|
46
|
+
@aws[:xray][:sampling_rule_name] = v
|
47
|
+
end
|
48
|
+
|
42
49
|
def decrement_ref_counter
|
43
50
|
@ref_counter -= 1
|
44
51
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'multi_json'
|
2
2
|
require 'aws-xray-sdk/logger'
|
3
3
|
|
4
4
|
module XRay
|
@@ -14,7 +14,7 @@ module XRay
|
|
14
14
|
def self.aws
|
15
15
|
@@aws ||= begin
|
16
16
|
file = File.open(CONF_PATH)
|
17
|
-
{ elastic_beanstalk:
|
17
|
+
{ elastic_beanstalk: MultiJson.load(file) }
|
18
18
|
rescue StandardError => e
|
19
19
|
@@aws = {}
|
20
20
|
Logging.logger.warn %(can not get the environment config due to: #{e.message}.)
|
@@ -12,11 +12,12 @@ module XRay
|
|
12
12
|
# and send them to the X-Ray daemon. It is also responsible for managing
|
13
13
|
# context.
|
14
14
|
class Recorder
|
15
|
-
attr_reader :config
|
15
|
+
attr_reader :config, :origin
|
16
16
|
|
17
17
|
def initialize(user_config: nil)
|
18
18
|
@config = Configuration.new
|
19
19
|
@config.configure(user_config) unless user_config.nil?
|
20
|
+
@origin = nil
|
20
21
|
end
|
21
22
|
|
22
23
|
# Begin a segment for the current context. The recorder
|
@@ -31,7 +32,7 @@ module XRay
|
|
31
32
|
sample = sampled.nil? ? config.sample? : sampled
|
32
33
|
if sample
|
33
34
|
segment = Segment.new name: seg_name, trace_id: trace_id, parent_id: parent_id
|
34
|
-
populate_runtime_context(segment)
|
35
|
+
populate_runtime_context(segment, sample)
|
35
36
|
else
|
36
37
|
segment = DummySegment.new name: seg_name, trace_id: trace_id, parent_id: parent_id
|
37
38
|
end
|
@@ -204,14 +205,14 @@ module XRay
|
|
204
205
|
|
205
206
|
private_class_method
|
206
207
|
|
207
|
-
def populate_runtime_context(segment)
|
208
|
+
def populate_runtime_context(segment, sample)
|
208
209
|
@aws ||= begin
|
209
210
|
aws = {}
|
210
211
|
config.plugins.each do |p|
|
211
212
|
meta = p.aws
|
212
213
|
if meta.is_a?(Hash) && !meta.empty?
|
213
214
|
aws.merge! meta
|
214
|
-
|
215
|
+
@origin = p::ORIGIN
|
215
216
|
end
|
216
217
|
end
|
217
218
|
xray_meta = { xray:
|
@@ -230,6 +231,8 @@ module XRay
|
|
230
231
|
|
231
232
|
segment.aws = @aws
|
232
233
|
segment.service = @service
|
234
|
+
segment.origin = @origin
|
235
|
+
segment.sampling_rule_name = sample if sample.is_a?(String)
|
233
236
|
end
|
234
237
|
end
|
235
238
|
end
|