drone 1.0.4 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +13 -0
- data/Guardfile +12 -0
- data/Rakefile +22 -37
- data/drone.gemspec +2 -6
- data/examples/collectd.rb +51 -0
- data/examples/common.rb +24 -0
- data/examples/json.rb +49 -0
- data/examples/redis_storage.rb +60 -0
- data/examples/simple.rb +3 -3
- data/extensions/drone_collectd/Gemfile +7 -0
- data/extensions/drone_collectd/LICENSE +20 -0
- data/extensions/drone_collectd/README.md +24 -0
- data/extensions/drone_collectd/drone_collectd.gemspec +28 -0
- data/extensions/drone_collectd/lib/drone_collectd.rb +7 -0
- data/extensions/drone_collectd/lib/drone_collectd/collectd.rb +97 -0
- data/extensions/drone_collectd/lib/drone_collectd/parser.rb +86 -0
- data/extensions/drone_collectd/specs/common.rb +3 -0
- data/extensions/drone_collectd/specs/unit/parser_spec.rb +49 -0
- data/extensions/drone_json/Gemfile +6 -0
- data/extensions/drone_json/LICENSE +20 -0
- data/extensions/drone_json/README.md +9 -0
- data/extensions/drone_json/drone_json.gemspec +32 -0
- data/extensions/drone_json/lib/drone_json.rb +9 -0
- data/extensions/drone_json/lib/drone_json/json.rb +100 -0
- data/extensions/drone_json/specs/common.rb +63 -0
- data/extensions/drone_redis/Gemfile +7 -0
- data/extensions/drone_redis/drone_redis.gemspec +22 -0
- data/extensions/drone_redis/lib/drone_redis.rb +8 -0
- data/extensions/drone_redis/lib/drone_redis/redis.rb +218 -0
- data/lib/drone.rb +1 -0
- data/lib/drone/errors.rb +11 -0
- data/lib/drone/metrics/histogram.rb +7 -6
- data/lib/drone/metrics/meter.rb +1 -1
- data/lib/drone/monitoring.rb +2 -2
- data/lib/drone/storage/memory.rb +1 -0
- data/lib/drone/utils/exponentially_decaying_sample.rb +79 -24
- data/lib/drone/version.rb +1 -1
- data/specs/{unit → metrics}/histogram_spec.rb +5 -1
- data/specs/metrics/meter_spec.rb +10 -2
- data/specs/metrics/timer_spec.rb +7 -1
- data/specs/{unit/monitoring_spec.rb → monitoring_spec.rb} +25 -1
- data/specs/{unit → utils}/ewma_spec.rb +1 -0
- data/specs/utils/exponentially_decaying_sample_spec.rb +140 -0
- data/specs/{unit → utils}/uniform_sample_spec.rb +0 -0
- metadata +72 -93
- data/specs/unit/exponentially_decaying_sample_spec.rb +0 -86
data/Gemfile
CHANGED
@@ -2,3 +2,16 @@ source "http://rubygems.org"
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in drone.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
group(:test) do
|
7
|
+
gem 'rake'
|
8
|
+
gem 'mocha'
|
9
|
+
gem 'schmurfy-bacon', :path => '/Users/schmurfy/Dev/personal/bacon'
|
10
|
+
gem 'schmurfy-em-spec'
|
11
|
+
gem 'delorean'
|
12
|
+
gem 'simplecov'
|
13
|
+
gem 'guard'
|
14
|
+
gem 'guard-bacon'
|
15
|
+
gem 'rb-fsevent'
|
16
|
+
gem 'growl'
|
17
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
|
5
|
+
# parameters:
|
6
|
+
# output => the formatted to use
|
7
|
+
# backtrace => number of lines, nil = everything
|
8
|
+
guard 'bacon', :output => "BetterOutput", :backtrace => 4 do
|
9
|
+
watch(%r{^lib/drone/(.+)\.rb$}) { |m| "specs/#{m[1]}_spec.rb" }
|
10
|
+
watch(%r{specs/.+\.rb$})
|
11
|
+
end
|
12
|
+
|
data/Rakefile
CHANGED
@@ -1,49 +1,34 @@
|
|
1
|
-
require
|
1
|
+
require "bundler/gem_tasks"
|
2
2
|
|
3
|
-
|
3
|
+
task :default => :"test:core"
|
4
4
|
|
5
|
-
|
5
|
+
EXTENSIONS = Dir[File.expand_path('../extensions/*', __FILE__)].map{|path| File.basename(path) }
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
namespace :test do
|
8
|
+
desc "core specs"
|
9
|
+
task :core do
|
10
|
+
require 'bacon'
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
# extensions
|
20
|
-
Dir["extensions/**/*.gemspec"].each do |path|
|
21
|
-
build_gem(path)
|
12
|
+
Dir[File.expand_path('../specs/**/*_spec.rb', __FILE__)].each do |file|
|
13
|
+
load(file)
|
14
|
+
end
|
15
|
+
|
16
|
+
EXTENSIONS.each do |ext|
|
17
|
+
Rake::Task["test:#{ext}"].invoke
|
18
|
+
end
|
22
19
|
end
|
23
20
|
|
24
|
-
end
|
25
|
-
|
26
21
|
|
27
|
-
# task :release do
|
28
|
-
# Dir.chdir(File.expand_path('../pkg', __FILE__)) do
|
29
|
-
# %()
|
30
|
-
#
|
31
|
-
# end
|
32
|
-
# end
|
33
22
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
23
|
+
EXTENSIONS.each do |ext|
|
24
|
+
desc "specs for #{ext}"
|
25
|
+
task ext do
|
26
|
+
require 'bacon'
|
27
|
+
Dir[File.expand_path("../extensions/#{ext}/specs/**/*_spec.rb", __FILE__)].each do |file|
|
28
|
+
load(file)
|
29
|
+
end
|
39
30
|
end
|
40
31
|
end
|
32
|
+
|
41
33
|
end
|
42
34
|
|
43
|
-
begin
|
44
|
-
require 'yard'
|
45
|
-
require 'bluecloth'
|
46
|
-
YARD::Rake::YardocTask.new(:doc)
|
47
|
-
rescue LoadError
|
48
|
-
|
49
|
-
end
|
data/drone.gemspec
CHANGED
@@ -19,11 +19,7 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
20
|
s.require_paths = ["lib"]
|
21
21
|
|
22
|
-
s.add_dependency("eventmachine",
|
22
|
+
s.add_dependency("eventmachine", ">= 0.12.10")
|
23
|
+
s.add_dependency("flt", "~> 1.3.0")
|
23
24
|
|
24
|
-
s.add_development_dependency("mocha")
|
25
|
-
s.add_development_dependency("bacon")
|
26
|
-
s.add_development_dependency("schmurfy-em-spec")
|
27
|
-
s.add_development_dependency("delorean")
|
28
|
-
s.add_development_dependency("simplecov")
|
29
25
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require File.expand_path('../common', __FILE__)
|
2
|
+
init_environment('drone_collectd')
|
3
|
+
|
4
|
+
Drone::init_drone()
|
5
|
+
Drone::register_gauge("cpu:user/gauge"){ rand(200) }
|
6
|
+
|
7
|
+
class User
|
8
|
+
include Drone::Monitoring
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
12
|
+
end
|
13
|
+
|
14
|
+
monitor_rate("apps:app1/meter")
|
15
|
+
def rename(new_name)
|
16
|
+
@name = new_name
|
17
|
+
end
|
18
|
+
|
19
|
+
monitor_time("apps:app1/timer")
|
20
|
+
def do_something
|
21
|
+
# just eat some cpu
|
22
|
+
0.upto(rand(2000)) do |n|
|
23
|
+
str = "a"
|
24
|
+
200.times{ str << "b" }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
EM::run do
|
30
|
+
Drone::add_output(:collectd, 2,
|
31
|
+
:hostname => 'my_app',
|
32
|
+
:address => '127.0.0.1',
|
33
|
+
:port => 25826,
|
34
|
+
:percentiles => [0.5, 0.75, 0.95]
|
35
|
+
)
|
36
|
+
Drone::start_monitoring()
|
37
|
+
|
38
|
+
counter1 = Drone::register_counter("apps:app1/counter")
|
39
|
+
counter1.increment()
|
40
|
+
|
41
|
+
a = User.new("bob")
|
42
|
+
|
43
|
+
EM::add_periodic_timer(2) do
|
44
|
+
rand(100).times{|n| a.rename("user#{n}") }
|
45
|
+
counter1.increment()
|
46
|
+
end
|
47
|
+
|
48
|
+
EM::add_periodic_timer(1) do
|
49
|
+
a.do_something()
|
50
|
+
end
|
51
|
+
end
|
data/examples/common.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
|
5
|
+
def init_environment(extension = nil)
|
6
|
+
if extension
|
7
|
+
# use the correct Gemfile
|
8
|
+
ENV['BUNDLE_GEMFILE'] = File.expand_path("../../extensions/#{extension}/Gemfile", __FILE__)
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'bundler/setup'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
|
14
|
+
|
15
|
+
require 'drone'
|
16
|
+
|
17
|
+
if extension
|
18
|
+
$LOAD_PATH.unshift(File.expand_path("../../extensions/#{extension}/lib", __FILE__))
|
19
|
+
require extension
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
|
data/examples/json.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.expand_path('../common', __FILE__)
|
2
|
+
init_environment('drone_json')
|
3
|
+
|
4
|
+
Drone::init_drone()
|
5
|
+
Drone::register_gauge("cpu:0/user"){ rand(200) }
|
6
|
+
|
7
|
+
class User
|
8
|
+
include Drone::Monitoring
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
12
|
+
end
|
13
|
+
|
14
|
+
monitor_rate("users:rename")
|
15
|
+
def rename(new_name)
|
16
|
+
@name = new_name
|
17
|
+
end
|
18
|
+
|
19
|
+
monitor_time("users:do_something")
|
20
|
+
def do_something
|
21
|
+
# just eat some cpu
|
22
|
+
0.upto(rand(2000)) do |n|
|
23
|
+
str = "a"
|
24
|
+
200.times{ str << "b" }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
EM::run do
|
30
|
+
Drone::add_output(:json, '127.0.0.1', 3001)
|
31
|
+
Drone::start_monitoring()
|
32
|
+
|
33
|
+
counter1 = Drone::register_counter("something_counted")
|
34
|
+
counter1.increment()
|
35
|
+
|
36
|
+
a = User.new("bob")
|
37
|
+
|
38
|
+
EM::add_periodic_timer(2) do
|
39
|
+
rand(100).times do |n|
|
40
|
+
ret = a.rename("user#{n}")
|
41
|
+
end
|
42
|
+
|
43
|
+
counter1.increment()
|
44
|
+
end
|
45
|
+
|
46
|
+
EM::add_periodic_timer(1) do
|
47
|
+
a.do_something()
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require File.expand_path('../common', __FILE__)
|
2
|
+
init_environment('drone_redis')
|
3
|
+
|
4
|
+
Drone::init_drone(
|
5
|
+
Drone::Schedulers::EMScheduler,
|
6
|
+
Drone::Storage::Redis.new('127.0.0.1', 1)
|
7
|
+
)
|
8
|
+
|
9
|
+
Drone::register_gauge("cpu:0/user"){ rand(200) }
|
10
|
+
|
11
|
+
class User
|
12
|
+
include Drone::Monitoring
|
13
|
+
|
14
|
+
def initialize(name)
|
15
|
+
@name = name
|
16
|
+
end
|
17
|
+
|
18
|
+
monitor_rate("users:rename:rate")
|
19
|
+
def rename(new_name)
|
20
|
+
@name = new_name
|
21
|
+
end
|
22
|
+
|
23
|
+
monitor_time("users:do_something:time")
|
24
|
+
monitor_rate("users:do_something:rate")
|
25
|
+
def do_something
|
26
|
+
# just eat some cpu
|
27
|
+
0.upto(rand(2000)) do |n|
|
28
|
+
str = "a"
|
29
|
+
200.times{ str << "b" }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
EM::run do
|
35
|
+
# Drone::add_output(:json, '127.0.0.1', 3001)
|
36
|
+
|
37
|
+
Fiber.new do
|
38
|
+
Drone::start_monitoring()
|
39
|
+
end.resume
|
40
|
+
|
41
|
+
counter1 = Drone::register_counter("something_counted")
|
42
|
+
a = nil
|
43
|
+
Fiber.new do
|
44
|
+
counter1.increment()
|
45
|
+
a = User.new("bob")
|
46
|
+
end.resume
|
47
|
+
|
48
|
+
EM::add_periodic_timer(2) do
|
49
|
+
Fiber.new do
|
50
|
+
rand(100).times{|n| a.rename("user#{n}") }
|
51
|
+
counter1.increment()
|
52
|
+
end.resume
|
53
|
+
end
|
54
|
+
|
55
|
+
EM::add_periodic_timer(1) do
|
56
|
+
Fiber.new do
|
57
|
+
a.do_something()
|
58
|
+
end.resume
|
59
|
+
end
|
60
|
+
end
|
data/examples/simple.rb
CHANGED
@@ -21,9 +21,9 @@ class User
|
|
21
21
|
monitor_time("users:do_something:time")
|
22
22
|
monitor_rate("users:do_something:rate")
|
23
23
|
def do_something
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
fb = Fiber.current
|
25
|
+
EM::add_timer(1){ fb.resume() }
|
26
|
+
Fiber.yield
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011-2011 Julien Ammous
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,24 @@
|
|
1
|
+
## What is this
|
2
|
+
|
3
|
+
It is an output interface to collectd for Drone.<br/>
|
4
|
+
You can find more about Drone [here](https://github.com/schmurfy/drone)
|
5
|
+
|
6
|
+
# Supported Runtimes
|
7
|
+
|
8
|
+
- MRI 1.8.7+
|
9
|
+
- Rubinius 1.2.2+
|
10
|
+
|
11
|
+
|
12
|
+
# How to use
|
13
|
+
|
14
|
+
First you obviously need a collectd server (or any server able to receive collectd network packets),
|
15
|
+
after that you need to add those lines to your types.db config file if you use collectd:
|
16
|
+
|
17
|
+
meter mean:GAUGE:U:U, rate1:GAUGE:U:U, rate5:GAUGE:U:U, rate15:GAUGE:U:U
|
18
|
+
timer min:GAUGE:0:U, max:GAUGE:0:U, mean:GAUGE:0:U, stddev:GAUGE:U:U, median:GAUGE:0:U, p75:GAUGE:0:U, p95:GAUGE:0:U
|
19
|
+
|
20
|
+
They are required to be able to understand timers and meters sent from Drone, you can update the timer
|
21
|
+
as you wish to add/remove pecentiles (check examples/collectd to see how to configure the interface for that).
|
22
|
+
|
23
|
+
|
24
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../../../lib/drone/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "drone_collectd"
|
6
|
+
s.version = Drone::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Julien Ammous"]
|
9
|
+
s.email = []
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Drone Collectd Interface}
|
12
|
+
s.description = %q{Collectd Interface for Drone}
|
13
|
+
|
14
|
+
s.rubyforge_project = "drone_collectd"
|
15
|
+
|
16
|
+
s.files = Dir['LICENSE', 'README.md', 'lib/**/*']
|
17
|
+
s.test_files = Dir['specs/**/*']
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_dependency('drone', '~> 1.0.4')
|
21
|
+
s.add_dependency('eventmachine', '~> 0.12.10')
|
22
|
+
|
23
|
+
s.add_development_dependency("mocha")
|
24
|
+
s.add_development_dependency("bacon")
|
25
|
+
s.add_development_dependency("schmurfy-em-spec")
|
26
|
+
s.add_development_dependency("delorean")
|
27
|
+
s.add_development_dependency("simplecov")
|
28
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require File.expand_path('../parser', __FILE__)
|
2
|
+
|
3
|
+
module Drone
|
4
|
+
module Interfaces
|
5
|
+
|
6
|
+
##
|
7
|
+
# Send data to collectd periodically, this interface except
|
8
|
+
# a specific format for the metric names which is:
|
9
|
+
#
|
10
|
+
# plugin[:plugin_instance]/type[:type_instance]
|
11
|
+
# the simplest form being:
|
12
|
+
# plugin/type
|
13
|
+
#
|
14
|
+
class Collectd < Base
|
15
|
+
|
16
|
+
# 1.9 only ...
|
17
|
+
# NAME_FORMAT = %r{(?<plugin>\w+)(:(?<plugin_instance>\w+))?/(?<type>\w+)(:(?<type_instance>\w+))?}
|
18
|
+
|
19
|
+
NAME_FORMAT = %r{(\w+)(?::(\w+))?/(\w+)(?::(\w+))?}
|
20
|
+
|
21
|
+
##
|
22
|
+
# Instantiate a collectd interface
|
23
|
+
#
|
24
|
+
# @param [Numeric] period the period passed to the Base class
|
25
|
+
# @param [String] hostname the hostname to use for collectd
|
26
|
+
# @param [String] address the address where the collectd daemon is listening
|
27
|
+
# @param [Numeric] port The collectd daemon port
|
28
|
+
#
|
29
|
+
def initialize(period, args = {})
|
30
|
+
super(period)
|
31
|
+
@hostname = args.delete(:hostname)
|
32
|
+
@address = args.delete(:address) || '127.0.0.1'
|
33
|
+
@port = args.delete(:port) || 25826
|
34
|
+
@reported_percentiles = args.delete(:percentiles)
|
35
|
+
@socket = EM::open_datagram_socket('0.0.0.0', nil)
|
36
|
+
|
37
|
+
unless args.empty?
|
38
|
+
raise ArgumentError, "unknown keys: #{args.keys}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def output
|
43
|
+
|
44
|
+
Drone::each_metric do |m|
|
45
|
+
# parse the name
|
46
|
+
if NAME_FORMAT.match(m.name)
|
47
|
+
# build the packet
|
48
|
+
data = DroneCollectd::CollectdPacket.new
|
49
|
+
data.host = @hostname
|
50
|
+
data.time = Time.now.to_i
|
51
|
+
data.interval = @period
|
52
|
+
data.plugin = $1.to_s
|
53
|
+
data.plugin_instance = $2.to_s
|
54
|
+
data.type = $3.to_s
|
55
|
+
data.type_instance = $4.to_s
|
56
|
+
|
57
|
+
case m
|
58
|
+
when Metrics::Counter
|
59
|
+
data.add_value(:counter, m.value )
|
60
|
+
|
61
|
+
when Metrics::Gauge
|
62
|
+
data.add_value(:gauge, m.value )
|
63
|
+
|
64
|
+
when Metrics::Meter
|
65
|
+
# mean:GAUGE:U:U, rate1:GAUGE:U:U, rate5:GAUGE:U:U, rate15:GAUGE:U:U
|
66
|
+
data.add_value(:gauge, m.mean_rate )
|
67
|
+
data.add_value(:gauge, m.one_minute_rate )
|
68
|
+
data.add_value(:gauge, m.five_minutes_rate )
|
69
|
+
data.add_value(:gauge, m.fifteen_minutes_rate )
|
70
|
+
|
71
|
+
when Metrics::Timer
|
72
|
+
# min:GAUGE:0:U, max:GAUGE:0:U, mean:GAUGE:0:U, stddev:GAUGE:U:U, median:GAUGE:0:U, p75:GAUGE:0:U, p95:GAUGE:0:U
|
73
|
+
data.add_value(:gauge, m.min )
|
74
|
+
data.add_value(:gauge, m.max )
|
75
|
+
data.add_value(:gauge, m.mean )
|
76
|
+
data.add_value(:gauge, m.stdDev )
|
77
|
+
|
78
|
+
percs = m.percentiles( *@reported_percentiles )
|
79
|
+
percs.each do |p|
|
80
|
+
data.add_value(:gauge, p )
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
# and send it
|
86
|
+
@socket.send_datagram(data.build_packet, @address, @port)
|
87
|
+
else
|
88
|
+
puts "Metric with incorrect name ignored: #{m.name}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|