appsignal 2.11.0.beta.5-java → 2.11.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.semaphore/semaphore.yml +197 -0
  3. data/CHANGELOG.md +18 -0
  4. data/README.md +16 -1
  5. data/Rakefile +11 -5
  6. data/build_matrix.yml +13 -0
  7. data/ext/agent.yml +17 -25
  8. data/ext/appsignal_extension.c +1 -1
  9. data/ext/base.rb +12 -9
  10. data/gemfiles/no_dependencies.gemfile +7 -0
  11. data/gemfiles/resque-2.gemfile +0 -1
  12. data/gemfiles/webmachine.gemfile +1 -0
  13. data/lib/appsignal/cli/diagnose/utils.rb +8 -11
  14. data/lib/appsignal/cli/install.rb +5 -8
  15. data/lib/appsignal/config.rb +47 -15
  16. data/lib/appsignal/helpers/instrumentation.rb +32 -0
  17. data/lib/appsignal/hooks.rb +1 -0
  18. data/lib/appsignal/hooks/action_mailer.rb +22 -0
  19. data/lib/appsignal/hooks/active_support_notifications.rb +72 -0
  20. data/lib/appsignal/hooks/shoryuken.rb +43 -4
  21. data/lib/appsignal/transaction.rb +30 -2
  22. data/lib/appsignal/version.rb +1 -1
  23. data/spec/lib/appsignal/config_spec.rb +18 -1
  24. data/spec/lib/appsignal/hooks/action_mailer_spec.rb +54 -0
  25. data/spec/lib/appsignal/hooks/active_support_notifications/finish_with_state_shared_examples.rb +35 -0
  26. data/spec/lib/appsignal/hooks/active_support_notifications/instrument_shared_examples.rb +145 -0
  27. data/spec/lib/appsignal/hooks/active_support_notifications/start_finish_shared_examples.rb +69 -0
  28. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +9 -137
  29. data/spec/lib/appsignal/hooks/resque_spec.rb +10 -2
  30. data/spec/lib/appsignal/hooks/shoryuken_spec.rb +151 -104
  31. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +4 -2
  32. data/spec/lib/appsignal/transaction_spec.rb +55 -0
  33. data/spec/lib/appsignal_spec.rb +30 -0
  34. data/spec/support/helpers/dependency_helper.rb +4 -0
  35. metadata +14 -5
@@ -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, VALUE value) {
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);
@@ -119,30 +119,33 @@ def download_archive(type)
119
119
 
120
120
  version = AGENT_CONFIG["version"]
121
121
  filename = ARCH_CONFIG[type]["filename"]
122
- attempted_mirror_urls = []
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
- return open(
129
+ args = [
131
130
  download_url,
132
131
  :ssl_ca_cert => CA_CERT_PATH,
133
132
  :proxy => http_proxy
134
- )
135
- rescue
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
- "Attempted to download the archive from the following urls:\n" \
144
- "#{attempted_mirror_urls_mapped.join("\n")}\n" \
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 => '../'
@@ -2,7 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  gem 'resque', "~> 2.0"
4
4
  gem 'sinatra'
5
- gem 'mime-types', '~> 2.6'
6
5
 
7
6
  gemspec :path => '../'
8
7
 
@@ -1,5 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem 'webmachine'
4
+ gem 'webrick'
4
5
 
5
6
  gemspec :path => '../'
@@ -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
- method = :safe_load
40
- arguments << \
41
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0")
42
- # Use keyword params for Ruby 2.6 and up
43
- { :permitted_classes => [Time] }
44
- else
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
- method = :load
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
- arguments = [file_contents]
282
- if ruby_2_6_or_up?
283
- arguments << { :trim_mode => "-" }
284
- else
285
- arguments << nil
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"))
@@ -81,6 +81,50 @@ module Appsignal
81
81
  "APPSIGNAL_TRANSACTION_DEBUG_MODE" => :transaction_debug_mode,
82
82
  "APP_REVISION" => :revision
83
83
  }.freeze
