appsignal 2.11.0-java → 2.11.4-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.yml +2 -0
- data/.semaphore/semaphore.yml +197 -0
- data/CHANGELOG.md +19 -0
- data/README.md +16 -1
- data/Rakefile +20 -11
- data/build_matrix.yml +13 -0
- data/ext/agent.yml +17 -25
- data/ext/appsignal_extension.c +1 -1
- data/ext/base.rb +12 -9
- data/gemfiles/no_dependencies.gemfile +7 -0
- data/gemfiles/resque-2.gemfile +0 -1
- data/gemfiles/webmachine.gemfile +1 -0
- data/lib/appsignal/cli/diagnose/utils.rb +8 -11
- data/lib/appsignal/cli/install.rb +5 -8
- data/lib/appsignal/helpers/instrumentation.rb +32 -0
- data/lib/appsignal/hooks.rb +1 -0
- data/lib/appsignal/hooks/action_mailer.rb +22 -0
- data/lib/appsignal/hooks/active_support_notifications.rb +72 -0
- data/lib/appsignal/hooks/shoryuken.rb +43 -4
- data/lib/appsignal/integrations/object.rb +4 -34
- data/lib/appsignal/integrations/object_ruby_19.rb +37 -0
- data/lib/appsignal/integrations/object_ruby_modern.rb +64 -0
- data/lib/appsignal/system.rb +4 -0
- data/lib/appsignal/transaction.rb +30 -2
- data/lib/appsignal/version.rb +1 -1
- data/spec/lib/appsignal/hooks/action_mailer_spec.rb +54 -0
- data/spec/lib/appsignal/hooks/active_support_notifications/finish_with_state_shared_examples.rb +35 -0
- data/spec/lib/appsignal/hooks/active_support_notifications/instrument_shared_examples.rb +145 -0
- data/spec/lib/appsignal/hooks/active_support_notifications/start_finish_shared_examples.rb +69 -0
- data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +9 -137
- data/spec/lib/appsignal/hooks/resque_spec.rb +10 -2
- data/spec/lib/appsignal/hooks/shoryuken_spec.rb +151 -104
- data/spec/lib/appsignal/hooks/sidekiq_spec.rb +4 -2
- data/spec/lib/appsignal/integrations/object_19_spec.rb +266 -0
- data/spec/lib/appsignal/integrations/object_spec.rb +29 -10
- data/spec/lib/appsignal/transaction_spec.rb +55 -0
- data/spec/lib/appsignal_spec.rb +30 -0
- data/spec/support/helpers/dependency_helper.rb +4 -0
- metadata +16 -3
data/ext/appsignal_extension.c
CHANGED
@@ -485,7 +485,7 @@ static VALUE data_append_boolean(VALUE self, VALUE value) {
|
|
485
485
|
return Qnil;
|
486
486
|
}
|
487
487
|
|
488
|
-
static VALUE data_append_nil(VALUE self
|
488
|
+
static VALUE data_append_nil(VALUE self) {
|
489
489
|
appsignal_data_t* data;
|
490
490
|
|
491
491
|
Data_Get_Struct(self, appsignal_data_t, data);
|
data/ext/base.rb
CHANGED
@@ -119,30 +119,33 @@ def download_archive(type)
|
|
119
119
|
|
120
120
|
version = AGENT_CONFIG["version"]
|
121
121
|
filename = ARCH_CONFIG[type]["filename"]
|
122
|
-
|
122
|
+
download_errors = []
|
123
123
|
|
124
124
|
AGENT_CONFIG["mirrors"].each do |mirror|
|
125
125
|
download_url = [mirror, version, filename].join("/")
|
126
|
-
attempted_mirror_urls << download_url
|
127
126
|
report["download"]["download_url"] = download_url
|
128
127
|
|
129
128
|
begin
|
130
|
-
|
129
|
+
args = [
|
131
130
|
download_url,
|
132
131
|
:ssl_ca_cert => CA_CERT_PATH,
|
133
132
|
:proxy => http_proxy
|
134
|
-
|
135
|
-
|
133
|
+
]
|
134
|
+
if URI.respond_to?(:open) # rubocop:disable Style/GuardClause
|
135
|
+
return URI.open(*args)
|
136
|
+
else
|
137
|
+
return open(*args)
|
138
|
+
end
|
139
|
+
rescue => error
|
140
|
+
download_errors << "- URL: #{download_url}\n Error: #{error.class}: #{error.message}"
|
136
141
|
next
|
137
142
|
end
|
138
143
|
end
|
139
144
|
|
140
|
-
attempted_mirror_urls_mapped = attempted_mirror_urls.map { |mirror| "- #{mirror}" }
|
141
145
|
abort_installation(
|
142
146
|
"Could not download archive from any of our mirrors. " \
|
143
|
-
"
|
144
|
-
"#{
|
145
|
-
"Please make sure your network allows access to any of these mirrors."
|
147
|
+
"Please make sure your network allows access to any of these mirrors.\n" \
|
148
|
+
"Attempted to download the archive from the following urls:\n#{download_errors.join("\n")}"
|
146
149
|
)
|
147
150
|
end
|
148
151
|
|
@@ -2,4 +2,11 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
gem 'rack', '~> 1.6'
|
4
4
|
|
5
|
+
ruby_version = Gem::Version.new(RUBY_VERSION)
|
6
|
+
if ruby_version < Gem::Version.new("2.0.0")
|
7
|
+
# Newer versions of this gem have rexml as a dependency which doesn't work on
|
8
|
+
# Ruby 1.9
|
9
|
+
gem "crack", "0.4.4"
|
10
|
+
end
|
11
|
+
|
5
12
|
gemspec :path => '../'
|
data/gemfiles/resque-2.gemfile
CHANGED
data/gemfiles/webmachine.gemfile
CHANGED
@@ -34,20 +34,17 @@ module Appsignal
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def self.parse_yaml(contents)
|
37
|
-
arguments = [contents]
|
38
37
|
if YAML.respond_to? :safe_load
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
[Time]
|
46
|
-
end
|
38
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0")
|
39
|
+
# Use keyword params for Ruby 2.6 and up
|
40
|
+
YAML.safe_load(contents, :permitted_classes => [Time])
|
41
|
+
else
|
42
|
+
YAML.safe_load(contents, [Time])
|
43
|
+
end
|
47
44
|
else
|
48
|
-
|
45
|
+
# Support for Ruby versions without YAML.safe_load
|
46
|
+
YAML.load(contents) # rubocop:disable Security/YAMLLoad
|
49
47
|
end
|
50
|
-
YAML.send(method, *arguments)
|
51
48
|
end
|
52
49
|
end
|
53
50
|
end
|
@@ -278,14 +278,11 @@ module Appsignal
|
|
278
278
|
"../../../resources/appsignal.yml.erb"
|
279
279
|
)
|
280
280
|
file_contents = File.read(filename)
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
arguments << "-"
|
287
|
-
end
|
288
|
-
template = ERB.new(*arguments)
|
281
|
+
template = if ruby_2_6_or_up?
|
282
|
+
ERB.new(file_contents, :trim_mode => "-")
|
283
|
+
else
|
284
|
+
ERB.new(file_contents, nil, "-")
|
285
|
+
end
|
289
286
|
config = template.result(OpenStruct.new(data).instance_eval { binding })
|
290
287
|
|
291
288
|
FileUtils.mkdir_p(File.join(Dir.pwd, "config"))
|
@@ -380,6 +380,38 @@ module Appsignal
|
|
380
380
|
end
|
381
381
|
alias :tag_job :tag_request
|
382
382
|
|
383
|
+
# Add breadcrumbs to the transaction.
|
384
|
+
#
|
385
|
+
# Breadcrumbs can be used to trace what path a user has taken
|
386
|
+
# before encounterin an error.
|
387
|
+
#
|
388
|
+
# Only the last 20 added breadcrumbs will be saved.
|
389
|
+
#
|
390
|
+
# @example
|
391
|
+
# Appsignal.add_breadcrumb("Navigation", "http://blablabla.com", "", { :response => 200 }, Time.now.utc)
|
392
|
+
# Appsignal.add_breadcrumb("Network", "[GET] http://blablabla.com", "", { :response => 500 })
|
393
|
+
# Appsignal.add_breadcrumb("UI", "closed modal(change_password)", "User closed modal without actions")
|
394
|
+
#
|
395
|
+
# @param category [String] category of breadcrumb
|
396
|
+
# e.g. "UI", "Network", "Navigation", "Console".
|
397
|
+
# @param action [String] name of breadcrumb
|
398
|
+
# e.g "The user clicked a button", "HTTP 500 from http://blablabla.com"
|
399
|
+
# @option message [String] optional message in string format
|
400
|
+
# @option metadata [Hash<String,String>] key/value metadata in <string, string> format
|
401
|
+
# @option time [Time] time of breadcrumb, should respond to `.to_i` defaults to `Time.now.utc`
|
402
|
+
# @return [void]
|
403
|
+
#
|
404
|
+
# @see Transaction#add_breadcrumb
|
405
|
+
# @see http://docs.appsignal.com/ruby/instrumentation/breadcrumbs.html
|
406
|
+
# Breadcrumb reference
|
407
|
+
# @since 2.12.0
|
408
|
+
def add_breadcrumb(category, action, message = "", metadata = {}, time = Time.now.utc)
|
409
|
+
return unless active?
|
410
|
+
transaction = Appsignal::Transaction.current
|
411
|
+
return false unless transaction
|
412
|
+
transaction.add_breadcrumb(category, action, message, metadata, time)
|
413
|
+
end
|
414
|
+
|
383
415
|
# Instrument helper for AppSignal.
|
384
416
|
#
|
385
417
|
# For more help, read our custom instrumentation guide, listed under "See
|
data/lib/appsignal/hooks.rb
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Appsignal
|
2
|
+
class Hooks
|
3
|
+
class ActionMailerHook < Appsignal::Hooks::Hook
|
4
|
+
register :action_mailer
|
5
|
+
|
6
|
+
def dependencies_present?
|
7
|
+
defined?(::ActionMailer)
|
8
|
+
end
|
9
|
+
|
10
|
+
def install
|
11
|
+
ActiveSupport::Notifications
|
12
|
+
.subscribe("process.action_mailer") do |_, _, _, _, payload|
|
13
|
+
Appsignal.increment_counter(
|
14
|
+
:action_mailer_process,
|
15
|
+
1,
|
16
|
+
:mailer => payload[:mailer], :action => payload[:action]
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -23,6 +23,21 @@ module Appsignal
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
+
instrumenter = ::ActiveSupport::Notifications::Instrumenter
|
27
|
+
|
28
|
+
if instrumenter.method_defined?(:start) && instrumenter.method_defined?(:finish)
|
29
|
+
install_start_finish
|
30
|
+
else
|
31
|
+
install_instrument
|
32
|
+
end
|
33
|
+
|
34
|
+
# rubocop:disable Style/GuardClause
|
35
|
+
if instrumenter.method_defined?(:finish_with_state)
|
36
|
+
install_finish_with_state
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def install_instrument
|
26
41
|
::ActiveSupport::Notifications::Instrumenter.class_eval do
|
27
42
|
alias instrument_without_appsignal instrument
|
28
43
|
|
@@ -46,6 +61,63 @@ module Appsignal
|
|
46
61
|
end
|
47
62
|
end
|
48
63
|
end
|
64
|
+
|
65
|
+
def install_start_finish
|
66
|
+
::ActiveSupport::Notifications::Instrumenter.class_eval do
|
67
|
+
alias start_without_appsignal start
|
68
|
+
|
69
|
+
def start(name, payload = {})
|
70
|
+
# Events that start with a bang are internal to Rails
|
71
|
+
instrument_this = name[0] != BANG
|
72
|
+
|
73
|
+
Appsignal::Transaction.current.start_event if instrument_this
|
74
|
+
|
75
|
+
start_without_appsignal(name, payload)
|
76
|
+
end
|
77
|
+
|
78
|
+
alias finish_without_appsignal finish
|
79
|
+
|
80
|
+
def finish(name, payload = {})
|
81
|
+
# Events that start with a bang are internal to Rails
|
82
|
+
instrument_this = name[0] != BANG
|
83
|
+
|
84
|
+
if instrument_this
|
85
|
+
title, body, body_format = Appsignal::EventFormatter.format(name, payload)
|
86
|
+
Appsignal::Transaction.current.finish_event(
|
87
|
+
name.to_s,
|
88
|
+
title,
|
89
|
+
body,
|
90
|
+
body_format
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
finish_without_appsignal(name, payload)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def install_finish_with_state
|
100
|
+
::ActiveSupport::Notifications::Instrumenter.class_eval do
|
101
|
+
alias finish_with_state_without_appsignal finish_with_state
|
102
|
+
|
103
|
+
def finish_with_state(listeners_state, name, payload = {})
|
104
|
+
# Events that start with a bang are internal to Rails
|
105
|
+
instrument_this = name[0] != BANG
|
106
|
+
|
107
|
+
if instrument_this
|
108
|
+
title, body, body_format = Appsignal::EventFormatter.format(name, payload)
|
109
|
+
Appsignal::Transaction.current.finish_event(
|
110
|
+
name.to_s,
|
111
|
+
title,
|
112
|
+
body,
|
113
|
+
body_format
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
117
|
+
finish_with_state_without_appsignal(listeners_state, name, payload)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
49
121
|
end
|
50
122
|
end
|
51
123
|
end
|
@@ -5,21 +5,60 @@ module Appsignal
|
|
5
5
|
# @api private
|
6
6
|
class ShoryukenMiddleware
|
7
7
|
def call(worker_instance, queue, sqs_msg, body)
|
8
|
-
|
8
|
+
batch = sqs_msg.is_a?(Array)
|
9
|
+
attributes =
|
10
|
+
if batch
|
11
|
+
# We can't instrument batched message separately, the `yield` will
|
12
|
+
# perform all the batched messages.
|
13
|
+
# To provide somewhat useful metadata, Get first message based on
|
14
|
+
# SentTimestamp, and use its attributes as metadata for the
|
15
|
+
# transaction. We can't combine them all because then they would
|
16
|
+
# overwrite each other and the last message (in an sorted order)
|
17
|
+
# would be used as the source of the metadata. With the
|
18
|
+
# oldest/first message at least some useful information is stored
|
19
|
+
# such as the first received time and the number of retries for the
|
20
|
+
# first message. The newer message should have lower values and
|
21
|
+
# timestamps in their metadata.
|
22
|
+
first_msg = sqs_msg.min do |a, b|
|
23
|
+
a.attributes["SentTimestamp"].to_i <=> b.attributes["SentTimestamp"].to_i
|
24
|
+
end
|
25
|
+
# Add batch => true metadata so people can recognize when a
|
26
|
+
# transaction is about a batch of messages.
|
27
|
+
first_msg.attributes.merge(:batch => true)
|
28
|
+
else
|
29
|
+
sqs_msg.attributes.merge(:message_id => sqs_msg.message_id)
|
30
|
+
end
|
31
|
+
metadata = { :queue => queue }.merge(attributes)
|
9
32
|
options = {
|
10
33
|
:class => worker_instance.class.name,
|
11
34
|
:method => "perform",
|
12
35
|
:metadata => metadata
|
13
36
|
}
|
14
37
|
|
15
|
-
args =
|
38
|
+
args =
|
39
|
+
if batch
|
40
|
+
bodies = {}
|
41
|
+
sqs_msg.each_with_index do |msg, index|
|
42
|
+
# Store all separate bodies on a hash with the key being the
|
43
|
+
# message_id
|
44
|
+
bodies[msg.message_id] = body[index]
|
45
|
+
end
|
46
|
+
bodies
|
47
|
+
else
|
48
|
+
case body
|
49
|
+
when Hash
|
50
|
+
body
|
51
|
+
else
|
52
|
+
{ :params => body }
|
53
|
+
end
|
54
|
+
end
|
16
55
|
options[:params] = Appsignal::Utils::HashSanitizer.sanitize(
|
17
56
|
args,
|
18
57
|
Appsignal.config[:filter_parameters]
|
19
58
|
)
|
20
59
|
|
21
|
-
if
|
22
|
-
options[:queue_start] = Time.at(
|
60
|
+
if attributes.key?("SentTimestamp")
|
61
|
+
options[:queue_start] = Time.at(attributes["SentTimestamp"].to_i / 1000)
|
23
62
|
end
|
24
63
|
|
25
64
|
Appsignal.monitor_transaction("perform_job.shoryuken", options) do
|
@@ -4,38 +4,8 @@ if defined?(Appsignal)
|
|
4
4
|
Appsignal::Environment.report_enabled("object_instrumentation")
|
5
5
|
end
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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"
|
14
|
-
end
|
15
|
-
Appsignal.instrument name do
|
16
|
-
send "appsignal_uninstrumented_#{method_name}", *args, &block
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.appsignal_instrument_method(method_name, options = {})
|
22
|
-
alias_method "appsignal_uninstrumented_#{method_name}", method_name
|
23
|
-
define_method method_name do |*args, &block|
|
24
|
-
name = options.fetch(:name) do
|
25
|
-
"#{method_name}.#{appsignal_reverse_class_name}.other"
|
26
|
-
end
|
27
|
-
Appsignal.instrument name do
|
28
|
-
send "appsignal_uninstrumented_#{method_name}", *args, &block
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.appsignal_reverse_class_name
|
34
|
-
return "AnonymousClass" unless name
|
35
|
-
name.split("::").reverse.join(".")
|
36
|
-
end
|
37
|
-
|
38
|
-
def appsignal_reverse_class_name
|
39
|
-
self.class.appsignal_reverse_class_name
|
40
|
-
end
|
7
|
+
if RUBY_VERSION < "2.0"
|
8
|
+
require "appsignal/integrations/object_ruby_19"
|
9
|
+
else
|
10
|
+
require "appsignal/integrations/object_ruby_modern"
|
41
11
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Object
|
4
|
+
def self.appsignal_instrument_class_method(method_name, options = {})
|
5
|
+
singleton_class.send \
|
6
|
+
:alias_method, "appsignal_uninstrumented_#{method_name}", method_name
|
7
|
+
singleton_class.send(:define_method, method_name) do |*args, &block|
|
8
|
+
name = options.fetch(:name) do
|
9
|
+
"#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
|
10
|
+
end
|
11
|
+
Appsignal.instrument name do
|
12
|
+
send "appsignal_uninstrumented_#{method_name}", *args, &block
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.appsignal_instrument_method(method_name, options = {})
|
18
|
+
alias_method "appsignal_uninstrumented_#{method_name}", method_name
|
19
|
+
define_method method_name do |*args, &block|
|
20
|
+
name = options.fetch(:name) do
|
21
|
+
"#{method_name}.#{appsignal_reverse_class_name}.other"
|
22
|
+
end
|
23
|
+
Appsignal.instrument name do
|
24
|
+
send "appsignal_uninstrumented_#{method_name}", *args, &block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.appsignal_reverse_class_name
|
30
|
+
return "AnonymousClass" unless name
|
31
|
+
name.split("::").reverse.join(".")
|
32
|
+
end
|
33
|
+
|
34
|
+
def appsignal_reverse_class_name
|
35
|
+
self.class.appsignal_reverse_class_name
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Object
|
4
|
+
if Appsignal::System.ruby_2_7_or_newer?
|
5
|
+
def self.appsignal_instrument_class_method(method_name, options = {})
|
6
|
+
singleton_class.send \
|
7
|
+
:alias_method, "appsignal_uninstrumented_#{method_name}", method_name
|
8
|
+
singleton_class.send(:define_method, method_name) do |*args, **kwargs, &block|
|
9
|
+
name = options.fetch(:name) do
|
10
|
+
"#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
|
11
|
+
end
|
12
|
+
Appsignal.instrument name do
|
13
|
+
send "appsignal_uninstrumented_#{method_name}", *args, **kwargs, &block
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.appsignal_instrument_method(method_name, options = {})
|
19
|
+
alias_method "appsignal_uninstrumented_#{method_name}", method_name
|
20
|
+
define_method method_name do |*args, **kwargs, &block|
|
21
|
+
name = options.fetch(:name) do
|
22
|
+
"#{method_name}.#{appsignal_reverse_class_name}.other"
|
23
|
+
end
|
24
|
+
Appsignal.instrument name do
|
25
|
+
send "appsignal_uninstrumented_#{method_name}", *args, **kwargs, &block
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
else
|
30
|
+
def self.appsignal_instrument_class_method(method_name, options = {})
|
31
|
+
singleton_class.send \
|
32
|
+
:alias_method, "appsignal_uninstrumented_#{method_name}", method_name
|
33
|
+
singleton_class.send(:define_method, method_name) do |*args, &block|
|
34
|
+
name = options.fetch(:name) do
|
35
|
+
"#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
|
36
|
+
end
|
37
|
+
Appsignal.instrument name do
|
38
|
+
send "appsignal_uninstrumented_#{method_name}", *args, &block
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.appsignal_instrument_method(method_name, options = {})
|
44
|
+
alias_method "appsignal_uninstrumented_#{method_name}", method_name
|
45
|
+
define_method method_name do |*args, &block|
|
46
|
+
name = options.fetch(:name) do
|
47
|
+
"#{method_name}.#{appsignal_reverse_class_name}.other"
|
48
|
+
end
|
49
|
+
Appsignal.instrument name do
|
50
|
+
send "appsignal_uninstrumented_#{method_name}", *args, &block
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.appsignal_reverse_class_name
|
57
|
+
return "AnonymousClass" unless name
|
58
|
+
name.split("::").reverse.join(".")
|
59
|
+
end
|
60
|
+
|
61
|
+
def appsignal_reverse_class_name
|
62
|
+
self.class.appsignal_reverse_class_name
|
63
|
+
end
|
64
|
+
end
|