skylight-core 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/lib/skylight/core/config.rb +454 -0
  3. data/lib/skylight/core/errors.rb +6 -0
  4. data/lib/skylight/core/fanout.rb +44 -0
  5. data/lib/skylight/core/formatters/http.rb +23 -0
  6. data/lib/skylight/core/gc.rb +107 -0
  7. data/lib/skylight/core/instrumentable.rb +144 -0
  8. data/lib/skylight/core/instrumenter.rb +249 -0
  9. data/lib/skylight/core/middleware.rb +101 -0
  10. data/lib/skylight/core/normalizers/action_controller/process_action.rb +50 -0
  11. data/lib/skylight/core/normalizers/action_controller/send_file.rb +50 -0
  12. data/lib/skylight/core/normalizers/action_view/render_collection.rb +22 -0
  13. data/lib/skylight/core/normalizers/action_view/render_partial.rb +21 -0
  14. data/lib/skylight/core/normalizers/action_view/render_template.rb +21 -0
  15. data/lib/skylight/core/normalizers/active_job/enqueue_at.rb +21 -0
  16. data/lib/skylight/core/normalizers/active_model_serializers/render.rb +26 -0
  17. data/lib/skylight/core/normalizers/active_record/instantiation.rb +17 -0
  18. data/lib/skylight/core/normalizers/active_record/sql.rb +33 -0
  19. data/lib/skylight/core/normalizers/active_support/cache.rb +20 -0
  20. data/lib/skylight/core/normalizers/active_support/cache_clear.rb +16 -0
  21. data/lib/skylight/core/normalizers/active_support/cache_decrement.rb +16 -0
  22. data/lib/skylight/core/normalizers/active_support/cache_delete.rb +16 -0
  23. data/lib/skylight/core/normalizers/active_support/cache_exist.rb +16 -0
  24. data/lib/skylight/core/normalizers/active_support/cache_fetch_hit.rb +16 -0
  25. data/lib/skylight/core/normalizers/active_support/cache_generate.rb +16 -0
  26. data/lib/skylight/core/normalizers/active_support/cache_increment.rb +16 -0
  27. data/lib/skylight/core/normalizers/active_support/cache_read.rb +16 -0
  28. data/lib/skylight/core/normalizers/active_support/cache_read_multi.rb +16 -0
  29. data/lib/skylight/core/normalizers/active_support/cache_write.rb +16 -0
  30. data/lib/skylight/core/normalizers/coach/handler_finish.rb +36 -0
  31. data/lib/skylight/core/normalizers/coach/middleware_finish.rb +23 -0
  32. data/lib/skylight/core/normalizers/couch_potato/query.rb +20 -0
  33. data/lib/skylight/core/normalizers/data_mapper/sql.rb +12 -0
  34. data/lib/skylight/core/normalizers/default.rb +27 -0
  35. data/lib/skylight/core/normalizers/elasticsearch/request.rb +20 -0
  36. data/lib/skylight/core/normalizers/faraday/request.rb +37 -0
  37. data/lib/skylight/core/normalizers/grape/endpoint.rb +30 -0
  38. data/lib/skylight/core/normalizers/grape/endpoint_render.rb +26 -0
  39. data/lib/skylight/core/normalizers/grape/endpoint_run.rb +33 -0
  40. data/lib/skylight/core/normalizers/grape/endpoint_run_filters.rb +23 -0
  41. data/lib/skylight/core/normalizers/moped/query.rb +100 -0
  42. data/lib/skylight/core/normalizers/sequel/sql.rb +12 -0
  43. data/lib/skylight/core/normalizers/sql.rb +49 -0
  44. data/lib/skylight/core/normalizers.rb +170 -0
  45. data/lib/skylight/core/probes/action_controller.rb +31 -0
  46. data/lib/skylight/core/probes/action_view.rb +37 -0
  47. data/lib/skylight/core/probes/active_model_serializers.rb +55 -0
  48. data/lib/skylight/core/probes/elasticsearch.rb +37 -0
  49. data/lib/skylight/core/probes/excon/middleware.rb +72 -0
  50. data/lib/skylight/core/probes/excon.rb +26 -0
  51. data/lib/skylight/core/probes/faraday.rb +22 -0
  52. data/lib/skylight/core/probes/grape.rb +80 -0
  53. data/lib/skylight/core/probes/httpclient.rb +46 -0
  54. data/lib/skylight/core/probes/middleware.rb +58 -0
  55. data/lib/skylight/core/probes/mongo.rb +171 -0
  56. data/lib/skylight/core/probes/mongoid.rb +21 -0
  57. data/lib/skylight/core/probes/moped.rb +39 -0
  58. data/lib/skylight/core/probes/net_http.rb +64 -0
  59. data/lib/skylight/core/probes/redis.rb +71 -0
  60. data/lib/skylight/core/probes/sequel.rb +33 -0
  61. data/lib/skylight/core/probes/sinatra.rb +69 -0
  62. data/lib/skylight/core/probes/tilt.rb +27 -0
  63. data/lib/skylight/core/probes.rb +129 -0
  64. data/lib/skylight/core/railtie.rb +166 -0
  65. data/lib/skylight/core/subscriber.rb +124 -0
  66. data/lib/skylight/core/test.rb +98 -0
  67. data/lib/skylight/core/trace.rb +190 -0
  68. data/lib/skylight/core/user_config.rb +61 -0
  69. data/lib/skylight/core/util/allocation_free.rb +26 -0
  70. data/lib/skylight/core/util/clock.rb +56 -0
  71. data/lib/skylight/core/util/deploy.rb +132 -0
  72. data/lib/skylight/core/util/gzip.rb +21 -0
  73. data/lib/skylight/core/util/inflector.rb +112 -0
  74. data/lib/skylight/core/util/logging.rb +127 -0
  75. data/lib/skylight/core/util/platform.rb +77 -0
  76. data/lib/skylight/core/util/proxy.rb +13 -0
  77. data/lib/skylight/core/util.rb +14 -0
  78. data/lib/skylight/core/vendor/active_support/notifications.rb +207 -0
  79. data/lib/skylight/core/vendor/active_support/per_thread_registry.rb +52 -0
  80. data/lib/skylight/core/vendor/thread_safe/non_concurrent_cache_backend.rb +133 -0
  81. data/lib/skylight/core/vendor/thread_safe/synchronized_cache_backend.rb +76 -0
  82. data/lib/skylight/core/vendor/thread_safe.rb +126 -0
  83. data/lib/skylight/core/version.rb +6 -0
  84. data/lib/skylight/core/vm/gc.rb +70 -0
  85. data/lib/skylight/core.rb +99 -0
  86. metadata +254 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5f2101f565718179a58601c833bd3005ce789b2159c689dcf4a68f02ee4ead8e