84
+ # @api private
85
+ ENV_STRING_KEYS = %w[
86
+ APPSIGNAL_APP_NAME
87
+ APPSIGNAL_CA_FILE_PATH
88
+ APPSIGNAL_DNS_SERVERS
89
+ APPSIGNAL_FRONTEND_ERROR_CATCHING_PATH
90
+ APPSIGNAL_HOSTNAME
91
+ APPSIGNAL_HTTP_PROXY
92
+ APPSIGNAL_LOG
93
+ APPSIGNAL_LOG_PATH
94
+ APPSIGNAL_PUSH_API_ENDPOINT
95
+ APPSIGNAL_PUSH_API_KEY
96
+ APPSIGNAL_WORKING_DIRECTORY_PATH
97
+ APPSIGNAL_WORKING_DIR_PATH
98
+ APP_REVISION
99
+ ].freeze
100
+ # @api private
101
+ ENV_BOOLEAN_KEYS = %w[
102
+ APPSIGNAL_ACTIVE
103
+ APPSIGNAL_DEBUG
104
+ APPSIGNAL_ENABLE_ALLOCATION_TRACKING
105
+ APPSIGNAL_ENABLE_FRONTEND_ERROR_CATCHING
106
+ APPSIGNAL_ENABLE_GC_INSTRUMENTATION
107
+ APPSIGNAL_ENABLE_HOST_METRICS
108
+ APPSIGNAL_ENABLE_MINUTELY_PROBES
109
+ APPSIGNAL_FILES_WORLD_ACCESSIBLE
110
+ APPSIGNAL_INSTRUMENT_NET_HTTP
111
+ APPSIGNAL_INSTRUMENT_REDIS
112
+ APPSIGNAL_INSTRUMENT_SEQUEL
113
+ APPSIGNAL_RUNNING_IN_CONTAINER
114
+ APPSIGNAL_SEND_ENVIRONMENT_METADATA
115
+ APPSIGNAL_SEND_PARAMS
116
+ APPSIGNAL_SKIP_SESSION_DATA
117
+ APPSIGNAL_TRANSACTION_DEBUG_MODE
118
+ ].freeze
119
+ # @api private
120
+ ENV_ARRAY_KEYS = %w[
121
+ APPSIGNAL_FILTER_PARAMETERS
122
+ APPSIGNAL_FILTER_SESSION_DATA
123
+ APPSIGNAL_IGNORE_ACTIONS
124
+ APPSIGNAL_IGNORE_ERRORS
125
+ APPSIGNAL_IGNORE_NAMESPACES
126
+ APPSIGNAL_REQUEST_HEADERS
127
+ ].freeze
84
128
 
85
129
  # Mapping of old and deprecated AppSignal configuration keys
