circonus 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/circonus-cli +31 -0
- data/bin/circonus-data-cli +25 -0
- data/examples/add_apache_node.rb +100 -0
- data/examples/add_dns_monitor.rb +115 -0
- data/examples/add_http_check.rb +88 -0
- data/examples/add_nginx_alerts.rb +132 -0
- data/examples/add_nginx_graphs.rb +131 -0
- data/examples/add_nginx_node.rb +87 -0
- data/examples/add_snmp_node.rb +142 -0
- data/examples/cache_copy.rb +21 -0
- data/examples/update_nginx_graph.rb +131 -0
- data/examples/util.rb +173 -0
- data/lib/circonus.rb +196 -0
- data/lib/circonus/values.rb +160 -0
- metadata +108 -0
@@ -0,0 +1,131 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Update an nginx graph from a list of hosts attached to a template *HACK*
|
4
|
+
# update_nginx_graph.rb template_name graphid
|
5
|
+
# --FIXME probably not the best way to get a list of hosts.. working on a better way
|
6
|
+
#
|
7
|
+
# -- David Nicklay
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
require 'circonus'
|
12
|
+
require "#{ENV['HOME']}/.circonus.rb"
|
13
|
+
@c = Circonus.new(@apitoken,@appname,@agent)
|
14
|
+
|
15
|
+
if ((ARGV.length < 1) or ARGV[0].match('^-')) then
|
16
|
+
print "Usage: update_nginx_graph.rb template_name [graphid]\n"
|
17
|
+
exit(-1)
|
18
|
+
end
|
19
|
+
template_name = ARGV[0]
|
20
|
+
graphid = ARGV[1]
|
21
|
+
|
22
|
+
agents = @c.list_broker
|
23
|
+
agentid = agents.select { |a| a['_name'] == @agent }.first['_cid']
|
24
|
+
|
25
|
+
title = "#{template_name} - requests"
|
26
|
+
data = {
|
27
|
+
"title"=>"nginx #{title}",
|
28
|
+
"style"=>"area",
|
29
|
+
"max_right_y"=>nil,
|
30
|
+
"min_right_y"=>nil,
|
31
|
+
"min_left_y"=>nil,
|
32
|
+
"max_left_y"=>nil,
|
33
|
+
"guides"=>[],
|
34
|
+
"datapoints"=> [],
|
35
|
+
"composites"=> []
|
36
|
+
}
|
37
|
+
|
38
|
+
dpstub = {
|
39
|
+
"axis"=>"l",
|
40
|
+
"stack"=>nil,
|
41
|
+
"metric_type"=>"numeric",
|
42
|
+
"data_formula"=>nil,
|
43
|
+
"name"=>nil,
|
44
|
+
"derive"=>"counter",
|
45
|
+
"metric_name"=>nil,
|
46
|
+
"color"=>"#33aa33",
|
47
|
+
"check_id"=>nil,
|
48
|
+
"legend_formula"=>nil,
|
49
|
+
"hidden"=>false
|
50
|
+
}
|
51
|
+
|
52
|
+
# No SUM(*) is available, so we have to generate a formula ourselves:
|
53
|
+
# Generate a total formula =A+B+C...... using the number of datapoints
|
54
|
+
def get_total_formula(npoints)
|
55
|
+
i = 0
|
56
|
+
formula = "="
|
57
|
+
a = 'A'..'ZZZZ'
|
58
|
+
a.each do |x|
|
59
|
+
i += 1
|
60
|
+
formula += x
|
61
|
+
break if i == npoints
|
62
|
+
formula += "+"
|
63
|
+
end
|
64
|
+
return formula
|
65
|
+
end
|
66
|
+
|
67
|
+
# get unique values from an array of hashes given an index to compare on
|
68
|
+
def get_unique(array,index)
|
69
|
+
a = array.sort_by { |x| x[index] }
|
70
|
+
return a.inject([]) do |result,item|
|
71
|
+
result << item if !result.last||result.last[index]!=item[index]
|
72
|
+
result
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Get list of hosts on template
|
77
|
+
hosts = @c.list_template.select { |t| t['name'] == template_name }.first['hosts']
|
78
|
+
# Get list of check ids to use:
|
79
|
+
bundles = @c.list_check_bundle().select { |b| hosts.include? b['target'] }
|
80
|
+
checkids = get_unique(bundles,'target').map { |j| j['_checks'].first }
|
81
|
+
checkids.each do |checkid|
|
82
|
+
cid = checkid.to_a.first.gsub(/^.*\//,'')
|
83
|
+
%w{ requests }.each do |metric|
|
84
|
+
dp = dpstub.clone
|
85
|
+
dp['name'] = "nginx #{title} - #{metric}"
|
86
|
+
dp['metric_name'] = metric
|
87
|
+
if %w{ accepted handled requests }.include? metric then
|
88
|
+
dp['derive'] = "counter"
|
89
|
+
end
|
90
|
+
dp['color'] = nil
|
91
|
+
|
92
|
+
dp['stack'] = 0
|
93
|
+
dp['check_id'] = cid
|
94
|
+
dp['hidden'] = true
|
95
|
+
|
96
|
+
data['datapoints'] << dp
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
# Do composite total:
|
103
|
+
formula = get_total_formula(data['datapoints'].length)
|
104
|
+
totaldp = {
|
105
|
+
"name"=>"Total Reqs/s",
|
106
|
+
"axis"=>"l",
|
107
|
+
"stack"=>nil,
|
108
|
+
"legend_formula"=>"=ceil(VAL)",
|
109
|
+
"color"=>"#33aa33",
|
110
|
+
"data_formula"=>formula,
|
111
|
+
"hidden"=>false
|
112
|
+
}
|
113
|
+
data['composites'] << totaldp
|
114
|
+
|
115
|
+
|
116
|
+
guidepct = {
|
117
|
+
"data_formula"=>"99%",
|
118
|
+
"name"=>"99th Percentile",
|
119
|
+
"color"=>"#ea3a92",
|
120
|
+
"hidden"=>false,
|
121
|
+
"legend_formula"=>"=ceil(VAL)"
|
122
|
+
}
|
123
|
+
data['guides'] << guidepct
|
124
|
+
|
125
|
+
if graphid.nil? then
|
126
|
+
r = @c.add_graph(data)
|
127
|
+
else
|
128
|
+
r = @c.update_graph(graphid,data)
|
129
|
+
end
|
130
|
+
pp r
|
131
|
+
|
@@ -0,0 +1,87 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Add a single nginx host to circonus
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'circonus'
|
6
|
+
require 'optparse'
|
7
|
+
require "#{ENV['HOME']}/.circonus.rb"
|
8
|
+
|
9
|
+
def do_update_check_bundle(data)
|
10
|
+
search_check_bundle = @c.list_check_bundle({'display_name' => data['display_name']})
|
11
|
+
existing = false
|
12
|
+
if search_check_bundle.any? # already exists...
|
13
|
+
existing = true
|
14
|
+
r = @c.update_check_bundle(search_check_bundle.first['_cid'],data)
|
15
|
+
else
|
16
|
+
r = @c.add_check_bundle(data)
|
17
|
+
end
|
18
|
+
if not r.nil? then
|
19
|
+
pp r
|
20
|
+
print "Success (#{existing ? 'updating' : 'adding'} #{data['display_name']})\n"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
options = {}
|
26
|
+
options[:tags] = []
|
27
|
+
OptionParser.new { |opts|
|
28
|
+
opts.banner = "Usage: #{File.basename($0)} [-h] hostname [-t tag1,tag2,...]\n"
|
29
|
+
opts.on( '-h', '--help', "This usage menu") do
|
30
|
+
puts opts
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
opts.on( '-t','--tags TAGLIST',"Apply comma separated list of tags" ) do |t|
|
34
|
+
options[:tags] += t.split(/,/)
|
35
|
+
end
|
36
|
+
}.parse!
|
37
|
+
|
38
|
+
def usage()
|
39
|
+
print <<EOF
|
40
|
+
Usage: #{File.basename($0)} hostname [-t tag1,tag2,... ]
|
41
|
+
-h,--help This usage menu
|
42
|
+
-t,--tags Comma separated list of tag names to apply (default is an empty list)
|
43
|
+
EOF
|
44
|
+
end
|
45
|
+
|
46
|
+
host = ARGV[0]
|
47
|
+
if host.nil? then
|
48
|
+
usage()
|
49
|
+
exit -1
|
50
|
+
end
|
51
|
+
|
52
|
+
@c = Circonus.new(@apitoken,@appname,@agent)
|
53
|
+
|
54
|
+
agents = @c.list_broker
|
55
|
+
agentid = agents.select { |a| a['_name'] == @agent }.first['_cid']
|
56
|
+
|
57
|
+
print "Adding nginx for host #{host}\n"
|
58
|
+
data = {
|
59
|
+
:agent_id => agentid,
|
60
|
+
:target => host,
|
61
|
+
:module => "nginx",
|
62
|
+
}
|
63
|
+
bundle = {
|
64
|
+
"type" => "nginx",
|
65
|
+
"target" => host,
|
66
|
+
"tags" => options[:tags],
|
67
|
+
"timeout" => 10,
|
68
|
+
"period" => 60,
|
69
|
+
"display_name" => "#{host} nginx",
|
70
|
+
"brokers" => [
|
71
|
+
agentid
|
72
|
+
],
|
73
|
+
"metrics" => [
|
74
|
+
],
|
75
|
+
"config" => {
|
76
|
+
"url" => "http://#{host}/server-status"
|
77
|
+
}
|
78
|
+
}
|
79
|
+
%w{ handled waiting requests accepted active duration reading writing }.each do |metric|
|
80
|
+
bundle['metrics'] << {
|
81
|
+
'type' => 'numeric',
|
82
|
+
'name' => metric
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
do_update_check_bundle(bundle)
|
87
|
+
|
@@ -0,0 +1,142 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Add a single snmp based host to circonus
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'circonus'
|
6
|
+
require 'snmp'
|
7
|
+
require 'pp'
|
8
|
+
require 'optparse'
|
9
|
+
|
10
|
+
|
11
|
+
options = {}
|
12
|
+
options[:tags] = []
|
13
|
+
OptionParser.new { |opts|
|
14
|
+
opts.banner = "Usage: #{File.basename($0)} [-h] hostname [-t tag1,tag2,...]\n"
|
15
|
+
opts.on( '-h', '--help', "This usage menu") do
|
16
|
+
puts opts
|
17
|
+
exit
|
18
|
+
end
|
19
|
+
opts.on( '-t','--tags TAGLIST',"Apply comma separated list of tags" ) do |t|
|
20
|
+
options[:tags] += t.split(/,/)
|
21
|
+
end
|
22
|
+
}.parse!
|
23
|
+
|
24
|
+
|
25
|
+
def usage()
|
26
|
+
print <<EOF
|
27
|
+
Usage: add_snmp_node.rb hostname [-t tag1,tag2,... ]
|
28
|
+
-h,--help This usage menu
|
29
|
+
-t,--tags Comma separated list of tag names to apply (default is an empty list)
|
30
|
+
EOF
|
31
|
+
end
|
32
|
+
|
33
|
+
host = ARGV[0]
|
34
|
+
if host.nil? then
|
35
|
+
usage()
|
36
|
+
exit -1
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# Make a guess as to what ethernet interface to query for data
|
41
|
+
def get_ethernet_oids(host)
|
42
|
+
ifTable_columns = ["ifDescr", "ifOutOctets","ifIndex"]
|
43
|
+
eth_name = nil
|
44
|
+
eth_octets = nil
|
45
|
+
eth_index = nil
|
46
|
+
SNMP::Manager.open(:Host => host) do |manager|
|
47
|
+
manager.walk(ifTable_columns) do |row|
|
48
|
+
next if row[0].value.to_s.match('^lo')
|
49
|
+
if eth_name.nil? then
|
50
|
+
eth_name = row[0].value
|
51
|
+
eth_octets = row[1].value
|
52
|
+
eth_index = row[2].value
|
53
|
+
end
|
54
|
+
if row[1].value > eth_octets then
|
55
|
+
eth_name = row[0].value
|
56
|
+
eth_octets = row[1].value
|
57
|
+
eth_index = row[2].value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
if eth_index.nil?
|
62
|
+
eth_index = 0
|
63
|
+
end
|
64
|
+
return {
|
65
|
+
"ifOutOctets" => ".1.3.6.1.2.1.2.2.1.16.#{eth_index}",
|
66
|
+
"ifInOctets" => ".1.3.6.1.2.1.2.2.1.10.#{eth_index}"
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
require "#{ENV['HOME']}/.circonus.rb"
|
71
|
+
@c = Circonus.new(@apitoken,@appname,@agent)
|
72
|
+
|
73
|
+
agentid = @c.search_broker(@agent,'_name').first['_cid']
|
74
|
+
|
75
|
+
print "Adding snmp check for host #{host}\n"
|
76
|
+
data_stub = {
|
77
|
+
"type" => nil,
|
78
|
+
"target" => nil,
|
79
|
+
"timeout" => 10,
|
80
|
+
"period" => 60,
|
81
|
+
"display_name" => nil,
|
82
|
+
"brokers" => [],
|
83
|
+
"metrics" => [],
|
84
|
+
"config" => {}
|
85
|
+
}
|
86
|
+
bundle = data_stub.clone()
|
87
|
+
bundle['tags'] = options[:tags]
|
88
|
+
bundle['target'] = host
|
89
|
+
bundle['type'] = 'snmp'
|
90
|
+
bundle['display_name'] = "#{host} snmp"
|
91
|
+
bundle['brokers'] << agentid
|
92
|
+
bundle['config'] = {
|
93
|
+
"community" => "public",
|
94
|
+
"version" => "2c",
|
95
|
+
"port" => "161",
|
96
|
+
}
|
97
|
+
oids = {
|
98
|
+
"memAvailSwap"=> ".1.3.6.1.4.1.2021.4.4.0",
|
99
|
+
"memAvailReal"=> ".1.3.6.1.4.1.2021.4.6.0",
|
100
|
+
"memTotalSwap"=> ".1.3.6.1.4.1.2021.4.3.0",
|
101
|
+
"memTotalReal"=> ".1.3.6.1.4.1.2021.4.5.0",
|
102
|
+
"memTotalFree"=> ".1.3.6.1.4.1.2021.4.11.0",
|
103
|
+
"memShared"=> ".1.3.6.1.4.1.2021.4.13.0",
|
104
|
+
"memBuffer"=> ".1.3.6.1.4.1.2021.4.14.0",
|
105
|
+
"memCached"=> ".1.3.6.1.4.1.2021.4.15.0",
|
106
|
+
"ssCpuRawUser"=> ".1.3.6.1.4.1.2021.11.50.0",
|
107
|
+
"ssCpuRawNice"=> ".1.3.6.1.4.1.2021.11.51.0",
|
108
|
+
"ssCpuRawSystem"=> ".1.3.6.1.4.1.2021.11.52.0",
|
109
|
+
"ssCpuRawIdle"=> ".1.3.6.1.4.1.2021.11.53.0",
|
110
|
+
"ssCpuRawWait"=> ".1.3.6.1.4.1.2021.11.54.0",
|
111
|
+
"ssCpuRawKernel"=> ".1.3.6.1.4.1.2021.11.55.0",
|
112
|
+
"ssCpuRawInterrupt"=> ".1.3.6.1.4.1.2021.11.56.0",
|
113
|
+
"ssCpuRawSoftIRQ"=> ".1.3.6.1.4.1.2021.11.61.0",
|
114
|
+
"ssCpuIdle"=> ".1.3.6.1.4.1.2021.11.11.0"
|
115
|
+
}
|
116
|
+
|
117
|
+
begin
|
118
|
+
eth_oids = get_ethernet_oids(host)
|
119
|
+
rescue
|
120
|
+
eth_oids = {}
|
121
|
+
end
|
122
|
+
oids.merge!(eth_oids)
|
123
|
+
|
124
|
+
oids.each do |name,oid|
|
125
|
+
bundle['config']["oid_#{name}"] = oid
|
126
|
+
bundle['metrics'] << {
|
127
|
+
'type' => 'numeric',
|
128
|
+
'name' => name
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
search_bundles = @c.search_check_bundle(bundle['display_name'],'display_name')
|
133
|
+
if search_bundles.any? # already exists...
|
134
|
+
r = @c.update_check_bundle(search_bundles.first['_cid'],bundle)
|
135
|
+
else
|
136
|
+
r = @c.add_check_bundle(bundle)
|
137
|
+
end
|
138
|
+
if not r.nil? then
|
139
|
+
print "Success\n"
|
140
|
+
#pp r
|
141
|
+
end
|
142
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Make a cached copy of all of what is in circonus API
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'circonus'
|
6
|
+
require 'json'
|
7
|
+
require "#{ENV['HOME']}/.circonus.rb"
|
8
|
+
@c = Circonus.new(@apitoken,@appname,@agent)
|
9
|
+
|
10
|
+
@c.methods.select { |m| m.match('^list_') }.each do |m|
|
11
|
+
begin
|
12
|
+
data = @c.send m.to_sym
|
13
|
+
type = m.sub('list_','')
|
14
|
+
Dir.mkdir 'cache' unless File.directory?('cache')
|
15
|
+
f = File.open File.join('cache',"#{type}.json"), "w"
|
16
|
+
f.write JSON.pretty_generate(data)
|
17
|
+
f.close
|
18
|
+
rescue RestClient::ResourceNotFound => e
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,131 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Update an nginx graph from a list of hosts attached to a template *HACK*
|
4
|
+
# update_nginx_graph.rb template_name graphid
|
5
|
+
# --FIXME probably not the best way to get a list of hosts.. working on a better way
|
6
|
+
#
|
7
|
+
# -- David Nicklay
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
require 'circonus'
|
12
|
+
require "#{ENV['HOME']}/.circonus.rb"
|
13
|
+
@c = Circonus.new(@apitoken,@appname,@agent)
|
14
|
+
|
15
|
+
if ((ARGV.length < 1) or ARGV[0].match('^-')) then
|
16
|
+
print "Usage: update_nginx_graph.rb template_name [graphid]\n"
|
17
|
+
exit(-1)
|
18
|
+
end
|
19
|
+
template_name = ARGV[0]
|
20
|
+
graphid = ARGV[1]
|
21
|
+
|
22
|
+
agents = @c.list_broker
|
23
|
+
agentid = agents.select { |a| a['_name'] == @agent }.first['_cid']
|
24
|
+
|
25
|
+
title = "#{template_name} - requests"
|
26
|
+
data = {
|
27
|
+
"title"=>"nginx #{title}",
|
28
|
+
"style"=>"area",
|
29
|
+
"max_right_y"=>nil,
|
30
|
+
"min_right_y"=>nil,
|
31
|
+
"min_left_y"=>nil,
|
32
|
+
"max_left_y"=>nil,
|
33
|
+
"guides"=>[],
|
34
|
+
"datapoints"=> [],
|
35
|
+
"composites"=> []
|
36
|
+
}
|
37
|
+
|
38
|
+
dpstub = {
|
39
|
+
"axis"=>"l",
|
40
|
+
"stack"=>nil,
|
41
|
+
"metric_type"=>"numeric",
|
42
|
+
"data_formula"=>nil,
|
43
|
+
"name"=>nil,
|
44
|
+
"derive"=>"counter",
|
45
|
+
"metric_name"=>nil,
|
46
|
+
"color"=>"#33aa33",
|
47
|
+
"check_id"=>nil,
|
48
|
+
"legend_formula"=>nil,
|
49
|
+
"hidden"=>false
|
50
|
+
}
|
51
|
+
|
52
|
+
# No SUM(*) is available, so we have to generate a formula ourselves:
|
53
|
+
# Generate a total formula =A+B+C...... using the number of datapoints
|
54
|
+
def get_total_formula(npoints)
|
55
|
+
i = 0
|
56
|
+
formula = "="
|
57
|
+
a = 'A'..'ZZZZ'
|
58
|
+
a.each do |x|
|
59
|
+
i += 1
|
60
|
+
formula += x
|
61
|
+
break if i == npoints
|
62
|
+
formula += "+"
|
63
|
+
end
|
64
|
+
return formula
|
65
|
+
end
|
66
|
+
|
67
|
+
# get unique values from an array of hashes given an index to compare on
|
68
|
+
def get_unique(array,index)
|
69
|
+
a = array.sort_by { |x| x[index] }
|
70
|
+
return a.inject([]) do |result,item|
|
71
|
+
result << item if !result.last||result.last[index]!=item[index]
|
72
|
+
result
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Get list of hosts on template
|
77
|
+
hosts = @c.list_template.select { |t| t['name'] == template_name }.first['hosts']
|
78
|
+
# Get list of check ids to use:
|
79
|
+
bundles = @c.list_check_bundle().select { |b| hosts.include? b['target'] }
|
80
|
+
checkids = get_unique(bundles,'target').map { |j| j['_checks'].first }
|
81
|
+
checkids.each do |checkid|
|
82
|
+
cid = checkid.to_a.first.gsub(/^.*\//,'')
|
83
|
+
%w{ requests }.each do |metric|
|
84
|
+
dp = dpstub.clone
|
85
|
+
dp['name'] = "nginx #{title} - #{metric}"
|
86
|
+
dp['metric_name'] = metric
|
87
|
+
if %w{ accepted handled requests }.include? metric then
|
88
|
+
dp['derive'] = "counter"
|
89
|
+
end
|
90
|
+
dp['color'] = nil
|
91
|
+
|
92
|
+
dp['stack'] = 0
|
93
|
+
dp['check_id'] = cid
|
94
|
+
dp['hidden'] = true
|
95
|
+
|
96
|
+
data['datapoints'] << dp
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
# Do composite total:
|
103
|
+
formula = get_total_formula(data['datapoints'].length)
|
104
|
+
totaldp = {
|
105
|
+
"name"=>"Total Reqs/s",
|
106
|
+
"axis"=>"l",
|
107
|
+
"stack"=>nil,
|
108
|
+
"legend_formula"=>"=ceil(VAL)",
|
109
|
+
"color"=>"#33aa33",
|
110
|
+
"data_formula"=>formula,
|
111
|
+
"hidden"=>false
|
112
|
+
}
|
113
|
+
data['composites'] << totaldp
|
114
|
+
|
115
|
+
|
116
|
+
guidepct = {
|
117
|
+
"data_formula"=>"99%",
|
118
|
+
"name"=>"99th Percentile",
|
119
|
+
"color"=>"#ea3a92",
|
120
|
+
"hidden"=>false,
|
121
|
+
"legend_formula"=>"=ceil(VAL)"
|
122
|
+
}
|
123
|
+
data['guides'] << guidepct
|
124
|
+
|
125
|
+
if graphid.nil? then
|
126
|
+
r = @c.add_graph(data)
|
127
|
+
else
|
128
|
+
r = @c.update_graph(graphid,data)
|
129
|
+
end
|
130
|
+
pp r
|
131
|
+
|