appsignal 3.0.0.beta.1-java → 3.0.3-java
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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +1 -1
- data/.semaphore/semaphore.yml +88 -88
- data/CHANGELOG.md +41 -1
- data/Rakefile +12 -4
- data/appsignal.gemspec +7 -5
- data/build_matrix.yml +11 -11
- data/ext/agent.yml +17 -17
- data/gemfiles/no_dependencies.gemfile +0 -7
- data/lib/appsignal.rb +1 -2
- data/lib/appsignal/config.rb +1 -1
- data/lib/appsignal/extension.rb +50 -0
- data/lib/appsignal/helpers/instrumentation.rb +69 -5
- data/lib/appsignal/hooks.rb +16 -0
- data/lib/appsignal/hooks/action_cable.rb +10 -2
- data/lib/appsignal/hooks/sidekiq.rb +9 -142
- data/lib/appsignal/integrations/object.rb +21 -43
- data/lib/appsignal/integrations/railtie.rb +0 -4
- data/lib/appsignal/integrations/sidekiq.rb +171 -0
- data/lib/appsignal/minutely.rb +6 -0
- data/lib/appsignal/transaction.rb +2 -2
- data/lib/appsignal/version.rb +1 -1
- data/spec/lib/appsignal/config_spec.rb +2 -0
- data/spec/lib/appsignal/extension_install_failure_spec.rb +0 -7
- data/spec/lib/appsignal/extension_spec.rb +43 -9
- data/spec/lib/appsignal/hooks/action_cable_spec.rb +88 -0
- data/spec/lib/appsignal/hooks/sidekiq_spec.rb +60 -458
- data/spec/lib/appsignal/hooks_spec.rb +41 -0
- data/spec/lib/appsignal/integrations/object_spec.rb +91 -4
- data/spec/lib/appsignal/integrations/sidekiq_spec.rb +524 -0
- data/spec/lib/appsignal/transaction_spec.rb +17 -0
- data/spec/lib/appsignal/utils/data_spec.rb +133 -87
- data/spec/lib/appsignal_spec.rb +162 -47
- data/spec/lib/puma/appsignal_spec.rb +28 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/testing.rb +11 -1
- metadata +9 -8
- data/gemfiles/rails-4.0.gemfile +0 -6
- data/gemfiles/rails-4.1.gemfile +0 -6
data/lib/appsignal/hooks.rb
CHANGED
@@ -69,6 +69,22 @@ module Appsignal
|
|
69
69
|
text.size > 200 ? "#{text[0...197]}..." : text
|
70
70
|
end
|
71
71
|
end
|
72
|
+
|
73
|
+
# Alias integration constants that have moved to their own module.
|
74
|
+
def self.const_missing(name)
|
75
|
+
case name
|
76
|
+
when :SidekiqPlugin
|
77
|
+
require "appsignal/integrations/sidekiq"
|
78
|
+
callers = caller
|
79
|
+
Appsignal::Utils::DeprecationMessage.message \
|
80
|
+
"The constant Appsignal::Hooks::SidekiqPlugin has been deprecated. " \
|
81
|
+
"Please update the constant name to Appsignal::Integrations::SidekiqMiddleware " \
|
82
|
+
"in the following file to remove this message.\n#{callers.first}"
|
83
|
+
Appsignal::Integrations::SidekiqMiddleware
|
84
|
+
else
|
85
|
+
super
|
86
|
+
end
|
87
|
+
end
|
72
88
|
end
|
73
89
|
end
|
74
90
|
|
@@ -25,7 +25,11 @@ module Appsignal
|
|
25
25
|
def install_callbacks
|
26
26
|
ActionCable::Channel::Base.set_callback :subscribe, :around, :prepend => true do |channel, inner|
|
27
27
|
# The request is only the original websocket request
|
28
|
-
|
28
|
+
connection = channel.connection
|
29
|
+
# #env is not available on the Rails ConnectionStub class used in the
|
30
|
+
# Rails app test suite. If we call `#env` it causes an error to occur
|
31
|
+
# in apps' test suites.
|
32
|
+
env = connection.respond_to?(:env) ? connection.env : {}
|
29
33
|
request = ActionDispatch::Request.new(env)
|
30
34
|
env[Appsignal::Hooks::ActionCableHook::REQUEST_ID] ||=
|
31
35
|
request.request_id || SecureRandom.uuid
|
@@ -53,7 +57,11 @@ module Appsignal
|
|
53
57
|
|
54
58
|
ActionCable::Channel::Base.set_callback :unsubscribe, :around, :prepend => true do |channel, inner|
|
55
59
|
# The request is only the original websocket request
|
56
|
-
|
60
|
+
connection = channel.connection
|
61
|
+
# #env is not available on the Rails ConnectionStub class used in the
|
62
|
+
# Rails app test suite. If we call `#env` it causes an error to occur
|
63
|
+
# in apps' test suites.
|
64
|
+
env = connection.respond_to?(:env) ? connection.env : {}
|
57
65
|
request = ActionDispatch::Request.new(env)
|
58
66
|
env[Appsignal::Hooks::ActionCableHook::REQUEST_ID] ||=
|
59
67
|
request.request_id || SecureRandom.uuid
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "yaml"
|
4
|
-
|
5
3
|
module Appsignal
|
6
4
|
class Hooks
|
7
5
|
class SidekiqHook < Appsignal::Hooks::Hook
|
@@ -12,153 +10,22 @@ module Appsignal
|
|
12
10
|
end
|
13
11
|
|
14
12
|
def install
|
13
|
+
require "appsignal/integrations/sidekiq"
|
15
14
|
Appsignal::Minutely.probes.register :sidekiq, Appsignal::Probes::SidekiqProbe
|
16
15
|
|
17
16
|
::Sidekiq.configure_server do |config|
|
18
|
-
config.
|
19
|
-
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
# @api private
|
26
|
-
class SidekiqPlugin # rubocop:disable Metrics/ClassLength
|
27
|
-
include Appsignal::Hooks::Helpers
|
28
|
-
|
29
|
-
EXCLUDED_JOB_KEYS = %w[
|
30
|
-
args backtrace class created_at enqueued_at error_backtrace error_class
|
31
|
-
error_message failed_at jid retried_at retry wrapped
|
32
|
-
].freeze
|
33
|
-
|
34
|
-
def call(_worker, item, _queue)
|
35
|
-
job_status = nil
|
36
|
-
transaction = Appsignal::Transaction.create(
|
37
|
-
item["jid"],
|
38
|
-
Appsignal::Transaction::BACKGROUND_JOB,
|
39
|
-
Appsignal::Transaction::GenericRequest.new(
|
40
|
-
:queue_start => item["enqueued_at"]
|
41
|
-
)
|
42
|
-
)
|
43
|
-
|
44
|
-
Appsignal.instrument "perform_job.sidekiq" do
|
45
|
-
begin
|
46
|
-
yield
|
47
|
-
rescue Exception => exception # rubocop:disable Lint/RescueException
|
48
|
-
job_status = :failed
|
49
|
-
transaction.set_error(exception)
|
50
|
-
raise exception
|
51
|
-
end
|
52
|
-
end
|
53
|
-
ensure
|
54
|
-
if transaction
|
55
|
-
transaction.set_action_if_nil(formatted_action_name(item))
|
56
|
-
|
57
|
-
params = filtered_arguments(item)
|
58
|
-
transaction.params = params if params
|
59
|
-
|
60
|
-
formatted_metadata(item).each do |key, value|
|
61
|
-
transaction.set_metadata key, value
|
62
|
-
end
|
63
|
-
transaction.set_http_or_background_queue_start
|
64
|
-
Appsignal::Transaction.complete_current!
|
65
|
-
queue = item["queue"] || "unknown"
|
66
|
-
if job_status
|
67
|
-
increment_counter "queue_job_count", 1,
|
68
|
-
:queue => queue,
|
69
|
-
:status => job_status
|
70
|
-
end
|
71
|
-
increment_counter "queue_job_count", 1,
|
72
|
-
:queue => queue,
|
73
|
-
:status => :processed
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
private
|
78
|
-
|
79
|
-
def increment_counter(key, value, tags = {})
|
80
|
-
Appsignal.increment_counter "sidekiq_#{key}", value, tags
|
81
|
-
end
|
17
|
+
config.error_handlers << \
|
18
|
+
Appsignal::Integrations::SidekiqErrorHandler.new
|
82
19
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
"#{sidekiq_action_name}#perform"
|
91
|
-
end
|
92
|
-
|
93
|
-
def filtered_arguments(job)
|
94
|
-
arguments = parse_arguments(job)
|
95
|
-
return unless arguments
|
96
|
-
|
97
|
-
Appsignal::Utils::HashSanitizer.sanitize(
|
98
|
-
arguments,
|
99
|
-
Appsignal.config[:filter_parameters]
|
100
|
-
)
|
101
|
-
end
|
102
|
-
|
103
|
-
def formatted_metadata(item)
|
104
|
-
{}.tap do |hash|
|
105
|
-
(item || {}).each do |key, value|
|
106
|
-
next if EXCLUDED_JOB_KEYS.include?(key)
|
107
|
-
|
108
|
-
hash[key] = truncate(string_or_inspect(value))
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
# Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L316-L334
|
114
|
-
def parse_action_name(job)
|
115
|
-
args = job.fetch("args", [])
|
116
|
-
job_class = job["class"]
|
117
|
-
case job_class
|
118
|
-
when "Sidekiq::Extensions::DelayedModel"
|
119
|
-
safe_load(args[0], job_class) do |target, method, _|
|
120
|
-
"#{target.class}##{method}"
|
121
|
-
end
|
122
|
-
when /\ASidekiq::Extensions::Delayed/
|
123
|
-
safe_load(args[0], job_class) do |target, method, _|
|
124
|
-
"#{target}.#{method}"
|
125
|
-
end
|
126
|
-
else
|
127
|
-
job_class
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L336-L358
|
132
|
-
def parse_arguments(job)
|
133
|
-
args = job.fetch("args", [])
|
134
|
-
case job["class"]
|
135
|
-
when /\ASidekiq::Extensions::Delayed/
|
136
|
-
safe_load(args[0], args) do |_, _, arg|
|
137
|
-
arg
|
138
|
-
end
|
139
|
-
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
140
|
-
nil # Set in the ActiveJob integration
|
141
|
-
else
|
142
|
-
# Sidekiq Enterprise argument encryption.
|
143
|
-
# More information: https://github.com/mperham/sidekiq/wiki/Ent-Encryption
|
144
|
-
if job["encrypt".freeze]
|
145
|
-
# No point in showing 150+ bytes of random garbage
|
146
|
-
args[-1] = "[encrypted data]".freeze
|
20
|
+
config.server_middleware do |chain|
|
21
|
+
if chain.respond_to? :prepend
|
22
|
+
chain.prepend Appsignal::Integrations::SidekiqMiddleware
|
23
|
+
else
|
24
|
+
chain.add Appsignal::Integrations::SidekiqMiddleware
|
25
|
+
end
|
147
26
|
end
|
148
|
-
args
|
149
27
|
end
|
150
28
|
end
|
151
|
-
|
152
|
-
# Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L403-L412
|
153
|
-
def safe_load(content, default)
|
154
|
-
yield(*YAML.load(content))
|
155
|
-
rescue => error
|
156
|
-
# Sidekiq issue #1761: in dev mode, it's possible to have jobs enqueued
|
157
|
-
# which haven't been loaded into memory yet so the YAML can't be
|
158
|
-
# loaded.
|
159
|
-
Appsignal.logger.warn "Unable to load YAML: #{error.message}"
|
160
|
-
default
|
161
|
-
end
|
162
29
|
end
|
163
30
|
end
|
164
31
|
end
|
@@ -5,56 +5,34 @@ if defined?(Appsignal)
|
|
5
5
|
end
|
6
6
|
|
7
7
|
class Object
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
"#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
|
15
|
-
end
|
16
|
-
Appsignal.instrument name do
|
17
|
-
send "appsignal_uninstrumented_#{method_name}", *args, **kwargs, &block
|
18
|
-
end
|
8
|
+
def self.appsignal_instrument_class_method(method_name, options = {})
|
9
|
+
singleton_class.send \
|
10
|
+
:alias_method, "appsignal_uninstrumented_#{method_name}", method_name
|
11
|
+
singleton_class.send(:define_method, method_name) do |*args, &block|
|
12
|
+
name = options.fetch(:name) do
|
13
|
+
"#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
|
19
14
|
end
|
20
|
-
|
21
|
-
|
22
|
-
def self.appsignal_instrument_method(method_name, options = {})
|
23
|
-
alias_method "appsignal_uninstrumented_#{method_name}", method_name
|
24
|
-
define_method method_name do |*args, **kwargs, &block|
|
25
|
-
name = options.fetch(:name) do
|
26
|
-
"#{method_name}.#{appsignal_reverse_class_name}.other"
|
27
|
-
end
|
28
|
-
Appsignal.instrument name do
|
29
|
-
send "appsignal_uninstrumented_#{method_name}", *args, **kwargs, &block
|
30
|
-
end
|
15
|
+
Appsignal.instrument name do
|
16
|
+
send "appsignal_uninstrumented_#{method_name}", *args, &block
|
31
17
|
end
|
32
18
|
end
|
33
|
-
|
34
|
-
|
35
|
-
singleton_class.send
|
36
|
-
:alias_method, "appsignal_uninstrumented_#{method_name}", method_name
|
37
|
-
singleton_class.send(:define_method, method_name) do |*args, &block|
|
38
|
-
name = options.fetch(:name) do
|
39
|
-
"#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
|
40
|
-
end
|
41
|
-
Appsignal.instrument name do
|
42
|
-
send "appsignal_uninstrumented_#{method_name}", *args, &block
|
43
|
-
end
|
44
|
-
end
|
19
|
+
|
20
|
+
if singleton_class.respond_to?(:ruby2_keywords, true) # rubocop:disable Style/GuardClause
|
21
|
+
singleton_class.send(:ruby2_keywords, method_name)
|
45
22
|
end
|
23
|
+
end
|
46
24
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
end
|
25
|
+
def self.appsignal_instrument_method(method_name, options = {})
|
26
|
+
alias_method "appsignal_uninstrumented_#{method_name}", method_name
|
27
|
+
define_method method_name do |*args, &block|
|
28
|
+
name = options.fetch(:name) do
|
29
|
+
"#{method_name}.#{appsignal_reverse_class_name}.other"
|
30
|
+
end
|
31
|
+
Appsignal.instrument name do
|
32
|
+
send "appsignal_uninstrumented_#{method_name}", *args, &block
|
56
33
|
end
|
57
34
|
end
|
35
|
+
ruby2_keywords method_name if respond_to?(:ruby2_keywords, true)
|
58
36
|
end
|
59
37
|
|
60
38
|
def self.appsignal_reverse_class_name
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module Appsignal
|
6
|
+
module Integrations
|
7
|
+
# Error handler for Sidekiq to report errors from jobs and internal Sidekiq
|
8
|
+
# errors.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class SidekiqErrorHandler
|
12
|
+
def call(exception, sidekiq_context)
|
13
|
+
transaction = Appsignal::Transaction.current
|
14
|
+
|
15
|
+
if transaction.nil_transaction?
|
16
|
+
# Sidekiq error outside of the middleware scope.
|
17
|
+
# Can be a job JSON parse error or some other error happening in
|
18
|
+
# Sidekiq.
|
19
|
+
transaction = Appsignal::Transaction.create(
|
20
|
+
SecureRandom.uuid, # Newly generated job id
|
21
|
+
Appsignal::Transaction::BACKGROUND_JOB,
|
22
|
+
Appsignal::Transaction::GenericRequest.new({})
|
23
|
+
)
|
24
|
+
transaction.set_action_if_nil("SidekiqInternal")
|
25
|
+
transaction.set_metadata("sidekiq_error", sidekiq_context[:context])
|
26
|
+
transaction.params = { :jobstr => sidekiq_context[:jobstr] }
|
27
|
+
end
|
28
|
+
|
29
|
+
transaction.set_error(exception)
|
30
|
+
Appsignal::Transaction.complete_current!
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @api private
|
35
|
+
class SidekiqMiddleware # rubocop:disable Metrics/ClassLength
|
36
|
+
include Appsignal::Hooks::Helpers
|
37
|
+
|
38
|
+
EXCLUDED_JOB_KEYS = %w[
|
39
|
+
args backtrace class created_at enqueued_at error_backtrace error_class
|
40
|
+
error_message failed_at jid retried_at retry wrapped
|
41
|
+
].freeze
|
42
|
+
|
43
|
+
def call(_worker, item, _queue)
|
44
|
+
job_status = nil
|
45
|
+
transaction = Appsignal::Transaction.create(
|
46
|
+
item["jid"],
|
47
|
+
Appsignal::Transaction::BACKGROUND_JOB,
|
48
|
+
Appsignal::Transaction::GenericRequest.new(
|
49
|
+
:queue_start => item["enqueued_at"]
|
50
|
+
)
|
51
|
+
)
|
52
|
+
|
53
|
+
Appsignal.instrument "perform_job.sidekiq" do
|
54
|
+
yield
|
55
|
+
end
|
56
|
+
rescue Exception => exception # rubocop:disable Lint/RescueException
|
57
|
+
job_status = :failed
|
58
|
+
raise exception
|
59
|
+
ensure
|
60
|
+
if transaction
|
61
|
+
transaction.set_action_if_nil(formatted_action_name(item))
|
62
|
+
|
63
|
+
params = filtered_arguments(item)
|
64
|
+
transaction.params = params if params
|
65
|
+
|
66
|
+
formatted_metadata(item).each do |key, value|
|
67
|
+
transaction.set_metadata key, value
|
68
|
+
end
|
69
|
+
transaction.set_http_or_background_queue_start
|
70
|
+
Appsignal::Transaction.complete_current! unless exception
|
71
|
+
|
72
|
+
queue = item["queue"] || "unknown"
|
73
|
+
if job_status
|
74
|
+
increment_counter "queue_job_count", 1,
|
75
|
+
:queue => queue,
|
76
|
+
:status => job_status
|
77
|
+
end
|
78
|
+
increment_counter "queue_job_count", 1,
|
79
|
+
:queue => queue,
|
80
|
+
:status => :processed
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def increment_counter(key, value, tags = {})
|
87
|
+
Appsignal.increment_counter "sidekiq_#{key}", value, tags
|
88
|
+
end
|
89
|
+
|
90
|
+
def formatted_action_name(job)
|
91
|
+
sidekiq_action_name = parse_action_name(job)
|
92
|
+
return unless sidekiq_action_name
|
93
|
+
|
94
|
+
complete_action = sidekiq_action_name =~ /\.|#/
|
95
|
+
return sidekiq_action_name if complete_action
|
96
|
+
|
97
|
+
"#{sidekiq_action_name}#perform"
|
98
|
+
end
|
99
|
+
|
100
|
+
def filtered_arguments(job)
|
101
|
+
arguments = parse_arguments(job)
|
102
|
+
return unless arguments
|
103
|
+
|
104
|
+
Appsignal::Utils::HashSanitizer.sanitize(
|
105
|
+
arguments,
|
106
|
+
Appsignal.config[:filter_parameters]
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
def formatted_metadata(item)
|
111
|
+
{}.tap do |hash|
|
112
|
+
(item || {}).each do |key, value|
|
113
|
+
next if EXCLUDED_JOB_KEYS.include?(key)
|
114
|
+
|
115
|
+
hash[key] = truncate(string_or_inspect(value))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L316-L334
|
121
|
+
def parse_action_name(job)
|
122
|
+
args = job.fetch("args", [])
|
123
|
+
job_class = job["class"]
|
124
|
+
case job_class
|
125
|
+
when "Sidekiq::Extensions::DelayedModel"
|
126
|
+
safe_load(args[0], job_class) do |target, method, _|
|
127
|
+
"#{target.class}##{method}"
|
128
|
+
end
|
129
|
+
when /\ASidekiq::Extensions::Delayed/
|
130
|
+
safe_load(args[0], job_class) do |target, method, _|
|
131
|
+
"#{target}.#{method}"
|
132
|
+
end
|
133
|
+
else
|
134
|
+
job_class
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L336-L358
|
139
|
+
def parse_arguments(job)
|
140
|
+
args = job.fetch("args", [])
|
141
|
+
case job["class"]
|
142
|
+
when /\ASidekiq::Extensions::Delayed/
|
143
|
+
safe_load(args[0], args) do |_, _, arg|
|
144
|
+
arg
|
145
|
+
end
|
146
|
+
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
147
|
+
nil # Set in the ActiveJob integration
|
148
|
+
else
|
149
|
+
# Sidekiq Enterprise argument encryption.
|
150
|
+
# More information: https://github.com/mperham/sidekiq/wiki/Ent-Encryption
|
151
|
+
if job["encrypt".freeze]
|
152
|
+
# No point in showing 150+ bytes of random garbage
|
153
|
+
args[-1] = "[encrypted data]".freeze
|
154
|
+
end
|
155
|
+
args
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L403-L412
|
160
|
+
def safe_load(content, default)
|
161
|
+
yield(*YAML.load(content))
|
162
|
+
rescue => error
|
163
|
+
# Sidekiq issue #1761: in dev mode, it's possible to have jobs enqueued
|
164
|
+
# which haven't been loaded into memory yet so the YAML can't be
|
165
|
+
# loaded.
|
166
|
+
Appsignal.logger.warn "Unable to load YAML: #{error.message}"
|
167
|
+
default
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|