86
130
  DEPRECATED_CONFIG_KEY_MAPPING = {
@@ -358,33 +402,21 @@ module Appsignal
358
402
  config = {}
359
403
 
360
404
  # Configuration with string type
361
- %w[APPSIGNAL_PUSH_API_KEY APPSIGNAL_APP_NAME APPSIGNAL_PUSH_API_ENDPOINT
362
- APPSIGNAL_FRONTEND_ERROR_CATCHING_PATH APPSIGNAL_HTTP_PROXY
363
- APPSIGNAL_LOG APPSIGNAL_LOG_PATH APPSIGNAL_WORKING_DIR_PATH
364
- APPSIGNAL_HOSTNAME APPSIGNAL_CA_FILE_PATH APP_REVISION].each do |var|
405
+ ENV_STRING_KEYS.each do |var|
365
406
  env_var = ENV[var]
366
407
  next unless env_var
367
408
  config[ENV_TO_KEY_MAPPING[var]] = env_var
368
409
  end
369
410
 
370
411
  # Configuration with boolean type
371
- %w[APPSIGNAL_ACTIVE APPSIGNAL_DEBUG APPSIGNAL_INSTRUMENT_NET_HTTP
372
- APPSIGNAL_INSTRUMENT_REDIS APPSIGNAL_INSTRUMENT_SEQUEL
373
- APPSIGNAL_SKIP_SESSION_DATA APPSIGNAL_ENABLE_FRONTEND_ERROR_CATCHING
374
- APPSIGNAL_ENABLE_ALLOCATION_TRACKING APPSIGNAL_ENABLE_GC_INSTRUMENTATION
375
- APPSIGNAL_RUNNING_IN_CONTAINER APPSIGNAL_ENABLE_HOST_METRICS
376
- APPSIGNAL_SEND_ENVIRONMENT_METADATA APPSIGNAL_SEND_PARAMS
377
- APPSIGNAL_ENABLE_MINUTELY_PROBES APPSIGNAL_FILES_WORLD_ACCESSIBLE
378
- APPSIGNAL_TRANSACTION_DEBUG_MODE].each do |var|
412
+ ENV_BOOLEAN_KEYS.each do |var|
379
413
  env_var = ENV[var]
380
414
  next unless env_var
381
415
  config[ENV_TO_KEY_MAPPING[var]] = env_var.casecmp("true").zero?
382
416
  end
383
417
 
384
418
  # Configuration with array of strings type
385
- %w[APPSIGNAL_IGNORE_ACTIONS APPSIGNAL_IGNORE_ERRORS
386
- APPSIGNAL_IGNORE_NAMESPACES APPSIGNAL_FILTER_PARAMETERS
387
- APPSIGNAL_FILTER_SESSION_DATA APPSIGNAL_REQUEST_HEADERS].each do |var|
419
+ ENV_ARRAY_KEYS.each do |var|
388
420
  env_var = ENV[var]
389
421
  next unless env_var
390
422
  config[ENV_TO_KEY_MAPPING[var]] = env_var.split(",")
@@ -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
@@ -96,6 +96,7 @@ module Appsignal
96
96
  end
97
97
 
98
98
  require "appsignal/hooks/action_cable"
99
+ require "appsignal/hooks/action_mailer"
99
100
  require "appsignal/hooks/active_job"
100
101
  require "appsignal/hooks/active_support_notifications"
101
102
  require "appsignal/hooks/celluloid"
@@ -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
- metadata = { :queue => queue }.merge(sqs_msg.attributes)
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 = body.is_a?(Hash) ? body : { :params => body }
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 sqs_msg.attributes.key?("SentTimestamp")
22
- options[:queue_start] = Time.at(sqs_msg.attributes["SentTimestamp"].to_i / 1000)
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
@@ -11,6 +11,7 @@ module Appsignal
11
11
  BLANK = "".freeze
12
12
  ALLOWED_TAG_KEY_TYPES = [Symbol, String].freeze
13
13
  ALLOWED_TAG_VALUE_TYPES = [Symbol, String, Integer].freeze
14
+ BREADCRUMB_LIMIT = 20
14
15
 
15
16
  class << self
16
17
  def create(id, namespace, request, options = {})
@@ -58,7 +59,7 @@ module Appsignal
58
59
  end
59
60
  end
60
61
 
61
- attr_reader :ext, :transaction_id, :action, :namespace, :request, :paused, :tags, :options, :discarded
62
+ attr_reader :ext, :transaction_id, :action, :namespace, :request, :paused, :tags, :options, :discarded, :breadcrumbs
62
63
 
63
64
  # @!attribute params
64
65
  # Attribute for parameters of the transaction.
@@ -80,6 +81,7 @@ module Appsignal
80
81
  @paused = false
81
82
  @discarded = false
82
83
  @tags = {}
84
+ @breadcrumbs = []
83
85
  @store = Hash.new({})
84
86
  @options = options
85
87
  @options[:params_method] ||= :params
@@ -156,6 +158,31 @@ module Appsignal
156
158
  @tags.merge!(given_tags)
157
159
  end
158
160
 
161
+ # Add breadcrumbs to the transaction.
162
+ #
163
+ # @param category [String] category of breadcrumb
164
+ # e.g. "UI", "Network", "Navigation", "Console".
165
+ # @param action [String] name of breadcrumb
166
+ # e.g "The user clicked a button", "HTTP 500 from http://blablabla.com"
167
+ # @option message [String] optional message in string format
168
+ # @option metadata [Hash<String,String>] key/value metadata in <string, string> format
169
+ # @option time [Time] time of breadcrumb, should respond to `.to_i` defaults to `Time.now.utc`
170
+ # @return [void]
171
+ #
172
+ # @see Appsignal.add_breadcrumb
173
+ # @see http://docs.appsignal.com/ruby/instrumentation/breadcrumbs.html
174
+ # Breadcrumb reference
175
+ def add_breadcrumb(category, action, message = "", metadata = {}, time = Time.now.utc)
176
+ @breadcrumbs.push(
177
+ :time => time.to_i,
178
+ :category => category,
179
+ :action => action,
180
+ :message => message,
181
+ :metadata => metadata
182
+ )
183
+ @breadcrumbs = @breadcrumbs.last(BREADCRUMB_LIMIT)
184
+ end
185
+
159
186
  # Set an action name for the transaction.
160
187
  #
161
188
  # An action name is used to identify the location of a certain sample;
@@ -287,7 +314,8 @@ module Appsignal
287
314
  :environment => sanitized_environment,
288
315
  :session_data => sanitized_session_data,
289
316
  :metadata => metadata,
290
- :tags => sanitized_tags
317
+ :tags => sanitized_tags,
318
+ :breadcrumbs => breadcrumbs
291
319
  }.each do |key, data|
292
320
  set_sample_data(key, data)
293
321
  end