zabbix_nudge 0.1.3
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.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +4 -0
- data/Rakefile +8 -0
- data/bin/zabbix_nudge +20 -0
- data/bin/zabbix_nudge_jmx +25 -0
- data/bin/zabbix_nudged +11 -0
- data/examples/tomcat_template.xml +1279 -0
- data/lib/zabbix_nudge.rb +79 -0
- data/lib/zabbix_nudge/jmx.rb +61 -0
- data/lib/zabbix_nudge/jmx_cli.rb +54 -0
- data/lib/zabbix_nudge/nudge_cli.rb +74 -0
- data/lib/zabbix_nudge/version.rb +3 -0
- data/spec/fixtures/cms_perm_response.txt +7 -0
- data/spec/fixtures/tomcat_template.xml +92 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/zabbix_nudge_spec.rb +32 -0
- data/zabbix_nudge.gemspec +37 -0
- metadata +264 -0
data/lib/zabbix_nudge.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require "zabbix_nudge/version"
|
2
|
+
require "active_support/core_ext/string/inflections.rb"
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'yajl/json_gem'
|
5
|
+
require 'zabbix'
|
6
|
+
require "socket"
|
7
|
+
require "zabbix_nudge/jmx"
|
8
|
+
|
9
|
+
module ZabbixNudge
|
10
|
+
|
11
|
+
def self.root
|
12
|
+
@root ="#{File.expand_path('../..',__FILE__)}"
|
13
|
+
end
|
14
|
+
|
15
|
+
class Nudge
|
16
|
+
def initialize(templates, options = {} )
|
17
|
+
options[:zabbix_server_name] = 'localhost' unless options[:zabbix_server_name]
|
18
|
+
options[:zabbix_server_port] = '10051' unless options[:zabbix_server_port]
|
19
|
+
options[:sender_hostname] = Socket.gethostname unless options[:sender_hostname]
|
20
|
+
|
21
|
+
@options = options
|
22
|
+
@templates = template_files(templates)
|
23
|
+
@items = template_items(@templates)
|
24
|
+
end
|
25
|
+
|
26
|
+
def template_files(templates)
|
27
|
+
template_files = []
|
28
|
+
template_files = Dir.glob(File.join(templates,'*.xml')) if !templates.is_a?(Array) && File.directory?(templates)
|
29
|
+
template_files = templates.map{ |template| template if File.exist?(template) }.compact if templates.is_a?(Array)
|
30
|
+
template_files = [templates] if !templates.is_a?(Array) && File.exist?(templates) && !File.directory?(templates)
|
31
|
+
template_files
|
32
|
+
end
|
33
|
+
|
34
|
+
def template_items(templates)
|
35
|
+
parsed_items = Hash.new
|
36
|
+
|
37
|
+
templates.each do |template|
|
38
|
+
template_items = Nokogiri::XML(File.open(template))
|
39
|
+
items = template_items.xpath('//item').map {|item| item.attributes['key'].text}.compact
|
40
|
+
items.each do |item|
|
41
|
+
parts = item.match(/([^\[]+)\[([^\]]+)/)
|
42
|
+
unless parts.nil?
|
43
|
+
key = parts[1].underscore.to_sym
|
44
|
+
attributes = parts[2]
|
45
|
+
(parsed_items[key]) ? parsed_items[key] << attributes : parsed_items[key] = [attributes]
|
46
|
+
else
|
47
|
+
puts "not evaluating: "+item
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
parsed_items
|
52
|
+
end
|
53
|
+
|
54
|
+
def send(items = :all)
|
55
|
+
|
56
|
+
return if @items.nil?
|
57
|
+
|
58
|
+
|
59
|
+
pushers = ( items == :all) ? ( @items.keys.map{ |key| "ZabbixNudge::#{key.to_s.camelize}".constantize }) : ["ZabbixNudge::#{items.camelize}".constantize]
|
60
|
+
|
61
|
+
processed_items = Hash.new
|
62
|
+
|
63
|
+
pushers.map do |pusher|
|
64
|
+
pusher_key = pusher.to_s.demodulize.underscore.to_sym
|
65
|
+
processed_items.update pusher.new(@items[pusher_key],@options[pusher_key]).send(:processed_items)
|
66
|
+
end
|
67
|
+
|
68
|
+
zbx = Zabbix::Sender::Buffer.new :host => @options[:zabbix_server_name], :port => @options[:zabbix_server_port]
|
69
|
+
|
70
|
+
processed_items.each do |key,value|
|
71
|
+
zbx.append key, value, :host => @options[:sender_hostname]
|
72
|
+
end
|
73
|
+
zbx.flush
|
74
|
+
|
75
|
+
processed_items
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
module ZabbixNudge
|
4
|
+
|
5
|
+
class Jmx
|
6
|
+
|
7
|
+
include HTTParty
|
8
|
+
base_uri 'http://localhost:8080'
|
9
|
+
format :json
|
10
|
+
|
11
|
+
def initialize(items=[] ,options={})
|
12
|
+
self.class.base_uri options[:base_uri] if options[:base_uri]
|
13
|
+
@payload = Array.new
|
14
|
+
@items = items
|
15
|
+
@items.each do |item| mbean,attribute,path = item.split(';')
|
16
|
+
tokens = { "mbean" => mbean, "attribute" => attribute, "type" => "READ"}
|
17
|
+
tokens["path"] = path if path
|
18
|
+
@payload << tokens
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# http://www.jolokia.org/reference/html/protocol.html#search
|
23
|
+
|
24
|
+
def search(mbean,config={})
|
25
|
+
self.class.post('/jolokia', :body => { "type" => "SEARCH", "mbean" => mbean, "config" => config}.to_json)
|
26
|
+
end
|
27
|
+
|
28
|
+
# http://www.jolokia.org/reference/html/protocol.html#list
|
29
|
+
# use options = { "maxDepth" => 2} to limit depth
|
30
|
+
|
31
|
+
def list(path,config={})
|
32
|
+
self.class.post('/jolokia', :body => { "type" => "LIST", "path" => path, "config" => config}.to_json)
|
33
|
+
end
|
34
|
+
|
35
|
+
def read(mbean,attribute,path=nil,config={})
|
36
|
+
payload = { "type" => "READ", "mbean" => mbean, "attribute" => attribute}
|
37
|
+
payload["path"] = path if path
|
38
|
+
payload["config"] = config if config.length > 0
|
39
|
+
ap payload.to_json
|
40
|
+
self.class.post('/jolokia', :body => payload.to_json, :timeout => 5)
|
41
|
+
end
|
42
|
+
|
43
|
+
def version
|
44
|
+
self.class.post('/jolokia', :body => { "type" => "VERSION"}.to_json)
|
45
|
+
end
|
46
|
+
|
47
|
+
def processed_items
|
48
|
+
data = self.class.post('/jolokia', :body => @payload.to_json, :timeout => 5) rescue nil
|
49
|
+
result = Hash.new
|
50
|
+
data.each{|datum| result[result_key(datum)] = datum['value'] if datum['request']} if data
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def result_key(datum)
|
57
|
+
"#{self.class.to_s.demodulize.underscore}[#{datum['request']['mbean']};#{datum['request']['attribute']};#{datum['request']['path']}]"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'mixlib/cli'
|
2
|
+
|
3
|
+
module ZabbixNudge
|
4
|
+
class NudgeCLI
|
5
|
+
include Mixlib::CLI
|
6
|
+
|
7
|
+
option :jmx__base_uri,
|
8
|
+
:long => "--jmx_base_uri uri",
|
9
|
+
:description => "The jmx base uri"
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class JmxCLI
|
14
|
+
include Mixlib::CLI
|
15
|
+
|
16
|
+
option :base_uri,
|
17
|
+
:short => "-b uri",
|
18
|
+
:default => "localhost:8080",
|
19
|
+
:long => "--base_uri uri",
|
20
|
+
:description => "The jmx base uri"
|
21
|
+
|
22
|
+
option :command,
|
23
|
+
:short => "-o command",
|
24
|
+
:default => "read",
|
25
|
+
:long => "--command action",
|
26
|
+
:description => "The command to perform",
|
27
|
+
:proc => Proc.new { |o| o.to_sym }
|
28
|
+
|
29
|
+
option :mbean,
|
30
|
+
:short => "-m mbean",
|
31
|
+
:long => "--mbean mbean",
|
32
|
+
:description => "The name of the mbean e.g. java.lang:type=Memory (needed for read command)"
|
33
|
+
|
34
|
+
option :attribute,
|
35
|
+
:short => "-a attribute",
|
36
|
+
:long => "--attribute attribute",
|
37
|
+
:description => "The name of the action e.g. HeapMemoryUsage (needed for read command)"
|
38
|
+
|
39
|
+
option :path,
|
40
|
+
:short => "-p path",
|
41
|
+
:long => "--path path",
|
42
|
+
:description => "The name of the path e.g. used (needed for read and list command)"
|
43
|
+
|
44
|
+
option :help,
|
45
|
+
:short => "-h",
|
46
|
+
:long => "--help",
|
47
|
+
:description => "Show this message",
|
48
|
+
:on => :tail,
|
49
|
+
:boolean => true,
|
50
|
+
:show_options => true,
|
51
|
+
:exit => 0
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'mixlib/cli'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module ZabbixNudge
|
5
|
+
class NudgeCLI
|
6
|
+
include Mixlib::CLI
|
7
|
+
include Socket
|
8
|
+
|
9
|
+
option :config_file,
|
10
|
+
:short => "-c CONFIG",
|
11
|
+
:long => "--config CONFIG",
|
12
|
+
:default => '/etc/zabbix-nudge/config.rb',
|
13
|
+
:description => "The configuration file to use"
|
14
|
+
|
15
|
+
option :templates,
|
16
|
+
:short => "-t FILE",
|
17
|
+
:long => "--template FILE",
|
18
|
+
:default => File.join(ZabbixNudge.root,"examples"),
|
19
|
+
:description => "The template file(s) to use, can be a file a comma separated list of files or a directory",
|
20
|
+
:proc => Proc.new { |t| (t =~ /,/) ? t.split(',') : t }
|
21
|
+
|
22
|
+
option :zabbix_server_name,
|
23
|
+
:short => "-z SERVER",
|
24
|
+
:long => "--zabbix-server SERVER",
|
25
|
+
:default => 'localhost',
|
26
|
+
:description => "Hostname or IP address of Zabbix Server. Default is localhost"
|
27
|
+
|
28
|
+
option :zabbix_server_port,
|
29
|
+
:short => "-p SERVER PORT",
|
30
|
+
:long => "--port SERVER PORT",
|
31
|
+
:default => '10051',
|
32
|
+
:description => "Specify port number of server trapper running on the server. Default is 10051"
|
33
|
+
|
34
|
+
option :sender_hostname,
|
35
|
+
:short => "-s HOST",
|
36
|
+
:long => "--host HOSTNAME",
|
37
|
+
:default => Socket.gethostname,
|
38
|
+
:description => "Specify host name. Default is current hostname"
|
39
|
+
|
40
|
+
option :jolokia_hostname,
|
41
|
+
:short => "-P JHOSTNAME",
|
42
|
+
:long => "--jolokiahostname JHOSTNAME",
|
43
|
+
:default => "localhost",
|
44
|
+
:description => "Specify jolokia hostname. Default is localhost"
|
45
|
+
|
46
|
+
option :jolokia_port,
|
47
|
+
:short => "-P JPORT",
|
48
|
+
:long => "--jolokiaport JPORT",
|
49
|
+
:default => "8080",
|
50
|
+
:description => "Specify jolokia port. Default is 8080."
|
51
|
+
|
52
|
+
option :log_level,
|
53
|
+
:short => "-l LEVEL",
|
54
|
+
:long => "--log_level LEVEL",
|
55
|
+
:description => "Set the log level (debug, info, warn, error, fatal)",
|
56
|
+
:proc => Proc.new { |l| l.to_sym }
|
57
|
+
|
58
|
+
option :show_send_values,
|
59
|
+
:short => "-v",
|
60
|
+
:long => "--show_send_values",
|
61
|
+
:description => "shows what values got send to zabbix",
|
62
|
+
:boolean => true
|
63
|
+
|
64
|
+
option :help,
|
65
|
+
:short => "-h",
|
66
|
+
:long => "--help",
|
67
|
+
:description => "Show this message",
|
68
|
+
:on => :tail,
|
69
|
+
:boolean => true,
|
70
|
+
:show_options => true,
|
71
|
+
:exit => 0
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
HTTP/1.1 200 OK
|
2
|
+
Server: Apache-Coyote/1.1
|
3
|
+
Content-Type: text/plain;charset=utf-8
|
4
|
+
Content-Length: 512
|
5
|
+
Date: Wed, 15 Jun 2011 15:17:12 GMT
|
6
|
+
|
7
|
+
[{"timestamp":1308151032,"status":200,"request":{"mbean":"java.lang:name=CMS Perm Gen,type=MemoryPool","path":"committed","attribute":"Usage","type":"read"},"value":31518720},{"timestamp":1308151032,"status":200,"request":{"mbean":"java.lang:name=CMS Perm Gen,type=MemoryPool","path":"max","attribute":"Usage","type":"read"},"value":67108864},{"timestamp":1308151032,"status":200,"request":{"mbean":"java.lang:name=CMS Perm Gen,type=MemoryPool","path":"used","attribute":"Usage","type":"read"},"value":18890360}]
|
@@ -0,0 +1,92 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
<zabbix_export version="1.0" date="22.12.07" time="03.42">
|
3
|
+
<hosts>
|
4
|
+
<host name="Template_Tomcat">
|
5
|
+
<proxy_hostid>0</proxy_hostid>
|
6
|
+
<useip>0</useip>
|
7
|
+
<dns/>
|
8
|
+
<ip>0.0.0.0</ip>
|
9
|
+
<port>0</port>
|
10
|
+
<status>3</status>
|
11
|
+
<groups>
|
12
|
+
<group>Templates</group>
|
13
|
+
<group>Web Server</group>
|
14
|
+
</groups>
|
15
|
+
<items>
|
16
|
+
<item type="7" key="jmx[java.lang:name=CMS Perm Gen,type=MemoryPool;Usage;committed]" value_type="3">
|
17
|
+
<description>memorypool cms perm gen committed</description>
|
18
|
+
<delay>30</delay>
|
19
|
+
<history>90</history>
|
20
|
+
<trends>365</trends>
|
21
|
+
<units>B</units>
|
22
|
+
<formula>1</formula>
|
23
|
+
<snmp_community>public</snmp_community>
|
24
|
+
<snmp_oid>interfaces.ifTable.ifEntry.ifInOctets.1</snmp_oid>
|
25
|
+
<snmp_port>161</snmp_port>
|
26
|
+
</item>
|
27
|
+
<item type="7" key="jmx[java.lang:name=CMS Perm Gen,type=MemoryPool;Usage;max]" value_type="3">
|
28
|
+
<description>memorypool cms perm gen max</description>
|
29
|
+
<delay>3600</delay>
|
30
|
+
<history>90</history>
|
31
|
+
<trends>365</trends>
|
32
|
+
<units>B</units>
|
33
|
+
<formula>1</formula>
|
34
|
+
<snmp_community>public</snmp_community>
|
35
|
+
<snmp_oid>interfaces.ifTable.ifEntry.ifInOctets.1</snmp_oid>
|
36
|
+
<snmp_port>161</snmp_port>
|
37
|
+
</item>
|
38
|
+
<item type="7" key="jmx[java.lang:name=CMS Perm Gen,type=MemoryPool;Usage;used]" value_type="3">
|
39
|
+
<description>memorypool cms perm gen used</description>
|
40
|
+
<delay>30</delay>
|
41
|
+
<history>90</history>
|
42
|
+
<trends>365</trends>
|
43
|
+
<units>B</units>
|
44
|
+
<formula>1</formula>
|
45
|
+
<snmp_community>public</snmp_community>
|
46
|
+
<snmp_oid>interfaces.ifTable.ifEntry.ifInOctets.1</snmp_oid>
|
47
|
+
<snmp_port>161</snmp_port>
|
48
|
+
</item>
|
49
|
+
</items>
|
50
|
+
<triggers/>
|
51
|
+
<graphs>
|
52
|
+
<graph name="memorypool cms perm gen" width="900" height="200">
|
53
|
+
<show_work_period>1</show_work_period>
|
54
|
+
<show_triggers>1</show_triggers>
|
55
|
+
<ymin_type>0</ymin_type>
|
56
|
+
<ymax_type>0</ymax_type>
|
57
|
+
<ymin_item_key></ymin_item_key>
|
58
|
+
<ymax_item_key></ymax_item_key>
|
59
|
+
<show_work_period>0</show_work_period>
|
60
|
+
<show_triggers>0</show_triggers>
|
61
|
+
<graphtype>1</graphtype>
|
62
|
+
<yaxismin>0.0000</yaxismin>
|
63
|
+
<yaxismax>100.0000</yaxismax>
|
64
|
+
<show_legend>0</show_legend>
|
65
|
+
<show_3d>0</show_3d>
|
66
|
+
<percent_left>0.0000</percent_left>
|
67
|
+
<percent_right>0.0000</percent_right>
|
68
|
+
<graph_elements>
|
69
|
+
<graph_element item="{HOSTNAME}:jmx[java.lang:name=CMS Perm Gen,type=MemoryPool;Usage;committed]">
|
70
|
+
<color>000099</color>
|
71
|
+
<yaxisside>1</yaxisside>
|
72
|
+
<calc_fnc>2</calc_fnc>
|
73
|
+
<periods_cnt>5</periods_cnt>
|
74
|
+
</graph_element>
|
75
|
+
<graph_element item="{HOSTNAME}:jmx[java.lang:name=CMS Perm Gen,type=MemoryPool;Usage;max]">
|
76
|
+
<color>990000</color>
|
77
|
+
<yaxisside>1</yaxisside>
|
78
|
+
<calc_fnc>2</calc_fnc>
|
79
|
+
<periods_cnt>5</periods_cnt>
|
80
|
+
</graph_element>
|
81
|
+
<graph_element item="{HOSTNAME}:jmx[java.lang:name=CMS Perm Gen,type=MemoryPool;Usage;used]">
|
82
|
+
<color>009900</color>
|
83
|
+
<yaxisside>1</yaxisside>
|
84
|
+
<calc_fnc>2</calc_fnc>
|
85
|
+
<periods_cnt>5</periods_cnt>
|
86
|
+
</graph_element>
|
87
|
+
</graph_elements>
|
88
|
+
</graph>
|
89
|
+
</graphs>
|
90
|
+
</host>
|
91
|
+
</hosts>
|
92
|
+
</zabbix_export>
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
$: << File.join(File.dirname(__FILE__), "/../lib")
|
4
|
+
require 'zabbix_nudge'
|
5
|
+
require 'fakeweb'
|
6
|
+
|
7
|
+
FakeWeb.allow_net_connect = false
|
8
|
+
|
9
|
+
def fixture_file(filename)
|
10
|
+
return '' if filename == ''
|
11
|
+
file_path = File.expand_path(File.dirname(__FILE__) + '/fixtures/' + filename)
|
12
|
+
File.read(file_path)
|
13
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), "/spec_helper")
|
4
|
+
require 'ap'
|
5
|
+
|
6
|
+
describe ZabbixNudge::Nudge do
|
7
|
+
|
8
|
+
context 'Initialization' do
|
9
|
+
|
10
|
+
FakeWeb.register_uri(:post, "http://localhost:4568/jolokia", :response => fixture_file("cms_perm_response.txt"))
|
11
|
+
|
12
|
+
it 'should accept a template file' do
|
13
|
+
@m = ZabbixNudge::Nudge.new(File.join(File.dirname(__FILE__), 'fixtures/tomcat_template.xml'),:jmx => {:base_uri => "http://localhost:4568"})
|
14
|
+
@m.send.should == {"jmx[java.lang:name=CMS Perm Gen,type=MemoryPool;Usage;max]"=>67108864, "jmx[java.lang:name=CMS Perm Gen,type=MemoryPool;Usage;used]"=>18890360, "jmx[java.lang:name=CMS Perm Gen,type=MemoryPool;Usage;committed]"=>31518720}
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'process items' do
|
21
|
+
|
22
|
+
|
23
|
+
before(:each) do
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should get the title" do
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
$:.push File.expand_path("../lib", __FILE__)
|
4
|
+
require "zabbix_nudge/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "zabbix_nudge"
|
8
|
+
s.version = ZabbixNudge::VERSION
|
9
|
+
s.authors = ["Myroslav Rys"]
|
10
|
+
s.email = ["stonevil@gmail.com"]
|
11
|
+
s.homepage = "http://www.stone.org.ua"
|
12
|
+
s.license = 'GPL'
|
13
|
+
|
14
|
+
s.summary = %q{Collect metrics from different sources and push (nudge) data to Zabbix}
|
15
|
+
s.description = %q{zabbix-nudge is a gem to collect metrics from different sources, parse zabbix templates and push (nudge) the data to zabbix proxy or server}
|
16
|
+
|
17
|
+
s.rubyforge_project = "zabbix-nudge"
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
21
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
|
24
|
+
s.add_dependency 'nokogiri', '~> 1.4.4'
|
25
|
+
s.add_dependency 'zabbix', '~> 0.3.0'
|
26
|
+
s.add_dependency 'httparty', '~> 0.7.8'
|
27
|
+
s.add_dependency 'activesupport', '~> 3.0.8'
|
28
|
+
s.add_dependency "i18n", "~> 0.6.0"
|
29
|
+
s.add_dependency 'yajl-ruby', '~> 0.8.2'
|
30
|
+
s.add_dependency "awesome_print", "~> 0.4.0"
|
31
|
+
s.add_dependency "mixlib-cli", "~> 1.2.0"
|
32
|
+
s.add_dependency "daemons", "~> 1.1.9"
|
33
|
+
|
34
|
+
s.add_development_dependency 'rspec', '~> 2.6.0'
|
35
|
+
s.add_development_dependency 'fakeweb', '~> 1.3.0'
|
36
|
+
|
37
|
+
end
|