malevich 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/bin/malevich +38 -0
- data/example/cpu.rb +31 -0
- data/example/disk.rb +59 -0
- data/example/disk_stat.rb +28 -0
- data/example/dns_check.rb +7 -0
- data/example/exim.rb +15 -0
- data/example/find_files.rb +21 -0
- data/example/http.rb +25 -0
- data/example/iptables.rb +27 -0
- data/example/la.rb +21 -0
- data/example/mdadm.rb +44 -0
- data/example/megacli.rb +13 -0
- data/example/memory.rb +31 -0
- data/example/net.rb +25 -0
- data/example/net_stat.rb +25 -0
- data/example/nginx.rb +22 -0
- data/example/ntp.rb +15 -0
- data/example/pgsql.rb +71 -0
- data/example/runit.rb +48 -0
- data/example/status_file.rb +17 -0
- data/example/tw_cli.rb +17 -0
- data/lib/malevich.rb +59 -0
- data/lib/malevich/dsl.rb +78 -0
- data/lib/malevich/init.rb +17 -0
- data/lib/malevich/loader.rb +80 -0
- data/lib/malevich/monitor.rb +40 -0
- data/lib/malevich/plugin.rb +70 -0
- data/lib/malevich/plugin/error.rb +12 -0
- data/lib/malevich/plugin/event.rb +68 -0
- data/lib/malevich/plugin/http.rb +25 -0
- data/lib/malevich/plugin/init.rb +5 -0
- data/lib/malevich/plugin/shell_out.rb +28 -0
- data/lib/malevich/plugin/time.rb +9 -0
- data/lib/malevich/responders/error.rb +30 -0
- data/lib/malevich/responders/http.rb +46 -0
- data/lib/malevich/responders/init.rb +4 -0
- data/lib/malevich/responders/riemann.rb +59 -0
- data/lib/malevich/responders/udp.rb +59 -0
- data/lib/malevich/version.rb +3 -0
- data/malevich.gemspec +32 -0
- data/test/plugin_helpers_spec.rb +6 -0
- metadata +219 -0
data/example/net.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
interval 60
|
2
|
+
always_start true
|
3
|
+
|
4
|
+
settings :include_alias, false
|
5
|
+
settings :filter, %W('rx bytes', 'rx errs', 'rx drop', 'tx bytes', 'tx errs', 'tx drop')
|
6
|
+
settings :words, %W('rx bytes', 'rx packets', 'rx errs', 'rx drop', 'rx fifo', 'rx frame',
|
7
|
+
'rx compressed', 'rx multicast', 'tx bytes', 'tx packets', 'tx drops',
|
8
|
+
'tx fifo', 'tx colls', 'tx carrier', 'tx compressed')
|
9
|
+
|
10
|
+
collect "linux" do
|
11
|
+
File.read('/proc/net/dev').each_line do |line|
|
12
|
+
iface = line.split(':')[0].strip
|
13
|
+
iface.gsub!(/\./, '_')
|
14
|
+
next if (iface =~ /\./ && !settings.include_alias)
|
15
|
+
next unless line =~ /(\w*)\:\s*([\s\d]+)\s*/
|
16
|
+
settings.words.map do |service|
|
17
|
+
service
|
18
|
+
end.zip(
|
19
|
+
$2.split(/\s+/).map { |str| str.to_i }
|
20
|
+
).each do |service, value|
|
21
|
+
next unless settings.filter.include? service
|
22
|
+
event(:service => "net #{iface} #{service}", :metric => value.to_f/interval, :diff => true)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/example/net_stat.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
interval 5
|
2
|
+
settings :ports, [80, 3994]
|
3
|
+
|
4
|
+
collect "linux" do
|
5
|
+
|
6
|
+
filter = nil
|
7
|
+
settings.ports.each do |port|
|
8
|
+
if filter == nil
|
9
|
+
filter = "\\( src *:#{port}"
|
10
|
+
else
|
11
|
+
filter += " or src *:#{port}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
filter += " \\) and not dst 127.0.0.1:*"
|
15
|
+
cmd = 'ss -t -4 -n state established ' + filter + ' | wc -l'
|
16
|
+
|
17
|
+
count = shell!(cmd).to_i - 1
|
18
|
+
|
19
|
+
event(
|
20
|
+
:service => "netstat tcp #{settings.ports.join(', ')}",
|
21
|
+
:metric => count,
|
22
|
+
:description => "count established connects: #{count} to ports #{settings.ports.join(', ')}"
|
23
|
+
)
|
24
|
+
|
25
|
+
end
|
data/example/nginx.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
always_start true
|
2
|
+
interval 60
|
3
|
+
|
4
|
+
settings :file, '/etc/nginx/sites-enabled/status'
|
5
|
+
settings :url, 'http://127.0.0.1:11311/status'
|
6
|
+
settings :nginx_status_1, %W(accepts handled requests)
|
7
|
+
settings :nginx_status_2, %W(reading writing waiting)
|
8
|
+
|
9
|
+
run_if "linux", "mac_os_x" do
|
10
|
+
File.exists? settings.file
|
11
|
+
end
|
12
|
+
|
13
|
+
collect "linux", "mac_os_x" do
|
14
|
+
lines = http_get(settings.url).split("\n")
|
15
|
+
lines[2].scan(/\d+/).each_with_index do |value, index|
|
16
|
+
event(:service => "nginx #{settings.nginx_status_1[index]}", :metric => value.to_f/interval, :diff => true)
|
17
|
+
end
|
18
|
+
event(:service => 'nginx active', :metric => lines[0].split(':')[1].strip.to_i)
|
19
|
+
lines[3].scan(/\d+/).each_with_index do |value, index|
|
20
|
+
event(:service => plugin.service + " #{settings.nginx_status_2[index]}", :metric => value.to_i)
|
21
|
+
end
|
22
|
+
end
|
data/example/ntp.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'net/ntp'
|
2
|
+
|
3
|
+
interval 60
|
4
|
+
warning 5
|
5
|
+
critical 10
|
6
|
+
settings :host, 'pool.ntp.org'
|
7
|
+
settings :timeout, 30
|
8
|
+
|
9
|
+
collect "linux", "mac_os_x" do
|
10
|
+
event(
|
11
|
+
:service => "ntp #{settings.host}",
|
12
|
+
:description => "Ntp lag with host #{settings.host}",
|
13
|
+
:metric => (::Net::NTP.get(plugin.host, 'ntp', settings.timeout).time.to_f - Time.now.to_f).abs
|
14
|
+
)
|
15
|
+
end
|
data/example/pgsql.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
interval 60
|
2
|
+
|
3
|
+
warning 120 # replication lag
|
4
|
+
critical 500 # replication lag
|
5
|
+
settings :host, '127.0.0.1'
|
6
|
+
settings :user, 'postgres'
|
7
|
+
settings :pgsql, '/usr/bin/psql'
|
8
|
+
settings :db4monit, 'riemann_monit'
|
9
|
+
settings :conn_warn, 5 # reserved pool connections
|
10
|
+
settings :conn_crit, 3 # reserved pool connections
|
11
|
+
|
12
|
+
run_if "linux" do
|
13
|
+
File.exists? settings.pgsql
|
14
|
+
end
|
15
|
+
|
16
|
+
collect "linux" do
|
17
|
+
|
18
|
+
# helpers
|
19
|
+
def run_sql(sql, db='postgres')
|
20
|
+
shell_out!("#{settings.psql} -h #{settings.host} -U #{settings.user} -tnc \"#{sql}\" #{db}").stdout
|
21
|
+
end
|
22
|
+
|
23
|
+
def in_recovery?
|
24
|
+
run_sql('select pg_is_in_recovery()') == 't'
|
25
|
+
end
|
26
|
+
|
27
|
+
def db4monit_exists?
|
28
|
+
run_sql("select 1 from pg_database where datname = '#{settings.db4monit}'") == '1'
|
29
|
+
end
|
30
|
+
|
31
|
+
def run_master_sql
|
32
|
+
run_sql("create database #{settings.db4monit}") unless db4monit_exists?
|
33
|
+
run_sql(
|
34
|
+
"drop table if exists timestamp; \
|
35
|
+
create table timestamp ( id int primary key, value timestamp default now() ); \
|
36
|
+
insert into timestamp (id) values (1); \
|
37
|
+
", settings.db4monit)
|
38
|
+
end
|
39
|
+
|
40
|
+
def repl_lag
|
41
|
+
unixnow - run_sql("select extract(epoch from value::timestamp) from timestamp where id = 1;", settings.db4monit).to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
def connections
|
45
|
+
max_conn = run_sql('show max_connections').to_i
|
46
|
+
res_conn = run_sql('show superuser_reserved_connections').to_i
|
47
|
+
cur_conn = run_sql('select count(1) from pg_stat_activity;').to_i
|
48
|
+
[cur_conn, (max_conn - res_conn - cur_conn)]
|
49
|
+
end
|
50
|
+
|
51
|
+
# check status
|
52
|
+
|
53
|
+
cur_conn, res_conn = connections
|
54
|
+
if in_recovery?
|
55
|
+
event(:service => 'pgsql replication lag', :description => 'Postgresql replication lag', :metric => repl_lag)
|
56
|
+
else
|
57
|
+
run_master_sql
|
58
|
+
end
|
59
|
+
|
60
|
+
event(:service => 'pgsql connections', :description => 'Postgresql current connections', :state => 'ok', :metric => cur_conn)
|
61
|
+
|
62
|
+
# check reserved pool size
|
63
|
+
if res_conn < settings.conn_warn
|
64
|
+
if res_conn > settings.conn_crit
|
65
|
+
event(:service => 'pgsql reserved connections', :description => 'Postgresql reserved connections state', :state => 'warning', :metric => res_conn)
|
66
|
+
else
|
67
|
+
event(:service => 'pgsql reserved connections', :description => 'Postgresql reserved connections state', :state => 'critical', :metric => res_conn)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
data/example/runit.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
interval 60
|
2
|
+
always_start true
|
3
|
+
|
4
|
+
run_if "linux" do
|
5
|
+
Dir.exists? '/etc/service'
|
6
|
+
end
|
7
|
+
|
8
|
+
collect "linux" do
|
9
|
+
|
10
|
+
@status_history ||= Array.new
|
11
|
+
|
12
|
+
def uptime(service)
|
13
|
+
pid_file = File.join(service, 'supervise', 'pid')
|
14
|
+
return 0 unless File.exist?(pid_file)
|
15
|
+
unixnow - File.mtime(pid_file).to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
def runned?(service)
|
19
|
+
stat_file = File.join(service, 'supervise', 'stat')
|
20
|
+
return false unless File.exists?(stat_file)
|
21
|
+
File.read(stat_file).strip == 'run'
|
22
|
+
end
|
23
|
+
|
24
|
+
def human_srv(service)
|
25
|
+
service.gsub(/\/etc\/service\//, '')
|
26
|
+
end
|
27
|
+
|
28
|
+
Dir.glob('/etc/service/*').each do |srv|
|
29
|
+
srv_uptime = uptime(srv)
|
30
|
+
srv_runned = runned?(srv)
|
31
|
+
srv_name = human_srv(srv)
|
32
|
+
|
33
|
+
# сервис запущен и работает дольше чем мы приходили к нему в прошлый раз
|
34
|
+
if srv_runned && srv_uptime > interval
|
35
|
+
@status_history.delete(srv_name)
|
36
|
+
event(:service => "runit #{srv_name}", :state => 'ok', :description => "runit service #{srv_name} running")
|
37
|
+
else
|
38
|
+
# сервис запущен но работает подозрительно мало, но последний раз замечен не был
|
39
|
+
if srv_uptime < interval && srv_runned && !@status_history.include?(srv_name)
|
40
|
+
@status_history << srv_name
|
41
|
+
else
|
42
|
+
# во всех остальных случаях сообщаем о проблеме
|
43
|
+
event(:service => "runit #{srv_name}", :state => 'critical', :description => "runit service #{srv_name} not running", :metric => srv_uptime)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
interval 60
|
2
|
+
|
3
|
+
settings :file, '/var/tmp/error.txt'
|
4
|
+
settings :max_lines, 100
|
5
|
+
settings :report_lines, 5
|
6
|
+
settings :service, 'check state file'
|
7
|
+
|
8
|
+
critical 1
|
9
|
+
|
10
|
+
collect "linux" do
|
11
|
+
content = File.read(settings.file).split("\n").delete_if { |x| x.strip.empty? }
|
12
|
+
event(
|
13
|
+
:service => "#{settings.service} #{settings.file}",
|
14
|
+
:description => content.last(settings.report_lines).join("\n"),
|
15
|
+
:metric => content.count
|
16
|
+
)
|
17
|
+
end
|
data/example/tw_cli.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
interval 180
|
2
|
+
always_start true
|
3
|
+
|
4
|
+
settings :cmd, "/usr/sbin/tw_cli show | awk '/^c/{print $1}' | xargs -rI{} /usr/sbin/tw_cli /{} show | awk '/^[upb]/&&!/[ \t](OK|VERIFYING|VERIFY-PAUSED)/' |wc -l"
|
5
|
+
critical 1
|
6
|
+
|
7
|
+
run_if "linux" do
|
8
|
+
File.exists? '/usr/sbin/tw_cli'
|
9
|
+
end
|
10
|
+
|
11
|
+
collect "linux" do
|
12
|
+
event(
|
13
|
+
:service => 'twcli',
|
14
|
+
:metric => shell!(settings.cmd).to_i,
|
15
|
+
:description => 'Hardware raid tw_cli status'
|
16
|
+
)
|
17
|
+
end
|
data/lib/malevich.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'malevich/init'
|
2
|
+
|
3
|
+
module Malevich
|
4
|
+
class Kernel
|
5
|
+
|
6
|
+
include Malevich::Loader
|
7
|
+
|
8
|
+
attr_reader :config, :cmd, :plugins, :ohai, :monitor, :events, :dsl
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@config = Hashie::Mash.new # for config from file
|
12
|
+
@cmd = Hashie::Mash.new
|
13
|
+
@plugins = Hashie::Mash.new # for plugin containers (errors, settings)
|
14
|
+
@logger = Logger.new(STDOUT)
|
15
|
+
@monitor = Malevich::Monitor.new
|
16
|
+
@events = ::Queue.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_plugin(file)
|
20
|
+
return unless malevich.cmd[:test_given]
|
21
|
+
Malevich::DSL.test(file).run!
|
22
|
+
exit 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def log(level = :info, message)
|
26
|
+
@logger.send(level.to_sym, message)
|
27
|
+
end
|
28
|
+
|
29
|
+
def log_level=(level = 'INFO')
|
30
|
+
@logger.level = Logger.const_get(level)
|
31
|
+
end
|
32
|
+
|
33
|
+
def ohai
|
34
|
+
if Time.now.to_i - @ohai_timestamp.to_i > 60 * 60
|
35
|
+
Kernel::log :info, 'Load ohai data'
|
36
|
+
real_ohai = Ohai::System.new
|
37
|
+
real_ohai.all_plugins
|
38
|
+
@ohai_timestamp = Time.now
|
39
|
+
@ohai = real_ohai.data
|
40
|
+
else
|
41
|
+
@ohai
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module Kernel
|
49
|
+
|
50
|
+
def malevich
|
51
|
+
$__malevich_utils ||= Malevich::Kernel.new
|
52
|
+
end
|
53
|
+
|
54
|
+
def log(level = :info, message)
|
55
|
+
speaker = self.class == Class ? "C: #{self.to_s}" : "I: #{self.class.to_s}"
|
56
|
+
malevich.log(level, "[#{speaker}] #{message}")
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
data/lib/malevich/dsl.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module Malevich
|
2
|
+
class DSL
|
3
|
+
|
4
|
+
PLUGIN_EXT = '.rb'
|
5
|
+
|
6
|
+
attr_reader :plugins
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@plugins = Array.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def interval(val)
|
13
|
+
plugins.last.interval = val.to_f
|
14
|
+
end
|
15
|
+
|
16
|
+
def always_start(val)
|
17
|
+
plugins.last.always_start = !!val
|
18
|
+
end
|
19
|
+
alias :auto :always_start
|
20
|
+
alias :auto_start :always_start
|
21
|
+
|
22
|
+
def run_if(*name, &block)
|
23
|
+
return unless plugins.last.suitable_platform?(name)
|
24
|
+
plugins.last.run_if = block
|
25
|
+
end
|
26
|
+
|
27
|
+
def state(attr, val)
|
28
|
+
plugins.last.settings[attr] = val
|
29
|
+
end
|
30
|
+
alias :states :state
|
31
|
+
|
32
|
+
def critical(val)
|
33
|
+
plugins.last.settings[:critical] = val
|
34
|
+
end
|
35
|
+
alias :critical= :critical
|
36
|
+
|
37
|
+
def warning(val)
|
38
|
+
plugins.last.settings[:warning] = val
|
39
|
+
end
|
40
|
+
alias :warning= :warning
|
41
|
+
|
42
|
+
def settings(attr, val)
|
43
|
+
plugins.last.settings[attr] = val
|
44
|
+
end
|
45
|
+
alias :config :settings
|
46
|
+
alias :defaults :settings
|
47
|
+
alias :plugin :settings
|
48
|
+
|
49
|
+
def collect(*name, &block)
|
50
|
+
return unless plugins.last.suitable_platform?(name)
|
51
|
+
plugins.last.collect = block
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.load(paths)
|
55
|
+
dsl = Malevich::DSL.new
|
56
|
+
paths.map do |path|
|
57
|
+
File.directory?(path) ? Dir["#{path}/*#{PLUGIN_EXT}"].sort : path
|
58
|
+
end.flatten.each do |path|
|
59
|
+
begin
|
60
|
+
log :debug, "Load plugin #{path}"
|
61
|
+
dsl.plugins << Malevich::Plugin.new(File.basename(path, PLUGIN_EXT))
|
62
|
+
dsl.instance_eval(File.read(path), path)
|
63
|
+
rescue ScriptError
|
64
|
+
dsl.plugins.pop # todo: plugin.new creates
|
65
|
+
log :error, "ScriptError: plugin from file #{path}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
dsl.plugins
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.test(file)
|
72
|
+
dsl = Malevich::DSL.new
|
73
|
+
dsl.instance_eval(File.read(file), file)
|
74
|
+
dsl.plugins[0]
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'yaml'
|
3
|
+
require 'find'
|
4
|
+
require 'hashie'
|
5
|
+
require 'ohai'
|
6
|
+
require 'logger'
|
7
|
+
require 'thread'
|
8
|
+
require 'socket'
|
9
|
+
require 'resolv'
|
10
|
+
|
11
|
+
require 'malevich/version'
|
12
|
+
require 'malevich/dsl'
|
13
|
+
require 'malevich/plugin'
|
14
|
+
require 'malevich/monitor'
|
15
|
+
require 'malevich/loader'
|
16
|
+
require 'malevich/responders/init'
|
17
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Malevich
|
2
|
+
module Loader
|
3
|
+
|
4
|
+
def load_responders
|
5
|
+
Malevich::Responder.constants.select do |c|
|
6
|
+
next unless Class === Malevich::Responder.const_get(c)
|
7
|
+
monitor << Malevich::Responder.const_get(c).new
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def load_plugins(plugin_path, config_file)
|
12
|
+
|
13
|
+
def find_plugin(name)
|
14
|
+
@all_plugins.find {|p| p.name == name}
|
15
|
+
end
|
16
|
+
|
17
|
+
@all_plugins = Malevich::DSL.load(plugin_path)
|
18
|
+
@all_names = Array.new
|
19
|
+
@plugins_to_run = Array.new
|
20
|
+
|
21
|
+
self.config.deep_merge! YAML.load_file(config_file) #rescue {}
|
22
|
+
self.config.each do |name, val|
|
23
|
+
@all_names << name
|
24
|
+
next if val.nil?
|
25
|
+
# dup plugins for val - array
|
26
|
+
if val.kind_of?(Array)
|
27
|
+
parent = find_plugin(name)
|
28
|
+
if parent.nil?
|
29
|
+
Kernel::log :error, "Unable to find parent plugin for #{name}"
|
30
|
+
next
|
31
|
+
end
|
32
|
+
val.each_with_index do |p_settings, i|
|
33
|
+
child = parent.dup
|
34
|
+
child.name = "#{name}_#{i}"
|
35
|
+
child.settings.deep_merge!(parent.settings.dup)
|
36
|
+
child.settings.deep_merge!(p_settings)
|
37
|
+
@all_plugins << child
|
38
|
+
@all_names << child.name
|
39
|
+
end
|
40
|
+
# delete parent plugin
|
41
|
+
@all_plugins.delete(parent)
|
42
|
+
next
|
43
|
+
end
|
44
|
+
|
45
|
+
# dup plugin with parent
|
46
|
+
if val.is_a?(Hash) && val.has_key?('parent')
|
47
|
+
parent_name = val['parent']
|
48
|
+
parent = find_plugin(parent_name)
|
49
|
+
if parent.nil?
|
50
|
+
Kernel::log :error, "Plugin #{parent_name} not found"
|
51
|
+
next
|
52
|
+
end
|
53
|
+
child = parent.dup
|
54
|
+
child.name = name
|
55
|
+
child.settings.deep_merge!(parent.settings.dup)
|
56
|
+
child.settings.deep_merge!(val)
|
57
|
+
@all_plugins << child
|
58
|
+
@all_names << child.name
|
59
|
+
next
|
60
|
+
end
|
61
|
+
# over plugins merge
|
62
|
+
@all_plugins.each {|p| p.settings.deep_merge!(val) if name == p.name }
|
63
|
+
end
|
64
|
+
# add plugin if it always_start or get settings and runnable
|
65
|
+
@all_plugins.each do |p|
|
66
|
+
unless p.always_start || @all_names.include?(p.name)
|
67
|
+
Kernel::log(:info, "Plugin '#{p.name}' not started, because it not 'always_start' and not in config")
|
68
|
+
next
|
69
|
+
end
|
70
|
+
@plugins_to_run << p if p.runnable?
|
71
|
+
end
|
72
|
+
@all_plugins = nil
|
73
|
+
@all_names = nil
|
74
|
+
# start plugins!
|
75
|
+
@plugins_to_run.each {|p| monitor << p }
|
76
|
+
@plugins_to_run = nil
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|