4
+ data.tar.gz: 34f6391c7691bdb47b33bef9dc0fddc3808d98d82eb809adaebe3dfb4d4a6aa6
5
+ SHA512:
6
+ metadata.gz: f867502b872f1604375b24a7a4591c4d733f38327b1d985a7bd8a5d2b64cff5a7fa59482357d88fc7fd0e1314c7200f054f448df07e398c1c14aa00e0bcb148c
7
+ data.tar.gz: b021ff2ed79a9f52cd9eaab91a3377547318195e6c7a2a11fed443565527245029b85970720fd9ef573064fe3443743339fc10ed4d8eb4cb6b2056fb68a9879b
@@ -0,0 +1,454 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+ require 'thread'
4
+ require 'erb'
5
+ require 'json'
6
+ require 'skylight/core/util/logging'
7
+ require 'skylight/core/util/proxy'
8
+ require 'skylight/core/errors'
9
+
10
+ module Skylight::Core
11
+ class Config
12
+ include Util::Logging
13
+
14
+ # @api private
15
+ MUTEX = Mutex.new
16
+
17
+ def self.env_matcher; /^(?:SK|SKYLIGHT)_(.+)$/ end
18
+ def self.native_env_prefix; "SKYLIGHT_" end
19
+
20
+ # Map environment variable keys with Skylight configuration keys
21
+ def self.env_to_key
22
+ {
23
+ # == Logging ==
24
+ 'LOG_FILE' => :log_file,
25
+ 'LOG_LEVEL' => :log_level,
26
+ 'ALERT_LOG_FILE' => :alert_log_file,
27
+ 'LOG_SQL_PARSE_ERRORS' => :log_sql_parse_errors,
28
+
29
+ # == Proxy ==
30
+ 'PROXY_URL' => :proxy_url,
31
+
32
+ # == Instrumenter ==
33
+ "ENABLE_SEGMENTS" => :enable_segments,
34
+
35
+ # == User config settings ==
36
+ "USER_CONFIG_PATH" => :'user_config_path',
37
+
38
+ # == Heroku settings ==
39
+ #
40
+ "HEROKU_DYNO_INFO_PATH" => :'heroku.dyno_info_path'
41
+ }
42
+ end
43
+
44
+ # Default values for Skylight configuration keys
45
+ def self.default_values
46
+ {
47
+ :log_file => '-'.freeze,
48
+ :log_level => 'INFO'.freeze,
49
+ :alert_log_file => '-'.freeze,
50
+ :log_sql_parse_errors => false,
51
+ :enable_segments => true,
52
+ :'heroku.dyno_info_path' => '/etc/heroku/dyno'
53
+ }
54
+ end
55
+
56
+ def self.required_keys
57
+ # Nothing is required in this base class.
58
+ {}
59
+ end
60
+
61
+ def self.server_validated_keys
62
+ # Nothing is validated for now, but this is a list of symbols
63
+ # for the key we want to validate.
64
+ []
65
+ end
66
+
67
+ def self.native_env_keys
68
+ [
69
+ :version,
70
+ :root,
71
+ :proxy_url
72
+ ]
73
+ end
74
+
75
+ # Maps legacy config keys to new config keys
76
+ def self.legacy_keys
77
+ # No legacy keys for now
78
+ {}
79
+ end
80
+
81
+ def self.validators
82
+ # None for now
83
+ {}
84
+ end
85
+
86
+ # @api private
87
+ attr_reader :environment
88
+
89
+ # @api private
90
+ def initialize(*args)
91
+ attrs = {}
92
+
93
+ if Hash === args.last
94
+ attrs = args.pop.dup
95
+ end
96
+
97
+ @values = {}
98
+ @priority = {}
99
+ @regexp = nil
100
+
101
+ p = attrs.delete(:priority)
102
+
103
+ if @environment = args[0]
104
+ @regexp = /^#{Regexp.escape(@environment)}\.(.+)$/
105
+ end
106
+
107
+ attrs.each do |k, v|
108
+ self[k] = v
109
+ end
110
+
111
+ if p
112
+ p.each do |k, v|
113
+ @priority[self.class.remap_key(k)] = v
114
+ end
115
+ end
116
+ end
117
+
118
+ def self.load(opts = {}, env = ENV)
119
+ attrs = {}
120
+ version = nil
121
+
122
+ path = opts.delete(:file)
123
+ environment = opts.delete(:environment)
124
+
125
+ if path
126
+ error = nil
127
+ begin
128
+ attrs = YAML.load(ERB.new(File.read(path)).result)
129
+ error = "empty file" unless attrs
130
+ error = "invalid format" if attrs && !attrs.is_a?(Hash)
131
+ rescue Exception => e
132
+ error = e.message
133
+ end
134
+
135
+ raise ConfigError, "could not load config file; msg=#{error}" if error
136
+
137
+ version = File.mtime(path).to_i
138
+ end
139
+
140
+ if env
141
+ attrs[:priority] = remap_env(env)
142
+ end
143
+
144
+ config = new(environment, attrs)
145
+
146
+ opts.each do |k, v|
147
+ config[k] = v
148
+ end
149
+
150
+ config
151
+ end
152
+
153
+ def self.remap_key(key)
154
+ key = key.to_sym
155
+ legacy_keys[key] || key
156
+ end
157
+
158
+ # @api private
159
+ def self.remap_env(env)
160
+ ret = {}
161
+
162
+ return ret unless env
163
+
164
+ # Only set if it exists, we don't want to set to a nil value
165
+ if proxy_url = Util::Proxy.detect_url(env)
166
+ ret[:proxy_url] = proxy_url
167
+ end
168
+
169
+ env.each do |k, val|
170
+ next unless k =~ env_matcher
171
+
172
+ if key = env_to_key[$1]
173
+ ret[key] =
174
+ case val
175
+ when /^false$/i then false
176
+ when /^true$/i then true
177
+ when /^(nil|null)$/i then nil
178
+ when /^\d+$/ then val.to_i
179
+ when /^\d+\.\d+$/ then val.to_f
180
+ else val
181
+ end
182
+ end
183
+ end
184
+
185
+ ret
186
+ end
187
+
188
+ # @api private
189
+ def validate!
190
+ self.class.required_keys.each do |k, v|
191
+ unless get(k)
192
+ raise ConfigError, "#{v} required"
193
+ end
194
+ end
195
+
196
+ log_file = self[:log_file]
197
+ alert_log_file = self[:alert_log_file]
198
+
199
+ check_logfile_permissions(log_file, "log_file")
200
+ check_logfile_permissions(alert_log_file, "alert_log_file")
201
+
202
+ true
203
+ end
204
+
205
+ def validate_with_server
206
+ true
207
+ end
208
+
209
+ def check_file_permissions(file, key)
210
+ file_root = File.dirname(file)
211
+
212
+ # Try to make the directory, don't blow up if we can't. Our writable? check will fail later.
213
+ FileUtils.mkdir_p file_root rescue nil
214
+
215
+ if File.exist?(file) && !FileTest.writable?(file)
216
+ raise ConfigError, "File `#{file}` is not writable. Please set #{key} in your config to a writable path"
217
+ end
218
+
219
+ unless FileTest.writable?(file_root)
220
+ raise ConfigError, "Directory `#{file_root}` is not writable. Please set #{key} in your config to a writable path"
221
+ end
222
+ end
223
+
224
+ def check_logfile_permissions(log_file, key)
225
+ return if log_file == '-' # STDOUT
226
+ log_file = File.expand_path(log_file, root)
227
+ check_file_permissions(log_file, key)
228
+ end
229
+
230
+ def key?(key)
231
+ key = self.class.remap_key(key)
232
+ @priority.key?(key) || @values.key?(key)
233
+ end
234
+
235
+ def get(key, default = nil, &blk)
236
+ key = self.class.remap_key(key)
237
+
238
+ return @priority[key] if @priority.key?(key)
239
+ return @values[key] if @values.key?(key)
240
+ return self.class.default_values[key] if self.class.default_values.key?(key)
241
+
242
+ if default
243
+ return default
244
+ elsif blk
245
+ return blk.call(key)
246
+ end
247
+
248
+ nil
249
+ end
250
+
251
+ alias [] get
252
+
253
+ def set(key, val, scope = nil)
254
+ if scope
255
+ key = [scope, key].join('.')
256
+ end
257
+
258
+ if Hash === val
259
+ val.each do |k, v|
260
+ set(k, v, key)
261
+ end
262
+ else
263
+ k = self.class.remap_key(key)
264
+
265
+ if validator = self.class.validators[k]
266
+ blk, msg = validator
267
+
268
+ unless blk.call(val, self)
269
+ error_msg = "invalid value for #{k} (#{val})"
270
+ error_msg << ", #{msg}" if msg
271
+ raise ConfigError, error_msg
272
+ end
273
+ end
274
+
275
+ if @regexp && k =~ @regexp
276
+ @priority[$1.to_sym] = val
277
+ end
278
+
279
+ @values[k] = val
280
+ end
281
+ end
282
+
283
+ alias []= set
284
+
285
+ def send_or_get(v)
286
+ respond_to?(v) ? send(v) : get(v)
287
+ end
288
+
289
+ def duration_ms(key, default = nil)
290
+ if (v = self[key]) && v.to_s =~ /^\s*(\d+)(s|sec|ms|micros|nanos)?\s*$/
291
+ v = $1.to_i
292
+ case $2
293
+ when "ms"
294
+ v
295
+ when "micros"
296
+ v / 1_000
297
+ when "nanos"
298
+ v / 1_000_000
299
+ else # "s", "sec", nil
300
+ v * 1000
301
+ end
302
+ else
303
+ default
304
+ end
305
+ end
306
+
307
+ def to_json
308
+ JSON.generate(
309
+ config: {
310
+ priority: @priority,
311
+ values: @values
312
+ }
313
+ )
314
+ end
315
+
316
+ def to_native_env
317
+ ret = []
318
+
319
+ self.class.native_env_keys.each do |key|
320
+ value = send_or_get(key)
321
+ unless value.nil?
322
+ env_key = self.class.env_to_key.key(key) || key.upcase
323
+ ret << "#{self.class.native_env_prefix}#{env_key}" << cast_for_env(value)
324
+ end
325
+ end
326
+
327
+ ret
328
+ end
329
+
330
+ def write(path)
331
+ raise "not implemented"
332
+ end
333
+
334
+ #
335
+ #
336
+ # ===== Helpers =====
337
+ #
338
+ #
339
+
340
+ def version
341
+ VERSION
342
+ end
343
+
344
+ # @api private
345
+ def gc
346
+ @gc ||= GC.new(self, get('gc.profiler', VM::GC.new))
347
+ end
348
+
349
+ # @api private
350
+ def ignored_endpoints
351
+ @ignored_endpoints ||=
352
+ begin
353
+ ignored_endpoints = get(:ignored_endpoints)
354
+
355
+ # If, for some odd reason you have a comma in your endpoint name, use the
356
+ # YML config instead.
357
+ if ignored_endpoints.is_a?(String)
358
+ ignored_endpoints = ignored_endpoints.split(/\s*,\s*/)
359
+ end
360
+
361
+ val = Array(get(:ignored_endpoint))
362
+ val.concat(Array(ignored_endpoints))
363
+ val
364
+ end
365
+ end
366
+
367
+ def root
368
+ self[:root] || Dir.pwd
369
+ end
370
+
371
+ def logger
372
+ @logger ||=
373
+ MUTEX.synchronize do
374
+ load_logger
375
+ end
376
+ end
377
+
378
+ def logger=(logger)
379
+ @logger = logger
380
+ end
381
+
382
+ def alert_logger
383
+ @alert_logger ||= MUTEX.synchronize do
384
+ unless l = @alert_logger
385
+ out = get(:alert_log_file)
386
+ out = Util::AlertLogger.new(load_logger) if out == '-'
387
+
388
+ l = create_logger(out)
389
+ l.level = Logger::DEBUG
390
+ end
391
+
392
+ l
393
+ end
394
+ end
395
+
396
+ def alert_logger=(logger)
397
+ @alert_logger = logger
398
+ end
399
+
400
+ def enable_segments?
401
+ !!get(:enable_segments)
402
+ end
403
+
404
+ def user_config
405
+ @user_config ||= UserConfig.new(self)
406
+ end
407
+
408
+ def on_heroku?
409
+ File.exist?(get(:'heroku.dyno_info_path'))
410
+ end
411
+
412
+ private
413
+
414
+ def create_logger(out)
415
+ if out.is_a?(String)
416
+ out = File.expand_path(out, root)
417
+ # May be redundant since we also do this in the permissions check
418
+ FileUtils.mkdir_p(File.dirname(out))
419
+ end
420
+
421
+ Logger.new(out)
422
+ rescue
423
+ Logger.new(STDOUT)
424
+ end
425
+
426
+ def load_logger
427
+ unless l = @logger
428
+ out = get(:log_file)
429
+ out = STDOUT if out == '-'
430
+
431
+ l = create_logger(out)
432
+ l.level =
433
+ case get(:log_level)
434
+ when /^debug$/i then Logger::DEBUG
435
+ when /^info$/i then Logger::INFO
436
+ when /^warn$/i then Logger::WARN
437
+ when /^error$/i then Logger::ERROR
438
+ end
439
+ end
440
+
441
+ l
442
+ end
443
+
444
+ def cast_for_env(v)
445
+ case v
446
+ when true then 'true'
447
+ when false then 'false'
448
+ when nil then 'nil'
449
+ else v.to_s
450
+ end
451
+ end
452
+
453
+ end
454
+ end
@@ -0,0 +1,6 @@
1
+ module Skylight
2
+ module Core
3
+ # @api private
4
+ class ConfigError < RuntimeError; end
5
+ end
6
+ end
@@ -0,0 +1,44 @@
1
+ module Skylight
2
+ module Core
3
+ module Fanout
4
+
5
+ def self.registered
6
+ @registered ||= []
7
+ end
8
+
9
+ def self.register(obj)
10
+ registered.push(obj)
11
+ end
12
+
13
+ def self.unregister(obj)
14
+ registered.delete(obj)
15
+ end
16
+
17
+ def self.trace(*args)
18
+ registered.map{|r| r.trace(*args) }
19
+ end
20
+
21
+ def self.instrument(*args)
22
+ if block_given?
23
+ spans = instrument(*args)
24
+ begin
25
+ yield spans
26
+ ensure
27
+ done(spans)
28
+ end
29
+ else
30
+ registered.map do |r|
31
+ [r, r.instrument(*args)]
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.done(spans, meta=nil)
37
+ spans.reverse.each do |(target, span)|
38
+ target.done(span, meta)
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,23 @@
1
+ module Skylight
2
+ module Core
3
+ module Formatters
4
+ module HTTP
5
+
6
+ # Build instrumentation options for HTTP queries
7
+ #
8
+ # @param [String] method HTTP method, e.g. get, post
9
+ # @param [String] scheme HTTP scheme, e.g. http, https
10
+ # @param [String] host Request host, e.g. example.com
11
+ # @param [String, Integer] port Request port
12
+ # @param [String] path Request path
13
+ # @param [String] query Request query string
14
+ # @return [Hash] a hash containing `:category`, `:title`, and `:annotations`
15
+ def self.build_opts(method, scheme, host, port, path, query)
16
+ { category: "api.http.#{method.downcase}",
17
+ title: "#{method.upcase} #{host}",
18
+ meta: { host: host } }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,107 @@
1
+ require 'thread'
2
+
3
+ module Skylight::Core
4
+ # @api private
5
+ class GC
6
+ METHODS = [ :enable, :total_time ]
7
+ TH_KEY = :SK_GC_CURR_WINDOW
8
+ MAX_COUNT = 1000
9
+ MAX_TIME = 30_000_000
10
+
11
+ include Util::Logging
12
+
13
+ attr_reader :config
14
+
15
+ def initialize(config, profiler)
16
+ @listeners = []
17
+ @config = config
18
+ @lock = Mutex.new
19
+ @time = 0
20
+
21
+ if METHODS.all? { |m| profiler.respond_to?(m) }
22
+ @profiler = profiler
23
+ @time = @profiler.total_time
24
+ else
25
+ debug "disabling GC profiling"
26
+ end
27
+ end
28
+
29
+ def enable
30
+ @profiler.enable if @profiler
31
+ end
32
+
33
+ def track
34
+ unless @profiler
35
+ win = Window.new(nil)
36
+ else
37
+ win = Window.new(self)
38
+
39
+ @lock.synchronize do
40
+ __update
41
+ @listeners << win
42
+
43
+ # Cleanup any listeners that might have leaked
44
+ until @listeners[0].time < MAX_TIME
45
+ @listeners.shift
46
+ end
47
+
48
+ if @listeners.length > MAX_COUNT
49
+ @listeners.shift
50
+ end
51
+ end
52
+ end
53
+
54
+ win
55
+ end
56
+
57
+ def release(win)
58
+ @lock.synchronize do
59
+ @listeners.delete(win)
60
+ end
61
+ end
62
+
63
+ def update
64
+ @lock.synchronize do
65
+ __update
66
+ end
67
+
68
+ nil
69
+ end
70
+
71
+ private
72
+
73
+ def __update
74
+ time = @profiler.total_time
75
+ diff = time - @time
76
+ @time = time
77
+
78
+ if diff > 0
79
+ @listeners.each do |l|
80
+ l.add(diff)
81
+ end
82
+ end
83
+ end
84
+
85
+ class Window
86
+ attr_reader :time
87
+
88
+ def initialize(global)
89
+ @global = global
90
+ @time = 0
91
+ end
92
+
93
+ def update
94
+ @global.update if @global
95
+ end
96
+
97
+ def add(time)
98
+ @time += time
99
+ end
100
+
101
+ def release
102
+ @global.release(self) if @global
103
+ end
104
+ end
105
+
106
+ end
107
+ end